import importlib.metadata
import warnings
from abc import ABC, abstractmethod
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Literal

from agentmux.core.component import (
    Agent,
    Command,
    Component,
    ComponentType,
    Instructions,
    MCPConfig,
    Rule,
    Skill,
)


@dataclass
class ConvertedFile:
    path: Path
    content: str | None = None
    source: Path | None = None
    is_directory: bool = False
    merge: bool = False
    scope: Literal["global", "project"] = "global"


class ToolAdapter(ABC):
    @property
    @abstractmethod
    def name(self) -> str: ...

    @abstractmethod
    def detect(self) -> bool: ...

    @abstractmethod
    def get_global_base_dir(self) -> Path: ...

    @abstractmethod
    def get_project_subdir(self) -> Path: ...

    @abstractmethod
    def convert_agent(self, agent: Agent) -> list[ConvertedFile]: ...

    @abstractmethod
    def convert_command(self, command: Command) -> list[ConvertedFile]: ...

    @abstractmethod
    def convert_skill(self, skill: Skill) -> list[ConvertedFile]: ...

    @abstractmethod
    def convert_rule(self, rule: Rule) -> list[ConvertedFile]: ...

    @abstractmethod
    def convert_instructions(self, instructions: Instructions) -> list[ConvertedFile]: ...

    @abstractmethod
    def convert_mcp(self, mcp: MCPConfig) -> list[ConvertedFile]: ...

    @abstractmethod
    def validate(self, component: Component) -> list[str]:
        """Return a list of validation error strings, empty if the component is valid."""
        ...

    def post_sync(
        self,
        base_dir: Path,
        synced_types: set[ComponentType],
    ) -> list[ConvertedFile]:
        """Called after all components are synced. Returns additional files to write."""
        return []

    def convert(self, component: Component, comp_type: ComponentType) -> list[ConvertedFile]:
        """Dispatch to the appropriate convert method based on component type."""
        dispatch: dict[ComponentType, Any] = {
            ComponentType.AGENT: self.convert_agent,
            ComponentType.COMMAND: self.convert_command,
            ComponentType.SKILL: self.convert_skill,
            ComponentType.RULE: self.convert_rule,
            ComponentType.INSTRUCTIONS: self.convert_instructions,
            ComponentType.MCP: self.convert_mcp,
        }
        handler = dispatch.get(comp_type)
        if handler is None:
            return []
        return handler(component)  # type: ignore[no-any-return]


class AdapterRegistry:
    _adapters: tuple[ToolAdapter, ...] | None = None

    @staticmethod
    def get_adapters() -> tuple[ToolAdapter, ...]:
        """Return all registered adapters, creating them on first call.

        Adapters are discovered via Python entry points in the ``agentmux.adapters`` group.
        Both built-in adapters (registered in agentmux's own pyproject.toml) and third-party
        adapters (installed separately) are discovered this way. First-registered name wins,
        so built-in adapters take precedence over third-party ones with the same name.

        Instances are cached in a class variable so they can be cleared
        in tests via ``AdapterRegistry.reset()``.
        """
        if AdapterRegistry._adapters is None:
            adapters: list[ToolAdapter] = []
            seen_names: set[str] = set()

            for ep in importlib.metadata.entry_points(group="agentmux.adapters"):
                try:
                    adapter_cls = ep.load()
                except Exception as exc:
                    warnings.warn(
                        f"Failed to load adapter entry point '{ep.name}': {exc}",
                        stacklevel=2,
                    )
                    continue

                if not (isinstance(adapter_cls, type) and issubclass(adapter_cls, ToolAdapter)):
                    warnings.warn(
                        f"Entry point '{ep.name}' did not return a ToolAdapter subclass, skipping.",
                        stacklevel=2,
                    )
                    continue

                try:
                    instance = adapter_cls()
                except Exception as exc:
                    warnings.warn(
                        f"Failed to instantiate adapter '{ep.name}': {exc}",
                        stacklevel=2,
                    )
                    continue

                if instance.name in seen_names:
                    continue

                seen_names.add(instance.name)
                adapters.append(instance)

            AdapterRegistry._adapters = tuple(adapters)
        return AdapterRegistry._adapters

    @staticmethod
    def get_adapter(name: str) -> ToolAdapter | None:
        for adapter in AdapterRegistry.get_adapters():
            if adapter.name == name:
                return adapter
        return None

    @staticmethod
    def reset() -> None:
        """Clear the cached adapter instances. Intended for test teardown."""
        AdapterRegistry._adapters = None
