"""A rough riding-time estimate for bikes (O1, pakiet 2026-10-03) -- labelled
"szacunek" everywhere it is shown, never a pace model.

Mountain Twin has no bike pace model (activities.py: bikes stay UNTIMED, so
no departure, arrival or night is invented). This estimate only answers "how
long might riding this day take": a flat speed per activity, corrected for
the gradient of each ~200 m piece of the route.

- Flat speed: road bike 25 km/h, gravel 18, MTB 13, touring bike 17.
- Uphill: speed / (1 + 0.12 * grade%), so 5% = 0.63x, 10% = 0.45x.
- Downhill: speed * (1 + 0.03 * |grade%|), at most 1.5x (braking, bends).
- Pieces with no elevation count as flat; their share is returned.

No stops, no wind, no surface beyond the activity's own flat speed, no
rider fitness: an order of magnitude, not a plan. Evidence type: a derived
estimate (ADR-003), kept apart from the pace engine's timeline.
"""

from __future__ import annotations

from collections.abc import Sequence
from typing import Any

from mountain_twin.route_analysis import prepare_route

METHOD = "bike_speed_estimate_v0_1"
FLAT_SPEED_KMH = {"road_bike": 25.0, "gravel": 18.0, "mtb": 13.0, "bike_touring": 17.0}
PIECE_M = 200.0
UPHILL_PER_PERCENT = 0.12
DOWNHILL_PER_PERCENT = 0.03
DOWNHILL_MAX = 1.5


def speed_factor(grade_percent: float) -> float:
    if grade_percent >= 0:
        return 1.0 / (1.0 + UPHILL_PER_PERCENT * grade_percent)
    return min(DOWNHILL_MAX, 1.0 + DOWNHILL_PER_PERCENT * -grade_percent)


def estimate_days(
    route_points: Sequence[Any], activity_id: str | None, spans: Sequence[tuple[int, int]]
) -> list[dict[str, Any]] | None:
    """One estimate per (start point index, end point index) span -- each
    day of a ride; None for an activity with no flat speed (on foot)."""
    flat = FLAT_SPEED_KMH.get(activity_id or "")
    if flat is None:
        return None
    prepared = prepare_route(route_points).points
    return [_estimate(prepared, flat, start, end) for start, end in spans]


def _estimate(prepared, flat: float, start_index: int, end_index: int) -> dict[str, Any]:
    stretch = [point for point in prepared if start_index <= point.point_index <= end_index]
    seconds = unknown_m = 0.0
    piece_start = stretch[0] if stretch else None
    for point in stretch[1:]:
        length = point.cumulative_distance_m - piece_start.cumulative_distance_m
        if length < PIECE_M and point is not stretch[-1]:
            continue
        rise = None
        if point.gpx_elevation_m is not None and piece_start.gpx_elevation_m is not None:
            rise = point.gpx_elevation_m - piece_start.gpx_elevation_m
        if rise is None:
            unknown_m += length
            factor = 1.0
        else:
            factor = speed_factor(100.0 * rise / length) if length > 0 else 1.0
        seconds += length / (flat * factor / 3.6)
        piece_start = point
    distance = (
        stretch[-1].cumulative_distance_m - stretch[0].cumulative_distance_m if stretch else 0.0
    )
    return {
        "method": METHOD,
        "state": "ESTIMATE",
        "flat_speed_kmh": flat,
        "moving_s": round(seconds),
        "distance_m": distance,
        "no_elevation_m": unknown_m,
    }
