"""Agent configuration loader — parses ``agent.yaml`` manifests."""

from __future__ import annotations

import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

import yaml

# ---------------------------------------------------------------------------
# Data classes
# ---------------------------------------------------------------------------


@dataclass
class SkillsConfig:
    """Which skills the agent can use."""

    builtin: list[str] = field(default_factory=list)
    dirs: list[str] = field(default_factory=list)
    disabled: list[str] = field(default_factory=list)


@dataclass
class GoalConfig:
    """A single autonomous goal definition."""

    name: str
    sensor: str
    action: str = ""
    interval: int = 300
    autonomy: str = "auto"
    skills: list[str] = field(default_factory=list)
    always_act: bool = False


@dataclass
class HubConfig:
    """Optional hub connection for multi-agent monitoring.

    ``url`` can be:
    - ``""`` — hub disabled
    - ``"auto"`` — discover hub via LAN multicast
    - ``"http://host:port"`` — explicit hub address
    """

    url: str = ""
    interval: int = 30
    discovery: bool = True  # announce this agent on the LAN
    accept_tasks: bool = True  # accept delegated tasks from other agents
    task_ttl: int = 300  # default TTL (seconds) for submitted tasks


@dataclass
class MCPServerConfig:
    """An MCP server configuration."""

    command: str
    args: list[str] = field(default_factory=list)
    env: dict[str, str] = field(default_factory=dict)
    description: str = ""
    enabled: bool = True


@dataclass
class AgentConfig:
    """Complete agent configuration parsed from ``agent.yaml``."""

    name: str
    description: str = ""

    # LLM
    provider: str = "azure"
    model: str = ""

    # Prompt
    prompt: str = ""

    # Skills
    skills: SkillsConfig = field(default_factory=SkillsConfig)

    # Goals (autonomous mode)
    goals: list[GoalConfig] = field(default_factory=list)

    # Hub (multi-agent monitoring)
    hub: HubConfig = field(default_factory=HubConfig)

    # MCP servers
    mcp_servers: dict[str, MCPServerConfig] = field(default_factory=dict)

    # Environment
    env_required: list[str] = field(default_factory=list)
    env_file: str = ".env"

    # Runtime
    max_turns: int = 10
    context_window: int = 120_000
    data_dir: str = ".chatixia"

    # Internal: directory containing agent.yaml (used for path resolution)
    _source_dir: Path = field(default_factory=Path.cwd)

    # ------------------------------------------------------------------
    # Helpers
    # ------------------------------------------------------------------

    def resolve_path(self, path: str) -> Path:
        """Resolve a path relative to the manifest directory."""
        p = Path(path).expanduser()
        if p.is_absolute():
            return p
        return (self._source_dir / p).resolve()

    def validate(self) -> list[str]:
        """Return a list of validation error messages (empty = valid)."""
        errors: list[str] = []
        if not self.name:
            errors.append("'name' is required")
        if self.provider not in ("azure", "openai", "ollama"):
            errors.append(f"Unknown provider: {self.provider!r} (expected azure|openai|ollama)")
        for goal in self.goals:
            if not goal.name:
                errors.append("Goal is missing 'name'")
            if not goal.sensor:
                errors.append(f"Goal '{goal.name}' is missing 'sensor'")
            if goal.autonomy not in ("auto", "notify", "approve"):
                errors.append(f"Goal '{goal.name}': invalid autonomy '{goal.autonomy}'")
        return errors

    def check_env(self) -> list[str]:
        """Return the names of required env vars that are currently unset."""
        all_required = list(self.env_required)
        # Infer provider-specific requirements
        if self.provider == "azure":
            for key in ("AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_API_KEY", "AZURE_OPENAI_DEPLOYMENT"):
                if key not in all_required:
                    all_required.append(key)
        elif self.provider == "openai":
            if "OPENAI_API_KEY" not in all_required:
                all_required.append("OPENAI_API_KEY")
        return [key for key in all_required if not os.getenv(key)]


# ---------------------------------------------------------------------------
# Loader
# ---------------------------------------------------------------------------


def load_config(path: str | Path) -> AgentConfig:
    """Load an ``AgentConfig`` from a YAML file or directory.

    If *path* is a directory, looks for ``agent.yaml`` (or ``.yml``) inside it.
    """
    path = Path(path)
    if path.is_dir():
        for candidate in ("agent.yaml", "agent.yml"):
            yaml_path = path / candidate
            if yaml_path.exists():
                break
        else:
            raise FileNotFoundError(f"No agent.yaml found in {path}")
        source_dir = path.resolve()
        path = yaml_path
    else:
        if not path.exists():
            raise FileNotFoundError(f"Manifest not found: {path}")
        source_dir = path.resolve().parent

    with open(path, encoding="utf-8") as fh:
        raw: dict[str, Any] = yaml.safe_load(fh) or {}

    return _parse_config(raw, source_dir)


def _parse_config(raw: dict[str, Any], source_dir: Path) -> AgentConfig:
    """Parse raw YAML dict into an ``AgentConfig``."""

    # -- Skills ---
    skills_raw = raw.get("skills", {})
    if isinstance(skills_raw, list):
        # Shorthand: skills: [calculator, echo]
        skills = SkillsConfig(builtin=skills_raw)
    elif isinstance(skills_raw, dict):
        skills = SkillsConfig(
            builtin=skills_raw.get("builtin", []),
            dirs=skills_raw.get("dirs", []),
            disabled=skills_raw.get("disabled", []),
        )
    else:
        skills = SkillsConfig()

    # -- Goals ---
    goals: list[GoalConfig] = []
    for g in raw.get("goals", []):
        if not isinstance(g, dict):
            continue
        goals.append(
            GoalConfig(
                name=g.get("name", ""),
                sensor=g.get("sensor", ""),
                action=g.get("action", ""),
                interval=int(g.get("interval", 300)),
                autonomy=g.get("autonomy", "auto"),
                skills=g.get("skills", []),
                always_act=bool(g.get("always_act", False)),
            )
        )

    # -- MCP servers ---
    mcp_servers: dict[str, MCPServerConfig] = {}
    for name, cfg in raw.get("mcp_servers", {}).items():
        if not isinstance(cfg, dict):
            continue
        mcp_servers[name] = MCPServerConfig(
            command=cfg.get("command", ""),
            args=cfg.get("args", []),
            env=cfg.get("env", {}),
            description=cfg.get("description", ""),
            enabled=cfg.get("enabled", True),
        )

    # -- Hub ---
    hub_raw = raw.get("hub", {})
    if isinstance(hub_raw, str):
        # Shorthand: hub: auto  or  hub: http://...
        hub = HubConfig(url=hub_raw)
    elif isinstance(hub_raw, dict):
        hub = HubConfig(
            url=hub_raw.get("url", ""),
            interval=int(hub_raw.get("interval", 30)),
            discovery=hub_raw.get("discovery", True),
            accept_tasks=hub_raw.get("accept_tasks", True),
            task_ttl=int(hub_raw.get("task_ttl", 300)),
        )
    else:
        hub = HubConfig()

    # -- Environment ---
    env_raw = raw.get("env", {})
    if isinstance(env_raw, dict):
        env_required = env_raw.get("required", [])
        env_file = env_raw.get("file", ".env")
    else:
        env_required = []
        env_file = ".env"

    return AgentConfig(
        name=raw.get("name", "unnamed-agent"),
        description=raw.get("description", ""),
        provider=raw.get("provider", "azure"),
        model=raw.get("model", ""),
        prompt=raw.get("prompt", ""),
        skills=skills,
        goals=goals,
        hub=hub,
        mcp_servers=mcp_servers,
        env_required=env_required,
        env_file=env_file,
        max_turns=int(raw.get("max_turns", 10)),
        context_window=int(raw.get("context_window", 120_000)),
        data_dir=raw.get("data_dir", ".chatixia"),
        _source_dir=source_dir,
    )
