"""Deterministic RECONSTRUCTED_PAST evidence composition.

This module reads compact engine provenance and references only. It does not
call providers, evaluate route points, or recalculate scientific results.
"""

from __future__ import annotations

from dataclasses import asdict
from typing import Any, Mapping

from mountain_twin.analysis_contract import AnalysisIdentity
from mountain_twin.history.contracts import (
    CrossResultReference,
    EngineEvidence,
    EvidenceClass,
    EvidenceEntry,
    HistoricalTimeWindow,
    RouteTimelineReference,
    TimeMachineMode,
    TimeMachineResult,
)
from mountain_twin.history.mapping import (
    evidence_class_for_deterministic_astronomy,
    evidence_class_for_terrain_derived,
    evidence_class_for_weather_source_type,
)
from mountain_twin.rci.contracts import CoverageState

_HISTORICAL_CLASSIFICATION_LIMITATION = (
    "HISTORICAL_REQUEST_CLASSIFICATION_IS_EXPLICIT_MODE_NOT_WALL_CLOCK"
)


def compose_reconstructed_past(
    *,
    unified_analysis: Any,
    requested_window: HistoricalTimeWindow,
    weather_resolution: Any | None = None,
    snow_result: Any | None = None,
    photographer_result: Any | None = None,
    rci_result: Any | None = None,
) -> TimeMachineResult:
    """Compose an evidence ledger from existing results for one route scenario.

    The caller supplies already-produced results. The explicit reconstructed
    mode is the historical request classification in v0.1; no wall-clock test
    is made because current contracts do not establish one deterministically.
    """
    unified = _mapping(unified_analysis)
    identity = _identity(unified.get("identity"))
    if identity is None or identity.analysis_type != "unified_route_analysis":
        raise ValueError("reconstructed History requires Unified Route Analysis")
    if identity.scenario_datetime != requested_window.requested_start:
        raise ValueError("requested History start must match Unified scenario start")
    route = _mapping(unified.get("route"))
    route_id = _string(route.get("route_id")) or identity.subject_id
    if route_id != identity.subject_id:
        raise ValueError("Unified route and identity must agree")
    scenario = _mapping(unified.get("scenario"))
    scenario_name = _required_string(scenario.get("name"), "Unified scenario name")
    unified_reference = result_reference_for_identity(identity)
    timeline_reference = RouteTimelineReference(
        route_id=route_id,
        scenario_name=scenario_name,
        unified_result=unified_reference,
    )
    resolution = (
        _mapping(weather_resolution)
        if weather_resolution is not None
        else _weather_resolution(unified)
    )
    engines = [
        _unified_evidence(unified, unified_reference),
        _weather_evidence(unified_reference, resolution),
        _astronomy_evidence(unified, unified_reference),
        _terrain_evidence(unified, unified_reference),
    ]
    if snow_result is not None:
        engines.append(_snow_evidence(snow_result))
    if photographer_result is not None:
        engines.append(_photographer_evidence(photographer_result))
    if rci_result is not None:
        engines.append(_rci_evidence(rci_result))
    gaps = tuple(sorted({gap for engine in engines for gap in engine.data_gaps}))
    return TimeMachineResult(
        identity=AnalysisIdentity(
            analysis_type="time_machine",
            semantic_type="historical_environmental_evidence",
            version="0.1",
            subject_id=route_id,
            scenario_datetime=requested_window.requested_start,
            timezone=requested_window.timezone,
        ),
        mode=TimeMachineMode.RECONSTRUCTED_PAST,
        requested_window=requested_window,
        route_timeline_references=(timeline_reference,),
        engines=tuple(engines),
        provenance={"unified_result_reference": unified_reference.to_dict()},
        limitations=(_HISTORICAL_CLASSIFICATION_LIMITATION,),
        data_gaps=gaps,
        diagnostics=("COMPOSED_FROM_EXISTING_ENGINE_RESULTS",),
    )


def result_reference_for_identity(identity: AnalysisIdentity) -> CrossResultReference:
    """Reference an existing result using its producer-owned AnalysisIdentity."""
    scenario = identity.scenario_datetime or "no-scenario-datetime"
    return CrossResultReference(
        producer=identity.analysis_type,
        result_reference=f"{identity.analysis_type}:{identity.version}:{identity.subject_id}:{scenario}",
    )


def _unified_evidence(
    unified: Mapping[str, Any], reference: CrossResultReference
) -> EngineEvidence:
    coverage = _coverage(_mapping(unified.get("coverage")).get("common_mode"))
    if coverage is CoverageState.UNKNOWN:
        coverage = _combined_coverage(
            _coverage(_mapping(unified.get("coverage")).get("weather", {}).get("mode")),
            _coverage(_mapping(unified.get("coverage")).get("solar", {}).get("mode")),
        )
    return EngineEvidence(
        engine_id="unified_route_analysis",
        availability=coverage,
        result_references=(reference,),
        evidence=(
            EvidenceEntry(
                evidence_class=EvidenceClass.DERIVED,
                temporal_coverage=coverage,
                source_metadata={"upstream_analysis_type": "unified_route_analysis"},
            ),
        ),
        limitations=_strings(unified.get("limitations")),
    )


def _weather_evidence(
    reference: CrossResultReference, resolution: Mapping[str, Any]
) -> EngineEvidence:
    source_type = _string(resolution.get("source_type"))
    coverage = _coverage(resolution.get("temporal_coverage"))
    if not resolution:
        coverage = CoverageState.UNKNOWN
    evidence_class = evidence_class_for_weather_source_type(source_type)
    source_references = ()
    artifact = _string(resolution.get("source_artifact"))
    if artifact:
        source_references = (CrossResultReference("weather.resolver", artifact),)
    gaps = []
    if evidence_class is EvidenceClass.UNKNOWN:
        gaps.append("WEATHER_EVIDENCE_PROVENANCE_UNKNOWN")
    if resolution and _string(resolution.get("model_identity")) is None:
        gaps.append("WEATHER_RESOLVED_MODEL_IDENTITY_UNKNOWN")
    if resolution and _string(resolution.get("model_run_at")) is None:
        gaps.append("WEATHER_MODEL_RUN_TIME_UNKNOWN")
    return EngineEvidence(
        engine_id="weather",
        availability=coverage,
        result_references=(reference,),
        evidence=(
            EvidenceEntry(
                evidence_class=evidence_class,
                temporal_coverage=coverage,
                source_type=source_type,
                provider=_string(resolution.get("provider")),
                product=_string(resolution.get("product")),
                source_metadata={
                    "cache_identity": _string(resolution.get("cache_identity")),
                    "requested_model_selection": _string(resolution.get("model_selection")),
                    "resolved_model_identity": _string(resolution.get("model_identity")),
                },
                source_references=source_references,
                valid_start=_string(resolution.get("valid_start")),
                valid_end=_string(resolution.get("valid_end")),
                fetched_at=_string(resolution.get("fetched_at")),
                model_run_at=_string(resolution.get("model_run_at")),
                reason_codes=_strings(resolution.get("reason_codes")),
            ),
        ),
        data_gaps=tuple(gaps),
    )


def _astronomy_evidence(
    unified: Mapping[str, Any], reference: CrossResultReference
) -> EngineEvidence:
    coverage = _coverage(_mapping(unified.get("coverage")).get("solar", {}).get("mode"))
    return EngineEvidence(
        engine_id="solar_astronomy",
        availability=coverage,
        result_references=(reference,),
        evidence=(
            EvidenceEntry(
                evidence_class=evidence_class_for_deterministic_astronomy(),
                temporal_coverage=coverage,
                source_metadata={"upstream_domain": "unified_route_analysis.solar"},
            ),
        ),
    )


def _terrain_evidence(
    unified: Mapping[str, Any], reference: CrossResultReference
) -> EngineEvidence:
    coverage = _coverage(_mapping(unified.get("coverage")).get("solar", {}).get("mode"))
    solar_provenance = _mapping(_mapping(unified.get("provenance")).get("solar"))
    terrain_reference = _string(solar_provenance.get("terrain_context_id"))
    sources = (CrossResultReference("terrain", terrain_reference),) if terrain_reference else ()
    return EngineEvidence(
        engine_id="terrain_light",
        availability=coverage,
        result_references=(reference,),
        evidence=(
            EvidenceEntry(
                evidence_class=evidence_class_for_terrain_derived(),
                temporal_coverage=coverage,
                source_references=sources,
                source_metadata={"upstream_domain": "unified_route_analysis.solar"},
                reason_codes=_strings(solar_provenance.get("reason_codes")),
            ),
        ),
    )


def _snow_evidence(result: Any) -> EngineEvidence:
    snow = _mapping(result)
    identity = _required_identity(snow.get("identity"), "Snow")
    reference = result_reference_for_identity(identity)
    facts = tuple(_mapping(item) for item in snow.get("snow_facts", ()))
    entries = []
    for fact_type in ("MODELLED_SNOWFALL", "MODELLED_GROUND_SNOW_DEPTH"):
        selected = [
            item for item in facts if _mapping(item.get("semantics")).get("fact_type") == fact_type
        ]
        if not selected:
            entries.append(
                EvidenceEntry(
                    evidence_class=EvidenceClass.UNKNOWN,
                    temporal_coverage=CoverageState.UNAVAILABLE,
                    source_metadata={"snow_fact_type": fact_type, "snow_evidence_type": "MODELLED"},
                    reason_codes=("SNOW_FACT_TYPE_UNAVAILABLE",),
                )
            )
            continue
        source_types = {
            _string(_mapping(item.get("source")).get("source_type")) for item in selected
        }
        evidence_classes = {evidence_class_for_weather_source_type(item) for item in source_types}
        evidence_class = (
            evidence_classes.pop() if len(evidence_classes) == 1 else EvidenceClass.UNKNOWN
        )
        source = _mapping(selected[0].get("source"))
        entries.append(
            EvidenceEntry(
                evidence_class=evidence_class,
                temporal_coverage=_combined_coverages(
                    _coverage(item.get("coverage")) for item in selected
                ),
                source_type=_single(source_types),
                provider=_string(source.get("provider")),
                product=_string(source.get("product")),
                source_metadata={
                    "snow_fact_type": fact_type,
                    "snow_evidence_type": "MODELLED",
                    "requested_model_selection": _string(source.get("requested_model_selection")),
                    "resolved_model_identity": _string(source.get("resolved_model_identity")),
                },
                model_run_at=_string(source.get("model_run_at")),
            )
        )
    availability = _combined_coverages(item.temporal_coverage for item in entries)
    return EngineEvidence(
        engine_id="snow",
        availability=availability,
        result_references=(reference,),
        evidence=tuple(entries),
        limitations=_strings(_mapping(snow.get("quality")).get("limitations")),
        data_gaps=("SNOW_FACT_TYPE_UNAVAILABLE",)
        if any(item.evidence_class is EvidenceClass.UNKNOWN for item in entries)
        else (),
    )


def _photographer_evidence(result: Any) -> EngineEvidence:
    photographer = _mapping(result)
    identity = _required_identity(photographer.get("identity"), "Photographer")
    reference = result_reference_for_identity(identity)
    terrain = _mapping(photographer.get("terrain_light"))
    atmosphere = _mapping(photographer.get("atmospheric_context"))
    source = _mapping(atmosphere.get("source"))
    atmosphere_class = evidence_class_for_weather_source_type(_string(source.get("source_type")))
    entries = (
        EvidenceEntry(
            evidence_class=evidence_class_for_deterministic_astronomy(),
            temporal_coverage=_convention_coverage(photographer),
            source_metadata={"photographer_component": "astronomical_conventions"},
        ),
        EvidenceEntry(
            evidence_class=evidence_class_for_terrain_derived(),
            temporal_coverage=_coverage(terrain.get("coverage")),
            source_metadata={"photographer_component": "terrain_light"},
        ),
        EvidenceEntry(
            evidence_class=atmosphere_class,
            temporal_coverage=_coverage(atmosphere.get("coverage")),
            source_type=_string(source.get("source_type")),
            provider=_string(source.get("provider")),
            product=_string(source.get("product")),
            source_metadata={"photographer_component": "atmospheric_context"},
            source_references=_reference_from_string(
                "photographer.atmospheric", atmosphere.get("source_reference")
            ),
            valid_start=_string(source.get("valid_start")),
            valid_end=_string(source.get("valid_end")),
            fetched_at=_string(source.get("fetched_at")),
            model_run_at=_string(source.get("model_run_at")),
            limitations=_strings(atmosphere.get("limitations")),
            reason_codes=_strings(atmosphere.get("reason_codes")),
        ),
    )
    return EngineEvidence(
        engine_id="photographer",
        availability=_combined_coverages(item.temporal_coverage for item in entries),
        result_references=(reference,),
        evidence=entries,
        limitations=_strings(_mapping(photographer.get("quality")).get("limitations")),
    )


def _rci_evidence(result: Any) -> EngineEvidence:
    rci = _mapping(result)
    metadata = _mapping(rci.get("analysis_metadata"))
    route = _mapping(rci.get("route_reference"))
    route_id = _required_string(route.get("route_id"), "RCI route ID")
    scenario = _mapping(rci.get("planning_reference"))
    scenario_time = _string(scenario.get("start_datetime"))
    reference = CrossResultReference(
        producer="rci",
        result_reference=f"rci:{rci.get('contract_version', 'unknown')}:{route_id}:{scenario_time or 'unknown'}",
    )
    quality = _mapping(rci.get("quality"))
    return EngineEvidence(
        engine_id="rci",
        availability=_coverage(quality.get("coverage")),
        result_references=(reference,),
        evidence=(
            EvidenceEntry(
                evidence_class=EvidenceClass.DERIVED,
                temporal_coverage=_coverage(quality.get("coverage")),
                source_metadata={"upstream_analysis_reference": metadata.get("analysis_reference")},
                reason_codes=_strings(quality.get("reason_codes")),
                limitations=_strings(quality.get("limitations")),
            ),
        ),
        data_gaps=("RCI_UPSTREAM_PROVENANCE_REFERENCE_UNAVAILABLE",)
        if not metadata.get("analysis_reference")
        else (),
    )


def _weather_resolution(unified: Mapping[str, Any]) -> Mapping[str, Any]:
    return _mapping(_mapping(_mapping(unified.get("provenance")).get("weather")).get("resolution"))


def _convention_coverage(photographer: Mapping[str, Any]) -> CoverageState:
    return _combined_coverage(
        _coverage(_mapping(photographer.get("golden_hour")).get("coverage")),
        _coverage(_mapping(photographer.get("blue_hour")).get("coverage")),
    )


def _combined_coverages(items) -> CoverageState:
    values = tuple(items)
    if not values:
        return CoverageState.UNAVAILABLE
    result = values[0]
    for value in values[1:]:
        result = _combined_coverage(result, value)
    return result


def _combined_coverage(first: CoverageState, second: CoverageState) -> CoverageState:
    if CoverageState.UNKNOWN in {first, second}:
        return CoverageState.UNKNOWN
    if CoverageState.UNAVAILABLE in {first, second}:
        return CoverageState.UNAVAILABLE if first is second else CoverageState.PARTIAL
    if CoverageState.PARTIAL in {first, second}:
        return CoverageState.PARTIAL
    if CoverageState.NOT_APPLICABLE in {first, second}:
        return CoverageState.NOT_APPLICABLE if first is second else CoverageState.PARTIAL
    return CoverageState.FULL


def _coverage(value: Any) -> CoverageState:
    raw = getattr(value, "value", value)
    try:
        return CoverageState(raw)
    except (TypeError, ValueError):
        return CoverageState.UNKNOWN


def _reference_from_string(producer: str, value: Any) -> tuple[CrossResultReference, ...]:
    reference = _string(value)
    return (CrossResultReference(producer, reference),) if reference else ()


def _identity(value: Any) -> AnalysisIdentity | None:
    mapping = _mapping(value)
    try:
        return AnalysisIdentity(
            analysis_type=mapping["analysis_type"],
            semantic_type=mapping["semantic_type"],
            version=mapping["version"],
            subject_id=mapping["subject_id"],
            scenario_datetime=mapping.get("scenario_datetime"),
            timezone=mapping.get("timezone"),
        )
    except (KeyError, TypeError):
        return None


def _required_identity(value: Any, label: str) -> AnalysisIdentity:
    identity = _identity(value)
    if identity is None:
        raise ValueError(f"{label} result requires AnalysisIdentity")
    return identity


def _mapping(value: Any) -> Mapping[str, Any]:
    if isinstance(value, Mapping):
        return value
    if hasattr(value, "__dataclass_fields__"):
        return asdict(value)
    return {}


def _strings(value: Any) -> tuple[str, ...]:
    return tuple(sorted({str(item) for item in value or () if item}))


def _string(value: Any) -> str | None:
    raw = getattr(value, "value", value)
    return str(raw) if raw is not None else None


def _single(values: set[str | None]) -> str | None:
    return next(iter(values)) if len(values) == 1 else None


def _required_string(value: Any, label: str) -> str:
    result = _string(value)
    if not result:
        raise ValueError(f"{label} is required")
    return result
