"""Agent configuration — parse and validate agent.yaml files."""

from __future__ import annotations

import os
import re
from dataclasses import dataclass, field
from pathlib import Path

import yaml
from dotenv import load_dotenv

from hatchdx import HdxError


# ---------------------------------------------------------------------------
# Errors
# ---------------------------------------------------------------------------


class AgentConfigError(HdxError):
    """Invalid agent.yaml configuration."""


# ---------------------------------------------------------------------------
# Dataclasses
# ---------------------------------------------------------------------------


@dataclass
class ModelConfig:
    """Model provider configuration."""

    provider: str
    model: str
    max_tokens: int = 4096
    temperature: float = 0.0
    base_url: str | None = None


@dataclass
class ServerConfig:
    """A single MCP server connection."""

    name: str
    path: str | None = None
    command: str | None = None
    args: list[str] = field(default_factory=list)
    url: str | None = None
    env: dict[str, str] = field(default_factory=dict)
    headers: dict[str, str] = field(default_factory=dict)


@dataclass
class ToolFilterConfig:
    """Per-server tool include/exclude filter."""

    include: list[str] = field(default_factory=list)
    exclude: list[str] = field(default_factory=list)


@dataclass
class AgentSettings:
    """Agent behavior settings with defaults."""

    max_tool_calls: int = 50
    tool_call_timeout: str = "30s"
    retry_on_error: bool = True
    max_retries: int = 1
    tool_namespacing: str = "auto"
    parallel_tool_calls: bool = False


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

    name: str
    description: str
    model: ModelConfig
    system_prompt: str
    servers: list[ServerConfig] = field(default_factory=list)
    version: str = "0.1.0"
    system_prompt_file: str | None = None
    tool_filter: dict[str, ToolFilterConfig] = field(default_factory=dict)
    settings: AgentSettings = field(default_factory=AgentSettings)


# ---------------------------------------------------------------------------
# .env file loading
# ---------------------------------------------------------------------------


def _load_dotenv_files(agent_dir: Path) -> None:
    """Load .env files into the process environment.

    Agent directory .env is loaded first (higher priority), then project root
    .env as fallback. Uses override=False so existing shell env vars always win
    and earlier-loaded values aren't clobbered.
    """
    # Load agent dir .env first (higher priority)
    agent_env = agent_dir / ".env"
    if agent_env.is_file():
        load_dotenv(agent_env, override=False)

    # Load project root .env second (fallback — won't override agent dir values)
    project_root = _find_project_root(agent_dir)
    if project_root and project_root != agent_dir:
        root_env = project_root / ".env"
        if root_env.is_file():
            load_dotenv(root_env, override=False)


def _find_project_root(start: Path) -> Path | None:
    """Walk up from start to find the project root directory."""
    current = start.resolve()
    for parent in [current, *current.parents]:
        if (parent / ".git").exists() or (parent / "pyproject.toml").exists():
            return parent
    return None


# ---------------------------------------------------------------------------
# Environment variable interpolation
# ---------------------------------------------------------------------------

_ENV_VAR_PATTERN = re.compile(r"\$\{([^}]+)}")


def _interpolate_env_vars(value: str) -> str:
    """Replace ${VAR} patterns with environment variable values.

    Raises AgentConfigError if a referenced variable is not set.
    """

    def _replace(match: re.Match) -> str:
        var_name = match.group(1)
        env_value = os.environ.get(var_name)
        if env_value is None:
            raise AgentConfigError(
                f"Missing environment variable: {var_name}\n\n"
                f"Set it with: export {var_name}=your-value-here\n"
                f"Or add it to a .env file in your agent directory"
            )
        return env_value

    return _ENV_VAR_PATTERN.sub(_replace, value)


def _interpolate_dict(data: dict) -> dict:
    """Recursively interpolate env vars in all string values of a dict."""
    result = {}
    for key, value in data.items():
        if isinstance(value, str):
            result[key] = _interpolate_env_vars(value)
        elif isinstance(value, dict):
            result[key] = _interpolate_dict(value)
        elif isinstance(value, list):
            result[key] = _interpolate_list(value)
        else:
            result[key] = value
    return result


def _interpolate_list(data: list) -> list:
    """Recursively interpolate env vars in all string values of a list."""
    result = []
    for item in data:
        if isinstance(item, str):
            result.append(_interpolate_env_vars(item))
        elif isinstance(item, dict):
            result.append(_interpolate_dict(item))
        elif isinstance(item, list):
            result.append(_interpolate_list(item))
        else:
            result.append(item)
    return result


# ---------------------------------------------------------------------------
# Validation
# ---------------------------------------------------------------------------

_NAME_PATTERN = re.compile(r"^[a-z][a-z0-9-]*$")
_VALID_PROVIDERS = {"anthropic", "openai", "local"}
_VALID_NAMESPACING = {"auto", "always", "never"}


def _validate_name(name: str) -> None:
    if not name:
        raise AgentConfigError("'name' is required")
    if not _NAME_PATTERN.match(name):
        raise AgentConfigError(
            f"Invalid agent name: '{name}'\n"
            "Must be lowercase letters, numbers, and hyphens (start with a letter)"
        )


def _validate_model(data: dict) -> ModelConfig:
    if not data:
        raise AgentConfigError("'model' section is required")
    provider = data.get("provider")
    if not provider:
        raise AgentConfigError("'model.provider' is required")
    if provider not in _VALID_PROVIDERS:
        raise AgentConfigError(
            f"Unknown provider: '{provider}'\n"
            f"Supported providers: {', '.join(sorted(_VALID_PROVIDERS))}"
        )
    model_name = data.get("model")
    if not model_name:
        raise AgentConfigError("'model.model' is required")
    return ModelConfig(
        provider=provider,
        model=model_name,
        max_tokens=data.get("max_tokens", 4096),
        temperature=data.get("temperature", 0.0),
        base_url=data.get("base_url"),
    )


def _validate_server(data: dict, index: int) -> ServerConfig:
    name = data.get("name")
    if not name:
        raise AgentConfigError(f"Server at index {index}: 'name' is required")

    path = data.get("path")
    command = data.get("command")
    url = data.get("url")

    sources = [s for s in (path, command, url) if s is not None]
    if len(sources) == 0:
        raise AgentConfigError(
            f"Server '{name}': must specify one of 'path', 'command', or 'url'"
        )
    if len(sources) > 1:
        raise AgentConfigError(
            f"Server '{name}': specify only one of 'path', 'command', or 'url'"
        )

    env = data.get("env", {})
    if not isinstance(env, dict):
        raise AgentConfigError(f"Server '{name}': 'env' must be a mapping")

    return ServerConfig(
        name=name,
        path=path,
        command=command,
        url=url,
        args=data.get("args", []),
        env=env,
        headers=data.get("headers", {}),
    )


def _validate_settings(data: dict | None) -> AgentSettings:
    if not data:
        return AgentSettings()

    namespacing = data.get("tool_namespacing", "auto")
    if namespacing not in _VALID_NAMESPACING:
        raise AgentConfigError(
            f"Invalid tool_namespacing: '{namespacing}'\n"
            f"Must be one of: {', '.join(sorted(_VALID_NAMESPACING))}"
        )

    return AgentSettings(
        max_tool_calls=data.get("max_tool_calls", 50),
        tool_call_timeout=data.get("tool_call_timeout", "30s"),
        retry_on_error=data.get("retry_on_error", True),
        max_retries=data.get("max_retries", 1),
        tool_namespacing=namespacing,
        parallel_tool_calls=data.get("parallel_tool_calls", False),
    )


def _validate_tool_filter(data: dict | None) -> dict[str, ToolFilterConfig]:
    if not data:
        return {}

    filters = {}
    for server_name, filter_data in data.items():
        if not isinstance(filter_data, dict):
            raise AgentConfigError(
                f"Tool filter for '{server_name}': must be a mapping"
            )
        include = filter_data.get("include", [])
        exclude = filter_data.get("exclude", [])
        if include and exclude:
            raise AgentConfigError(
                f"Tool filter for '{server_name}': "
                "cannot specify both 'include' and 'exclude'"
            )
        filters[server_name] = ToolFilterConfig(include=include, exclude=exclude)

    return filters


def _resolve_system_prompt(
    data: dict, config_dir: Path | None = None
) -> str:
    """Resolve system prompt from inline value or file reference.

    system_prompt_file takes precedence over system_prompt.
    """
    prompt_file = data.get("system_prompt_file")
    if prompt_file:
        if config_dir:
            prompt_path = config_dir / prompt_file
        else:
            prompt_path = Path(prompt_file)

        if not prompt_path.exists():
            raise AgentConfigError(
                f"System prompt file not found: {prompt_path}\n"
                "Check the 'system_prompt_file' path in agent.yaml"
            )
        return prompt_path.read_text().strip()

    prompt = data.get("system_prompt")
    if not prompt:
        raise AgentConfigError(
            "Agent must have either 'system_prompt' or 'system_prompt_file'"
        )
    return prompt.strip()


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------


def load_agent_config(path: Path) -> AgentConfig:
    """Load and validate an agent configuration from a YAML file.

    Args:
        path: Path to agent.yaml

    Returns:
        Validated AgentConfig

    Raises:
        AgentConfigError: If the config is invalid or missing required fields.
        FileNotFoundError: If the file doesn't exist.
    """
    if not path.exists():
        raise FileNotFoundError(f"Agent config not found: {path}")

    # Load .env files before parsing YAML so ${VAR} interpolation can find them
    _load_dotenv_files(path.parent)

    try:
        raw = yaml.safe_load(path.read_text())
    except yaml.YAMLError as e:
        raise AgentConfigError(f"Invalid YAML in {path}: {e}") from e

    if not isinstance(raw, dict):
        raise AgentConfigError(f"Expected a YAML mapping in {path}")

    # Interpolate environment variables
    data = _interpolate_dict(raw)

    # Validate required fields
    _validate_name(data.get("name", ""))

    description = data.get("description")
    if not description:
        raise AgentConfigError("'description' is required")

    model = _validate_model(data.get("model", {}))

    # Parse servers
    servers_data = data.get("servers", [])
    if not isinstance(servers_data, list):
        raise AgentConfigError("'servers' must be a list")
    servers = [_validate_server(s, i) for i, s in enumerate(servers_data)]

    # Parse settings, tool filter, system prompt
    settings = _validate_settings(data.get("settings"))
    tool_filter = _validate_tool_filter(data.get("tool_filter"))
    system_prompt = _resolve_system_prompt(data, config_dir=path.parent)

    return AgentConfig(
        name=data["name"],
        description=description,
        version=data.get("version", "0.1.0"),
        model=model,
        system_prompt=system_prompt,
        system_prompt_file=data.get("system_prompt_file"),
        servers=servers,
        settings=settings,
        tool_filter=tool_filter,
    )
