"""Compose independent, activity-neutral Snow facts from Unified evidence."""

from __future__ import annotations

from datetime import datetime, timedelta
from typing import Any, Mapping

from mountain_twin.analysis_contract import (
    AnalysisIdentity,
    FreshnessState,
    TemporalProvenance,
    WeatherSourceType,
)
from mountain_twin.rci.contracts import CoverageState, EvidenceQuality
from mountain_twin.snow.contracts import (
    MODELLED_GROUND_SNOW_DEPTH_SEMANTICS_V0_1,
    MODELLED_SNOWFALL_SEMANTICS_V0_1,
    SnowFact,
    SnowIntelligenceResult,
    SnowRouteTimelineReference,
    SnowSourceProvenance,
    SnowSpatialRepresentation,
    SnowTemporalProjection,
    derive_snow_fact_id,
)
from mountain_twin.snow.policy import MODELLED_SNOW_DEPTH_NEAREST_CONTEXT_POLICY_V0_1

_FACTS = (
    ("snowfall", MODELLED_SNOWFALL_SEMANTICS_V0_1),
    ("snow_depth", MODELLED_GROUND_SNOW_DEPTH_SEMANTICS_V0_1),
)


def compose_snow_intelligence(unified_analysis: Any) -> SnowIntelligenceResult:
    """Make per-point Snow facts without recalculating or widening Unified.

    Snowfall and ground depth deliberately remain separate facts.  The only
    route values retained are the support needed to identify each fact.
    """
    route = _mapping(_value(unified_analysis, "route", {}))
    scenario = _mapping(_value(unified_analysis, "scenario", {}))
    identity = _mapping(_value(unified_analysis, "identity", {}))
    route_id = _required_string(route.get("route_id"), "Unified analysis route ID")
    scenario_name = _required_string(scenario.get("name"), "Unified analysis scenario name")
    points = tuple(_value(unified_analysis, "points", ()))
    expected_count = route.get("point_count")
    if isinstance(expected_count, int) and expected_count != len(points):
        raise ValueError("Unified route point matrix is incomplete for Snow composition")
    upstream_reference = _analysis_reference(identity, route_id, scenario_name)
    provenance = _mapping(_value(unified_analysis, "provenance", {}))
    weather_provenance = _mapping(provenance.get("weather", {}))
    resolution = _mapping(weather_provenance.get("resolution", {}))
    point_provenance = _mapping(weather_provenance.get("point_provenance", {}))

    facts = tuple(
        fact
        for point in points
        for variable, semantics in _FACTS
        for fact in (
            _compose_fact(
                point=point,
                variable=variable,
                semantics=semantics,
                upstream_reference=upstream_reference,
                resolution=resolution,
                point_provenance=_mapping(
                    point_provenance.get(str(_value(point, "point_index")), {})
                ),
                fallback_provider=weather_provenance.get("provider"),
            ),
        )
    )
    coverage, coverage_reasons = _result_coverage(facts)
    return SnowIntelligenceResult(
        identity=AnalysisIdentity(
            analysis_type="snow_intelligence",
            semantic_type="route_snow_context",
            version="0.1",
            subject_id=route_id,
            scenario_datetime=scenario.get("start_datetime"),
            timezone=route.get("timezone") or identity.get("timezone"),
        ),
        analysis_metadata={
            "upstream_analysis_reference": upstream_reference,
            "upstream_analysis_type": identity.get("analysis_type"),
            "upstream_analysis_version": identity.get("version"),
            "snow_computation": "REUSED_FROM_UNIFIED_ROUTE_ANALYSIS",
            "timeline_computation": "REUSED_FROM_UNIFIED_ROUTE_ANALYSIS",
            "state_series_retained_publicly": False,
        },
        route_timeline_reference=SnowRouteTimelineReference(
            route_id=route_id,
            scenario_name=scenario_name,
            upstream_analysis_reference=upstream_reference,
            timeline_reference=f"{upstream_reference}:timeline",
        ),
        snow_facts=facts,
        quality=EvidenceQuality(
            coverage=coverage,
            source="unified_route_analysis.weather",
            derived_status="TYPED_MODELLED_SNOW_EVIDENCE",
            reason_codes=coverage_reasons,
            limitations=(
                "SNOWFALL_AND_GROUND_SNOW_DEPTH_REMAIN_INDEPENDENT_EVIDENCE_FACTS",
                "RESULT_DOES_NOT_INFER_SNOWPACK_SAFETY_OR_ROUTE_PASSABILITY",
            ),
        ),
        provenance={
            "upstream_analysis_reference": upstream_reference,
            "upstream_weather_reference": "unified_route_analysis.points[].weather",
            "upstream_weather_provenance_reference": "unified_route_analysis.provenance.weather",
            "resolver": dict(resolution),
        },
        diagnostics=(
            "NO_SYNTHETIC_SNOW_TRUTH_OR_SNOW_SIGNAL_COMPOSED",
            "MISSING_OR_UNAVAILABLE_EVIDENCE_IS_NOT_ZERO",
        ),
    )


def _compose_fact(
    *,
    point,
    variable,
    semantics,
    upstream_reference,
    resolution,
    point_provenance,
    fallback_provider,
):
    weather = _mapping(_value(point, "weather", {}))
    item = _mapping(_mapping(weather.get("values", {})).get(variable, {}))
    value = _number_or_none(item.get("value"))
    coverage = _coverage(item.get("coverage"), value)
    valid_times = tuple(
        str(time) for time in item.get("temporal_contributing_valid_times", ()) if time
    )
    if value is not None and not valid_times:
        valid_times = (str(_value(point, "planned_arrival_time")),)
    semantics_document = _mapping(item.get("field_semantics", {}))
    if semantics_document and semantics_document.get("fact_type") != semantics.fact_type.value:
        raise ValueError(f"Unified {variable} semantics do not match Snow contract")
    if item.get("unit") not in {None, semantics.canonical_unit}:
        raise ValueError(f"Unified {variable} unit does not match Snow contract")
    policy_id = item.get("projection_policy_id")
    if variable == "snow_depth":
        if policy_id not in {None, MODELLED_SNOW_DEPTH_NEAREST_CONTEXT_POLICY_V0_1.policy_id}:
            raise ValueError("Unified snow-depth projection policy does not match Snow contract")
        policy_id = MODELLED_SNOW_DEPTH_NEAREST_CONTEXT_POLICY_V0_1.policy_id
    source = _source(
        resolution=resolution,
        point_provenance=point_provenance,
        provider=weather.get("provider") or fallback_provider,
        source_type=weather.get("source_type"),
    )
    reasons = tuple(sorted(set(_strings(item.get("reason_codes")) | _strings(source.reason_codes))))
    limitations = tuple(
        sorted(
            set(
                _strings(item.get("limitation_codes"))
                | _strings(source.limitations)
                | set(semantics.limitations)
            )
        )
    )
    if not item:
        reasons = tuple(sorted(set(reasons) | {"UNIFIED_SNOW_EVIDENCE_NOT_PRESENT"}))
    if value is None and not reasons:
        reasons = ("UNIFIED_SNOW_VALUE_UNAVAILABLE",)
    interval_start, interval_end = _snowfall_interval(
        semantics.native_temporal_semantics.value, valid_times
    )
    return SnowFact(
        fact_id=derive_snow_fact_id(
            semantics=semantics,
            upstream_reference=f"{upstream_reference}:points[{_value(point, 'point_index')}].weather.values.{variable}",
            route_point_index=_value(point, "point_index"),
            provider_valid_reference=valid_times[0] if len(valid_times) == 1 else None,
            projection_policy_id=policy_id,
        ),
        semantics=semantics,
        value=value,
        coverage=coverage,
        source=source,
        spatial_representation=_spatial(item, variable),
        temporal_projection=_temporal(item, variable),
        projection_policy_id=policy_id,
        upstream_reference=f"{upstream_reference}:points[{_value(point, 'point_index')}].weather.values.{variable}",
        limitation_codes=limitations,
        reason_codes=reasons,
        route_point_index=_value(point, "point_index"),
        route_distance_m=_value(point, "route_distance_m"),
        planned_arrival_time=_value(point, "planned_arrival_time"),
        provider_valid_times=valid_times,
        source_interval_start=interval_start,
        source_interval_end=interval_end,
    )


def _source(*, resolution, point_provenance, provider, source_type):
    source_value = resolution.get("source_type", source_type)
    try:
        source = WeatherSourceType(source_value) if source_value else None
    except ValueError:
        source = None
    temporal = _temporal_provenance(point_provenance)
    freshness = (
        temporal.freshness_state if temporal else _freshness(resolution.get("freshness_state"))
    )
    return SnowSourceProvenance(
        resolver_state=resolution.get("mode"),
        source_type=source,
        provider=provider,
        product=resolution.get("product"),
        requested_model_selection=resolution.get("model_selection"),
        resolved_model_identity=resolution.get("model_identity"),
        model_run_at=resolution.get("model_run_at"),
        temporal_provenance=temporal,
        freshness_state=freshness,
        reason_codes=tuple(
            sorted(
                _strings(resolution.get("reason_codes"))
                | _strings(point_provenance.get("consistency_reason_codes"))
            )
        ),
        limitations=(),
    )


def _temporal_provenance(data):
    if not data:
        return None
    fields = {key: data[key] for key in TemporalProvenance.__dataclass_fields__ if key in data}
    if not fields:
        return None
    if "freshness_state" in fields:
        fields["freshness_state"] = _freshness(fields["freshness_state"])
    if "intended_use_context" not in fields:
        return None
    return TemporalProvenance(**fields)


def _snowfall_interval(semantics, valid_times):
    if semantics != "PRECEDING_HOUR_AMOUNT" or len(valid_times) != 1:
        return None, None
    try:
        end = datetime.fromisoformat(valid_times[0])
    except ValueError:
        return None, None
    return (end - timedelta(hours=1)).isoformat(), end.isoformat()


def _spatial(item, variable):
    explicit = item.get("snow_spatial_representation") if variable == "snow_depth" else None
    value = explicit or item.get("representation_state") or item.get("spatial_representation")
    return {
        "EXACT_SOURCE_SUPPORT": SnowSpatialRepresentation.EXACT_SOURCE_SUPPORT,
        "PROVIDER_SAMPLE": SnowSpatialRepresentation.EXACT_SOURCE_SUPPORT,
        "NEAREST_PROVIDER_ROUTE_SAMPLE": SnowSpatialRepresentation.NEAREST_PROVIDER_ROUTE_SAMPLE,
        "NEAREST_CONTEXT": SnowSpatialRepresentation.NEAREST_PROVIDER_ROUTE_SAMPLE,
    }.get(value, SnowSpatialRepresentation.UNKNOWN)


def _temporal(item, variable):
    explicit = item.get("snow_temporal_projection") if variable == "snow_depth" else None
    value = (
        explicit or item.get("temporal_representation_state") or item.get("temporal_representation")
    )
    return {
        "EXACT_SOURCE_SUPPORT": SnowTemporalProjection.EXACT_SOURCE_SUPPORT,
        "PROVIDER_TIME": SnowTemporalProjection.EXACT_SOURCE_SUPPORT,
        "NEAREST_PROVIDER_HOUR": SnowTemporalProjection.NEAREST_PROVIDER_HOUR,
        "NEAREST_CONTEXT": SnowTemporalProjection.NEAREST_PROVIDER_HOUR,
    }.get(value, SnowTemporalProjection.UNKNOWN)


def _coverage(value, number):
    try:
        return (
            CoverageState(value)
            if value
            else (CoverageState.FULL if number is not None else CoverageState.UNAVAILABLE)
        )
    except ValueError:
        return CoverageState.UNKNOWN


def _result_coverage(facts):
    states = {fact.coverage for fact in facts}
    if not facts:
        return CoverageState.UNAVAILABLE, ("SNOW_ROUTE_POINTS_UNAVAILABLE",)
    if states == {CoverageState.FULL}:
        return CoverageState.FULL, ()
    if states <= {CoverageState.UNAVAILABLE}:
        return CoverageState.UNAVAILABLE, ("SNOW_EVIDENCE_UNAVAILABLE",)
    if CoverageState.UNKNOWN in states:
        return CoverageState.UNKNOWN, ("SNOW_EVIDENCE_COVERAGE_UNKNOWN",)
    return CoverageState.PARTIAL, ("SNOW_EVIDENCE_COVERAGE_PARTIAL",)


def _analysis_reference(identity, route_id, scenario_name):
    return f"unified:{identity.get('version', 'unknown')}:{route_id}:{scenario_name}"


def _value(item, name, default=None):
    return item.get(name, default) if isinstance(item, Mapping) else getattr(item, name, default)


def _mapping(value):
    return value if isinstance(value, Mapping) else {}


def _required_string(value, label):
    if not isinstance(value, str) or not value:
        raise ValueError(f"{label} is required")
    return value


def _number_or_none(value):
    return float(value) if isinstance(value, (int, float)) and not isinstance(value, bool) else None


def _strings(value):
    return {str(item) for item in value or () if item is not None}


def _freshness(value):
    try:
        return FreshnessState(value) if value else FreshnessState.UNKNOWN
    except ValueError:
        return FreshnessState.UNKNOWN
