"""Configuration loader for Fluxibly.

Provides YAML loading with environment variable expansion,
and factory methods for LLMConfig, AgentConfig, and tool configs.
"""

from __future__ import annotations

import copy
import os
import re
from pathlib import Path
from typing import Any

import yaml

from fluxibly.agent.base import AgentConfig
from fluxibly.llm.base import LLMConfig

# Prebuilt tools shipped with fluxibly (searched when resolving tool names)
_PREBUILT_TOOLS_DIR = str(
    Path(__file__).resolve().parent.parent / "tools" / "prebuilt"
)


def load_yaml(path: str | Path) -> dict[str, Any]:
    """Load and parse a YAML file with environment variable expansion.

    Expands ``${VAR_NAME}`` references in YAML values using ``os.environ``.
    If the variable is not set, the placeholder is left as-is.

    Args:
        path: Path to the YAML file.

    Returns:
        Parsed dict.

    Raises:
        FileNotFoundError: If file doesn't exist.
        ValueError: If YAML parsing fails.
    """
    filepath = Path(path)
    if not filepath.exists():
        raise FileNotFoundError(f"YAML file not found: {filepath}")

    raw = filepath.read_text()

    # Expand ${VAR_NAME} placeholders
    def _expand_env(match: re.Match[str]) -> str:
        var_name = match.group(1)
        return os.environ.get(var_name, match.group(0))

    expanded = re.sub(r"\$\{(\w+)\}", _expand_env, raw)

    try:
        content = yaml.safe_load(expanded)
    except yaml.YAMLError as e:
        raise ValueError(f"Failed to parse YAML: {filepath}") from e

    if content is None:
        return {}
    if not isinstance(content, dict):
        raise ValueError(
            f"Expected dict in YAML, got {type(content)}: {filepath}"
        )

    return content


def load_llm_config(path: str | Path) -> LLMConfig:
    """Load LLMConfig from a YAML file."""
    data = load_yaml(path)
    return LLMConfig(**data)


def load_agent_config(name_or_path: str | Path) -> AgentConfig:
    """Resolve an agent identifier to its AgentConfig.

    Resolution order:
        1. If ``name_or_path`` is a file path (contains ``/`` or ends with ``.yaml``)
           → load directly
        2. Otherwise treat as a name → search common directories:
           ``./agents/<name>.yaml``, ``./agents/<name>/config.yaml``
        3. Not found → raise ``FileNotFoundError``

    Args:
        name_or_path: Agent name or path to YAML file.

    Returns:
        Parsed AgentConfig.
    """
    path = _resolve_config_path(str(name_or_path), "agent", ["agents"])
    data = load_yaml(path)
    return AgentConfig(**data)


def _tool_search_dirs() -> list[str]:
    """Return search directories for tool name resolution.

    Searches ``./tools/`` first (user project), then the fluxibly
    prebuilt tools directory shipped with the package.
    """
    return ["tools", _PREBUILT_TOOLS_DIR]


def load_tool_config(name_or_path: str | Path) -> dict[str, Any]:
    """Resolve a tool identifier to its unified tool dict.

    Resolution order:
        1. If ``name_or_path`` is a file path → load directly
        2. Otherwise treat as a name → search common directories:
           ``./tools/<name>.yaml``, ``./tools/<name>/<name>.yaml``,
           ``./tools/<name>/config.yaml``, then prebuilt tools
        3. Not found → raise ``FileNotFoundError``

    Args:
        name_or_path: Tool name or path to YAML file.

    Returns:
        Parsed tool config dict (unified tool format).
    """
    path = _resolve_config_path(str(name_or_path), "tool", _tool_search_dirs())
    return load_yaml(path)


def load_tool_config_with_path(
    name_or_path: str | Path,
) -> tuple[dict[str, Any], Path]:
    """Resolve a tool identifier to its config dict and resolved file path.

    Same resolution logic as :func:`load_tool_config`, but also returns the
    resolved YAML path so the caller can check for a sibling ``.py`` handler.

    Args:
        name_or_path: Tool name or path to YAML file.

    Returns:
        Tuple of (parsed tool config dict, resolved Path to YAML file).
    """
    path = _resolve_config_path(str(name_or_path), "tool", _tool_search_dirs())
    return load_yaml(path), path


def merge_configs(
    base: dict[str, Any], override: dict[str, Any]
) -> dict[str, Any]:
    """Deep-merge two configuration dicts. Override wins.

    Nested dicts are merged recursively. Lists and primitives are replaced.
    """
    result = copy.deepcopy(base)
    for key, value in override.items():
        if (
            key in result
            and isinstance(result[key], dict)
            and isinstance(value, dict)
        ):
            result[key] = merge_configs(result[key], value)
        else:
            result[key] = copy.deepcopy(value)
    return result


def _resolve_config_path(
    name_or_path: str,
    resource_type: str,
    search_dirs: list[str],
) -> Path:
    """Resolve a name or path to a concrete file path.

    Args:
        name_or_path: Name (e.g. ``"triage"``) or path (e.g. ``"agents/triage.yaml"``).
        resource_type: For error messages (e.g. ``"agent"``, ``"tool"``).
        search_dirs: Directories to search when resolving by name.

    Returns:
        Resolved Path to the YAML file.

    Raises:
        FileNotFoundError: If the config cannot be found.
    """
    is_path = (
        "/" in name_or_path
        or "\\" in name_or_path
        or name_or_path.endswith((".yaml", ".yml"))
    )

    if is_path:
        p = Path(name_or_path)
        if p.exists():
            return p
        # Try resolve relative
        resolved = p.resolve()
        if resolved.exists():
            return resolved
        raise FileNotFoundError(
            f"{resource_type.title()} config not found at path: {name_or_path}"
        )

    # Name-based search
    candidates: list[Path] = []
    for search_dir in search_dirs:
        d = Path(search_dir)
        candidates.append(d / f"{name_or_path}.yaml")
        candidates.append(d / name_or_path / f"{name_or_path}.yaml")
        candidates.append(d / name_or_path / "config.yaml")

    for candidate in candidates:
        if candidate.exists():
            return candidate

    # Deep search: look for <name>/<name>.yaml recursively under search dirs
    # (e.g. prebuilt tools may be nested: prebuilt/shell/shell.yaml)
    target = f"{name_or_path}/{name_or_path}.yaml"
    for search_dir in search_dirs:
        d = Path(search_dir)
        if not d.is_dir():
            continue
        for match in d.rglob(target):
            return match

    raise FileNotFoundError(
        f"{resource_type.title()} '{name_or_path}' not found. "
        f"Searched: {[str(c) for c in candidates]}"
    )


class ConfigLoader:
    """Loads configuration from a directory of YAML files.

    Supports profiles (base + override merging) and environment variable expansion.
    """

    def __init__(self, config_dir: str | Path = "config") -> None:
        self.config_dir = Path(config_dir)

    def load_llm_config(self, filename: str = "llm.yaml") -> LLMConfig:
        """Load LLMConfig from config directory."""
        return load_llm_config(self.config_dir / filename)

    def load_agent_config(self, filename: str = "agent.yaml") -> AgentConfig:
        """Load AgentConfig from config directory."""
        return load_agent_config(self.config_dir / filename)

    def load_tool_config(self, filename: str) -> dict[str, Any]:
        """Load tool config from config directory."""
        return load_tool_config(self.config_dir / filename)

    def load_mcp_servers(
        self, filename: str = "mcp_servers.yaml"
    ) -> dict[str, Any]:
        """Load MCP server definitions.

        Returns:
            The ``mcp_servers`` dict from the YAML.
        """
        data = load_yaml(self.config_dir / filename)
        return data.get("mcp_servers", {})

    def load_profile(self, profile_name: str) -> dict[str, Any]:
        """Load a configuration profile.

        If ``profile_name`` looks like a file path, loads directly.
        Otherwise looks in ``config_dir/profiles/{profile_name}.yaml``.
        """
        is_path = (
            "/" in profile_name
            or "\\" in profile_name
            or profile_name.endswith((".yaml", ".yml"))
        )

        if is_path:
            profile_path = Path(profile_name)
            if not profile_path.is_absolute():
                profile_path = profile_path.resolve()
        else:
            profile_path = (
                self.config_dir / "profiles" / f"{profile_name}.yaml"
            )

        if not profile_path.exists():
            raise FileNotFoundError(f"Profile not found: {profile_path}")

        return load_yaml(profile_path)
