"""Experimental arbitrary-time solar queries backed by terrain profiles."""

from __future__ import annotations

from dataclasses import asdict, dataclass
from datetime import datetime
from enum import Enum
from typing import Any

from mountain_twin.solar.states import (
    AstronomicalState,
    TerrainSolarVisibility,
    classify_astronomical_state,
)
from mountain_twin.solar.sun_engine import solar_position
from mountain_twin.terrain.cache import CacheIdentity, HorizonProfileCache
from mountain_twin.terrain.profile import HorizonProfile, query_profile


@dataclass(frozen=True)
class SolarQueryResult:
    latitude: float
    longitude: float
    scenario_datetime: str
    solar_azimuth_deg: float
    solar_elevation_deg: float
    astronomical_state: AstronomicalState
    terrain_solar_visibility: TerrainSolarVisibility
    horizon_angle_deg: float | None
    controlling_distance_m: float | None
    final_range_m: float | None
    status: str
    profile_key: str
    terrain_provenance: dict[str, Any]
    surface_semantics: str
    observer_model: str
    observer_height_m: float
    approximation_method: str
    convergence_state: str
    reason_codes: tuple[str, ...]
    terrain_interpolation_performed: bool

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


class SolarQueryEngine:
    """Query solar geometry and cached terrain independently of solar time."""

    def __init__(self, cache: HorizonProfileCache):
        self.cache = cache

    def query(
        self,
        identity: CacheIdentity,
        instant: datetime,
        *,
        latitude: float | None = None,
        longitude: float | None = None,
    ) -> SolarQueryResult:
        profile = self.cache.get_profile(identity)
        return self.query_loaded(
            profile,
            identity,
            instant,
            latitude=latitude,
            longitude=longitude,
        )

    def query_loaded(
        self,
        profile: HorizonProfile,
        identity: CacheIdentity,
        instant: datetime,
        *,
        latitude: float | None = None,
        longitude: float | None = None,
    ) -> SolarQueryResult:
        query_latitude = profile.latitude if latitude is None else latitude
        query_longitude = profile.longitude if longitude is None else longitude
        if (query_latitude, query_longitude) != (profile.latitude, profile.longitude):
            raise ValueError("solar query coordinates must exactly match profile coordinates")
        solar_elevation, solar_azimuth = solar_position(instant, query_latitude, query_longitude)
        astronomical_state = classify_astronomical_state(solar_elevation)
        if astronomical_state is not AstronomicalState.DAY:
            return SolarQueryResult(
                latitude=query_latitude,
                longitude=query_longitude,
                scenario_datetime=instant.isoformat(),
                solar_azimuth_deg=solar_azimuth,
                solar_elevation_deg=solar_elevation,
                astronomical_state=astronomical_state,
                terrain_solar_visibility=TerrainSolarVisibility.NOT_APPLICABLE,
                horizon_angle_deg=None,
                controlling_distance_m=None,
                final_range_m=None,
                status=TerrainSolarVisibility.NOT_APPLICABLE.value,
                profile_key=identity.key(),
                terrain_provenance=identity.manifest(),
                surface_semantics=profile.surface_semantics,
                observer_model=profile.observer_model,
                observer_height_m=profile.observer_height_m,
                approximation_method="terrain_horizon_profile_circular_linear_v0_2",
                convergence_state="not_applicable",
                reason_codes=("TERRAIN_VISIBILITY_NOT_APPLICABLE",),
                terrain_interpolation_performed=False,
            )
        sample = query_profile(profile, solar_azimuth, method=identity.interpolation_method)
        reasons = list(sample.reason_codes)
        if sample.horizon_angle_deg is None:
            visibility = TerrainSolarVisibility.UNKNOWN
            if not reasons:
                reasons.append("HORIZON_PROFILE_UNKNOWN")
        else:
            visibility = (
                TerrainSolarVisibility.SHADOW
                if sample.horizon_angle_deg >= solar_elevation
                else TerrainSolarVisibility.DIRECT
            )
        return SolarQueryResult(
            latitude=query_latitude,
            longitude=query_longitude,
            scenario_datetime=instant.isoformat(),
            solar_azimuth_deg=solar_azimuth,
            solar_elevation_deg=solar_elevation,
            astronomical_state=astronomical_state,
            terrain_solar_visibility=visibility,
            horizon_angle_deg=sample.horizon_angle_deg,
            controlling_distance_m=sample.controlling_distance_m,
            final_range_m=sample.final_range_m,
            status=visibility.value,
            profile_key=identity.key(),
            terrain_provenance=identity.manifest(),
            surface_semantics=profile.surface_semantics,
            observer_model=profile.observer_model,
            observer_height_m=profile.observer_height_m,
            approximation_method="terrain_horizon_profile_circular_linear_v0_2",
            convergence_state=sample.convergence_state,
            reason_codes=tuple(reasons),
            terrain_interpolation_performed=True,
        )


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
