"""Configuration loading and defaults."""

from __future__ import annotations

from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Any

import yaml


@dataclass
class QCConfig:
    max_gap_fraction: float = 0.35
    max_ambiguity_fraction: float = 0.10
    min_effective_codon_sites: int = 20
    max_branch_imbalance_ratio: float = 30.0


@dataclass
class RepresentationConfig:
    embedding_version: str = "v1_bounded8"
    projection_dim: int = 16
    projection_seed: int = 0
    smoothing_bandwidth: float = 0.15


@dataclass
class NullModelConfig:
    model_name: str = "mg94_neutral"
    kappa: float = 2.0
    omega: float = 1.0
    codon_frequency_mode: str = "f3x4"
    rate_variation: bool = False
    gamma_alpha: float = 0.5
    gamma_categories: int = 4


@dataclass
class FitConfig:
    null_replicates_train: int = 200
    ridge: float = 1e-4


@dataclass
class ScoreConfig:
    null_replicates_score: int = 999
    two_sided: bool = False
    mc_warning_threshold: int = 199


@dataclass
class RuntimeConfig:
    jobs: int = 1
    seed: int = 1
    lazy_plots: bool = False


@dataclass
class BABAPPAConfig:
    qc: QCConfig
    representation: RepresentationConfig
    null_model: NullModelConfig
    fit: FitConfig
    score: ScoreConfig
    runtime: RuntimeConfig

    def to_dict(self) -> dict[str, Any]:
        return asdict(self)


def default_config() -> BABAPPAConfig:
    return BABAPPAConfig(
        qc=QCConfig(),
        representation=RepresentationConfig(),
        null_model=NullModelConfig(),
        fit=FitConfig(),
        score=ScoreConfig(),
        runtime=RuntimeConfig(),
    )


def _deep_update(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
    for key, value in override.items():
        if isinstance(value, dict) and isinstance(base.get(key), dict):
            base[key] = _deep_update(base[key], value)
        else:
            base[key] = value
    return base


def config_from_dict(data: dict[str, Any]) -> BABAPPAConfig:
    cfg = default_config().to_dict()
    cfg = _deep_update(cfg, data)
    return BABAPPAConfig(
        qc=QCConfig(**cfg["qc"]),
        representation=RepresentationConfig(**cfg["representation"]),
        null_model=NullModelConfig(**cfg["null_model"]),
        fit=FitConfig(**cfg["fit"]),
        score=ScoreConfig(**cfg["score"]),
        runtime=RuntimeConfig(**cfg["runtime"]),
    )


def load_config(path: str | Path | None) -> BABAPPAConfig:
    if path is None:
        return default_config()
    cfg_path = Path(path)
    if not cfg_path.exists():
        raise FileNotFoundError(f"Config file not found: {cfg_path}")
    text = cfg_path.read_text(encoding="utf-8")
    if cfg_path.suffix.lower() in {".yaml", ".yml"}:
        payload = yaml.safe_load(text) or {}
    elif cfg_path.suffix.lower() == ".toml":
        try:
            import tomllib
        except ModuleNotFoundError as exc:  # pragma: no cover
            raise RuntimeError("tomllib is required for TOML config") from exc
        payload = tomllib.loads(text)
    else:
        raise ValueError("Config must be YAML (.yaml/.yml) or TOML (.toml)")
    if not isinstance(payload, dict):
        raise ValueError("Config root must be a mapping")
    return config_from_dict(payload)


def write_default_config(path: str | Path) -> None:
    cfg = default_config().to_dict()
    out_path = Path(path)
    out_path.parent.mkdir(parents=True, exist_ok=True)
    out_path.write_text(yaml.safe_dump(cfg, sort_keys=False), encoding="utf-8")
