"""Matriz de distancias y tiempos 1-a-N via Valhalla sources_to_targets."""

from __future__ import annotations

import math
from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor, as_completed
from threading import Lock
from typing import Any
from urllib import request as urllib_request
import json


def valhalla_matrix_1_to_n(
    origins: list[tuple[float, float]],
    destinations: list[tuple[float, float]],
    pairs: list[tuple[int, int]],
    base_url: str = "http://localhost:8002",
    costing: str = "auto",
    max_targets_per_request: int = 400,
    max_distance_m: float = 30_000.0,
    timeout_s: float = 120.0,
    concurrency: int = 12,
    search_radius_m: float = 0.0,
) -> list[dict]:
    """Calcula distancias y tiempos para pares origen-destino usando Valhalla.

    Args:
        origins: Lista de (lat, lon) de los posibles orígenes.
        destinations: Lista de (lat, lon) de los posibles destinos.
        pairs: Lista de (origin_idx, dest_idx) con los índices a calcular.
        base_url: URL base del servidor Valhalla.
        costing: Modelo de coste ('auto', 'bicycle', 'pedestrian', etc.).
        max_targets_per_request: Máximo de destinos por request a Valhalla.
        max_distance_m: Distancia de fallback en metros cuando Valhalla falla.
        timeout_s: Timeout en segundos por request.
        concurrency: Número de threads paralelos.
        search_radius_m: Radio de búsqueda de nodo en metros (0 = default).

    Returns:
        Lista paralela a 'pairs' con dicts {"meters": float, "seconds": float, "error": int}.
        error=1 si Valhalla falló para ese par; meters=max_distance_m, seconds=NaN en error.
    """
    url = f"{base_url.rstrip('/')}/sources_to_targets"
    n_pairs = len(pairs)
    meters: list[float] = [max_distance_m] * n_pairs
    seconds: list[float] = [math.nan] * n_pairs
    errors: list[int] = [1] * n_pairs

    def _loc(lat: float, lon: float) -> dict[str, Any]:
        loc: dict[str, Any] = {"lat": float(lat), "lon": float(lon)}
        if search_radius_m and search_radius_m > 0:
            loc["search_radius"] = float(search_radius_m)
        return loc

    def _post_json(payload: dict) -> dict:
        data = json.dumps(payload).encode()
        req = urllib_request.Request(
            url,
            data=data,
            headers={"Content-Type": "application/json"},
            method="POST",
        )
        with urllib_request.urlopen(req, timeout=timeout_s) as resp:
            return json.loads(resp.read().decode("utf-8"))

    # Group pair indices by origin index
    groups: dict[int, list[int]] = defaultdict(list)
    for pair_pos, (oi, _di) in enumerate(pairs):
        groups[oi].append(pair_pos)

    # Build tasks: (origin_idx, [pair_positions_chunk])
    tasks: list[tuple[int, list[int]]] = []
    for oi, pair_positions in groups.items():
        for i in range(0, len(pair_positions), max_targets_per_request):
            tasks.append((oi, pair_positions[i : i + max_targets_per_request]))

    lock = Lock()

    def _run_chunk(oi: int, chunk_positions: list[int]) -> None:
        source = [_loc(*origins[oi])]
        targets = [_loc(*destinations[pairs[pos][1]]) for pos in chunk_positions]
        payload = {"sources": source, "targets": targets, "costing": costing}
        try:
            data = _post_json(payload)
            matrix = data["sources_to_targets"][0]
            with lock:
                for j, pos in enumerate(chunk_positions):
                    cell = matrix[j]
                    d = cell.get("distance")
                    t = cell.get("time")
                    meters[pos] = d * 1000 if d is not None else max_distance_m
                    seconds[pos] = float(t) if t is not None else math.nan
                    errors[pos] = 0
        except Exception:
            with lock:
                for pos in chunk_positions:
                    meters[pos] = max_distance_m
                    seconds[pos] = math.nan
                    errors[pos] = 1

    with ThreadPoolExecutor(max_workers=concurrency) as ex:
        futures = [ex.submit(_run_chunk, oi, chunk) for oi, chunk in tasks]
        for fut in as_completed(futures):
            fut.result()  # propagate thread exceptions silently — already handled inside

    return [
        {"meters": meters[i], "seconds": seconds[i], "error": errors[i]}
        for i in range(n_pairs)
    ]
