"""Deployment configuration -- loaded from YAML (albus.yaml).

Models:
    DeploymentConfig, MCPServerSpec, ModelRoutingConfig, etc.
"""

from __future__ import annotations

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

import yaml
from pydantic import BaseModel, Field, computed_field, model_validator

from core.common.paths import config_search_paths
from core.types.agents import AgentDefinition
from server.config._helpers import substitute_env_vars

logger = logging.getLogger(__name__)


# =============================================================================
# ENUMS
# =============================================================================


class MCPTransport(str, Enum):
    """Supported MCP transport types."""

    STDIO = "stdio"
    SSE = "sse"


# =============================================================================
# SPEC MODELS
# =============================================================================


class MCPServerSpec(BaseModel):
    """MCP server connection specification."""

    id: str
    transport: MCPTransport | str = MCPTransport.STDIO
    command: str | None = None
    args: list[str] = Field(default_factory=list)
    env: dict[str, str] = Field(default_factory=dict)
    url: str | None = None
    headers: dict[str, str] = Field(default_factory=dict)
    timeout_s: int = 30
    retry_attempts: int = 3

    @model_validator(mode="after")
    def validate_transport_requirements(self) -> MCPServerSpec:
        """Validate transport-specific requirements."""
        transport = self.transport.value if isinstance(self.transport, MCPTransport) else self.transport
        if transport == "stdio" and not self.command:
            raise ValueError(f"MCP server '{self.id}': stdio transport requires 'command'")
        if transport == "sse" and not self.url:
            raise ValueError(f"MCP server '{self.id}': sse transport requires 'url'")
        return self


class ModelRoutingConfig(BaseModel):
    """Model routing configuration."""

    default_profile: str = "balanced"
    routing: dict[str, str] = Field(default_factory=dict)

    def get_model(self, capability: str) -> str | None:
        """Get model for capability with hierarchical fallback."""
        if capability in self.routing:
            return self.routing[capability]
        if "." in capability:
            parent = capability.rsplit(".", 1)[0]
            if parent in self.routing:
                return self.routing[parent]
        return None


class WebhookBinding(BaseModel):
    """Maps an external event source to an internal webhook topic."""

    source: str
    topic: str
    filter: dict[str, Any] = Field(default_factory=dict)
    transform: dict[str, str] = Field(default_factory=dict)


class SkillsConfig(BaseModel):
    """Skills discovery and loading configuration."""

    directories: list[str] = Field(default_factory=list)
    auto_activate: list[str] = Field(default_factory=list)


class PathwaysConfig(BaseModel):
    """Pathway discovery and loading configuration."""

    directories: list[str] = Field(default_factory=list)
    auto_deploy: list[str] = Field(default_factory=list)


# =============================================================================
# DEPLOYMENT CONFIG
# =============================================================================


class DeploymentConfig(BaseModel):
    """Deployment configuration from YAML."""

    agent_specs: list[AgentDefinition] = Field(default_factory=list)
    pathways_config: PathwaysConfig = Field(default_factory=PathwaysConfig)
    schema_version: int = 1
    mcp_servers: list[MCPServerSpec] = Field(default_factory=list)
    bindings: list[WebhookBinding] = Field(default_factory=list)
    models: ModelRoutingConfig = Field(default_factory=ModelRoutingConfig)
    skills: SkillsConfig = Field(default_factory=SkillsConfig)
    source_file: str | None = Field(default=None, exclude=True)

    agents: list[str] = Field(default_factory=list)
    agent_directories: list[str] = Field(default_factory=list)

    @computed_field
    @property
    def source(self) -> str:
        return self.source_file or "none"

    @classmethod
    def from_yaml(cls, path: str | Path) -> DeploymentConfig:
        """Load configuration from YAML file."""
        path = Path(path)
        if not path.exists():
            raise FileNotFoundError(f"Config file not found: {path}")

        with open(path) as f:
            raw = yaml.safe_load(f) or {}

        data = substitute_env_vars(raw)
        return cls.parse_yaml_data(data, source_file=str(path))

    @classmethod
    def parse_yaml_data(cls, data: dict[str, Any], source_file: str | None = None) -> DeploymentConfig:
        """Parse YAML data into DeploymentConfig."""
        schema_version = int(data.get("schema_version", 1) or 1)

        agent_specs, agent_ids = cls._parse_agents(data.get("agents", []))
        agent_directories: list[str] = data.get("agent_directories", []) or []

        mcp_servers = [
            MCPServerSpec.model_validate(spec)
            for spec in (data.get("mcp_servers") or [])
            if isinstance(spec, dict)
        ]

        models_data = data.get("models", {}) or {}
        models = ModelRoutingConfig(
            default_profile=str(models_data.get("default_profile", "balanced")).strip(),
            routing=dict(models_data.get("routing", {})),
        )

        bindings = cls._parse_bindings(data.get("bindings", {}))

        skills_data = data.get("skills", {}) or {}
        skills = SkillsConfig(
            directories=skills_data.get("directories", []),
            auto_activate=skills_data.get("auto_activate", []),
        )

        pathways_config = cls._parse_pathways_config(data)

        return cls(
            agent_specs=agent_specs,
            pathways_config=pathways_config,
            agents=agent_ids,
            agent_directories=agent_directories,
            schema_version=schema_version,
            mcp_servers=mcp_servers,
            bindings=bindings,
            models=models,
            skills=skills,
            source_file=source_file,
        )

    @staticmethod
    def _parse_agents(agents_raw: str | list[str | dict[str, Any]] | None) -> tuple[list[AgentDefinition], list[str]]:
        """Parse agents from YAML data."""
        agent_specs: list[AgentDefinition] = []
        agent_ids: list[str] = []

        if agents_raw in ("*", ["*"]):
            return [], ["*"]

        if isinstance(agents_raw, str):
            agent_ids = [a.strip() for a in agents_raw.split(",") if a.strip()]
            return [AgentDefinition(id=aid) for aid in agent_ids], agent_ids

        if isinstance(agents_raw, list):
            for item in agents_raw:
                if isinstance(item, str):
                    agent_ids.append(item)
                    agent_specs.append(AgentDefinition(id=item))
                elif isinstance(item, dict) and item.get("id"):
                    agent_ids.append(item["id"])
                    agent_specs.append(AgentDefinition.model_validate(item))

        return agent_specs, agent_ids

    @staticmethod
    def _parse_bindings(bindings_data: dict[str, Any] | None) -> list[WebhookBinding]:
        """Parse webhook bindings from YAML data."""
        bindings: list[WebhookBinding] = []
        if not isinstance(bindings_data, dict):
            return bindings

        for source_prefix, source_bindings in bindings_data.items():
            if not isinstance(source_bindings, dict):
                continue
            for event_name, binding_spec in source_bindings.items():
                source = f"{source_prefix}.{event_name}"
                if isinstance(binding_spec, str):
                    bindings.append(WebhookBinding(source=source, topic=binding_spec))
                elif isinstance(binding_spec, dict):
                    bindings.append(WebhookBinding(
                        source=source,
                        topic=binding_spec.get("topic", event_name),
                        filter=binding_spec.get("filter", {}),
                        transform=binding_spec.get("transform", {}),
                    ))
        return bindings

    @staticmethod
    def _parse_pathways_config(data: dict[str, Any]) -> PathwaysConfig:
        """Parse pathways config from YAML data."""
        pathways_config_data = data.get("pathways_config", {}) or {}
        if not pathways_config_data and data.get("pathway_directories"):
            pathways_config_data = {"directories": data.get("pathway_directories", [])}
        return PathwaysConfig(
            directories=pathways_config_data.get("directories", []),
            auto_deploy=pathways_config_data.get("auto_deploy", []),
        )

    @classmethod
    def load(cls, config_path: str | Path | None = None) -> DeploymentConfig:
        """Load deployment config from YAML.

        Search order (when no explicit path given):
        1. $ALBUS_CONFIG env var
        2. ./albus.yaml
        3. ./config/albus.yaml
        4. ~/.config/albus/albus.yaml
        5. ./server.yaml (legacy)
        6. ./config/server.yaml (legacy)
        """
        if config_path:
            path = Path(config_path)
            if path.exists():
                return cls.from_yaml(path)
            return cls()

        for candidate in config_search_paths():
            if candidate.exists():
                logger.info("Loading deployment config from: %s", candidate)
                return cls.from_yaml(candidate)

        logger.info("No deployment config file found, using empty config")
        return cls()

    def validate_config(self) -> list[str]:
        """Validate the configuration, return list of errors."""
        errors: list[str] = []
        if self.schema_version != 1:
            errors.append(f"Unsupported schema_version: {self.schema_version}")

        mcp_ids = [m.id for m in self.mcp_servers]
        if len(mcp_ids) != len(set(mcp_ids)):
            errors.append("Duplicate MCP server IDs detected")

        return errors

    def get_agent_spec(self, agent_id: str) -> AgentDefinition | None:
        """Get agent spec by ID."""
        for spec in self.agent_specs:
            if spec.id == agent_id:
                return spec
        return None
