"""
Geospatial utilities — Haversine distance and walking-time estimation.

Introduced for INDL-43 (Memorial Location Map & Directions). Kept dependency-free
(pure ``math``) and reusable by INDL-41/42 if plot-distance calculations are needed
later.

All public helpers validate their numeric inputs and return ``None`` for missing or
out-of-range coordinates rather than raising, so callers on the public directions
endpoint never surface a 500 from a bad coordinate (SEC-07 / A06).
"""
from __future__ import annotations

import math
from typing import Optional

# Average slow/accessible walking pace appropriate for cemetery visitors.
# 1.2 m/s ≈ 4.3 km/h (Technical Decision in the INDL-43 PRD).
WALKING_SPEED_M_PER_S = 1.2

_EARTH_RADIUS_M = 6_371_000.0


def _valid_coord(lat: object, lng: object) -> bool:
    """Return True only when lat/lng are finite numbers within valid geographic bounds."""
    # Reject bools explicitly (bool is an int subclass) so True/False are not
    # silently treated as 1.0/0.0 coordinates by this reusable validator.
    if isinstance(lat, bool) or isinstance(lng, bool):
        return False
    try:
        lat_f = float(lat)  # type: ignore[arg-type]
        lng_f = float(lng)  # type: ignore[arg-type]
    except (TypeError, ValueError):
        return False
    if not (math.isfinite(lat_f) and math.isfinite(lng_f)):
        return False
    if not (-90.0 <= lat_f <= 90.0):
        return False
    if not (-180.0 <= lng_f <= 180.0):
        return False
    return True


def haversine_distance_m(
    lat1: Optional[float],
    lng1: Optional[float],
    lat2: Optional[float],
    lng2: Optional[float],
) -> Optional[float]:
    """
    Great-circle distance in metres between two points.

    Returns ``None`` if any coordinate is missing, non-numeric, or out of range
    (latitude outside [-90, 90] or longitude outside [-180, 180]). The internal
    ``asin`` argument is clamped to [-1, 1] to avoid a domain ``ValueError`` from
    floating-point rounding at very small distances (SEC-07).
    """
    if not (_valid_coord(lat1, lng1) and _valid_coord(lat2, lng2)):
        return None

    lat1_f, lng1_f = float(lat1), float(lng1)  # type: ignore[arg-type]
    lat2_f, lng2_f = float(lat2), float(lng2)  # type: ignore[arg-type]

    phi1 = math.radians(lat1_f)
    phi2 = math.radians(lat2_f)
    d_phi = math.radians(lat2_f - lat1_f)
    d_lambda = math.radians(lng2_f - lng1_f)

    a = (
        math.sin(d_phi / 2) ** 2
        + math.cos(phi1) * math.cos(phi2) * math.sin(d_lambda / 2) ** 2
    )
    # Clamp for numerical safety before asin.
    a = min(1.0, max(0.0, a))
    c = 2 * math.asin(math.sqrt(a))
    return _EARTH_RADIUS_M * c


def estimate_walking_minutes(distance_m: Optional[float]) -> Optional[int]:
    """
    Estimated walking time in whole minutes for a straight-line distance.

    Rounded up to the nearest minute with a floor of 1 minute for any positive
    distance. Returns ``None`` for missing/negative input and ``0`` only for an
    exact zero distance.
    """
    if distance_m is None:
        return None
    try:
        d = float(distance_m)
    except (TypeError, ValueError):
        return None
    if d < 0 or not math.isfinite(d):
        return None
    if d == 0:
        return 0
    minutes = math.ceil(d / WALKING_SPEED_M_PER_S / 60.0)
    return max(1, minutes)
