"""Project manifest (smarttools.yaml) handling.

Manages project-level tool dependencies and overrides.
"""

import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional, List, Dict

import yaml


MANIFEST_FILENAME = "smarttools.yaml"


@dataclass
class Dependency:
    """A tool dependency declaration."""
    name: str                    # owner/name format (e.g., "rob/summarize")
    version: str = "*"           # Version constraint (e.g., ">=1.0.0", "^1.2.0")

    @property
    def owner(self) -> Optional[str]:
        """Extract owner from name if present."""
        if "/" in self.name:
            return self.name.split("/")[0]
        return None

    @property
    def tool_name(self) -> str:
        """Extract tool name without owner."""
        if "/" in self.name:
            return self.name.split("/")[1]
        return self.name

    def to_dict(self) -> dict:
        return {
            "name": self.name,
            "version": self.version
        }

    @classmethod
    def from_dict(cls, data: dict) -> "Dependency":
        if isinstance(data, str):
            # Simple string format: "rob/summarize" or "rob/summarize@^1.0.0"
            if "@" in data:
                name, version = data.rsplit("@", 1)
                return cls(name=name, version=version)
            return cls(name=data)

        return cls(
            name=data["name"],
            version=data.get("version", "*")
        )


@dataclass
class ToolOverride:
    """Runtime overrides for a tool."""
    provider: Optional[str] = None
    # Future: other overrides like timeout, retries, etc.

    def to_dict(self) -> dict:
        d = {}
        if self.provider:
            d["provider"] = self.provider
        return d

    @classmethod
    def from_dict(cls, data: dict) -> "ToolOverride":
        return cls(
            provider=data.get("provider")
        )


@dataclass
class Manifest:
    """Project manifest (smarttools.yaml)."""
    name: str = "my-project"
    version: str = "1.0.0"
    dependencies: List[Dependency] = field(default_factory=list)
    overrides: Dict[str, ToolOverride] = field(default_factory=dict)

    def to_dict(self) -> dict:
        d = {
            "name": self.name,
            "version": self.version,
        }

        if self.dependencies:
            d["dependencies"] = [dep.to_dict() for dep in self.dependencies]

        if self.overrides:
            d["overrides"] = {
                name: override.to_dict()
                for name, override in self.overrides.items()
            }

        return d

    @classmethod
    def from_dict(cls, data: dict) -> "Manifest":
        dependencies = []
        for dep in data.get("dependencies", []):
            dependencies.append(Dependency.from_dict(dep))

        overrides = {}
        for name, override_data in data.get("overrides", {}).items():
            overrides[name] = ToolOverride.from_dict(override_data)

        return cls(
            name=data.get("name", "my-project"),
            version=data.get("version", "1.0.0"),
            dependencies=dependencies,
            overrides=overrides
        )

    def get_override(self, tool_name: str) -> Optional[ToolOverride]:
        """Get override for a tool by name (checks both full and short names)."""
        # Try exact match first
        if tool_name in self.overrides:
            return self.overrides[tool_name]

        # Try matching just the tool name part (without owner)
        short_name = tool_name.split("/")[-1] if "/" in tool_name else tool_name
        for override_name, override in self.overrides.items():
            override_short = override_name.split("/")[-1] if "/" in override_name else override_name
            if override_short == short_name:
                return override

        return None

    def add_dependency(self, name: str, version: str = "*") -> None:
        """Add or update a dependency."""
        # Check if already exists
        for dep in self.dependencies:
            if dep.name == name:
                dep.version = version
                return

        self.dependencies.append(Dependency(name=name, version=version))


def find_manifest(start_dir: Optional[Path] = None) -> Optional[Path]:
    """
    Find smarttools.yaml by searching up from start_dir.

    Args:
        start_dir: Directory to start searching from (default: cwd)

    Returns:
        Path to manifest file, or None if not found
    """
    if start_dir is None:
        start_dir = Path.cwd()

    current = start_dir.resolve()

    while current != current.parent:
        manifest_path = current / MANIFEST_FILENAME
        if manifest_path.exists():
            return manifest_path
        current = current.parent

    # Check root
    manifest_path = current / MANIFEST_FILENAME
    if manifest_path.exists():
        return manifest_path

    return None


def load_manifest(path: Optional[Path] = None) -> Optional[Manifest]:
    """
    Load a project manifest.

    Args:
        path: Path to manifest file, or None to search

    Returns:
        Manifest object, or None if not found
    """
    if path is None:
        path = find_manifest()

    if path is None or not path.exists():
        return None

    try:
        data = yaml.safe_load(path.read_text()) or {}
        return Manifest.from_dict(data)
    except Exception as e:
        print(f"Warning: Error loading manifest: {e}")
        return None


def save_manifest(manifest: Manifest, path: Optional[Path] = None) -> Path:
    """
    Save a manifest to disk.

    Args:
        manifest: Manifest to save
        path: Path to save to (default: ./smarttools.yaml)

    Returns:
        Path where manifest was saved
    """
    if path is None:
        path = Path.cwd() / MANIFEST_FILENAME

    path.write_text(yaml.dump(manifest.to_dict(), default_flow_style=False, sort_keys=False))
    return path


def create_manifest(
    name: str = "my-project",
    version: str = "1.0.0",
    path: Optional[Path] = None
) -> Manifest:
    """
    Create a new manifest.

    Args:
        name: Project name
        version: Project version
        path: Path to save to (optional)

    Returns:
        Created Manifest object
    """
    manifest = Manifest(name=name, version=version)

    if path is not None:
        save_manifest(manifest, path)

    return manifest


def parse_version_constraint(constraint: str) -> dict:
    """
    Parse a version constraint string.

    Args:
        constraint: Version constraint (e.g., ">=1.0.0", "^1.2.3", "~1.2.0")

    Returns:
        Dict with operator and version parts
    """
    constraint = constraint.strip()

    # Exact version
    if re.match(r'^\d+\.\d+\.\d+', constraint):
        return {"operator": "=", "version": constraint}

    # Any version
    if constraint == "*" or constraint == "latest":
        return {"operator": "*", "version": None}

    # Range operators
    patterns = [
        (r'^>=(.+)$', ">="),
        (r'^<=(.+)$', "<="),
        (r'^>(.+)$', ">"),
        (r'^<(.+)$', "<"),
        (r'^\^(.+)$', "^"),  # Compatible (same major)
        (r'^~(.+)$', "~"),   # Approximately (same minor)
    ]

    for pattern, operator in patterns:
        match = re.match(pattern, constraint)
        if match:
            return {"operator": operator, "version": match.group(1)}

    # Default to exact match
    return {"operator": "=", "version": constraint}
