"""Small provider-labelled contracts shared by Mountain Twin analyses.

The contract keeps generic evaluation state and provenance separate from an
analysis-specific payload.  It is deliberately descriptive: it carries
limitations and unresolved reasons, but it does not manufacture probabilities.
"""

from __future__ import annotations

from dataclasses import asdict, dataclass, field
from enum import Enum
from typing import Any, Mapping


class EvaluationState(str, Enum):
    """Generic scientific state, independent of an engine's domain status."""

    SUCCESS = "SUCCESS"
    UNRESOLVED = "UNRESOLVED"
    UNSUPPORTED = "UNSUPPORTED"
    NUMERICAL_FAILURE = "NUMERICAL_FAILURE"


class SolarVisibility(str, Enum):
    """Visibility outcome used by the solar payload."""

    DIRECT = "DIRECT"
    SHADOW = "SHADOW"
    NOT_APPLICABLE = "NOT_APPLICABLE"
    UNKNOWN = "UNKNOWN"


class WeatherSourceType(str, Enum):
    """Origin semantics for weather values; model output is not an observation."""

    FORECAST = "FORECAST"
    HISTORICAL_FORECAST = "HISTORICAL_FORECAST"
    REANALYSIS = "REANALYSIS"
    OBSERVATION = "OBSERVATION"
    NOWCAST = "NOWCAST"


class WeatherRepresentationState(str, Enum):
    """How a route value relates to provider samples."""

    PROVIDER_SAMPLE = "PROVIDER_SAMPLE"
    INTERPOLATED = "INTERPOLATED"
    NEAREST_CONTEXT = "NEAREST_CONTEXT"
    UNKNOWN = "UNKNOWN"


class WeatherTemporalRepresentationState(str, Enum):
    """How a value relates to provider-valid hourly times."""

    PROVIDER_TIME = "PROVIDER_TIME"
    TEMPORAL_INTERPOLATED = "TEMPORAL_INTERPOLATED"
    NEAREST_CONTEXT = "NEAREST_CONTEXT"
    UNKNOWN = "UNKNOWN"


class FreshnessState(str, Enum):
    """Freshness classification; thresholds are policy-specific, not universal."""

    FRESH = "FRESH"
    AGED = "AGED"
    STALE = "STALE"
    UNKNOWN = "UNKNOWN"


class IntendedUseContext(str, Enum):
    """Context in which a result is evaluated."""

    PLANNING_CONTEXT = "PLANNING_CONTEXT"
    LIVE_CONTEXT = "LIVE_CONTEXT"
    ALERT_CONTEXT = "ALERT_CONTEXT"


@dataclass(frozen=True)
class AnalysisIdentity:
    analysis_type: str
    semantic_type: str
    version: str
    subject_id: str
    scenario_datetime: str | None = None
    timezone: str | None = None


@dataclass(frozen=True)
class ResultLocation:
    latitude: float
    longitude: float
    point_index: int | None = None
    route_distance_m: float | None = None
    route_elevation_m: float | None = None


@dataclass(frozen=True)
class ProviderProvenance:
    provider: str | None
    product: str | None
    product_variant: str | None = None
    surface_semantics: str | None = None
    native_resolution: tuple[float, ...] | None = None
    horizontal_crs: str | None = None
    vertical_reference: str | None = None
    acquisition_context: str | None = None
    validity_context: str | None = None
    limitations: tuple[str, ...] = ()


@dataclass(frozen=True)
class MethodProvenance:
    method: str
    model_version: str | None = None
    observer_model: str | None = None
    interpolation_method: str | None = None
    range_policy: Mapping[str, Any] = field(default_factory=dict)
    thresholds: Mapping[str, Any] = field(default_factory=dict)
    assumptions: tuple[str, ...] = ()


@dataclass(frozen=True)
class ResultQuality:
    outcome: EvaluationState
    reason_codes: tuple[str, ...] = ()
    data_support_state: str = "not_evaluated"
    numerical_convergence_state: str = "not_evaluated"
    range_convergence_state: str = "not_evaluated"
    limitations: tuple[str, ...] = ()


@dataclass(frozen=True)
class SolarPayload:
    visibility: SolarVisibility
    solar_azimuth_deg: float
    solar_elevation_deg: float
    horizon_angle_deg: float | None
    controlling_distance_m: float | None
    observer_height_m: float
    ranges_tested_m: tuple[float, ...] = ()
    final_range_m: float | None = None
    converged_at_m: float | None = None
    astronomical_state: str | None = None
    terrain_solar_visibility: str | None = None


@dataclass(frozen=True)
class WeatherPayload:
    source_type: WeatherSourceType
    provider_valid_time: str
    provider_run_time: str | None
    data_freshness: str | None
    provider_grid_latitude: float | None
    provider_grid_longitude: float | None
    provider_grid_elevation_m: float | None
    route_minus_provider_elevation_m: float | None
    variables: Mapping[str, float | int | None]
    units: Mapping[str, str | None]
    missing_variable_reasons: Mapping[str, str]
    variable_semantics: Mapping[str, Mapping[str, Any]] = field(default_factory=dict)
    representation_state: WeatherRepresentationState = WeatherRepresentationState.PROVIDER_SAMPLE
    contributing_sample_ids: tuple[str, ...] = ()
    support_distances_m: tuple[float, ...] = ()
    interpolation_method: str | None = None
    elevation_adjustment_state: str = "not_applied"
    planned_arrival_time: str | None = None
    elapsed_route_seconds: float | None = None
    temporal_representation_state: WeatherTemporalRepresentationState | None = None
    contributing_valid_times: tuple[str, ...] = ()
    temporal_interpolation_method: str | None = None
    temporal_provenance: "TemporalProvenance | None" = None


@dataclass(frozen=True)
class TemporalProvenance:
    """Explicit valid/fetch/run times and policy-scoped freshness."""

    fetched_at: str | None
    valid_at: str | None
    contributing_valid_times: tuple[str, ...] = ()
    model_run_at: str | None = None
    age_at_evaluation_seconds: float | None = None
    freshness_state: FreshnessState = FreshnessState.UNKNOWN
    freshness_policy_id: str | None = None
    intended_use_context: IntendedUseContext = IntendedUseContext.PLANNING_CONTEXT
    consistency_reason_codes: tuple[str, ...] = ()


@dataclass(frozen=True)
class AnalysisResult:
    """One provider-labelled result at one location and scenario instant."""

    identity: AnalysisIdentity
    location: ResultLocation
    status: str
    quality: ResultQuality
    source: ProviderProvenance
    method: MethodProvenance
    payload: SolarPayload | WeatherPayload | Mapping[str, Any]

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


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


def solar_result_from_legacy(
    result: Any,
    *,
    terrain_context: Any | None = None,
    limitations: tuple[str, ...] = (),
    version: str = "0.1",
) -> AnalysisResult:
    """Adapt a legacy ``RoutePointSolarExposureResult`` without changing it.

    This is the first concrete producer adapter.  The original flat result
    remains available for backwards compatibility and is never mutated.
    """

    status = result.status.value if isinstance(result.status, Enum) else str(result.status)
    reasons = tuple(
        str(code.value if isinstance(code, Enum) else code) for code in result.reason_codes
    )
    if status in {
        SolarVisibility.DIRECT.value,
        SolarVisibility.SHADOW.value,
        SolarVisibility.NOT_APPLICABLE.value,
    }:
        outcome = EvaluationState.SUCCESS
    elif any("NUMERICAL" in code for code in reasons):
        outcome = EvaluationState.NUMERICAL_FAILURE
    elif any("SUPPORT" in code for code in reasons):
        outcome = EvaluationState.UNSUPPORTED
    else:
        outcome = EvaluationState.UNRESOLVED

    source = ProviderProvenance(
        provider=getattr(terrain_context, "provider", None),
        product=getattr(terrain_context, "product", None),
        surface_semantics=getattr(terrain_context, "surface_semantics", None),
        native_resolution=getattr(terrain_context, "native_resolution", None),
        horizontal_crs=getattr(terrain_context, "horizontal_crs", None),
        vertical_reference=getattr(terrain_context, "vertical_reference", None),
        limitations=limitations,
    )
    quality = ResultQuality(
        outcome=outcome,
        reason_codes=reasons,
        data_support_state="complete"
        if not any("SUPPORT" in code for code in reasons)
        else "incomplete",
        numerical_convergence_state="failed"
        if any("NUMERICAL" in code for code in reasons)
        else "complete",
        range_convergence_state=result.range_convergence_state,
        limitations=limitations,
    )
    method = MethodProvenance(
        method=result.analysis_method,
        model_version=version,
        observer_model=result.observer_model,
        interpolation_method="continuous bilinear grid-post"
        if "bilinear" in result.analysis_method
        else None,
        range_policy={"final_range_m": result.horizon_range_m},
        assumptions=("GPX timestamps are provenance only; scenario time is explicit.",),
    )
    return AnalysisResult(
        identity=AnalysisIdentity(
            analysis_type="solar_exposure",
            semantic_type=result.semantic_type.value,
            version=version,
            subject_id=result.route_id,
            scenario_datetime=result.scenario_datetime,
            timezone=result.timezone,
        ),
        location=ResultLocation(
            latitude=result.latitude,
            longitude=result.longitude,
            point_index=result.point_index,
            route_distance_m=result.route_distance_m,
            route_elevation_m=result.route_elevation_m,
        ),
        status=status,
        quality=quality,
        source=source,
        method=method,
        payload=SolarPayload(
            visibility=SolarVisibility(status),
            solar_azimuth_deg=result.solar_azimuth_deg,
            solar_elevation_deg=result.solar_elevation_deg,
            horizon_angle_deg=result.horizon_angle_deg,
            controlling_distance_m=result.controlling_distance_m,
            observer_height_m=result.observer_height_m,
            ranges_tested_m=result.ranges_tested_m,
            final_range_m=result.horizon_range_m,
            converged_at_m=result.converged_at_m,
            astronomical_state=getattr(result, "astronomical_state", None),
            terrain_solar_visibility=getattr(result, "terrain_solar_visibility", None),
        ),
    )
