"""Compact, evidence-aware contracts for future Time Machine composition.

These contracts intentionally contain references and evidence metadata, never
route-point matrices or a synthesized historical environmental state.
"""

from __future__ import annotations

import json
from dataclasses import asdict, dataclass, field
from datetime import datetime
from enum import Enum
from typing import Any, Mapping

from mountain_twin.analysis_contract import AnalysisIdentity
from mountain_twin.rci.contracts import CoverageState


class EvidenceClass(str, Enum):
    """Evidence origin/type; deliberately independent of quality or safety."""

    OBSERVED = "OBSERVED"
    REANALYSIS = "REANALYSIS"
    HISTORICAL_FORECAST = "HISTORICAL_FORECAST"
    FORECAST = "FORECAST"
    DETERMINISTIC_ASTRONOMY = "DETERMINISTIC_ASTRONOMY"
    TERRAIN_DERIVED = "TERRAIN_DERIVED"
    DERIVED = "DERIVED"
    UNKNOWN = "UNKNOWN"


class TimeMachineMode(str, Enum):
    """The only evidence-selection mode supported by History v0.1."""

    RECONSTRUCTED_PAST = "RECONSTRUCTED_PAST"


@dataclass(frozen=True)
class CrossResultReference:
    """Stable producer-owned reference to a result, without embedding it."""

    producer: str
    result_reference: str

    def __post_init__(self) -> None:
        if not self.producer or not self.result_reference:
            raise ValueError("cross-result reference requires producer and result reference")

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


@dataclass(frozen=True)
class HistoricalTimeWindow:
    """Requested timezone-aware time window, distinct from source-valid time."""

    requested_start: str
    requested_end: str
    timezone: str

    def __post_init__(self) -> None:
        start = _aware_time(self.requested_start, "requested start")
        end = _aware_time(self.requested_end, "requested end")
        if end < start:
            raise ValueError("historical requested window is reversed")
        if not self.timezone:
            raise ValueError("historical requested window requires timezone")

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


@dataclass(frozen=True)
class RouteTimelineReference:
    """Reference to one existing Unified route/timeline result for a scenario."""

    route_id: str
    scenario_name: str
    unified_result: CrossResultReference

    def __post_init__(self) -> None:
        if not self.route_id or not self.scenario_name:
            raise ValueError("route/timeline reference requires route and scenario")

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


@dataclass(frozen=True)
class EvidenceEntry:
    """One source/type entry supporting an engine result.

    Times are optional because current source artifacts do not always expose
    observation or model-run timestamps. They remain separate when available.
    """

    evidence_class: EvidenceClass
    temporal_coverage: CoverageState
    source_type: str | None = None
    provider: str | None = None
    product: str | None = None
    source_metadata: Mapping[str, Any] = field(default_factory=dict)
    source_references: tuple[CrossResultReference, ...] = ()
    valid_start: str | None = None
    valid_end: str | None = None
    observed_at: str | None = None
    fetched_at: str | None = None
    model_run_at: str | None = None
    limitations: tuple[str, ...] = ()
    reason_codes: tuple[str, ...] = ()

    def __post_init__(self) -> None:
        if (self.valid_start is None) != (self.valid_end is None):
            raise ValueError("evidence valid interval requires both start and end")
        if self.valid_start is not None:
            valid_start = _aware_time(self.valid_start, "evidence valid start")
            valid_end = _aware_time(self.valid_end, "evidence valid end")
            if valid_end < valid_start:
                raise ValueError("evidence valid interval is reversed")
        for label, value in (
            ("observed", self.observed_at),
            ("fetched", self.fetched_at),
            ("model run", self.model_run_at),
        ):
            if value is not None:
                _aware_time(value, f"evidence {label} time")

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


@dataclass(frozen=True)
class EngineEvidence:
    """Availability and evidence references for one producer-owned engine."""

    engine_id: str
    availability: CoverageState
    result_references: tuple[CrossResultReference, ...]
    evidence: tuple[EvidenceEntry, ...] = ()
    limitations: tuple[str, ...] = ()
    data_gaps: tuple[str, ...] = ()

    def __post_init__(self) -> None:
        if not self.engine_id:
            raise ValueError("engine evidence requires engine ID")
        _unique_references(self.result_references, "engine result references")
        _unique_references(
            tuple(reference for item in self.evidence for reference in item.source_references),
            "evidence source references",
        )

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


@dataclass(frozen=True)
class TimeMachineResult:
    """Future composition result that references, rather than copies, engines."""

    identity: AnalysisIdentity
    mode: TimeMachineMode
    requested_window: HistoricalTimeWindow
    route_timeline_references: tuple[RouteTimelineReference, ...]
    engines: tuple[EngineEvidence, ...]
    provenance: Mapping[str, Any] = field(default_factory=dict)
    limitations: tuple[str, ...] = ()
    data_gaps: tuple[str, ...] = ()
    diagnostics: tuple[str, ...] = ()
    contract_version: str = "0.1"

    def __post_init__(self) -> None:
        if self.identity.analysis_type != "time_machine":
            raise ValueError("Time Machine result requires time_machine analysis type")
        if self.identity.scenario_datetime != self.requested_window.requested_start:
            raise ValueError("Time Machine identity and requested start must agree")
        if self.identity.timezone != self.requested_window.timezone:
            raise ValueError("Time Machine identity and requested timezone must agree")
        if not self.route_timeline_references:
            raise ValueError("Time Machine result requires route/timeline references")
        route_ids = {item.route_id for item in self.route_timeline_references}
        if route_ids != {self.identity.subject_id}:
            raise ValueError("Time Machine route/timeline references must match subject route")
        engine_ids = [item.engine_id for item in self.engines]
        if len(engine_ids) != len(set(engine_ids)):
            raise ValueError("Time Machine engine IDs must be unique")

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


def serialize_time_machine_result(result: TimeMachineResult) -> str:
    """Canonical deterministic serialization for a Time Machine result."""
    return json.dumps(result.to_dict(), sort_keys=True, separators=(",", ":"), allow_nan=False)


def _aware_time(value: str, label: str) -> datetime:
    parsed = datetime.fromisoformat(value)
    if parsed.tzinfo is None or parsed.utcoffset() is None:
        raise ValueError(f"{label} must be timezone-aware")
    return parsed


def _unique_references(references: tuple[CrossResultReference, ...], label: str) -> None:
    values = [(item.producer, item.result_reference) for item in references]
    if len(values) != len(set(values)):
        raise ValueError(f"{label} must be unique")


def _json_value(value: Any) -> Any:
    if isinstance(value, Enum):
        return value.value
    if isinstance(value, Mapping):
        return {str(key): _json_value(item) for key, item in value.items()}
    if isinstance(value, tuple):
        return [_json_value(item) for item in value]
    if isinstance(value, list):
        return [_json_value(item) for item in value]
    if hasattr(value, "__dataclass_fields__"):
        return _json_value(asdict(value))
    return value
