"""Pydantic model for worker config validation.

Validates the entire ~/.hive-compute/config.yaml at load time so every
CLI command and the daemon get consistent, type-safe config. Replaces
scattered per-field isinstance checks that could never cover all edge cases.
"""

from __future__ import annotations

from copy import deepcopy
from pathlib import Path
from typing import Any

import yaml
from pydantic import BaseModel, Field, ValidationError, field_validator, model_validator

from hive_compute.model_ids import SUPPORTED_QUANTIZATIONS, canonical_model_identifier

SUPPORTED_BACKENDS = {"mlx-whisper", "parakeet-mlx", "whisper-cpp"}
SUPPORTED_THERMAL_PRESSURES = {"nominal", "fair", "serious", "critical"}

CONFIG_KEY_ALIASES = {
    "model": ("handlers", "transcribe", "model"),
    "quantization": ("handlers", "transcribe", "quantization"),
    "backend": ("handlers", "transcribe", "backend"),
    "language": ("handlers", "transcribe", "language"),
    "repo": ("handlers", "transcribe", "repo"),
    "supported_models": ("handlers", "transcribe", "supported_models"),
    "prewarm_model_on_startup": ("handlers", "transcribe", "prewarm_model_on_startup"),
}
CONFIG_SECTION_ALIASES = {
    "idle": "idle_detection",
    "idle_detection": "idle_detection",
    "polling": "polling",
    "handlers": "handlers",
}
SUPPORTED_CONFIG_UPDATE_PATHS = {
    ("coordinator_url",),
    ("worker_id",),
    ("handlers", "transcribe", "backend"),
    ("handlers", "transcribe", "model"),
    ("handlers", "transcribe", "quantization"),
    ("handlers", "transcribe", "language"),
    ("handlers", "transcribe", "repo"),
    ("handlers", "transcribe", "supported_models"),
    ("handlers", "transcribe", "prewarm_model_on_startup"),
    ("idle_detection", "min_idle_minutes"),
    ("idle_detection", "require_ac_power"),
    ("idle_detection", "check_thermal"),
    ("idle_detection", "max_thermal_pressure"),
    ("polling", "interval_seconds"),
    ("polling", "busy_interval_seconds"),
    ("polling", "idle_interval_seconds"),
}


class KeychainRef(BaseModel):
    service: str
    account: str


class TranscribeHandler(BaseModel):
    backend: str = "mlx-whisper"
    model: str = "large-v3"
    quantization: str = "4bit"
    language: str = "en"
    repo: str | None = None
    supported_models: list[str] = Field(default_factory=list)
    prewarm_model_on_startup: bool = True

    @field_validator("backend", mode="before")
    @classmethod
    def validate_backend(cls, value: Any) -> str:
        backend = str(value).strip().lower()
        if backend not in SUPPORTED_BACKENDS:
            allowed = ", ".join(sorted(SUPPORTED_BACKENDS))
            raise ValueError(f"unsupported backend '{value}'. Choose one of: {allowed}")
        return backend

    @field_validator("model", mode="before")
    @classmethod
    def validate_model(cls, value: Any) -> str:
        model = str(value).strip()
        if not model:
            raise ValueError("model cannot be empty")
        return model

    @field_validator("quantization", mode="before")
    @classmethod
    def validate_quantization(cls, value: Any) -> str:
        quantization = str(value).strip().lower()
        if quantization not in SUPPORTED_QUANTIZATIONS:
            allowed = ", ".join(sorted(SUPPORTED_QUANTIZATIONS))
            raise ValueError(f"unsupported quantization '{value}'. Choose one of: {allowed}")
        return quantization

    @field_validator("language", mode="before")
    @classmethod
    def validate_language(cls, value: Any) -> str:
        language = str(value).strip().lower()
        if not language:
            raise ValueError("language cannot be empty")
        if any(ch for ch in language if not (ch.isalpha() or ch == "-")):
            raise ValueError("language must contain only letters or hyphens")
        return language

    @field_validator("repo", mode="before")
    @classmethod
    def validate_repo(cls, value: Any) -> str | None:
        if value is None:
            return None
        repo = str(value).strip()
        return repo or None

    @field_validator("supported_models", mode="before")
    @classmethod
    def validate_supported_models(cls, value: Any) -> list[str]:
        if value is None:
            return []
        if isinstance(value, str):
            raw_models = [value]
        elif isinstance(value, list):
            raw_models = value
        else:
            raise ValueError("supported_models must be a list of model names")

        models: list[str] = []
        seen: set[str] = set()
        for raw_model in raw_models:
            model = str(raw_model).strip()
            if not model:
                raise ValueError("supported_models cannot contain empty model names")
            identifier = canonical_model_identifier(model, default_quantization="none")
            if identifier in seen:
                continue
            seen.add(identifier)
            models.append(identifier)
        return models

    @model_validator(mode="after")
    def ensure_supported_models_include_primary_model(self) -> TranscribeHandler:
        primary_identifier = canonical_model_identifier(self.model, self.quantization)
        if primary_identifier not in self.supported_models:
            self.supported_models.append(primary_identifier)
        return self


class IdleDetection(BaseModel):
    model_config = {"extra": "allow"}  # Allow check_thermal and other legacy fields

    min_idle_minutes: int = Field(default=5, ge=0)
    require_ac_power: bool = True
    check_thermal: bool = True
    max_thermal_pressure: str = "nominal"

    @field_validator("max_thermal_pressure", mode="before")
    @classmethod
    def validate_max_thermal_pressure(cls, value: Any) -> str:
        pressure = str(value).strip().lower()
        if pressure not in SUPPORTED_THERMAL_PRESSURES:
            allowed = ", ".join(sorted(SUPPORTED_THERMAL_PRESSURES))
            raise ValueError(f"unsupported thermal pressure '{value}'. Choose one of: {allowed}")
        return pressure


class Polling(BaseModel):
    interval_seconds: int = Field(default=30, ge=1)
    busy_interval_seconds: int = Field(default=5, ge=1)
    idle_interval_seconds: int = Field(default=60, ge=1)


class WorkerConfig(BaseModel):
    """Validated worker configuration from config.yaml."""
    coordinator_url: str = ""
    worker_id: str = ""
    worker_token: str | None = None
    worker_token_keychain: KeychainRef | None = None
    handlers: dict[str, TranscribeHandler] = Field(default_factory=lambda: {"transcribe": TranscribeHandler()})
    idle_detection: IdleDetection = Field(default_factory=IdleDetection)
    polling: Polling = Field(default_factory=Polling)

    @field_validator("worker_id", "coordinator_url", mode="before")
    @classmethod
    def coerce_to_string(cls, v: Any) -> str:
        if v is None:
            return ""
        if isinstance(v, (list, dict)):
            raise ValueError(f"expected a string, got {type(v).__name__}")
        if not isinstance(v, str):
            return str(v)
        return v


def load_worker_config_raw(config_path: str) -> dict:
    """Load, validate, and return config as dict. Raises ValueError on failure.

    Used by WorkerDaemon which handles errors itself (not SystemExit).
    """
    config_path_obj = Path(config_path).expanduser()
    if not config_path_obj.exists():
        raise FileNotFoundError(f"Config not found at {config_path_obj}. Run 'hive init' to create one.")

    with open(config_path_obj) as f:
        data = yaml.safe_load(f)

    if not isinstance(data, dict):
        raise ValueError(f"Config is not a YAML mapping (got {type(data).__name__}). Re-run 'hive init'.")

    config = WorkerConfig(**data)
    if not config.worker_id:
        raise ValueError("Config is missing worker_id. Re-run 'hive init'.")
    if not config.coordinator_url:
        raise ValueError("Config is missing coordinator_url. Re-run 'hive init'.")
    return config.model_dump()


def load_worker_config(config_path: str) -> WorkerConfig:
    """Load and validate worker config from YAML file.

    Raises SystemExit with a clean message on any validation error.
    """
    import click

    config_path_obj = Path(config_path).expanduser()
    if not config_path_obj.exists():
        click.echo("Not initialized. Run 'hive init' first.", err=True)
        raise SystemExit(1)

    try:
        with open(config_path_obj) as f:
            data = yaml.safe_load(f)
    except Exception as exc:
        click.echo(f"Config file could not be parsed: {exc}", err=True)
        raise SystemExit(1) from exc

    if not isinstance(data, dict):
        click.echo(f"Config is not a valid YAML mapping (got {type(data).__name__}). Re-run 'hive init'.", err=True)
        raise SystemExit(1)

    try:
        config = WorkerConfig(**data)
    except Exception as exc:
        click.echo(f"Config validation failed: {exc}", err=True)
        raise SystemExit(1) from exc

    # Fail fast on missing required operational fields
    missing = []
    if not config.worker_id:
        missing.append("worker_id")
    if not config.coordinator_url:
        missing.append("coordinator_url")
    if missing:
        click.echo(f"Config is missing required fields: {', '.join(missing)}. Re-run 'hive init'.", err=True)
        raise SystemExit(1)

    return config


def resolve_worker_config_key(key: str) -> tuple[str, ...]:
    raw_key = key.strip()
    if not raw_key:
        raise ValueError("Config key cannot be empty.")

    if raw_key in CONFIG_KEY_ALIASES:
        return CONFIG_KEY_ALIASES[raw_key]

    parts = [part.strip().replace("-", "_") for part in raw_key.split(".") if part.strip()]
    if not parts:
        raise ValueError("Config key cannot be empty.")

    parts[0] = CONFIG_SECTION_ALIASES.get(parts[0], parts[0])
    resolved = tuple(parts)
    if resolved not in SUPPORTED_CONFIG_UPDATE_PATHS:
        supported = [
            "backend",
            "language",
            "model",
            "prewarm_model_on_startup",
            "quantization",
            "repo",
            "supported_models",
            "idle.min-idle-minutes",
            "idle.require-ac-power",
            "idle.check-thermal",
            "polling.interval-seconds",
            "polling.busy-interval-seconds",
            "polling.idle-interval-seconds",
            "coordinator_url",
            "worker_id",
        ]
        raise ValueError(f"Unsupported config key '{key}'. Supported keys: {', '.join(supported)}")

    return resolved


def parse_worker_config_value(raw_value: str) -> Any:
    if raw_value == "":
        raise ValueError("Config value cannot be empty.")

    parsed = yaml.safe_load(raw_value)
    if parsed is None and raw_value.strip().lower() not in {"null", "~"}:
        return raw_value
    return parsed


def update_worker_config_value(
    config_data: dict[str, Any],
    key: str,
    raw_value: str,
) -> tuple[dict[str, Any], str, Any]:
    path = resolve_worker_config_key(key)
    parsed_value = parse_worker_config_value(raw_value)
    updated = deepcopy(config_data)

    current: dict[str, Any] = updated
    for segment in path[:-1]:
        next_value = current.get(segment)
        if not isinstance(next_value, dict):
            next_value = {}
            current[segment] = next_value
        current = next_value
    current[path[-1]] = parsed_value

    try:
        validated = WorkerConfig(**updated)
    except ValidationError as exc:
        error = exc.errors()[0]
        location = ".".join(str(part) for part in error["loc"])
        raise ValueError(f"Invalid value for {location}: {error['msg']}") from exc

    return validated.model_dump(), ".".join(path), parsed_value
