import json
from enum import Enum
from pathlib import Path
from typing import Any

from pydantic import BaseModel, Field

from agentmux.utils.frontmatter import dump as dump_frontmatter
from agentmux.utils.frontmatter import parse as parse_frontmatter

__all__ = [
    "ComponentType",
    "Agent",
    "Command",
    "Skill",
    "Rule",
    "Instructions",
    "MCPConfig",
    "Component",
]


class ComponentType(str, Enum):
    AGENT = "agent"
    COMMAND = "command"
    SKILL = "skill"
    RULE = "rule"
    INSTRUCTIONS = "instructions"
    MCP = "mcp"


class Agent(BaseModel):
    name: str
    description: str
    mode: str = "all"
    body: str
    source_path: Path | None = None
    extra_meta: dict[str, Any] = Field(default_factory=dict)

    @classmethod
    def from_file(cls, path: Path) -> "Agent":
        meta, body = parse_frontmatter(path.read_text())
        known_keys = {"name", "description", "mode"}
        extra_meta = {k: v for k, v in meta.items() if k not in known_keys}
        return cls(
            name=meta.get("name") or path.stem,
            description=meta.get("description", ""),
            mode=meta.get("mode", "all"),
            body=body.strip(),
            source_path=path,
            extra_meta=extra_meta,
        )

    def to_markdown(self) -> str:
        meta: dict[str, Any] = {"name": self.name, "description": self.description}
        if self.mode != "all":
            meta["mode"] = self.mode
        # Preserve any extra metadata so round-trips are lossless.
        meta.update(self.extra_meta)
        return dump_frontmatter(meta, self.body)


class Command(BaseModel):
    name: str
    description: str = ""
    body: str
    source_path: Path | None = None
    extra_meta: dict[str, Any] = Field(default_factory=dict)

    @classmethod
    def from_file(cls, path: Path) -> "Command":
        meta, body = parse_frontmatter(path.read_text())
        known_keys = {"name", "description"}
        extra_meta = {k: v for k, v in meta.items() if k not in known_keys}
        return cls(
            name=meta.get("name") or path.stem,
            description=meta.get("description", ""),
            body=body.strip(),
            source_path=path,
            extra_meta=extra_meta,
        )

    def to_markdown(self) -> str:
        meta: dict[str, Any] = {"name": self.name}
        if self.description:
            meta["description"] = self.description
        # Preserve any extra metadata so round-trips are lossless.
        meta.update(self.extra_meta)
        return dump_frontmatter(meta, self.body)


class Skill(BaseModel):
    name: str
    description: str
    body: str
    source_dir: Path | None = None
    extra_meta: dict[str, Any] = Field(default_factory=dict)

    @classmethod
    def from_dir(cls, path: Path) -> "Skill":
        skill_md = path / "SKILL.md"
        if not skill_md.exists():
            raise FileNotFoundError(
                f"SKILL.md not found in skill directory '{path}'. "
                f"Each skill directory must contain a SKILL.md file."
            )
        meta, body = parse_frontmatter(skill_md.read_text())
        known_keys = {"name", "description"}
        extra_meta = {k: v for k, v in meta.items() if k not in known_keys}
        return cls(
            name=meta.get("name") or path.name,
            description=meta.get("description", ""),
            body=body.strip(),
            source_dir=path,
            extra_meta=extra_meta,
        )


class Rule(BaseModel):
    name: str
    description: str = ""
    globs: str | None = None
    always_apply: bool = False
    body: str
    source_path: Path | None = None

    @classmethod
    def from_file(cls, path: Path) -> "Rule":
        meta, body = parse_frontmatter(path.read_text())
        always_apply = bool(meta.get("alwaysApply", meta.get("always_apply", False)))
        return cls(
            name=meta.get("name") or path.stem,
            description=meta.get("description", ""),
            globs=meta.get("globs"),
            always_apply=always_apply,
            body=body.strip(),
            source_path=path,
        )

    def to_mdc(self) -> str:
        meta: dict[str, Any] = {"alwaysApply": self.always_apply}
        if self.description:
            meta["description"] = self.description
        if self.globs:
            meta["globs"] = self.globs
        return dump_frontmatter(meta, self.body)


class Instructions(BaseModel):
    body: str
    source_path: Path | None = None

    @classmethod
    def from_file(cls, path: Path) -> "Instructions":
        _, body = parse_frontmatter(path.read_text())
        return cls(body=body.strip(), source_path=path)


class MCPConfig(BaseModel):
    servers: dict[str, dict[str, Any]]
    source_path: Path | None = None

    @classmethod
    def from_file(cls, path: Path) -> "MCPConfig":
        data = json.loads(path.read_text())
        if not isinstance(data, dict):
            raise ValueError(
                f"MCP config '{path}' must be a JSON object, got {type(data).__name__}"
            )
        if "mcpServers" in data:
            servers = data["mcpServers"]
        else:
            # Bare format: every value must be a dict that looks like a server
            # config (has at least one recognised MCP server key).  This prevents
            # silently treating arbitrary nested objects (e.g. {"metadata": {...}})
            # as server entries.
            _SERVER_KEYS = {"command", "args", "url", "type", "env"}
            invalid_keys = [
                k
                for k, v in data.items()
                if not isinstance(v, dict) or not (_SERVER_KEYS & v.keys())
            ]
            if invalid_keys:
                raise ValueError(
                    f"MCP config '{path}' has no 'mcpServers' key and contains entries that do "
                    f"not look like server configs (missing keys like 'command', 'url', etc.): "
                    f"{invalid_keys}. Expected either "
                    f'{{ "mcpServers": {{ ... }} }} or a bare {{ "server_name": {{ "command": "..." }} }} format.'
                )
            servers = data
        return cls(servers=servers, source_path=path)

    def to_json(self) -> str:
        return json.dumps({"mcpServers": self.servers}, indent=2)


Component = Agent | Command | Skill | Rule | Instructions | MCPConfig
