import json
import re
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__ = ["ClaudeCodeAdapter"]

# Claude Code requires agent names to use lowercase letters and hyphens only.
# This pattern matches any character that is not a lowercase letter or hyphen.
_AGENT_NAME_INVALID_CHARS = re.compile(r"[^a-z0-9-]")


def _to_claude_agent_name(name: str) -> str:
    """Transform an agent name to the Claude Code required format.

    Claude Code requires agent names to use lowercase letters and hyphens only.
    Transforms the input by lowercasing, replacing spaces and underscores with
    hyphens, replacing any remaining invalid characters with hyphens, and
    collapsing consecutive hyphens.

    Examples::

        "Code Reviewer"   -> "code-reviewer"
        "My_Agent"        -> "my-agent"
        "helper"          -> "helper"
        "  My Agent  "    -> "my-agent"
    """
    # Lowercase first, then replace spaces/underscores explicitly to hyphens
    transformed = name.strip().lower().replace(" ", "-").replace("_", "-")
    # Replace any remaining invalid characters with hyphens
    transformed = _AGENT_NAME_INVALID_CHARS.sub("-", transformed)
    # Collapse consecutive hyphens and strip leading/trailing hyphens
    transformed = re.sub(r"-{2,}", "-", transformed).strip("-")
    return transformed or "agent"


class ClaudeCodeAdapter(ToolAdapter):
    """Adapter for Anthropic's Claude Code AI coding agent.

    Configuration reference (from https://docs.anthropic.com/en/docs/claude-code):
      Agents:       ~/.claude/agents/*.md      |  .claude/agents/*.md
      Commands:     ~/.claude/commands/*.md    |  .claude/commands/*.md
      Skills:       ~/.claude/skills/*/SKILL.md | .claude/skills/*/SKILL.md
      Rules:        ~/.claude/rules/*.md       |  .claude/rules/*.md
      Instructions: ~/.claude/CLAUDE.md        |  .claude/CLAUDE.md
      MCP:          .mcp.json (project root)   —  { "mcpServers": { ... } }
    """

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

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

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

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

    def validate(self, component: Component) -> list[str]:
        """Claude Code supports all component types — return no errors."""
        return []

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

        Claude Code agent frontmatter fields (from official docs):
          - name (required): lowercase letters and hyphens only
          - description (required): Claude reads this to decide when to delegate
          - model (optional): 'sonnet', 'opus', 'haiku', or 'inherit' (default)
          - tools (optional): comma-separated list of tool names
          - disallowedTools (optional): tools to remove from the inherited list
          - permissionMode (optional): 'default', 'acceptEdits', 'dontAsk', etc.
          - maxTurns (optional): integer max agentic turns

        The `name` field must use lowercase letters and hyphens only per Claude Code docs.
        We transform the agent's canonical name to this format automatically.
        We set model='inherit' as the default so the agent uses the active model.
        """
        safe_name = sanitize_component_name(agent.name)
        claude_name = _to_claude_agent_name(agent.name)

        meta: dict[str, Any] = {
            "name": claude_name,
            "description": agent.description,
        }

        # Apply model default of 'inherit' if not specified in extra_meta.
        # This ensures the agent inherits the active model rather than having
        # no model constraint.
        if "model" not in agent.extra_meta:
            meta["model"] = "inherit"

        # Preserve any extra metadata (model, tools, disallowedTools, etc.)
        # from the source. Extra meta takes precedence over the model default.
        meta.update(agent.extra_meta)

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

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

        Claude Code command frontmatter fields (from official docs):
          - description (optional): shown in the / menu and used for auto-invocation
          - argument-hint (optional): shown during autocomplete, e.g. '[issue-number]'
          - allowed-tools (optional): comma-separated tool names

        The body is the prompt content — no special prompt key required.
        """
        meta: dict[str, Any] = {}
        if command.description:
            meta["description"] = command.description
        # Preserve extra metadata (argument-hint, allowed-tools, model, etc.)
        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/.

        Claude Code skill discovery: ~/.claude/skills/<name>/SKILL.md
        Same structure as Cursor, Gemini, and OpenCode 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]:
        """Convert to Claude Code rule format with optional paths frontmatter.

        Claude Code rule files live in .claude/rules/*.md (project scope).
        The only supported frontmatter field is 'paths' — a list of glob patterns
        that restrict which files this rule applies to. Rules without 'paths'
        apply unconditionally.

        Mapping from agentmux's canonical Rule:
          - rule.globs  -> paths: [rule.globs]   (wrap single string in a list)
          - No 'alwaysApply' equivalent — omitting 'paths' is the unconditional form
        """
        meta: dict[str, Any] = {}
        if rule.globs:
            # Claude Code expects 'paths' as a list of glob patterns.
            # agentmux's canonical 'globs' is a single string — wrap it.
            meta["paths"] = [rule.globs]

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

    def convert_instructions(self, instructions: Instructions) -> list[ConvertedFile]:
        """Convert to CLAUDE.md inside the .claude project subdirectory.

        Claude Code supports both ./CLAUDE.md and ./.claude/CLAUDE.md.
        We write to .claude/CLAUDE.md (project scope) so it lives alongside
        other Claude Code project config and can be committed to version control.
        """
        return [
            ConvertedFile(
                path=Path("CLAUDE.md"),
                content=instructions.body + "\n",
                source=None,  # always written — no frontmatter passthrough
                scope="project",
            )
        ]

    def convert_mcp(self, mcp: MCPConfig) -> list[ConvertedFile]:
        """Write MCP config to .mcp.json at the project root.

        Claude Code MCP format uses the 'mcpServers' wrapper (from official docs):
          {
            "mcpServers": {
              "server-name": {
                "command": "...",
                "args": [...],
                "env": {}
              }
            }
          }

        The path ../.mcp.json is relative to the .claude/ base_dir, which resolves
        to the project root when the sync engine joins base_dir / path.
        We set merge=True so the sync engine deep-merges into any existing .mcp.json.
        """
        payload = json.dumps({"mcpServers": mcp.servers})
        return [
            ConvertedFile(
                path=Path("..") / ".mcp.json",
                content=payload,
                merge=True,
                scope="project",
            )
        ]
