"""Deterministic segment-aware geometry and elevation analytics."""

from __future__ import annotations

from dataclasses import asdict
from itertools import groupby
from statistics import median
from typing import Any

from mountain_twin.trails.gpx import ParsedRoute
from mountain_twin.trails.trail_engine import bearing_deg, haversine_m


def smooth_elevations(values: list[float | None]) -> list[float | None]:
    """Centered five-point median, clipped at segment edges and missing-data gaps."""
    result: list[float | None] = [None] * len(values)
    start = 0
    while start < len(values):
        if values[start] is None:
            start += 1
            continue
        end = start
        while end < len(values) and values[end] is not None:
            end += 1
        for i in range(start, end):
            result[i] = float(median(values[max(start, i - 2) : min(end, i + 3)]))
        start = end
    return result


def normalize_route(
    route: ParsedRoute,
) -> tuple[dict[str, Any], list[dict[str, Any]], dict[str, Any]]:
    """Return summary, points, and GeoJSON without bridging track segment gaps.

    Ascent/descent are null if any elevations are missing, rather than presenting
    partial totals as complete. Repeated coordinates have no defined bearing.
    """
    metadata = route.metadata
    common = dict(
        route_id=metadata.route_id,
        route_group=metadata.route_group,
        tmb_day=metadata.tmb_day,
        assigned_hike_date=metadata.assigned_hike_date,
    )
    rows, features = [], []
    cumulative = 0.0
    ascent, descent = {"raw": 0.0, "smooth5": 0.0}, {"raw": 0.0, "smooth5": 0.0}
    for segment_index, segment_iter in groupby(route.points, key=lambda p: p.segment_index):
        segment = list(segment_iter)
        smoothed = smooth_elevations([p.elevation_m_raw for p in segment])
        coordinates = []
        for i, point in enumerate(segment):
            previous = segment[i - 1] if i else None
            distance = (
                haversine_m(previous.lat, previous.lon, point.lat, point.lon) if previous else 0.0
            )
            cumulative += distance
            bearing = (
                bearing_deg(previous.lat, previous.lon, point.lat, point.lon)
                if previous and distance > 0
                else None
            )
            grade = None
            if previous:
                for kind, current, prior in [
                    ("raw", point.elevation_m_raw, previous.elevation_m_raw),
                    ("smooth5", smoothed[i], smoothed[i - 1]),
                ]:
                    if current is not None and prior is not None:
                        delta = current - prior
                        ascent[kind] += max(delta, 0)
                        descent[kind] += max(-delta, 0)
                        if kind == "smooth5" and distance >= 1.0:
                            grade = 100 * delta / distance
            rows.append(
                {
                    **common,
                    **asdict(point),
                    "elevation_m_smooth5": smoothed[i],
                    "segment_distance_m": distance,
                    "cum_distance_km": cumulative / 1000,
                    "bearing_deg": bearing,
                    "grade_pct_smooth": grade,
                    "actual_hike_time_status": (
                        "authoritative" if point.actual_hike_time else "missing"
                    )
                    if metadata.timestamps_authoritative
                    else "source_timestamp_non_authoritative",
                }
            )
            coordinates.append(
                [point.lon, point.lat]
                + ([point.elevation_m_raw] if point.elevation_m_raw is not None else [])
            )
        # Separate features preserve disconnections; a singleton is a valid Point.
        features.append(
            {
                "type": "Feature",
                "properties": {
                    **common,
                    "segment_index": segment_index,
                    "track_index": segment[0].track_index,
                    "timestamps_authoritative": metadata.timestamps_authoritative,
                },
                "geometry": {
                    "type": "LineString" if len(coordinates) > 1 else "Point",
                    "coordinates": coordinates if len(coordinates) > 1 else coordinates[0],
                },
            }
        )
    elevations = [p.elevation_m_raw for p in route.points if p.elevation_m_raw is not None]
    complete = len(elevations) == len(route.points)
    first, last = route.points[0], route.points[-1]
    summary = {
        **common,
        "name": metadata.name,
        "region": metadata.region,
        "source_file": metadata.source_filename,
        "points": len(rows),
        "segments": len(features),
        "distance_km": cumulative / 1000,
        **{f"ascent_m_{kind}": value if complete else None for kind, value in ascent.items()},
        **{f"descent_m_{kind}": value if complete else None for kind, value in descent.items()},
        "min_elev_m": min(elevations) if elevations else None,
        "max_elev_m": max(elevations) if elevations else None,
        "missing_elevation_points": len(rows) - len(elevations),
        "start_lat": first.lat,
        "start_lon": first.lon,
        "end_lat": last.lat,
        "end_lon": last.lon,
        "timestamps_authoritative": metadata.timestamps_authoritative,
        "time_status": "authoritative where present"
        if metadata.timestamps_authoritative
        else "source timestamps non-authoritative",
        "source_sha256": route.source_sha256,
    }
    return summary, rows, {"type": "FeatureCollection", "features": features}
