"""Local, explicit weather-source resolution for planned route analysis."""

from __future__ import annotations

import json
from dataclasses import asdict, dataclass
from datetime import datetime
from enum import Enum
from pathlib import Path
from typing import Any

from mountain_twin.analysis_contract import (
    FreshnessState,
    IntendedUseContext,
    WeatherSourceType,
)
from mountain_twin.weather.temporal import TemporalWeatherField, load_temporal_field


class WeatherResolutionMode(str, Enum):
    """How a local source was resolved for the requested planning window."""

    HISTORICAL = "HISTORICAL"
    FORECAST = "FORECAST"
    UNAVAILABLE = "UNAVAILABLE"


@dataclass(frozen=True)
class WeatherResolution:
    """Deterministic source selection and temporal-support provenance."""

    mode: WeatherResolutionMode
    provider: str | None
    product: str | None
    source_type: WeatherSourceType | None
    model_selection: str | None
    model_identity: str | None
    source_artifact: str | None
    cache_identity: str | None
    valid_start: str | None
    valid_end: str | None
    fetched_at: str | None
    model_run_at: str | None
    intended_use: IntendedUseContext
    freshness_state: FreshnessState
    reason_codes: tuple[str, ...]
    requested_start: str
    requested_end: str
    temporal_coverage: str

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


@dataclass(frozen=True)
class ResolvedWeatherSource:
    resolution: WeatherResolution
    field: TemporalWeatherField | None


@dataclass(frozen=True)
class _LocalSource:
    mode: WeatherResolutionMode
    summary_path: Path
    cache_path: Path
    document: dict[str, Any]
    field: TemporalWeatherField
    valid_start: datetime
    valid_end: datetime


class LocalWeatherProviderResolver:
    """Resolve only explicitly inventoried local weather artifacts.

    This resolver deliberately does not fetch data.  A date is supported only
    when an inventoried source has the requested local planning date.
    """

    def __init__(self, root: Path, *, route_id: str = "tmb_day_01") -> None:
        self.root = root.resolve()
        self.route_id = route_id
        self.sources = self._load_sources()

    def resolve(self, requested_start: datetime, requested_end: datetime) -> ResolvedWeatherSource:
        if requested_start.tzinfo is None or requested_start.utcoffset() is None:
            raise ValueError("weather resolution start must be timezone-aware")
        if requested_end.tzinfo is None or requested_end.utcoffset() is None:
            raise ValueError("weather resolution end must be timezone-aware")
        if requested_end < requested_start:
            raise ValueError("weather resolution window is reversed")

        local_date = requested_start.astimezone(requested_start.tzinfo).date()
        candidates = [source for source in self.sources if source.valid_start.date() == local_date]
        if candidates:
            source = candidates[0]
            full = requested_start >= source.valid_start and requested_end <= source.valid_end
            resolution = self._resolution(
                source,
                requested_start,
                requested_end,
                temporal_coverage="FULL" if full else "PARTIAL",
                reason_codes=(() if full else ("WEATHER_REQUEST_WINDOW_PARTIAL",)),
            )
            return ResolvedWeatherSource(resolution, source.field)

        resolution = WeatherResolution(
            mode=WeatherResolutionMode.UNAVAILABLE,
            provider=None,
            product=None,
            source_type=None,
            model_selection=None,
            model_identity=None,
            source_artifact=None,
            cache_identity=None,
            valid_start=None,
            valid_end=None,
            fetched_at=None,
            model_run_at=None,
            intended_use=IntendedUseContext.PLANNING_CONTEXT,
            freshness_state=FreshnessState.UNKNOWN,
            reason_codes=("WEATHER_SOURCE_UNAVAILABLE_FOR_PLANNING_DATE",),
            requested_start=requested_start.isoformat(),
            requested_end=requested_end.isoformat(),
            temporal_coverage="UNAVAILABLE",
        )
        return ResolvedWeatherSource(resolution, None)

    def _load_sources(self) -> tuple[_LocalSource, ...]:
        candidates = (
            (
                WeatherResolutionMode.HISTORICAL,
                self.root / "docs/tmb_day_01_weather_historical_forecast_v0_1.json",
            ),
            (
                WeatherResolutionMode.FORECAST,
                self.root / "docs/tmb_day_01_weather_forecast_v0_1.json",
            ),
        )
        sources = []
        for mode, summary_path in candidates:
            if not summary_path.exists():
                continue
            document = json.loads(summary_path.read_text(encoding="utf-8"))
            if document.get("route_id", self.route_id) != self.route_id:
                continue
            cache_key = document.get("request_provenance", {}).get("cache_key")
            if not cache_key:
                continue
            cache_path = self.root / "data/cache/weather/open_meteo" / f"{cache_key}.json"
            if not cache_path.exists():
                continue
            field = load_temporal_field(summary_path, cache_path)
            starts = [sample.times[0] for sample in field.samples if sample.times]
            ends = [sample.times[-1] for sample in field.samples if sample.times]
            if not starts or not ends:
                continue
            sources.append(
                _LocalSource(
                    mode=mode,
                    summary_path=summary_path,
                    cache_path=cache_path,
                    document=document,
                    field=field,
                    valid_start=max(starts),
                    valid_end=min(ends),
                )
            )
        return tuple(sources)

    @staticmethod
    def _resolution(
        source: _LocalSource,
        requested_start: datetime,
        requested_end: datetime,
        *,
        temporal_coverage: str,
        reason_codes: tuple[str, ...],
    ) -> WeatherResolution:
        document = source.document
        samples = document.get("samples", ())
        first_contract = samples[0].get("contract", {}) if samples else {}
        method = first_contract.get("method", {})
        payload = first_contract.get("payload", {})
        provenance = document.get("request_provenance", {})
        return WeatherResolution(
            mode=source.mode,
            provider=document.get("provider"),
            product=document.get("endpoint_type") or document.get("api_family"),
            source_type=WeatherSourceType(document["source_type"]),
            model_selection=document.get("model_selection"),
            model_identity=method.get("model_version"),
            source_artifact=str(source.summary_path.relative_to(source.summary_path.parents[1])),
            cache_identity=provenance.get("cache_key"),
            valid_start=source.valid_start.isoformat(),
            valid_end=source.valid_end.isoformat(),
            fetched_at=document.get("fetched_at") or provenance.get("fetched_at"),
            model_run_at=payload.get("provider_run_time"),
            intended_use=IntendedUseContext.PLANNING_CONTEXT,
            freshness_state=FreshnessState.UNKNOWN,
            reason_codes=reason_codes,
            requested_start=requested_start.isoformat(),
            requested_end=requested_end.isoformat(),
            temporal_coverage=temporal_coverage,
        )


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