"""Configuration loading and validation for crucible projects."""

from __future__ import annotations

from dataclasses import dataclass, field
from pathlib import Path
from typing import List, Optional

import yaml


class ConfigError(Exception):
    """Raised when configuration is missing or invalid."""


@dataclass
class ContextWindowConfig:
    include_history: bool = True
    history_limit: int = 20
    include_best: bool = True


@dataclass
class AgentConfig:
    type: str = "claude-code"
    instructions: Optional[str] = None
    system_prompt: Optional[str] = None
    context_window: ContextWindowConfig = field(default_factory=ContextWindowConfig)


@dataclass
class FilesConfig:
    editable: List[str] = field(default_factory=list)
    readonly: List[str] = field(default_factory=list)
    hidden: List[str] = field(default_factory=list)


@dataclass
class CommandsConfig:
    run: str = ""
    eval: str = ""
    setup: Optional[str] = None


@dataclass
class MetricConfig:
    name: str = ""
    direction: str = ""


@dataclass
class ConstraintsConfig:
    timeout_seconds: int = 600
    max_retries: int = 3


@dataclass
class GitConfig:
    branch_prefix: str = "crucible"
    tag_failed: bool = True


@dataclass
class Config:
    name: str = ""
    description: str = ""
    files: FilesConfig = field(default_factory=FilesConfig)
    commands: CommandsConfig = field(default_factory=CommandsConfig)
    metric: MetricConfig = field(default_factory=MetricConfig)
    constraints: ConstraintsConfig = field(default_factory=ConstraintsConfig)
    agent: AgentConfig = field(default_factory=AgentConfig)
    git: GitConfig = field(default_factory=GitConfig)


def _build_context_window(data: dict) -> ContextWindowConfig:
    if not data:
        return ContextWindowConfig()
    return ContextWindowConfig(
        include_history=data.get("include_history", True),
        history_limit=data.get("history_limit", 20),
        include_best=data.get("include_best", True),
    )


def _build_agent(data: dict) -> AgentConfig:
    if not data:
        return AgentConfig()
    return AgentConfig(
        type=data.get("type", "claude-code"),
        instructions=data.get("instructions"),
        system_prompt=data.get("system_prompt"),
        context_window=_build_context_window(data.get("context_window", {})),
    )


def _require(raw: dict, *keys: str) -> None:
    """Validate that dotted key paths are present and non-empty."""
    for key in keys:
        parts = key.split(".")
        obj = raw
        for part in parts:
            if not isinstance(obj, dict) or part not in obj:
                raise ConfigError(f"Required field '{key}' is missing")
            obj = obj[part]
        if obj is None or obj == "" or obj == []:
            raise ConfigError(f"Required field '{key}' is empty")


def load_config(project_root: Path) -> Config:
    """Load and validate .crucible/config.yaml from *project_root*."""
    config_path = project_root / ".crucible" / "config.yaml"
    if not config_path.exists():
        raise ConfigError(f"Config file not found: {config_path}")

    with open(config_path) as f:
        raw = yaml.safe_load(f)

    if not isinstance(raw, dict):
        raise ConfigError("Config file is not a valid YAML mapping")

    _require(
        raw,
        "name",
        "files.editable",
        "commands.run",
        "commands.eval",
        "metric.name",
        "metric.direction",
    )

    direction = raw["metric"]["direction"]
    if direction not in ("minimize", "maximize"):
        raise ConfigError(f"metric.direction must be 'minimize' or 'maximize', got '{direction}'")

    files_data = raw.get("files", {})
    commands_data = raw.get("commands", {})
    metric_data = raw.get("metric", {})
    constraints_data = raw.get("constraints", {})
    git_data = raw.get("git", {})

    return Config(
        name=raw["name"],
        description=raw.get("description", ""),
        files=FilesConfig(
            editable=files_data.get("editable", []),
            readonly=files_data.get("readonly", []),
            hidden=files_data.get("hidden", []),
        ),
        commands=CommandsConfig(
            run=commands_data["run"],
            eval=commands_data["eval"],
            setup=commands_data.get("setup"),
        ),
        metric=MetricConfig(
            name=metric_data["name"],
            direction=metric_data["direction"],
        ),
        constraints=ConstraintsConfig(
            timeout_seconds=constraints_data.get("timeout_seconds", 600),
            max_retries=constraints_data.get("max_retries", 3),
        ),
        agent=_build_agent(raw.get("agent", {})),
        git=GitConfig(
            branch_prefix=git_data.get("branch_prefix", "crucible"),
            tag_failed=git_data.get("tag_failed", True),
        ),
    )
