import json
import logging
from pathlib import Path
from typing import Any

from agentmux.adapters.base import ConvertedFile, ToolAdapter
from agentmux.core.component import Agent, Command, Component, Instructions, MCPConfig, Rule, Skill
from agentmux.utils.frontmatter import dump as dump_frontmatter
from agentmux.utils.symlink import sanitize_component_name

__all__ = ["OpenCodeAdapter"]

_logger = logging.getLogger(__name__)

# Canonical agentmux mode -> OpenCode mode mapping.
# OpenCode recognises "primary", "subagent", and "all" (default when omitted).
_MODE_MAP: dict[str, str] = {
    "primary": "primary",
    "subagent": "subagent",
    "all": "all",
}


class OpenCodeAdapter(ToolAdapter):
    """Adapter for the OpenCode AI coding agent.

    Configuration reference (from https://opencode.ai/docs):
      Agents:   ~/.config/opencode/agents/*.md  |  .opencode/agents/*.md
      Commands: ~/.config/opencode/commands/*.md  |  .opencode/commands/*.md
      Skills:   ~/.config/opencode/skills/*/SKILL.md  |  .opencode/skills/*/SKILL.md
      Rules:    Not supported as standalone files (AGENTS.md is the instructions file)
      Instructions: ~/.config/opencode/AGENTS.md  (global)
      MCP:      opencode.jsonc under "mcp" key (global config file)
    """

    @property
    def name(self) -> str:
        return "opencode"

    def detect(self) -> bool:
        return Path.home().joinpath(".config", "opencode").exists()

    def get_global_base_dir(self) -> Path:
        return Path.home() / ".config" / "opencode"

    def get_project_subdir(self) -> Path:
        return Path(".opencode")

    def validate(self, component: Component) -> list[str]:
        if isinstance(component, Rule):
            return [
                "OpenCode does not support standalone rule files; use Instructions (AGENTS.md) instead"
            ]
        return []

    def convert_agent(self, agent: Agent) -> list[ConvertedFile]:
        """Convert to OpenCode markdown agent format.

        OpenCode agent frontmatter supports: description, mode, model, tools (dict of booleans).
        The `mode` field uses "primary" / "subagent" / "all" — map from agentmux's mode field.
        """
        opencode_mode = _MODE_MAP.get(agent.mode)
        if opencode_mode is None:
            _logger.warning(
                "OpenCode adapter: unknown agent mode %r for agent %r; defaulting to 'all'.",
                agent.mode,
                agent.name,
            )
            opencode_mode = "all"

        meta: dict[str, Any] = {
            "description": agent.description,
            "mode": opencode_mode,
        }
        # Preserve any extra metadata (e.g. model, tools, temperature) from the source.
        meta.update(agent.extra_meta)

        content = dump_frontmatter(meta, agent.body) + "\n"
        safe_name = sanitize_component_name(agent.name)
        return [
            ConvertedFile(
                path=Path("agents") / f"{safe_name}.md",
                content=content,
                source=None,  # always transformed — mode mapping must be applied
                scope="global",
            )
        ]

    def convert_command(self, command: Command) -> list[ConvertedFile]:
        """Convert to OpenCode markdown command format.

        OpenCode command frontmatter: description, agent, model.
        Body is the prompt template.
        """
        meta: dict[str, Any] = {}
        if command.description:
            meta["description"] = command.description
        # Preserve any extra metadata (e.g. agent, model) from the source.
        meta.update(command.extra_meta)

        filename = command.source_path.name if command.source_path else f"{command.name}.md"
        content = dump_frontmatter(meta, command.body) + "\n" if meta else command.body + "\n"

        return [
            ConvertedFile(
                path=Path("commands") / filename,
                content=content,
                source=None,  # always transformed — frontmatter may be reformatted
                scope="global",
            )
        ]

    def convert_skill(self, skill: Skill) -> list[ConvertedFile]:
        """Symlink the skill directory under skills/.

        OpenCode skill discovery: ~/.config/opencode/skills/<name>/SKILL.md
        Same structure as Cursor and Gemini adapters.
        """
        dir_name = skill.source_dir.name if skill.source_dir else skill.name
        return [
            ConvertedFile(
                path=Path("skills") / dir_name,
                content=None,
                source=skill.source_dir,
                is_directory=True,
                scope="global",
            )
        ]

    def convert_rule(self, rule: Rule) -> list[ConvertedFile]:
        """OpenCode does not have standalone rule files — return empty list.

        Use validate() to surface this as a warning. Rules should be expressed
        as part of the Instructions (AGENTS.md) file.
        """
        return []

    def convert_instructions(self, instructions: Instructions) -> list[ConvertedFile]:
        """Convert to AGENTS.md at the global OpenCode config directory.

        OpenCode reads global rules from ~/.config/opencode/AGENTS.md.
        Project-level AGENTS.md lives at the project root (Path("../AGENTS.md")
        relative to the .opencode/ base_dir), but defaulting to global scope is
        safer since project-level AGENTS.md may conflict with existing files.
        """
        return [
            ConvertedFile(
                path=Path("AGENTS.md"),
                content=instructions.body + "\n",
                source=None,  # always written — no frontmatter passthrough
                scope="global",
            )
        ]

    def convert_mcp(self, mcp: MCPConfig) -> list[ConvertedFile]:
        """Merge MCP servers into opencode.jsonc under the "mcp" key.

        OpenCode MCP format (opencode.jsonc):
          {
            "mcp": {
              "<server-name>": {
                "type": "local",
                "command": [...],
                ...
              }
            }
          }

        The servers dict maps directly to the "mcp" key — no "mcpServers" wrapper.
        We set merge=True so the sync engine deep-merges into the existing config file.
        """
        payload = json.dumps({"mcp": mcp.servers})
        return [
            ConvertedFile(
                path=Path("opencode.jsonc"),
                content=payload,
                merge=True,
                scope="global",
            )
        ]
