"""Тонкий клиент локального OSRM — реальное дорожное расстояние для /analyze (#39 A2).

В отличие от ORS (`ors_client.py`, внешний SaaS с лимитом 2000/день и /matrix POST),
это self-hosted OSRM (docker-compose service `osrm`, `settings.osrm_local_url`). Его
table-API — это **GET** с координатами в URL и `annotations=distance`, отдающий
**реальное дорожное расстояние в МЕТРАХ** (а не durations×скорость — это была бы
ORS-семантика):

    GET {url}/table/v1/driving/{lon0},{lat0};{lon1},{lat1};...?sources=0&annotations=distance
    → {"code":"Ok","distances":[[0, d1, d2, ...]]}   # метры; row[0] = origin→каждый dest

origin = центроид участка (первая координата, sources=0); destinations = POI.
Читаем `distances[0][1:]` (пропускаем self origin→origin = 0). Недостижимый POI =
`null` → возвращаем None по этому индексу (caller оставит straight-line).

Graceful: при ЛЮБОМ сбое (HTTP, timeout, code!=Ok, битая форма) → `OsrmLocalUnavailableError`.
Caller обязан поймать и сделать fallback на straight-line, НЕ валя /analyze.

OSRM запускается с `--max-table-size 8000`, поэтому ≤100 POI на analyze проходят
без усечения.
"""

from __future__ import annotations

import logging
from urllib.parse import quote

import httpx

from app.core.config import settings

logger = logging.getLogger(__name__)

# OSRM `--max-table-size` (число пар sources+destinations). Дефолт сервиса — 8000;
# с 1 origin это 7999 POI, что с запасом покрывает наши ≤100. Держим как guard.
MAX_TABLE_SIZE = 8000


class OsrmLocalUnavailableError(Exception):
    """Локальный OSRM недоступен (сетевой сбой / timeout / code!=Ok / битый ответ).

    Caller обязан поймать и сделать fallback (straight-line), не валя /analyze.
    """


def _fmt_coord(lon: float, lat: float) -> str:
    """OSRM-координата `lon,lat`, URL-safe (на случай экзотических локалей/инпута)."""
    return f"{quote(str(lon), safe='')},{quote(str(lat), safe='')}"


def get_road_distances_m(
    origin_lon: float,
    origin_lat: float,
    destinations: list[tuple[float, float]],
    *,
    profile: str = "driving",
    timeout_s: float | None = None,
    base_url: str | None = None,
) -> list[float | None]:
    """Дорожное расстояние (метры) от одного origin до каждого destination через OSRM /table.

    Args:
        origin_lon, origin_lat: координаты источника (центроид участка) = sources=0.
        destinations: список (lon, lat) — POI. ≤MAX_TABLE_SIZE-1 (для нас всегда).
        profile: OSRM-профиль ('driving' по умолчанию; 'foot' для пешего графа).
        timeout_s: httpx timeout (обязателен — иначе worker зависает).
            None → settings.osrm_distance_timeout_s.
        base_url: базовый URL OSRM-сервиса. None → settings.osrm_local_url
            (DRIVING-граф, поведение по умолчанию). Передаётся, например,
            settings.osrm_walk_local_url для маршрутизации walk-POI по FOOT-графу
            (#39 A3).

    Returns:
        Список той же длины, что `destinations`, выровненный по индексу. Элемент —
        дорожное расстояние в метрах (float) или None, если OSRM не смог построить
        маршрут до этой точки (POI на острове/без дорожной связи) — caller оставит
        straight-line по этому POI. Пустой `destinations` → [].

    Raises:
        OsrmLocalUnavailableError: сетевой сбой / timeout / HTTP!=2xx / code!=Ok /
            неожиданный формат / число расстояний != числу destinations. Caller
            делает straight-line fallback, /analyze не падает.
    """
    if not destinations:
        return []

    if len(destinations) > MAX_TABLE_SIZE - 1:
        # OSRM отвергнет таблицу больше max-table-size — считаем это «недоступностью»
        # для данного запроса (caller → straight-line по всем POI).
        raise OsrmLocalUnavailableError(
            f"OSRM table: {len(destinations)} destinations > max-table-size "
            f"({MAX_TABLE_SIZE - 1} с одним origin)"
        )

    effective_timeout = timeout_s if timeout_s is not None else settings.osrm_distance_timeout_s

    # Координаты `;`-joined: origin первой (→ sources=0), затем POI по порядку.
    coords = ";".join(
        [_fmt_coord(origin_lon, origin_lat)]
        + [_fmt_coord(lon, lat) for lon, lat in destinations]
    )
    base = (base_url if base_url is not None else settings.osrm_local_url).rstrip("/")
    url = f"{base}/table/v1/{profile}/{coords}"
    params = {"sources": "0", "annotations": "distance"}

    try:
        with httpx.Client(timeout=effective_timeout) as client:
            resp = client.get(url, params=params)
            resp.raise_for_status()
            data = resp.json()
    except httpx.HTTPStatusError as exc:
        raise OsrmLocalUnavailableError(
            f"OSRM table HTTP {exc.response.status_code}: {exc.response.text[:200]}"
        ) from exc
    except Exception as exc:  # сетевой сбой / timeout / JSON decode → fallback, не 500
        raise OsrmLocalUnavailableError(f"OSRM table request failed: {exc}") from exc

    if not isinstance(data, dict) or data.get("code") != "Ok":
        code = data.get("code") if isinstance(data, dict) else "<non-dict>"
        raise OsrmLocalUnavailableError(f"OSRM table: code != Ok ({code!r})")

    # Ответ: {"distances": [[0, d1, d2, ...]]} — метры, одна строка (один source).
    distances = data.get("distances")
    if not isinstance(distances, list) or not distances:
        raise OsrmLocalUnavailableError("OSRM table: пустой/битый distances в ответе")
    row = distances[0]
    if not isinstance(row, list) or not row:
        raise OsrmLocalUnavailableError("OSRM table: distances[0] не список / пуст")

    # row[0] = origin→origin = 0 (self) — пропускаем, читаем 1:.
    dest_distances = row[1:]
    out: list[float | None] = []
    for d in dest_distances:
        if d is None:
            out.append(None)  # OSRM не построил маршрут до этой точки
        else:
            out.append(float(d))

    # Длина должна совпадать с числом destinations — иначе zip у вызывающего
    # рассинхронит POI↔distance. Закрываем как OSRM-сбой → straight-line fallback.
    if len(out) != len(destinations):
        raise OsrmLocalUnavailableError(
            f"OSRM table: distances ({len(out)}) != destinations ({len(destinations)})"
        )
    return out
