from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any, List, Mapping, Sequence

from .data_fusion_policy import (
    DataFusionPolicy,
    DataFusionPolicyError,
    FieldPolicy,
    SourcePolicy,
)

EVIDENCE_KEYS = (
    "freshness",
    "consistency",
    "specificity",
    "stability",
    "corroboration",
)


@dataclass
class Candidate:
    value: Any
    source: str
    evidence: Mapping[str, float] = field(default_factory=dict)
    score: float | None = None
    metadata: Mapping[str, Any] = field(default_factory=dict)


@dataclass
class ElectionCandidate:
    value: Any
    source: str
    score: float
    evidence: Mapping[str, float]
    metadata: Mapping[str, Any] = field(default_factory=dict)


@dataclass
class ElectionResult:
    field: str
    winner: ElectionCandidate
    alternates: List[ElectionCandidate]
    reason_code: str | None = None
    alternates_reason_code: str | None = None


def _normalize_candidate(raw: Mapping[str, Any] | Candidate) -> Candidate:
    if isinstance(raw, Candidate):
        return raw
    if not isinstance(raw, Mapping):
        raise TypeError(f"Candidate must be mapping-like, received {type(raw)!r}")

    if "source" not in raw:
        raise DataFusionPolicyError("Candidate missing 'source' identifier")
    if "value" not in raw:
        raise DataFusionPolicyError("Candidate missing 'value' field")

    evidence = raw.get("evidence") or {}
    metadata = raw.get("metadata") or {}
    return Candidate(
        value=raw["value"],
        source=str(raw["source"]),
        score=raw.get("score"),
        evidence=evidence,
        metadata=metadata,
    )


def _compute_score(candidate: Candidate, source: SourcePolicy) -> float:
    if candidate.score is not None:
        return candidate.score

    score = source.authority
    evidence = candidate.evidence

    freshness = float(evidence.get("freshness", 0.0))
    if source.freshness_weight:
        score += freshness * source.freshness_weight

    stability = float(evidence.get("stability", 0.0))
    if source.stability_weight:
        score += stability * source.stability_weight

    for key in ("consistency", "specificity", "corroboration"):
        score += float(evidence.get(key, 0.0))

    return score


def _candidate_sort_key(
    candidate: ElectionCandidate, ladder_order: Mapping[str, int]
) -> tuple[float, int, str]:
    ladder_rank = ladder_order.get(candidate.source, len(ladder_order))
    return (-candidate.score, ladder_rank, str(candidate.value))


def elect_field_value(
    field: str,
    candidates: Sequence[Mapping[str, Any] | Candidate],
    policy: DataFusionPolicy,
) -> ElectionResult:
    """Select the best value for a field based on the configured policy."""

    if field not in policy.fields:
        raise DataFusionPolicyError(f"Field '{field}' is not defined in the policy")
    field_policy: FieldPolicy = policy.fields[field]
    ladder_order = {
        source_id: index for index, source_id in enumerate(field_policy.ladder)
    }

    normalized: List[ElectionCandidate] = []
    for raw_candidate in candidates:
        candidate = _normalize_candidate(raw_candidate)
        if candidate.source not in policy.sources:
            continue
        if candidate.source not in ladder_order:
            continue
        source_policy = policy.sources[candidate.source]
        score = _compute_score(candidate, source_policy)
        normalized.append(
            ElectionCandidate(
                value=candidate.value,
                source=candidate.source,
                score=score,
                evidence={
                    key: float(candidate.evidence.get(key, 0.0))
                    for key in EVIDENCE_KEYS
                    if key in candidate.evidence
                },
                metadata=dict(candidate.metadata),
            )
        )

    if not normalized:
        raise DataFusionPolicyError(
            f"No candidates available for field '{field}' after applying policy filters"
        )

    normalized.sort(key=lambda item: _candidate_sort_key(item, ladder_order))
    winner = normalized[0]
    alternates = normalized[1:]

    reason_code = None
    alternates_reason = None
    if field_policy.reason_codes:
        reason_code = field_policy.reason_codes.elected
        alternates_reason = field_policy.reason_codes.alternates

    return ElectionResult(
        field=field,
        winner=winner,
        alternates=alternates,
        reason_code=reason_code,
        alternates_reason_code=alternates_reason,
    )


__all__ = [
    "Candidate",
    "ElectionCandidate",
    "ElectionResult",
    "elect_field_value",
]
