from __future__ import annotations

from datetime import date
from pathlib import Path
from typing import Any, Dict, List, Mapping, Optional

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


class DataFusionPolicyError(RuntimeError):
    """Raised when the data fusion policy file cannot be loaded or validated."""


class SourcePolicy(BaseModel):
    type: str = Field(..., min_length=1)
    authority: float = Field(..., ge=0.0, le=1.0)
    freshness_weight: float = Field(0.0, ge=0.0, le=1.0)
    stability_weight: float = Field(0.0, ge=0.0, le=1.0)


class ReasonCodes(BaseModel):
    elected: Optional[str] = None
    alternates: Optional[str] = None
    rejected: Optional[str] = None


class FieldPolicy(BaseModel):
    ladder: List[str]
    election: str = Field(..., min_length=1)
    require_corroboration: bool = False
    stale_after_days: Optional[int] = Field(None, ge=1)
    min_sources: Optional[int] = Field(None, ge=0)
    tolerance_pct: Optional[float] = Field(None, ge=0.0)
    min_corroboration: Optional[int] = Field(None, ge=0)
    alternates_field: Optional[str] = None
    reason_codes: Optional[ReasonCodes] = None

    @model_validator(mode="after")
    def _validate_ladder(cls, model: "FieldPolicy") -> "FieldPolicy":
        if not model.ladder:
            raise ValueError("ladder must contain at least one source id")
        return model


class NumericFusionPolicy(BaseModel):
    trimmed_median_pct: float = Field(0.0, ge=0.0, le=50.0)
    drift_alert_pct: float = Field(0.0, ge=0.0)
    fallback_confidence: float = Field(0.0, ge=0.0, le=1.0)


class CorroborationPolicy(BaseModel):
    high_tier_bonus: float = 0.0
    low_tier_bonus: float = 0.0
    min_sources_for_consistency: int = Field(2, ge=1)


class AuditPolicy(BaseModel):
    enabled: bool = True
    checks: List[str] = Field(default_factory=list)


class DataFusionPolicy(BaseModel):
    version: float = Field(..., ge=0.0)
    updated_at: date
    sources: Mapping[str, SourcePolicy]
    fields: Mapping[str, FieldPolicy]
    numeric_fusion: Optional[NumericFusionPolicy] = None
    corroboration: Optional[CorroborationPolicy] = None
    audits: Optional[AuditPolicy] = None

    @model_validator(mode="after")
    def _validate_field_sources(cls, model: "DataFusionPolicy") -> "DataFusionPolicy":
        missing: Dict[str, List[str]] = {}
        source_keys = set(model.sources.keys())
        for field_name, field_policy in model.fields.items():
            unknown = [src for src in field_policy.ladder if src not in source_keys]
            if unknown:
                missing[field_name] = unknown
        if missing:
            details = ", ".join(
                f"{field_name}: {', '.join(sorted(srcs))}"
                for field_name, srcs in missing.items()
            )
            raise ValueError(f"Unknown sources referenced in field ladders: {details}")
        return model


def load_data_fusion_policy(
    path: Path | str,
    *,
    loader: Any = yaml.safe_load,
) -> DataFusionPolicy:
    """Load and validate the data fusion policy from YAML."""

    policy_path = Path(path)
    if not policy_path.exists():
        raise DataFusionPolicyError(f"Policy file not found: {policy_path}")

    try:
        with policy_path.open("r", encoding="utf-8") as handle:
            data = loader(handle)
    except yaml.YAMLError as exc:  # type: ignore[attr-defined]
        raise DataFusionPolicyError(f"Failed to parse YAML policy: {exc}") from exc
    except OSError as exc:
        raise DataFusionPolicyError(f"Failed to read policy file: {exc}") from exc

    if not isinstance(data, Mapping):
        raise DataFusionPolicyError("Policy file did not yield a mapping")

    try:
        return DataFusionPolicy.model_validate(dict(data))
    except ValidationError as exc:
        raise DataFusionPolicyError(
            f"Policy validation failed: {exc.errors()}"
        ) from exc


__all__ = [
    "AuditPolicy",
    "CorroborationPolicy",
    "DataFusionPolicy",
    "DataFusionPolicyError",
    "FieldPolicy",
    "NumericFusionPolicy",
    "ReasonCodes",
    "SourcePolicy",
    "load_data_fusion_policy",
]
