"""Experimental reusable terrain horizon profiles, independent of solar time."""

from __future__ import annotations

import math
from dataclasses import asdict, dataclass
from typing import Any, Callable


@dataclass(frozen=True)
class HorizonSample:
    azimuth_deg: float
    horizon_angle_deg: float | None
    controlling_distance_m: float | None
    final_range_m: float
    convergence_state: str
    reason_codes: tuple[str, ...] = ()


@dataclass(frozen=True)
class HorizonProfile:
    point_index: int
    latitude: float
    longitude: float
    provider: str
    product: str | None
    surface_semantics: str
    observer_model: str
    observer_height_m: float
    azimuth_resolution_deg: float
    azimuth_origin_deg: float
    interpolation_policy: str
    method_version: str
    samples: tuple[HorizonSample, ...]
    limitations: tuple[str, ...]

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


def azimuth_grid(resolution_deg: float, origin_deg: float = 0.0) -> tuple[float, ...]:
    if not math.isfinite(resolution_deg) or resolution_deg <= 0 or 360 % resolution_deg:
        raise ValueError("resolution must be a positive divisor of 360")
    return tuple(
        (origin_deg + index * resolution_deg) % 360 for index in range(round(360 / resolution_deg))
    )


def build_profile(
    *,
    point_index: int,
    latitude: float,
    longitude: float,
    provider: str,
    product: str | None,
    surface_semantics: str,
    observer_model: str,
    observer_height_m: float,
    resolution_deg: float,
    solver: Callable[[float], dict[str, Any]],
    limitations: tuple[str, ...] = (),
) -> HorizonProfile:
    samples = []
    for azimuth in azimuth_grid(resolution_deg):
        result = solver(azimuth)
        maximum = result.get("maximum") or {}
        complete = (
            result.get("horizon_status") == "complete"
            and result.get("horizon_angle_deg") is not None
        )
        samples.append(
            HorizonSample(
                azimuth_deg=azimuth,
                horizon_angle_deg=result.get("horizon_angle_deg") if complete else None,
                controlling_distance_m=maximum.get("distance_m") if complete else None,
                final_range_m=result.get("final_range_m", result.get("horizon_range_m", 0.0)),
                convergence_state=result.get(
                    "convergence_state", "converged" if complete else "unknown"
                ),
                reason_codes=tuple(result.get("reason_codes", ())) if not complete else (),
            )
        )
    return HorizonProfile(
        point_index=point_index,
        latitude=latitude,
        longitude=longitude,
        provider=provider,
        product=product,
        surface_semantics=surface_semantics,
        observer_model=observer_model,
        observer_height_m=observer_height_m,
        azimuth_resolution_deg=resolution_deg,
        azimuth_origin_deg=0.0,
        interpolation_policy="circular_linear",
        method_version="horizon_profile_v0_1",
        samples=tuple(samples),
        limitations=limitations,
    )


def query_profile(
    profile: HorizonProfile, azimuth_deg: float, *, method: str = "linear"
) -> HorizonSample:
    if not math.isfinite(azimuth_deg):
        raise ValueError("azimuth must be finite")
    angle = azimuth_deg % 360
    if not profile.samples:
        return HorizonSample(angle, None, None, 0.0, "unknown", ("HORIZON_PROFILE_EMPTY",))
    resolution = profile.azimuth_resolution_deg
    exact_index = round(angle / resolution) % len(profile.samples)
    exact_angle = (exact_index * resolution) % 360
    if math.isclose((angle - exact_angle) % 360, 0.0, abs_tol=1e-10) or method == "nearest":
        return profile.samples[exact_index]
    left_index = math.floor(angle / resolution) % len(profile.samples)
    right_index = (left_index + 1) % len(profile.samples)
    left, right = profile.samples[left_index], profile.samples[right_index]
    if left.horizon_angle_deg is None or right.horizon_angle_deg is None:
        source_reasons = tuple(sorted(set(left.reason_codes) | set(right.reason_codes)))
        convergence_state = (
            left.convergence_state
            if left.convergence_state == right.convergence_state
            else "unknown"
        )
        return HorizonSample(
            angle,
            None,
            None,
            max(left.final_range_m, right.final_range_m),
            convergence_state,
            ("HORIZON_PROFILE_INPUT_UNKNOWN", *source_reasons),
        )
    fraction = (angle - left.azimuth_deg) % 360 / resolution
    value = left.horizon_angle_deg + fraction * (right.horizon_angle_deg - left.horizon_angle_deg)
    distance = None
    if left.controlling_distance_m is not None and right.controlling_distance_m is not None:
        distance = left.controlling_distance_m + fraction * (
            right.controlling_distance_m - left.controlling_distance_m
        )
    return HorizonSample(
        angle,
        value,
        distance,
        max(left.final_range_m, right.final_range_m),
        "interpolated",
        ("HORIZON_PROFILE_AZIMUTH_INTERPOLATION",),
    )
