"""Typed Snow Intelligence facts and compact future-composition contracts.

These contracts establish semantics and provenance only.  They do not request
provider data, project route values, detect signals, or assess safety.
"""

from __future__ import annotations

import hashlib
import json
import math
from dataclasses import asdict, dataclass
from enum import Enum
from typing import Any, Mapping

from mountain_twin.analysis_contract import (
    AnalysisIdentity,
    FreshnessState,
    TemporalProvenance,
    WeatherSourceType,
)
from mountain_twin.rci.contracts import CoverageState, EvidenceQuality


class SnowFactType(str, Enum):
    """Environmental quantities that must not be silently conflated."""

    MODELLED_SNOWFALL = "MODELLED_SNOWFALL"
    MODELLED_GROUND_SNOW_DEPTH = "MODELLED_GROUND_SNOW_DEPTH"
    OBSERVED_FRACTIONAL_GROUND_SNOW_COVER = "OBSERVED_FRACTIONAL_GROUND_SNOW_COVER"


class SnowEvidenceType(str, Enum):
    """Origin class, independent of a value's numerical unit."""

    MODELLED = "MODELLED"
    OBSERVED = "OBSERVED"
    OBSERVATION_COMPOSITE = "OBSERVATION_COMPOSITE"


class SnowTemporalSemantics(str, Enum):
    """Native source-time meaning; separate from route-time projection."""

    PRECEDING_HOUR_AMOUNT = "PRECEDING_HOUR_AMOUNT"
    INSTANTANEOUS_SOURCE_VALUE = "INSTANTANEOUS_SOURCE_VALUE"
    OBSERVATION_WINDOW = "OBSERVATION_WINDOW"


class SnowSpatialRepresentation(str, Enum):
    """How a fact relates to its spatial source support."""

    EXACT_SOURCE_SUPPORT = "EXACT_SOURCE_SUPPORT"
    NEAREST_PROVIDER_ROUTE_SAMPLE = "NEAREST_PROVIDER_ROUTE_SAMPLE"
    SOURCE_RASTER_SAMPLE = "SOURCE_RASTER_SAMPLE"
    UNKNOWN = "UNKNOWN"


class SnowTemporalProjection(str, Enum):
    """How a route fact relates to its valid source time/window."""

    EXACT_SOURCE_SUPPORT = "EXACT_SOURCE_SUPPORT"
    NEAREST_PROVIDER_HOUR = "NEAREST_PROVIDER_HOUR"
    SOURCE_OBSERVATION_WINDOW = "SOURCE_OBSERVATION_WINDOW"
    UNKNOWN = "UNKNOWN"


@dataclass(frozen=True)
class SnowFieldSemantics:
    """Provider-specific source semantics plus Mountain Twin representation identity."""

    fact_type: SnowFactType
    evidence_type: SnowEvidenceType
    provider_field_name: str | None
    canonical_unit: str
    native_temporal_semantics: SnowTemporalSemantics
    upstream_semantics_id: str
    mountain_twin_representation_id: str
    source_representation: str
    limitations: tuple[str, ...] = ()

    def __post_init__(self) -> None:
        if not all(
            (
                self.canonical_unit,
                self.upstream_semantics_id,
                self.mountain_twin_representation_id,
                self.source_representation,
            )
        ):
            raise ValueError("snow field semantics require stable semantic identities")
        if self.fact_type is SnowFactType.MODELLED_SNOWFALL:
            if (
                self.evidence_type is not SnowEvidenceType.MODELLED
                or self.provider_field_name != "snowfall"
                or self.canonical_unit != "cm"
                or self.native_temporal_semantics is not SnowTemporalSemantics.PRECEDING_HOUR_AMOUNT
            ):
                raise ValueError("modelled snowfall semantics are invalid")
        if self.fact_type is SnowFactType.MODELLED_GROUND_SNOW_DEPTH:
            if (
                self.evidence_type is not SnowEvidenceType.MODELLED
                or self.provider_field_name != "snow_depth"
                or self.canonical_unit != "m"
                or self.native_temporal_semantics
                is not SnowTemporalSemantics.INSTANTANEOUS_SOURCE_VALUE
            ):
                raise ValueError("modelled ground snow-depth semantics are invalid")
        if self.fact_type is SnowFactType.OBSERVED_FRACTIONAL_GROUND_SNOW_COVER:
            if (
                self.evidence_type
                not in {SnowEvidenceType.OBSERVED, SnowEvidenceType.OBSERVATION_COMPOSITE}
                or self.canonical_unit != "%"
            ):
                raise ValueError("observed fractional snow-cover semantics are invalid")

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


MODELLED_SNOWFALL_SEMANTICS_V0_1 = SnowFieldSemantics(
    fact_type=SnowFactType.MODELLED_SNOWFALL,
    evidence_type=SnowEvidenceType.MODELLED,
    provider_field_name="snowfall",
    canonical_unit="cm",
    native_temporal_semantics=SnowTemporalSemantics.PRECEDING_HOUR_AMOUNT,
    upstream_semantics_id="open_meteo_hourly_snowfall_preceding_hour_v0_1",
    mountain_twin_representation_id="mt_modelled_snowfall_preceding_hour_v0_1",
    source_representation="MODELLED",
    limitations=(
        "MODELLED_SNOWFALL_IS_NOT_SNOW_DEPTH_OR_SNOW_COVER",
        "MODELLED_SNOWFALL_IS_NOT_INSTANTANEOUS_RATE_OR_DEPOSITED_GROUND_SNOW",
    ),
)

MODELLED_GROUND_SNOW_DEPTH_SEMANTICS_V0_1 = SnowFieldSemantics(
    fact_type=SnowFactType.MODELLED_GROUND_SNOW_DEPTH,
    evidence_type=SnowEvidenceType.MODELLED,
    provider_field_name="snow_depth",
    canonical_unit="m",
    native_temporal_semantics=SnowTemporalSemantics.INSTANTANEOUS_SOURCE_VALUE,
    upstream_semantics_id="open_meteo_modelled_snow_depth_instantaneous_v0_1",
    mountain_twin_representation_id="mt_modelled_ground_snow_depth_v0_1",
    source_representation="MODELLED",
    limitations=(
        "MODELLED_GROUND_SNOW_DEPTH_IS_NOT_OBSERVED_SNOW_DEPTH_OR_FRACTIONAL_SNOW_COVER",
        "MODELLED_GROUND_SNOW_DEPTH_AVAILABILITY_DEPENDS_ON_PROVIDER_RESPONSE",
    ),
)


@dataclass(frozen=True)
class SnowSourceProvenance:
    """Source/resolver provenance without inventing a resolved model identity."""

    resolver_state: str | None
    source_type: WeatherSourceType | None
    provider: str | None
    product: str | None
    requested_model_selection: str | None
    resolved_model_identity: str | None
    model_run_at: str | None
    temporal_provenance: TemporalProvenance | None = None
    freshness_state: FreshnessState | None = None
    reason_codes: tuple[str, ...] = ()
    limitations: tuple[str, ...] = ()

    def __post_init__(self) -> None:
        if self.resolved_model_identity == "best_match":
            raise ValueError("requested model selection is not a resolved model identity")
        if self.temporal_provenance is not None and self.freshness_state is not None:
            if self.freshness_state is not self.temporal_provenance.freshness_state:
                raise ValueError("snow freshness must agree with temporal provenance")

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


@dataclass(frozen=True)
class SnowFact:
    """One typed, source-labelled snow fact or explicit missing fact."""

    fact_id: str
    semantics: SnowFieldSemantics
    value: float | None
    coverage: CoverageState
    source: SnowSourceProvenance
    spatial_representation: SnowSpatialRepresentation
    temporal_projection: SnowTemporalProjection
    projection_policy_id: str | None = None
    upstream_reference: str | None = None
    limitation_codes: tuple[str, ...] = ()
    reason_codes: tuple[str, ...] = ()
    route_point_index: int | None = None
    route_distance_m: float | None = None
    planned_arrival_time: str | None = None
    provider_valid_times: tuple[str, ...] = ()
    source_interval_start: str | None = None
    source_interval_end: str | None = None

    def __post_init__(self) -> None:
        if not self.fact_id:
            raise ValueError("snow fact requires stable ID")
        if self.value is not None and (not math.isfinite(self.value) or self.value < 0):
            raise ValueError("snow fact value must be finite and non-negative when present")
        if self.semantics.fact_type is SnowFactType.OBSERVED_FRACTIONAL_GROUND_SNOW_COVER:
            if self.value is not None and self.value > 100:
                raise ValueError("fractional ground snow cover must be in percent 0..100")
            if self.source.source_type in {
                WeatherSourceType.FORECAST,
                WeatherSourceType.HISTORICAL_FORECAST,
                WeatherSourceType.REANALYSIS,
            }:
                raise ValueError("observed snow cover cannot use a modelled weather source type")
        elif self.semantics.evidence_type is SnowEvidenceType.MODELLED:
            if self.source.source_type in {
                WeatherSourceType.OBSERVATION,
                WeatherSourceType.NOWCAST,
            }:
                raise ValueError("modelled snow fact cannot use observation/nowcast source type")
        if self.semantics.fact_type is SnowFactType.MODELLED_GROUND_SNOW_DEPTH:
            if not self.projection_policy_id:
                raise ValueError("modelled ground snow depth requires a projection policy identity")
            if self.spatial_representation is SnowSpatialRepresentation.SOURCE_RASTER_SAMPLE:
                raise ValueError(
                    "modelled ground snow depth cannot use observed-raster representation"
                )
        if (self.source_interval_start is None) != (self.source_interval_end is None):
            raise ValueError("snow source interval requires both start and end")
        if (
            self.semantics.native_temporal_semantics
            is SnowTemporalSemantics.INSTANTANEOUS_SOURCE_VALUE
            and (self.source_interval_start is not None or self.source_interval_end is not None)
        ):
            raise ValueError("instantaneous snow fact cannot carry an interval")

    @property
    def is_missing(self) -> bool:
        return self.value is None

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


@dataclass(frozen=True)
class SnowRouteTimelineReference:
    """References upstream route/timeline analysis without copying its point matrix."""

    route_id: str
    scenario_name: str
    upstream_analysis_reference: str
    timeline_reference: str

    def __post_init__(self) -> None:
        if not all(
            (
                self.route_id,
                self.scenario_name,
                self.upstream_analysis_reference,
                self.timeline_reference,
            )
        ):
            raise ValueError("snow route/timeline reference requires stable identities")

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


@dataclass(frozen=True)
class SnowIntelligenceResult:
    """Minimal structural result for a later Snow Core composition step."""

    identity: AnalysisIdentity
    analysis_metadata: Mapping[str, Any]
    route_timeline_reference: SnowRouteTimelineReference
    snow_facts: tuple[SnowFact, ...]
    quality: EvidenceQuality
    provenance: Mapping[str, Any]
    diagnostics: tuple[str, ...] = ()
    contract_version: str = "0.1"

    def __post_init__(self) -> None:
        if self.identity.analysis_type != "snow_intelligence":
            raise ValueError("snow result analysis type is invalid")
        if self.identity.semantic_type != "route_snow_context":
            raise ValueError("snow result semantic type is invalid")
        if self.contract_version != "0.1":
            raise ValueError("unsupported snow contract version")
        if self.identity.subject_id != self.route_timeline_reference.route_id:
            raise ValueError("snow route reference must match result subject")
        if len({fact.fact_id for fact in self.snow_facts}) != len(self.snow_facts):
            raise ValueError("snow fact IDs must be unique")

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


def derive_snow_fact_id(
    *,
    semantics: SnowFieldSemantics,
    upstream_reference: str,
    route_point_index: int | None,
    provider_valid_reference: str | None,
    projection_policy_id: str | None = None,
) -> str:
    """Derive a stable fact identity from semantics and source support, not value."""
    identity = {
        "fact_type": semantics.fact_type.value,
        "mountain_twin_representation_id": semantics.mountain_twin_representation_id,
        "projection_policy_id": projection_policy_id,
        "provider_valid_reference": provider_valid_reference,
        "route_point_index": route_point_index,
        "upstream_reference": upstream_reference,
    }
    encoded = json.dumps(identity, sort_keys=True, separators=(",", ":"), allow_nan=False)
    return "snow-fact-" + hashlib.sha256(encoded.encode("utf-8")).hexdigest()[:24]


def serialize_snow_intelligence_result(result: SnowIntelligenceResult) -> str:
    """Canonical serialization without a duplicated upstream route-point matrix."""
    return json.dumps(result.to_dict(), sort_keys=True, separators=(",", ":"), allow_nan=False)


def _json_value(value: Any) -> Any:
    if isinstance(value, Enum):
        return value.value
    if isinstance(value, Mapping):
        return {str(key): _json_value(value[key]) for key in sorted(value, key=str)}
    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
