"""Compact Photographer weather context composed only from Unified facts."""

from __future__ import annotations

import math
from collections import Counter
from dataclasses import asdict
from typing import Any, Mapping, Sequence

from mountain_twin.photographer.contracts import (
    AtmosphericContextResult,
    AtmosphericContextScope,
    AtmosphericContextScopeType,
    AtmosphericVariableSummary,
    ConventionWindowResult,
    WeatherSourceContext,
    derive_atmospheric_context_id,
)
from mountain_twin.rci.contracts import CoverageState

PHOTOGRAPHER_ATMOSPHERIC_VARIABLES = (
    "temperature_2m",
    "relative_humidity_2m",
    "apparent_temperature",
    "precipitation",
    "rain",
    "snowfall",
    "precipitation_probability",
    "cloud_cover",
    "cloud_cover_low",
    "cloud_cover_mid",
    "cloud_cover_high",
    "wind_speed_10m",
    "wind_direction_10m",
    "wind_gusts_10m",
    "visibility",
    "freezing_level_height",
    "weather_code",
)

_SOURCE_REFERENCE = "unified_route_analysis.points[].weather"


def project_atmospheric_context(
    unified_analysis: Any,
    *,
    upstream_analysis_reference: str,
    golden_hour: ConventionWindowResult,
    blue_hour: ConventionWindowResult,
) -> AtmosphericContextResult:
    """Summarize existing weather samples without provider or interpolation calls."""
    records = _records(_value(unified_analysis, "points", ()))
    coverage, reasons = _weather_coverage(unified_analysis, records)
    source = _source_context(unified_analysis)
    route_variables = _variable_summaries(records, coverage)
    scopes = [
        _window_scope(records, coverage, upstream_analysis_reference, window, AtmosphericContextScopeType.GOLDEN_HOUR_WINDOW)
        for window in golden_hour.windows
    ]
    scopes.extend(
        _window_scope(records, coverage, upstream_analysis_reference, window, AtmosphericContextScopeType.BLUE_HOUR_WINDOW)
        for window in blue_hour.windows
    )
    direct_records = tuple(record for record in records if record.terrain_state == "DIRECT")
    scopes.append(
        _scope(
            direct_records,
            coverage,
            upstream_analysis_reference,
            AtmosphericContextScopeType.TERRAIN_DIRECT_SAMPLES,
            "terrain-direct-samples",
            empty_reason="NO_DIRECT_TERRAIN_SAMPLES",
        )
    )
    return AtmosphericContextResult(
        context_version="v0_1",
        coverage=coverage,
        source_reference=_SOURCE_REFERENCE,
        source=source,
        route_variables=route_variables,
        scopes=tuple(scopes),
        reason_codes=reasons,
        limitations=(
            "WEATHER_CONTEXT_NOT_LIGHT_TRANSMISSION_MODEL",
            "SAMPLED_MODELLED_RANGES_ARE_NOT_PHOTOGRAPHIC_INTERPRETATION",
            "NO_ATMOSPHERIC_OPTICS_OR_CAUSAL_VISIBILITY_INFERENCE",
        ),
    )


class _WeatherRecord:
    def __init__(self, point_index: int, weather: Mapping[str, Any], terrain_state: str | None):
        self.point_index = point_index
        self.weather = weather
        self.terrain_state = terrain_state


def _records(points: Sequence[Any]) -> tuple[_WeatherRecord, ...]:
    records = []
    previous_index = None
    for point in points:
        point_index = _value(point, "point_index")
        if not isinstance(point_index, int) or point_index < 0:
            raise ValueError("Unified weather point index must be non-negative")
        if previous_index is not None and point_index <= previous_index:
            raise ValueError("Unified weather points must be strictly ordered")
        previous_index = point_index
        solar = _mapping(_value(point, "solar", {}))
        terrain_state = _enum_value(
            solar.get("terrain_solar_visibility", solar.get("status"))
        )
        records.append(_WeatherRecord(point_index, _mapping(_value(point, "weather", {})), terrain_state))
    return tuple(records)


def _weather_coverage(unified_analysis: Any, records: Sequence[_WeatherRecord]) -> tuple[CoverageState, tuple[str, ...]]:
    mode = _enum_value(_value(_value(_value(unified_analysis, "coverage", {}), "weather", {}), "mode"))
    declared = {
        "FULL": CoverageState.FULL,
        "SAMPLED": CoverageState.PARTIAL,
        "PARTIAL": CoverageState.PARTIAL,
        "UNAVAILABLE": CoverageState.UNAVAILABLE,
    }.get(mode, CoverageState.UNKNOWN)
    if declared is CoverageState.UNAVAILABLE:
        return CoverageState.UNAVAILABLE, ("WEATHER_COVERAGE_UNAVAILABLE",)
    if not records:
        return CoverageState.UNAVAILABLE, ("WEATHER_ROUTE_POINTS_UNAVAILABLE",)
    if declared is CoverageState.FULL:
        return CoverageState.FULL, ()
    if declared is CoverageState.PARTIAL:
        return CoverageState.PARTIAL, ("WEATHER_COVERAGE_PARTIAL",)
    return CoverageState.UNKNOWN, ("WEATHER_COVERAGE_UNKNOWN",)


def _source_context(unified_analysis: Any) -> WeatherSourceContext:
    weather_provenance = _mapping(_value(_mapping(_value(unified_analysis, "provenance", {})), "weather", {}))
    resolution = _mapping(weather_provenance.get("resolution", {}))
    return WeatherSourceContext(
        resolution_mode=_string(resolution.get("mode")),
        provider=_string(resolution.get("provider", weather_provenance.get("provider"))),
        product=_string(resolution.get("product")),
        source_type=_string(resolution.get("source_type", weather_provenance.get("source_type"))),
        model_selection=_string(resolution.get("model_selection", weather_provenance.get("model_selection"))),
        model_identity=_string(resolution.get("model_identity")),
        source_artifact=_string(resolution.get("source_artifact")),
        cache_identity=_string(resolution.get("cache_identity")),
        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")),
        intended_use=_string(resolution.get("intended_use")),
        freshness_state=_string(resolution.get("freshness_state")),
        temporal_coverage=_string(resolution.get("temporal_coverage")),
        reason_codes=tuple(sorted(str(item) for item in resolution.get("reason_codes", ()))),
    )


def _window_scope(records, coverage, upstream_reference, window, scope_type):
    selected = tuple(
        record
        for record in records
        if window.first_in_window.route_point_index <= record.point_index <= window.last_in_window.route_point_index
    )
    return _scope(
        selected,
        coverage,
        upstream_reference,
        scope_type,
        window.window_id,
        empty_reason="WEATHER_WINDOW_HAS_NO_RETAINED_ROUTE_SAMPLES",
    )


def _scope(records, coverage, upstream_reference, scope_type, scope_reference, *, empty_reason):
    selected = tuple(records)
    scope_coverage = CoverageState.NOT_APPLICABLE if not selected else coverage
    return AtmosphericContextScope(
        context_id=derive_atmospheric_context_id(
            upstream_analysis_reference=upstream_reference,
            scope_type=scope_type,
            scope_reference=scope_reference,
        ),
        scope_type=scope_type,
        scope_reference=scope_reference,
        selected_sample_count=len(selected),
        coverage=scope_coverage,
        variables=_variable_summaries(selected, scope_coverage),
        route_start_point_index=selected[0].point_index if selected else None,
        route_end_point_index=selected[-1].point_index if selected else None,
        reason_codes=(empty_reason,) if not selected else (),
    )


def _variable_summaries(records: Sequence[_WeatherRecord], domain_coverage: CoverageState) -> tuple[AtmosphericVariableSummary, ...]:
    return tuple(_variable_summary(records, domain_coverage, variable) for variable in PHOTOGRAPHER_ATMOSPHERIC_VARIABLES)


def _variable_summary(records, domain_coverage, variable):
    selected_count = len(records)
    if selected_count == 0:
        return AtmosphericVariableSummary(
            variable_id=variable,
            unit=None,
            coverage=CoverageState.NOT_APPLICABLE,
            selected_sample_count=0,
            available_sample_count=0,
            minimum=None,
            maximum=None,
            reason_codes=("NO_CONTEXT_ROUTE_SAMPLES",),
        )
    if domain_coverage is CoverageState.UNAVAILABLE:
        return AtmosphericVariableSummary(
            variable_id=variable,
            unit=None,
            coverage=CoverageState.UNAVAILABLE,
            selected_sample_count=selected_count,
            available_sample_count=0,
            minimum=None,
            maximum=None,
            reason_codes=("WEATHER_COVERAGE_UNAVAILABLE",),
        )
    items = [_mapping(record.weather.get("values", {})).get(variable) for record in records]
    present = [item for item in items if isinstance(item, Mapping)]
    values = [_finite(item.get("value")) for item in present]
    finite_values = [value for value in values if value is not None]
    units = {str(item.get("unit")) for item, value in zip(present, values) if value is not None and item.get("unit") is not None}
    representation_counts = Counter(
        str(item.get("representation_state", "UNKNOWN")) for item in present
    )
    temporal_counts = Counter(
        str(item.get("temporal_representation_state", "UNKNOWN")) for item in present
    )
    limitations = tuple(sorted({str(code) for item in present for code in item.get("limitation_codes", ())}))
    reasons = []
    if len(units) > 1:
        reasons.append("WEATHER_UNIT_INCONSISTENT")
    if not present:
        coverage = CoverageState.UNAVAILABLE
        reasons.append("WEATHER_VARIABLE_NOT_RETAINED_IN_UNIFIED")
    elif not finite_values:
        coverage = CoverageState.UNKNOWN
        reasons.append("WEATHER_VARIABLE_VALUE_UNAVAILABLE_OR_NONFINITE")
    elif len(finite_values) != selected_count:
        coverage = CoverageState.PARTIAL
        reasons.append("WEATHER_VARIABLE_PARTIAL")
    elif domain_coverage is CoverageState.FULL:
        coverage = CoverageState.FULL
    elif domain_coverage is CoverageState.PARTIAL:
        coverage = CoverageState.PARTIAL
    else:
        coverage = CoverageState.UNKNOWN
    if len(units) > 1:
        coverage = CoverageState.UNKNOWN
        finite_values = []
    return AtmosphericVariableSummary(
        variable_id=variable,
        unit=next(iter(units)) if len(units) == 1 else None,
        coverage=coverage,
        selected_sample_count=selected_count,
        available_sample_count=len(finite_values),
        minimum=min(finite_values) if finite_values else None,
        maximum=max(finite_values) if finite_values else None,
        representation_counts=dict(sorted(representation_counts.items())),
        temporal_representation_counts=dict(sorted(temporal_counts.items())),
        limitation_codes=limitations,
        reason_codes=tuple(sorted(set(reasons))),
    )


def _finite(value: Any) -> float | None:
    if isinstance(value, bool):
        return None
    try:
        numeric = float(value)
    except (TypeError, ValueError):
        return None
    return numeric if math.isfinite(numeric) else None


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


def _value(item: Any, key: str, default: Any = None) -> Any:
    return item.get(key, default) if isinstance(item, Mapping) else getattr(item, key, default)


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


def _string(value: Any) -> str | None:
    return _enum_value(value)
