"""Тонкий клиент OpenRouteService (ORS) — переиспользуемый между фичами.

Сейчас ORS используется для:
- изохрон доступности (`/parcels/{cad}/isochrones`, см. parcels.get_isochrones)
- routing-based POI decay (#41, см. poi_score.compute_poi_routing_decay) — `/matrix`

Free tier: 2000 запросов/день. Поэтому:
- один HTTP-вызов на analyze (батч до ~1000 destinations в /matrix)
- явный httpx timeout (без него worker зависает)
- graceful: нет API-ключа / 429 / сетевой сбой → `OrsUnavailableError`, caller делает
  fallback на straight-line, а не валит analyze.
"""

from __future__ import annotations

import logging

import httpx

from app.core.config import settings

logger = logging.getLogger(__name__)

_ORS_MATRIX_BASE = "https://api.openrouteservice.org/v2/matrix"
# Профили ORS-routing. foot-walking — пеший радиус (метро/школа/магазин),
# driving-car — авто (для будущих авто-категорий).
VALID_PROFILES: frozenset[str] = frozenset(
    {"foot-walking", "cycling-regular", "driving-car"}
)
# ORS /matrix ограничивает foot-walking ~2000 пар (sources×destinations) на free tier.
MAX_MATRIX_DESTINATIONS = 1000
_DEFAULT_TIMEOUT_S = 12.0


class OrsUnavailableError(Exception):
    """ORS недоступен (нет ключа / rate-limit / сетевой сбой / битый ответ).

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


def is_configured() -> bool:
    """True если задан API-ключ ORS (иначе сразу fallback без HTTP)."""
    return bool(settings.openrouteservice_api_key)


def matrix_durations_min(
    origin_lon: float,
    origin_lat: float,
    destinations: list[tuple[float, float]],
    *,
    profile: str = "foot-walking",
    timeout_s: float = _DEFAULT_TIMEOUT_S,
) -> list[float | None]:
    """Время в пути (минуты) от одного origin до каждого destination через ORS /matrix.

    Args:
        origin_lon, origin_lat: координаты источника (центроид участка).
        destinations: список (lon, lat) — POI. Усечётся до MAX_MATRIX_DESTINATIONS.
        profile: ORS-профиль ('foot-walking' по умолчанию — пешая доступность).
        timeout_s: httpx timeout (обязателен — иначе worker зависает).

    Returns:
        Список той же длины, что усечённый `destinations`. Элемент — минуты в пути
        (float) или None, если ORS не смог построить маршрут до этой точки
        (например, POI на острове без пешеходной связи).

    Raises:
        OrsUnavailableError: нет ключа / mode невалиден / rate-limit / сетевой сбой /
            неожиданный формат ответа. Caller делает straight-line fallback.
    """
    if not is_configured():
        raise OrsUnavailableError("OPENROUTESERVICE_API_KEY не задан")
    if profile not in VALID_PROFILES:
        raise OrsUnavailableError(f"Недопустимый ORS profile: {profile!r}")
    if not destinations:
        return []

    dests = destinations[:MAX_MATRIX_DESTINATIONS]
    if len(destinations) > MAX_MATRIX_DESTINATIONS:
        logger.warning(
            "ors matrix: %d destinations усечено до %d (free-tier limit)",
            len(destinations),
            MAX_MATRIX_DESTINATIONS,
        )

    # ORS /matrix: locations[0] — source, остальные — destinations.
    locations: list[list[float]] = [[origin_lon, origin_lat]]
    locations.extend([lon, lat] for lon, lat in dests)
    body = {
        "locations": locations,
        "sources": [0],
        "destinations": list(range(1, len(locations))),
        "metrics": ["duration"],
        "units": "m",
    }
    headers = {
        "Authorization": settings.openrouteservice_api_key,
        "Content-Type": "application/json",
        "Accept": "application/json",
    }
    url = f"{_ORS_MATRIX_BASE}/{profile}"

    try:
        with httpx.Client(timeout=timeout_s) as client:
            resp = client.post(url, json=body, headers=headers)
            resp.raise_for_status()
            data = resp.json()
    except httpx.HTTPStatusError as exc:
        if exc.response.status_code == 429:
            raise OrsUnavailableError("ORS daily limit (2000 req) exceeded") from exc
        raise OrsUnavailableError(
            f"ORS matrix HTTP {exc.response.status_code}: {exc.response.text[:200]}"
        ) from exc
    except Exception as exc:  # сетевой сбой / JSON decode — fallback, не 500
        raise OrsUnavailableError(f"ORS matrix request failed: {exc}") from exc

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

    out: list[float | None] = []
    for sec in row:
        if sec is None:
            out.append(None)  # ORS не построил маршрут до этой точки
        else:
            out.append(float(sec) / 60.0)
    # Длина durations должна совпадать с числом destinations — иначе zip(strict=True)
    # у вызывающего бросит ValueError (не OrsUnavailableError) → 500. Закрываем как ORS-сбой.
    if len(out) != len(dests):
        raise OrsUnavailableError(
            f"ORS matrix: durations ({len(out)}) != destinations ({len(dests)})"
        )
    return out
