"""Base agent class."""

from __future__ import annotations

from abc import abstractmethod
from functools import cached_property
from pathlib import Path
from typing import TYPE_CHECKING, Any, ClassVar

from ai_rules.targets.base import ConfigTarget

if TYPE_CHECKING:
    from ai_rules.mcp import MCPManager, MCPStatus, OperationResult
    from ai_rules.skills import SkillStatus


class Agent(ConfigTarget):
    """Base class for AI agent configuration managers."""

    # Declarative instruction-file symlink. Agents with a runtime-resolved
    # location (e.g. Goose) override _instruction_symlinks() instead.
    instructions_target: ClassVar[str | None] = None
    instructions_source: ClassVar[str | None] = None
    skills_dir: ClassVar[Path | None] = None

    @property
    def target_id(self) -> str:
        return self.agent_id

    @property
    def settings_symlink_target(self) -> Path | None:
        return None

    @property
    @abstractmethod
    def agent_id(self) -> str:
        """Short identifier for the agent (e.g., 'claude', 'goose')."""
        pass

    def _instruction_symlinks(self) -> list[tuple[Path, Path]]:
        """Symlinks for the agent's instruction file (CLAUDE.md, AGENTS.md, ...)."""
        if self.instructions_target is None or self.instructions_source is None:
            return []
        return [
            (
                Path(self.instructions_target),
                self.config_dir / self.target_id / self.instructions_source,
            )
        ]

    def _settings_symlinks(self) -> list[tuple[Path, Path]]:
        """Symlink for the agent's settings file, if the bundled file exists."""
        config_file = self._base_settings_path
        target = self.settings_symlink_target
        if target is None or not config_file.exists():
            return []
        target_file = self.config.get_settings_file_for_symlink(
            self.target_id, config_file, force=bool(self._effective_preserved_fields)
        )
        return [(target, target_file)]

    def _extra_symlinks(self) -> list[tuple[Path, Path]]:
        """Hook for additional agent-specific symlinks (e.g. Claude extensions)."""
        return []

    @cached_property
    def symlinks(self) -> list[tuple[Path, Path]]:
        """Cached list of all agent symlinks (instructions, settings, extras)."""
        return (
            self._instruction_symlinks()
            + self._settings_symlinks()
            + self._extra_symlinks()
        )

    def get_mcp_manager(self) -> MCPManager | None:
        """Return the agent-specific MCPManager, or None if MCP is unsupported."""
        return None

    @property
    def _effective_preserved_fields(self) -> list[str]:
        fields = list(self.preserved_fields)
        mgr = self.get_mcp_manager()
        if mgr is not None and mgr.mcp_settings_key is not None:
            if mgr.mcp_settings_key not in fields:
                fields.append(mgr.mcp_settings_key)
            if mgr.mcp_tracking_key and mgr.mcp_tracking_key not in fields:
                fields.append(mgr.mcp_tracking_key)
        return fields

    def _merge_managed_mcps(self, merged: dict[str, Any]) -> None:
        """Merge managed MCPs into the settings cache.

        Reconciles managed entries (add/update/remove) while preserving
        unmanaged entries that the user added directly.
        """
        from ai_rules.mcp import is_managed_value

        mgr = self.get_mcp_manager()
        if mgr is None or mgr.mcp_settings_key is None:
            return

        mcp_key = mgr.mcp_settings_key
        native_mcps = mgr.get_native_mcps(self.config_dir, self.config)

        current = merged.get(mcp_key, {})
        if not isinstance(current, dict):
            current = {}

        if mgr.mcp_tracking_key:
            tracking = merged.get(mgr.mcp_tracking_key, {})
            tracked = set(tracking.get("names", []))
        else:
            tracked = {
                n
                for n, c in current.items()
                if isinstance(c, dict) and is_managed_value(c.get(mgr._marker_field))
            }
        for name in tracked - set(native_mcps.keys()):
            current.pop(name, None)

        current.update(native_mcps)

        if current:
            merged[mcp_key] = current
        else:
            merged.pop(mcp_key, None)

        if mgr.mcp_tracking_key:
            if native_mcps:
                merged[mgr.mcp_tracking_key] = {"names": sorted(native_mcps.keys())}
            else:
                merged.pop(mgr.mcp_tracking_key, None)

    def install_mcps(
        self, force: bool = False, dry_run: bool = False
    ) -> tuple[OperationResult, str, list[str]]:
        """Install MCPs by delegating to the agent's MCPManager."""
        mgr = self.get_mcp_manager()
        if mgr is None:
            raise RuntimeError(f"{type(self).__name__} does not support MCP management")
        return mgr.install_mcps(self.config_dir, self.config, force, dry_run)

    def uninstall_mcps(self) -> tuple[OperationResult, str]:
        """Uninstall MCPs by delegating to the agent's MCPManager."""
        mgr = self.get_mcp_manager()
        if mgr is None:
            raise RuntimeError(f"{type(self).__name__} does not support MCP management")
        return mgr.uninstall_mcps()

    def get_mcp_status(self) -> MCPStatus | None:
        """Return MCP status, or None if MCP is unsupported for this agent."""
        mgr = self.get_mcp_manager()
        if mgr is None:
            return None
        return mgr.get_status(self.config_dir, self.config)

    def get_skill_status(self) -> SkillStatus | None:
        """Get status of agent skills.

        Returns:
            SkillStatus object, or None if agent doesn't support skills
        """
        return None
