from __future__ import annotations

from dataclasses import dataclass, field
from pathlib import Path
from typing import List, Optional, Dict, Any
import re
import yaml


@dataclass
class KnownAgent:
    id: str
    aliases: List[str]
    repo_url: str = ""
    compose_subdir: str = "."
    startup_services: List[str] = field(default_factory=list)
    project_name: Optional[str] = None
    playbook: Optional[str] = None


@dataclass
class PlaybookCommand:
    label: str
    command: str
    cwd: str = "."
    required: bool = True


@dataclass
class PlaybookRunCommand:
    label: str
    service: str
    command: str
    category: str = "run"
    description: str = ""


@dataclass
class AgentPlaybook:
    id: str
    repo_url: str
    compose_subdir: str = "."
    startup_services: List[str] = field(default_factory=list)
    startup_timeout_seconds: int = 120
    monitor_targets: List[str] = field(default_factory=list)
    monitoring_mode: str = "hybrid"
    update_channel: str = "stable"  # stable|edge
    allow_mutable_images: List[str] = field(default_factory=list)
    env_copies: List[Dict[str, str]] = field(default_factory=list)
    commands: List[PlaybookCommand] = field(default_factory=list)
    post_commands: List[PlaybookCommand] = field(default_factory=list)
    run_commands: List[PlaybookRunCommand] = field(default_factory=list)


_CATALOG_PATH = Path(__file__).parent / "known_agents.yaml"
_PLAYBOOKS_DIR = Path(__file__).parent / "playbooks"


def _norm(value: str) -> str:
    v = (value or "").strip().lower()
    v = re.sub(r"\.git$", "", v)
    return v.rstrip("/")


def _load() -> List[KnownAgent]:
    if not _CATALOG_PATH.exists():
        return []
    raw = yaml.safe_load(_CATALOG_PATH.read_text()) or {}
    items = raw.get("agents", [])
    out: List[KnownAgent] = []
    for item in items:
        if not isinstance(item, dict):
            continue
        out.append(
            KnownAgent(
                id=str(item.get("id", "")).strip(),
                aliases=[str(a) for a in item.get("aliases", []) if a],
                repo_url=str(item.get("repo_url", "")).strip(),
                compose_subdir=str(item.get("compose_subdir", ".")).strip() or ".",
                startup_services=[str(s) for s in item.get("startup_services", []) if s],
                project_name=(str(item.get("project_name")).strip() if item.get("project_name") else None),
                playbook=(str(item.get("playbook")).strip() if item.get("playbook") else None),
            )
        )
    return out


def _load_playbook(name: str) -> Optional[AgentPlaybook]:
    path = _PLAYBOOKS_DIR / f"{name}.yaml"
    if not path.exists():
        return None
    raw = yaml.safe_load(path.read_text()) or {}
    if not isinstance(raw, dict):
        return None
    setup = raw.get("setup", {}) if isinstance(raw.get("setup"), dict) else {}
    run = raw.get("run", {}) if isinstance(raw.get("run"), dict) else {}
    cmd_objs: List[PlaybookCommand] = []
    for c in setup.get("commands", []) if isinstance(setup.get("commands"), list) else []:
        if not isinstance(c, dict) or not c.get("command"):
            continue
        cmd_objs.append(
            PlaybookCommand(
                label=str(c.get("label", "setup")).strip(),
                command=str(c.get("command")).strip(),
                cwd=str(c.get("cwd", ".")).strip() or ".",
                required=bool(c.get("required", True)),
            )
        )
    post_cmd_objs: List[PlaybookCommand] = []
    for c in setup.get("post_commands", []) if isinstance(setup.get("post_commands"), list) else []:
        if not isinstance(c, dict) or not c.get("command"):
            continue
        post_cmd_objs.append(
            PlaybookCommand(
                label=str(c.get("label", "post-setup")).strip(),
                command=str(c.get("command")).strip(),
                cwd=str(c.get("cwd", ".")).strip() or ".",
                required=bool(c.get("required", True)),
            )
        )
    run_cmd_objs: List[PlaybookRunCommand] = []
    for c in run.get("commands", []) if isinstance(run.get("commands"), list) else []:
        if not isinstance(c, dict) or not c.get("command") or not c.get("service"):
            continue
        run_cmd_objs.append(
            PlaybookRunCommand(
                label=str(c.get("label", "run")).strip(),
                service=str(c.get("service")).strip(),
                command=str(c.get("command")).strip(),
                category=str(c.get("category", "run")).strip() or "run",
                description=str(c.get("description", "")).strip(),
            )
        )
    return AgentPlaybook(
        id=str(raw.get("id", name)).strip(),
        repo_url=str(raw.get("repo_url", "")).strip(),
        compose_subdir=str(raw.get("compose_subdir", ".")).strip() or ".",
        startup_services=[str(s) for s in run.get("startup_services", []) if s],
        startup_timeout_seconds=int(run.get("startup_timeout_seconds", 120) or 120),
        monitor_targets=[str(t) for t in raw.get("monitor_targets", []) if t],
        monitoring_mode=str(raw.get("monitoring_mode", "hybrid")).strip().lower() or "hybrid",
        update_channel=str(raw.get("update_channel", "stable")).strip().lower() or "stable",
        allow_mutable_images=[str(x) for x in raw.get("allow_mutable_images", []) if x],
        env_copies=[{"from": str(x.get("from", "")), "to": str(x.get("to", ""))} for x in (setup.get("env_copies", []) if isinstance(setup.get("env_copies"), list) else []) if isinstance(x, dict)],
        commands=cmd_objs,
        post_commands=post_cmd_objs,
        run_commands=run_cmd_objs,
    )


def all_known_agents() -> List[KnownAgent]:
    return _load()


def resolve_known_agent(name_or_url: str) -> Optional[KnownAgent]:
    needle = _norm(name_or_url)
    if not needle:
        return None
    for agent in _load():
        keys = {_norm(agent.id), _norm(agent.repo_url)}
        keys.update({_norm(a) for a in agent.aliases})
        if needle in keys:
            return agent
    return None


def resolve_playbook(name_or_url: str) -> Optional[AgentPlaybook]:
    spec = resolve_known_agent(name_or_url)
    if not spec:
        return None
    if spec.playbook:
        pb = _load_playbook(spec.playbook)
        if pb:
            return pb
    # Backward-compatible synthetic playbook from catalog fields.
    if spec.repo_url:
        return AgentPlaybook(
            id=spec.id,
            repo_url=spec.repo_url,
            compose_subdir=spec.compose_subdir,
            startup_services=spec.startup_services or [],
            startup_timeout_seconds=120,
            monitor_targets=[],
            monitoring_mode="hybrid",
            update_channel="stable",
        )
    return None
