"""BRouter proxy client for interactive route drawing in Planning Workspace
(docs/route_drawing_v0_1_design.md, Krok 2). The browser never calls
brouter.de directly -- scripts/serve_visual_prototype.py's
POST /api/routing/segment proxies through this module, the same pattern
/api/runtime/map already uses for MapTiler: one swappable place to add
caching, rate limiting, or a self-hosted BRouter later without touching
the frontend.

BRouter's public server already returns elevation as each position's third
coordinate (verified by a live call -- see the design doc section 1); this
module does not sample a DEM or otherwise invent elevation.
"""

from __future__ import annotations

import json
import os
from typing import Any, Mapping, Sequence
from urllib.parse import urlencode

from mountain_twin.offline import urlopen  # AV-045: offline mode for tests

from .activities import ACTIVITIES_BY_ID, DEFAULT_ACTIVITY_ID
from .route_persistence import RoutePointInput
from .route_surface import point_surfaces_from_brouter

BROUTER_ENDPOINT = "https://brouter.de/brouter"


def brouter_endpoint(environ: Mapping[str, str] | None = None) -> str:
    """Where routing goes: public brouter.de by default; BROUTER_ENDPOINT in
    the server's environment switches to another BRouter (WP0, AV-019: the
    own server, e.g. http://57.131.197.101/brouter -- same API, the same
    brouter.de hiking-mountain profile, whole-planet segments), read per
    request so a test run can compare both."""
    environ = os.environ if environ is None else environ
    return (environ.get("BROUTER_ENDPOINT") or "").strip().rstrip("/") or BROUTER_ENDPOINT


# docs/route_drawing_v0_1_design.md section 1; since AV-031 the default
# activity's profile (trekking), from the one activity registry.
DEFAULT_PROFILE = ACTIVITIES_BY_ID[DEFAULT_ACTIVITY_ID].routing_profile


def fetch_route_segment(
    points: Sequence[RoutePointInput], *, profile: str = DEFAULT_PROFILE, opener=urlopen
) -> dict[str, Any]:
    """The snapped geometry only -- see fetch_route_segment_detail()."""
    return fetch_route_segment_detail(points, profile=profile, opener=opener)[0]


def fetch_route_segment_detail(
    points: Sequence[RoutePointInput], *, profile: str = DEFAULT_PROFILE, opener=urlopen
) -> tuple[dict[str, Any], list[dict[str, Any] | None]]:
    """A GeoJSON LineString snapped to real trails/roads through the given
    points, via the public BRouter server -- elevation included as each
    position's third coordinate, exactly the shape
    mountain_twin.journey.route_persistence.geometry_geojson() already
    produces and route_points_from_geometry_geojson() already parses.

    Raises ValueError for fewer than two points, RuntimeError on a
    network failure, a BRouter error response, or an unexpected response
    shape -- never a silently empty or fabricated geometry."""
    if len(points) < 2:
        raise ValueError("a route segment requires at least two points")
    lonlats = "|".join(f"{point.longitude},{point.latitude}" for point in points)
    query = urlencode(
        {"lonlats": lonlats, "profile": profile, "alternativeidx": "0", "format": "geojson"}
    )
    url = f"{brouter_endpoint()}?{query}"
    try:
        with opener(url, timeout=20) as response:
            raw = json.loads(response.read().decode("utf-8"))
    except OSError as error:
        raise RuntimeError("ROUTING_PROVIDER_NETWORK_FAILURE") from error
    except json.JSONDecodeError as error:
        raise RuntimeError("ROUTING_PROVIDER_INVALID_RESPONSE") from error
    features = raw.get("features")
    if not features:
        raise RuntimeError("ROUTING_PROVIDER_NO_ROUTE_FOUND")
    geometry = features[0].get("geometry", {})
    if geometry.get("type") != "LineString" or not geometry.get("coordinates"):
        raise RuntimeError("ROUTING_PROVIDER_UNEXPECTED_GEOMETRY")
    # AV-031 (WP2): the surface of each point's way, from the same answer.
    messages = (features[0].get("properties") or {}).get("messages") or []
    return geometry, point_surfaces_from_brouter(geometry["coordinates"], messages)
