using System.Collections.Concurrent;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using LeafletForBlazor;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using RutasDBUS.Modelos.Planificacion;
using RutasDBUS.Modelos.Geocodificacion;
using RutasDBUS.Modelos.RedBus;
using RutasDBUS.Servicios;
using RutasDBUS.Servicios.Caminata;
using RutasDBUS.Servicios.Horarios;
using RutasDBUS.Servicios.Planificacion;
using RutasDBUS.Servicios.RedBus;

namespace RutasDBUS.Components.Pages;

public partial class PlanificadorRutas : ComponentBase
{
        // =========================================================
        // UI / Estado
        // =========================================================
        private int _segundosPorParadaBus = 30;
        private int _segundosPorParadaBusManual = 30;
    
        private bool _paradasPintadas = false;
        private bool _pintandoParadas = false;
        private Task? _tareaCargaRed;
    
        private int _versionCalculo = 0;
        private int _versionPintadoRuta = 0;
        private CancellationTokenSource? _ctsCalculo;
        private (double Latitud, double Longitud)? _coordsMouse;
        private const double UmbralCosturaVisualMetros = 12.0;
        private const double UmbralUnionPeatonalAutomaticaMetros = 250.0;
        private const string DashArrayBusVuelta = "10 10";
        private const string DashArrayPiePunteado = "1 7";
    
        private bool _mostrarConfigAvanzada = false;
        /// <summary>
        /// Alterna la configuracion panel.
        /// </summary>
        private void ToggleConfigPanel() => _mostrarConfigAvanzada = !_mostrarConfigAvanzada;
    
        private int _nivelBrilloMapa = 0;
    
        // CONFIG
        private const double FactorPenalizacionDistanciaPiePorDefecto = 1.35;
        private const double ParametroPenalizacionDistanciaPieMetrosPorDefecto = 200.0;
        private const double CoeficienteEsfuerzoAlturaPorDefecto = 8.2;
        private const double FactorEquilibradoAndarBusPorDefecto = 6.0;
        private const double UmbralPendienteSubidaRutaPorDefecto = 2.0;
        private const double UmbralPendienteBajadaRutaPorDefecto = 5.0;
        private const double UmbralAlturaAlertaPendienteRutaMetrosPorDefecto = 6.0;
        private const double MetrosPorPiso = 3.0;

        private double _velocidadPieKmH = 3.6;
        private double _factorPenalizacionDistanciaPie = FactorPenalizacionDistanciaPiePorDefecto;
        private double _parametroPenalizacionDistanciaPieMetros = ParametroPenalizacionDistanciaPieMetrosPorDefecto;
        private double _coeficienteEsfuerzoAltura = CoeficienteEsfuerzoAlturaPorDefecto;
        private double _pesoTransbordoRankingMinutos = 4.0;
        private double _factorEquilibradoAndarBus = FactorEquilibradoAndarBusPorDefecto;
        private string _factorPenalizacionDistanciaPieTextoEdicion = "1.35";
        private string _parametroPenalizacionDistanciaPieTextoEdicion = "200";
        private string _coeficienteEsfuerzoAlturaTextoEdicion = "8.20";
        private string _pesoTransbordoRankingTextoEdicion = "4,0";
        private string _factorEquilibradoAndarBusTextoEdicion = "6";
        private double _velocidadBusKmH = 16.0;
        private int _minutosTransbordo = 5;
        private int _margenTransbordoSegundos = 120;

        /// <summary>
        /// Expone el factor de penalizacion peatonal con dos decimales para la UI.
        /// </summary>
        private string FactorPenalizacionDistanciaPieTexto
        {
            get => _factorPenalizacionDistanciaPieTextoEdicion;
            set
            {
                var texto = (value ?? "").Replace(',', '.');
                _factorPenalizacionDistanciaPieTextoEdicion = texto;

                if (!double.TryParse(texto, NumberStyles.Float, CultureInfo.InvariantCulture, out var valor))
                    return;

                if (double.IsFinite(valor))
                    _factorPenalizacionDistanciaPie = valor;
            }
        }

        /// <summary>
        /// Expone el parametro base de metros de la penalizacion peatonal para la UI.
        /// </summary>
        private string ParametroPenalizacionDistanciaPieTexto
        {
            get => _parametroPenalizacionDistanciaPieTextoEdicion;
            set
            {
                var texto = (value ?? "").Replace(',', '.');
                _parametroPenalizacionDistanciaPieTextoEdicion = texto;

                if (!double.TryParse(texto, NumberStyles.Float, CultureInfo.InvariantCulture, out var valor))
                    return;

                if (double.IsFinite(valor))
                    _parametroPenalizacionDistanciaPieMetros = valor;
            }
        }

        /// <summary>
        /// Expone el coeficiente usado para elevar el esfuerzo por pendiente.
        /// </summary>
        private string CoeficienteEsfuerzoAlturaTexto
        {
            get => _coeficienteEsfuerzoAlturaTextoEdicion;
            set
            {
                var texto = (value ?? "").Replace(',', '.');
                _coeficienteEsfuerzoAlturaTextoEdicion = texto;

                if (!double.TryParse(texto, NumberStyles.Float, CultureInfo.InvariantCulture, out var valor))
                    return;

                if (double.IsFinite(valor))
                    _coeficienteEsfuerzoAltura = valor;
            }
        }

        /// <summary>
        /// Sincroniza los textos editables de coeficiente y parámetro con sus valores numéricos.
        /// </summary>
        /// <summary>
        /// Expone el peso de transbordo usado solo para ordenar alternativas.
        /// </summary>
        private string PesoTransbordoRankingTexto
        {
            get => _pesoTransbordoRankingTextoEdicion;
            set
            {
                var texto = value ?? "";
                _pesoTransbordoRankingTextoEdicion = texto;

                if (!TryParseDuracionMinutos(texto, out var valor))
                    return;

                if (double.IsFinite(valor))
                    _pesoTransbordoRankingMinutos = Math.Max(0, valor);
            }
        }

        /// <summary>
        /// Expone el factor que convierte metros andando a metros equivalentes de bus en el modo equilibrado.
        /// </summary>
        private string FactorEquilibradoAndarBusTexto
        {
            get => _factorEquilibradoAndarBusTextoEdicion;
            set
            {
                var texto = (value ?? "").Replace(',', '.');
                _factorEquilibradoAndarBusTextoEdicion = texto;

                if (!double.TryParse(texto, NumberStyles.Float, CultureInfo.InvariantCulture, out var valor))
                    return;

                if (double.IsFinite(valor))
                    _factorEquilibradoAndarBus = Math.Max(0, valor);
            }
        }

        private static bool TryParseDuracionMinutos(string texto, out double minutos)
        {
            minutos = 0;
            texto = (texto ?? "").Trim().Replace(',', '.');
            if (string.IsNullOrWhiteSpace(texto))
                return false;

            var partes = texto.Split(':', StringSplitOptions.TrimEntries);
            if (partes.Length == 1)
                return double.TryParse(partes[0], NumberStyles.Float, CultureInfo.InvariantCulture, out minutos);

            if (partes.Length is < 2 or > 3)
                return false;

            if (!double.TryParse(partes[^1], NumberStyles.Float, CultureInfo.InvariantCulture, out var segundos))
                return false;
            if (!int.TryParse(partes[^2], NumberStyles.Integer, CultureInfo.InvariantCulture, out var minutosParte))
                return false;

            var horas = 0;
            if (partes.Length == 3 &&
                !int.TryParse(partes[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out horas))
            {
                return false;
            }

            minutos = Math.Max(0, (horas * 60.0) + minutosParte + (segundos / 60.0));
            return true;
        }

        private static string FormatearDuracionMinutos(double minutos)
        {
            if (!double.IsFinite(minutos) || minutos <= 0)
                return "0,0";

            return minutos.ToString("0.0", CultureInfo.InvariantCulture).Replace('.', ',');
        }

        private string FormatearEsfuerzoElevacion(double? distanciaMetros, double? pendientePorcentaje)
        {
            var esfuerzo = CalcularEsfuerzoElevacion(distanciaMetros, pendientePorcentaje);
            return esfuerzo is null
                ? "-"
                : esfuerzo.Value.ToString("0.00", CultureInfo.InvariantCulture);
        }

        private double? CalcularEsfuerzoElevacion(double? distanciaMetros, double? pendientePorcentaje)
        {
            if (distanciaMetros is null ||
                pendientePorcentaje is null ||
                !double.IsFinite(distanciaMetros.Value) ||
                !double.IsFinite(pendientePorcentaje.Value) ||
                distanciaMetros.Value <= 0)
            {
                return null;
            }

            var coeficiente = double.IsFinite(_coeficienteEsfuerzoAltura)
                ? _coeficienteEsfuerzoAltura
                : CoeficienteEsfuerzoAlturaPorDefecto;

            var umbralSubida = NormalizarUmbralPendienteRuta(_umbralPendienteSubidaRuta);
            if (pendientePorcentaje.Value < umbralSubida)
                return distanciaMetros.Value;

            var pendiente = pendientePorcentaje.Value / 100.0;
            var factorConPotencia = Math.Pow(1.0 + pendiente, coeficiente);
            var esfuerzo = distanciaMetros.Value * factorConPotencia;
            return double.IsFinite(esfuerzo)
                ? esfuerzo
                : null;
        }

        private void SincronizarTextosPenalizacionPie()
        {
            _factorPenalizacionDistanciaPieTextoEdicion = _factorPenalizacionDistanciaPie.ToString("0.00", CultureInfo.InvariantCulture);
            _parametroPenalizacionDistanciaPieTextoEdicion = _parametroPenalizacionDistanciaPieMetros.ToString("0", CultureInfo.InvariantCulture);
            _coeficienteEsfuerzoAlturaTextoEdicion = _coeficienteEsfuerzoAltura.ToString("0.00", CultureInfo.InvariantCulture);
            _pesoTransbordoRankingTextoEdicion = FormatearDuracionMinutos(_pesoTransbordoRankingMinutos);
            _factorEquilibradoAndarBusTextoEdicion = _factorEquilibradoAndarBus.ToString("0.##", CultureInfo.InvariantCulture);
        }

        /// <summary>
        /// Normaliza el texto del coeficiente al salir del campo.
        /// </summary>
        private async Task OnBlurFactorPenalizacionDistanciaPie()
        {
            SincronizarTextosPenalizacionPie();
            await PersistirConfiguracionAsync();
        }

        /// <summary>
        /// Normaliza el texto del parámetro base al salir del campo.
        /// </summary>
        private async Task OnBlurParametroPenalizacionDistanciaPie()
        {
            SincronizarTextosPenalizacionPie();
            await PersistirConfiguracionAsync();
        }

        /// <summary>
        /// Normaliza el coeficiente de esfuerzo de altura al salir del campo.
        /// </summary>
        private async Task OnBlurCoeficienteEsfuerzoAltura()
        {
            SincronizarTextosPenalizacionPie();
            await PersistirConfiguracionAsync();

            if (_mostrarElevacionesRuta)
                await RecalcularRankingTrasCambioEsfuerzoPendienteAsync();
            else
                StateHasChanged();
        }

        /// <summary>
        /// Normaliza el peso de transbordos al salir del campo.
        /// </summary>
        private async Task OnBlurPesoTransbordoRanking()
        {
            SincronizarTextosPenalizacionPie();
            await PersistirConfiguracionAsync();
            await RecalcularRutaEquilibradaTrasCambioFactorAsync();
        }

        /// <summary>
        /// Normaliza el factor equilibrado al salir del campo.
        /// </summary>
        private async Task OnBlurFactorEquilibradoAndarBus()
        {
            SincronizarTextosPenalizacionPie();
            await PersistirConfiguracionAsync();
            await RecalcularRutaEquilibradaTrasCambioFactorAsync();
        }
    
        private int _radioCandidatasMetros = 1200;
        private int _maxCandidatasPorLado = 12;
        private bool _usarOsrmParaRanking = false;
    
        private bool _activarRangoItinerarios = true;
        private int _itinerarioMin = 1;
        private int _itinerarioMax = 99;
    
        // Estado
        private string _textoBusquedaOrigen = string.Empty;
        private string _textoBusquedaDestino = string.Empty;
    
        private RealTimeMap? _mapa;
    
        private readonly RealTimeMap.LoadParameters _parametrosMapa = new()
        {
            location = new RealTimeMap.Location { latitude = 43.307251, longitude = -1.979497 },
            zoomLevel = 14,
            basemap = new RealTimeMap.Basemap
            {
                basemapLayers = new()
                {
                    new RealTimeMap.BasemapLayer
                    {
                        url = "https://osm.tecnosis.net/osm_tiles/{z}/{x}/{y}.png",
                        attribution = "© OpenStreetMap",
                        title = "",
                        detectRetina = true
                    }
                }
            }
        };
    
        private double[]? _coordenadasOrigen;
        private double[]? _coordenadasDestino;
    
        private string _mensajeRuta = string.Empty;
        private bool _hayRutaValida = false;
    
        private Parada? _paradaOrigenSeleccionada;
        private Parada? _paradaDestinoSeleccionada;
        private Parada? _primerTransbordoseleccionado;
        private int _numeroTransbordos = 0;
    
        private double _distanciaAPieInicio = 0;
        private double _distanciaAPieFin = 0;
        private double _distanciaEnBus = 0;
    
        private Parada? _paradaClicada;
    
        private readonly List<DetalleTramoRuta> _tramosRuta = new();
        private double _duracionTotalSegundos = 0;
        private double? _altitudOrigenResumenRuta = null;
        private double? _altitudDestinoResumenRuta = null;
        private string? _callejeroOrigenResumenRuta = null;
        private string? _callejeroDestinoResumenRuta = null;
    
        private readonly List<AlternativaRuta> _opcionesRuta = new();
        private int _indiceAlternativaSeleccionada = 0;
        private DateTime? _ultimaHoraReferenciaRuta = null;
        private TimeSpan? _tiempoUltimoCalculoRuta = null;
        private bool _mostrarElevacionesRuta = false;
        private bool _pintandoElevacionesRuta = false;
        private bool _mostrarConfiguracionPendientesRuta = false;
        private bool _panelRutasMovilContraido = false;
        private double _umbralPendienteSubidaRuta = UmbralPendienteSubidaRutaPorDefecto;
        private double _umbralPendienteBajadaRuta = UmbralPendienteBajadaRutaPorDefecto;
        private double _umbralAlturaAlertaPendienteRutaMetros = UmbralAlturaAlertaPendienteRutaMetrosPorDefecto;
        private double _alturaPendientePositivaSignificativaRutaMetros = 0.0;
        private double _distanciaPendientePositivaSignificativaRutaMetros = 0.0;

        private bool MostrarAlertaPendientesPositivasSignificativas =>
            _mostrarElevacionesRuta &&
            _alturaPendientePositivaSignificativaRutaMetros >
                NormalizarUmbralAlturaAlertaPendienteRutaMetros(_umbralAlturaAlertaPendienteRutaMetros);
        private readonly Dictionary<string, List<Parada>> _paradasItinerarioVisiblesPorId = new(StringComparer.OrdinalIgnoreCase);
        private readonly Dictionary<string, List<Parada>> _paradasRutaVisiblesPorId = new(StringComparer.OrdinalIgnoreCase);
    
        private const string CriterioRutaTiempo = "Tiempo";
        private const string CriterioRutaEquilibrado = "Equilibrado";
        private const string CriterioRutaAndar = "Andar";
        private const string PreferenciaRutaTiempo = "M\u00e1s r\u00e1pida";
        private const string PreferenciaRutaAndar = "Menos Andar";
        private string _criterioPrioritarioRuta = CriterioRutaEquilibrado;
        private string? _preferenciaEtiquetaRuta = null;

        private bool CriterioPrioritarioTiempoActivo =>
            string.Equals(_criterioPrioritarioRuta, CriterioRutaTiempo, StringComparison.OrdinalIgnoreCase);

        private bool CriterioPrioritarioEquilibradoActivo =>
            string.Equals(_criterioPrioritarioRuta, CriterioRutaEquilibrado, StringComparison.OrdinalIgnoreCase);

        private bool CriterioPrioritarioAndarActivo =>
            string.Equals(_criterioPrioritarioRuta, CriterioRutaAndar, StringComparison.OrdinalIgnoreCase);

        private void SincronizarPreferenciaDesdeCriterioRuta()
        {
            _preferenciaEtiquetaRuta = CriterioPrioritarioTiempoActivo
                ? PreferenciaRutaTiempo
                : CriterioPrioritarioAndarActivo
                    ? PreferenciaRutaAndar
                    : null;
        }

        private static string NormalizarCriterioRuta(string? criterio, string? preferenciaLegada = null)
        {
            if (string.Equals(criterio, CriterioRutaTiempo, StringComparison.OrdinalIgnoreCase))
                return CriterioRutaTiempo;
            if (string.Equals(criterio, CriterioRutaAndar, StringComparison.OrdinalIgnoreCase))
                return CriterioRutaAndar;
            if (string.Equals(criterio, CriterioRutaEquilibrado, StringComparison.OrdinalIgnoreCase))
                return CriterioRutaEquilibrado;

            if (string.Equals(preferenciaLegada, PreferenciaRutaTiempo, StringComparison.OrdinalIgnoreCase))
                return CriterioRutaTiempo;
            if (string.Equals(preferenciaLegada, PreferenciaRutaAndar, StringComparison.OrdinalIgnoreCase))
                return CriterioRutaAndar;

            return CriterioRutaEquilibrado;
        }
    
        // =========================================================
        // Helpers (texto/formato)
        // =========================================================
    
        /// <summary>
        /// Gestiona css desde variante.
        /// </summary>
        private string ColorCssDesdeVariante(InfoVariante v)
        {
            // Color real de la línea desde el JSON
            var css = RedBusServicio.ConvertirColorArgbACss(v.Linea.ColorArgb);
    
            // Fallback por seguridad si viniera vacío / inválido
            if (string.IsNullOrWhiteSpace(css))
                css = "#2563eb";
    
            return css;
        }
    
        private sealed class ColaDoble<T>
    {
        private readonly LinkedList<T> _list = new();
        public int Count => _list.Count;
        /// <summary>
        /// Gestiona front.
        /// </summary>
        public void PushFront(T item) => _list.AddFirst(item);
        /// <summary>
        /// Gestiona back.
        /// </summary>
        public void PushBack(T item) => _list.AddLast(item);
        /// <summary>
        /// Gestiona front.
        /// </summary>
        public T PopFront()
        {
            var v = _list.First!.Value;
            _list.RemoveFirst();
            return v;
        }
    }
    
    
        /// <summary>
        /// Construye el payload JS de paradas de una variante.
        /// </summary>
        private List<object> BuildStopsForVariante(InfoVariante v)
        {
            if (v?.CodigosParadas is null || v.CodigosParadas.Count == 0)
                return new List<object>();

            return BuildStopsParaRangoVariante(v, 0, v.CodigosParadas.Count - 1);
        }

        /// <summary>
        /// Construye el payload JS de paradas recorridas por un tramo concreto.
        /// </summary>
        private List<object> BuildStopsForTramoBus(TramoBus tramo)
        {
            if (tramo?.Variante?.CodigosParadas is null || tramo.Variante.CodigosParadas.Count == 0)
                return new List<object>();

            var ultimoIndice = tramo.Variante.CodigosParadas.Count - 1;
            var inicio = Math.Clamp(Math.Min(tramo.IndiceInicio, tramo.IndiceFin), 0, ultimoIndice);
            var fin = Math.Clamp(Math.Max(tramo.IndiceInicio, tramo.IndiceFin), 0, ultimoIndice);
            return BuildStopsParaRangoVariante(tramo.Variante, inicio, fin);
        }

        private List<object> BuildStopsParaRangoVariante(InfoVariante variante, int inicio, int fin)
        {
            var stops = new List<object>();
            var codigosVistos = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

            for (var indice = inicio; indice <= fin; indice++)
            {
                var codigo = variante.CodigosParadas[indice];
                if (!codigosVistos.Add(codigo) || !_paradaByCode.TryGetValue(codigo, out var parada))
                    continue;

                stops.Add(new
                {
                    lat = parada.Latitud,
                    lon = parada.Longitud,
                    code = parada.Codigo,
                    bearing = CalcularRumboParada(variante, indice)
                });
            }

            return stops;
        }

        /// <summary>
        /// Obtiene la tangente local de la geometria real al salir de la parada.
        /// En la terminal usa la tangente de llegada del ultimo trayecto.
        /// </summary>
        private double? CalcularRumboParada(InfoVariante variante, int indiceParada)
        {
            var idsTrayectos = variante.Variante?.IdsTrayectos;
            if (idsTrayectos is not null)
            {
                if (indiceParada >= 0 &&
                    indiceParada < idsTrayectos.Count &&
                    RedBusServicio.Trayectos.TryGetValue(idsTrayectos[indiceParada], out var trayectoSalida))
                {
                    var rumboSalida = CalcularRumboGeometriaTrayecto(trayectoSalida.Posiciones, desdeInicio: true);
                    if (rumboSalida is not null)
                        return rumboSalida;
                }

                var indiceTrayectoLlegada = indiceParada - 1;
                if (indiceTrayectoLlegada >= 0 &&
                    indiceTrayectoLlegada < idsTrayectos.Count &&
                    RedBusServicio.Trayectos.TryGetValue(idsTrayectos[indiceTrayectoLlegada], out var trayectoLlegada))
                {
                    var rumboLlegada = CalcularRumboGeometriaTrayecto(trayectoLlegada.Posiciones, desdeInicio: false);
                    if (rumboLlegada is not null)
                        return rumboLlegada;
                }
            }

            return CalcularRumboParadaPorCoordenadas(variante, indiceParada);
        }

        private static double? CalcularRumboGeometriaTrayecto(
            IReadOnlyList<double[]> posiciones,
            bool desdeInicio)
        {
            var puntos = posiciones
                .Where(punto =>
                    punto is { Length: >= 2 } &&
                    double.IsFinite(punto[0]) &&
                    double.IsFinite(punto[1]))
                .ToList();

            if (puntos.Count < 2)
                return null;

            const double distanciaMinimaRumboMetros = 3.0;
            double? rumboCorto = null;

            if (desdeInicio)
            {
                var origen = puntos[0];
                for (var indice = 1; indice < puntos.Count; indice++)
                {
                    var destino = puntos[indice];
                    var rumbo = CalcularRumboGrados(origen[0], origen[1], destino[0], destino[1]);
                    if (rumbo is null)
                        continue;

                    rumboCorto ??= rumbo;
                    if (CalcularDistanciaMetros(origen[0], origen[1], destino[0], destino[1]) >= distanciaMinimaRumboMetros)
                        return rumbo;
                }
            }
            else
            {
                var destino = puntos[^1];
                for (var indice = puntos.Count - 2; indice >= 0; indice--)
                {
                    var origen = puntos[indice];
                    var rumbo = CalcularRumboGrados(origen[0], origen[1], destino[0], destino[1]);
                    if (rumbo is null)
                        continue;

                    rumboCorto ??= rumbo;
                    if (CalcularDistanciaMetros(origen[0], origen[1], destino[0], destino[1]) >= distanciaMinimaRumboMetros)
                        return rumbo;
                }
            }

            return rumboCorto;
        }

        private double? CalcularRumboParadaPorCoordenadas(InfoVariante variante, int indice)
        {
            if (indice < 0 ||
                indice >= variante.CodigosParadas.Count ||
                !_paradaByCode.TryGetValue(variante.CodigosParadas[indice], out var actual))
            {
                return null;
            }

            for (var siguiente = indice + 1; siguiente < variante.CodigosParadas.Count; siguiente++)
            {
                if (!_paradaByCode.TryGetValue(variante.CodigosParadas[siguiente], out var paradaSiguiente))
                    continue;

                var rumbo = CalcularRumboGrados(
                    actual.Latitud,
                    actual.Longitud,
                    paradaSiguiente.Latitud,
                    paradaSiguiente.Longitud);
                if (rumbo is not null)
                    return rumbo;
            }

            for (var anterior = indice - 1; anterior >= 0; anterior--)
            {
                if (!_paradaByCode.TryGetValue(variante.CodigosParadas[anterior], out var paradaAnterior))
                    continue;

                var rumbo = CalcularRumboGrados(
                    paradaAnterior.Latitud,
                    paradaAnterior.Longitud,
                    actual.Latitud,
                    actual.Longitud);
                if (rumbo is not null)
                    return rumbo;
            }

            return null;
        }

        private static double? CalcularRumboGrados(
            double latitudOrigenGrados,
            double longitudOrigenGrados,
            double latitudDestinoGrados,
            double longitudDestinoGrados)
        {
            if (!double.IsFinite(latitudOrigenGrados) ||
                !double.IsFinite(longitudOrigenGrados) ||
                !double.IsFinite(latitudDestinoGrados) ||
                !double.IsFinite(longitudDestinoGrados))
            {
                return null;
            }

            var latitudOrigen = latitudOrigenGrados * Math.PI / 180.0;
            var latitudDestino = latitudDestinoGrados * Math.PI / 180.0;
            var diferenciaLongitud = (longitudDestinoGrados - longitudOrigenGrados) * Math.PI / 180.0;
            var y = Math.Sin(diferenciaLongitud) * Math.Cos(latitudDestino);
            var x = Math.Cos(latitudOrigen) * Math.Sin(latitudDestino) -
                    Math.Sin(latitudOrigen) * Math.Cos(latitudDestino) * Math.Cos(diferenciaLongitud);

            if (Math.Abs(x) < 1e-12 && Math.Abs(y) < 1e-12)
                return null;

            var rumbo = Math.Atan2(y, x) * 180.0 / Math.PI;
            return (rumbo + 360.0) % 360.0;
        }

        /// <summary>
        /// Construye las paradas visibles recorridas por un tramo concreto de bus.
        /// </summary>
        private List<Parada> BuildParadasForTramoBus(TramoBus tramo)
        {
            var stops = new List<Parada>();

            if (tramo?.Variante?.CodigosParadas is null || tramo.Variante.CodigosParadas.Count == 0)
                return stops;

            var inicio = Math.Clamp(Math.Min(tramo.IndiceInicio, tramo.IndiceFin), 0, tramo.Variante.CodigosParadas.Count - 1);
            var fin = Math.Clamp(Math.Max(tramo.IndiceInicio, tramo.IndiceFin), 0, tramo.Variante.CodigosParadas.Count - 1);
            var codigosVistos = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

            for (var indice = inicio; indice <= fin; indice++)
            {
                var codigo = tramo.Variante.CodigosParadas[indice];
                if (!codigosVistos.Add(codigo))
                    continue;

                if (_paradaByCode.TryGetValue(codigo, out var parada))
                    stops.Add(parada);
            }

            return stops;
        }

        /// <summary>
        /// Elimina el registro de paradas visibles de un itinerario activo.
        /// </summary>
        private void QuitarParadasItinerarioVisibles(string id)
        {
            _paradasItinerarioVisiblesPorId.Remove(id);
        }

        /// <summary>
        /// Limpia todas las paradas visibles de itinerarios activos.
        /// </summary>
        private void LimpiarParadasItinerarioVisibles()
        {
            _paradasItinerarioVisiblesPorId.Clear();
        }

        /// <summary>
        /// Registra paradas visibles de la solución seleccionada.
        /// </summary>
        private void RegistrarParadasRutaVisibles(string id, IEnumerable<Parada> paradas)
        {
            _paradasRutaVisiblesPorId[id] = paradas
                .GroupBy(p => p.Codigo, StringComparer.OrdinalIgnoreCase)
                .Select(g => g.First())
                .ToList();
        }

        /// <summary>
        /// Limpia todas las paradas visibles de la solución seleccionada.
        /// </summary>
        private void LimpiarParadasRutaVisibles()
        {
            _paradasRutaVisiblesPorId.Clear();
        }

        /// <summary>
        /// Devuelve las paradas destacadas actualmente visibles cuando la capa general está oculta.
        /// </summary>
        private List<Parada> ObtenerParadasDestacadasVisibles()
        {
            var resultado = new List<Parada>();
            var codigos = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

            foreach (var parada in _paradasRutaVisiblesPorId.Values.SelectMany(x => x))
            {
                if (codigos.Add(parada.Codigo))
                    resultado.Add(parada);
            }

            foreach (var parada in _paradasItinerarioVisiblesPorId.Values.SelectMany(x => x))
            {
                if (codigos.Add(parada.Codigo))
                    resultado.Add(parada);
            }

            return resultado;
        }

        /// <summary>
        /// Detecta si el click cae sobre una parada destacada de itinerario o solución cuando la capa general está oculta.
        /// </summary>
        private async Task<Parada?> DetectarParadaDestacadaHitAsync(double clickLat, double clickLon)
        {
            var parada = ObtenerParadasDestacadasVisibles()
                .Select(p => new
                {
                    Parada = p,
                    Distancia = CalcularDistanciaMetros(clickLat, clickLon, p.Latitud, p.Longitud)
                })
                .Where(x => x.Distancia <= 40)
                .OrderBy(x => x.Distancia)
                .Select(x => x.Parada)
                .FirstOrDefault();

            if (parada is null)
                return null;

            var hit = await JS.InvokeAsync<bool>(
                "rt.isClickInsideCirclePx",
                clickLat, clickLon,
                parada.Latitud, parada.Longitud,
                10);

            return hit ? parada : null;
        }
    
    
    
        /// <summary>
        /// Gestiona por pixel.
        /// </summary>
        private static double MetrosPorPixel(double lat, int zoom)
        {
            // WebMercator: m/px = 156543.03392 * cos(lat) / 2^zoom
            return 156543.03392 * Math.Cos(lat * Math.PI / 180.0) / Math.Pow(2, zoom);
        }
    
        /// <summary>
        /// Gestiona clic parada metros.
        /// </summary>
        private async Task<double> RadioClickParadaMetrosAsync(double lat)
        {
            int zoom = 14;
            try { zoom = await JS.InvokeAsync<int>("rt.getZoom"); } catch { }
    
            const double TOL_PX = 18; // 🎯 16-22 suele perfecto
            return TOL_PX * MetrosPorPixel(lat, zoom);
        }
    
    
        // ✅ Rango de líneas
        private bool _activarRangoLineas = false;
        private int _lineaMin = 1;
        private int _lineaMax = 200;
        private readonly HashSet<int> _lineasPermitidas = new();
    
    
    
    /// <summary>
    /// Alterna linea completa.
    /// </summary>
    private async Task ToggleLineaCompletaAsync(DetalleTramoRuta tramo)
    {
        if (tramo?.VarianteBus is null) return;
    
        var id = $"full_{tramo.VarianteBus.Linea.Codigo}_{tramo.VarianteBus.CodigoItinerario}_{tramo.VarianteBus.Sentido?.Codigo}_{tramo.VarianteBus.Variante.Codigo}";
    
        // Si está activa => QUITAR
        if (_previewLineasActivas.Contains(id))
        {
    await JS.InvokeVoidAsync("rt.drawPreviewLine", id, new List<double[]>(), new { });
    await JS.InvokeVoidAsync("rt.toggleItStops", id, new List<object>(), new { });
    _previewLineasActivas.Remove(id);
    return;
    
        }
    
        // Si no está activa => PINTAR
        var full = ConstruirGeometriaCompletaVariante(tramo.VarianteBus);
        if (full is null) return;
    
        var color = ColorCssDesdeVariante(tramo.VarianteBus);
        var dash = string.Equals(
            NormalizarSentidoCodigo(tramo.VarianteBus.Sentido?.Codigo),
            "V",
            StringComparison.OrdinalIgnoreCase)
            ? DashArrayBusVuelta
            : null;
    
        await JS.InvokeVoidAsync("rt.drawPreviewLine", id, LineaToLatLng(full), new
        {
            color,
            weight = 7,
            opacity = 0.95,
            dashArray = dash,
            arrowEnd = true,
            arrowBackPx = 26
        });
    
            await JS.InvokeVoidAsync("rt.toggleItStops", id, BuildStopsForVariante(tramo.VarianteBus), new
            {
                radius = 7,
                color = "#0f172a",
                fillColor = color,
                weight = 2,
                opacity = 1.0,
                fillOpacity = 1.0
            });
    
            _previewLineasActivas.Add(id);
    
    }
    
    
    
        /// <summary>
        /// Cuenta paradas recorridas.
        /// </summary>
        private int ContarParadasRecorridas(TramoBus tramo)
        {
            // Nº de paradas “pasadas” excluyendo la de inicio.
            // Ej: inicio=2 fin=5 => 3 paradas (3,4,5)
            return Math.Max(0, Math.Abs(tramo.IndiceFin - tramo.IndiceInicio));
        }
    
    
        /// <summary>
        /// Gestiona sin decimal.
        /// </summary>
        private static string CodigoSinDecimal(string? codigo)
        {
            if (string.IsNullOrWhiteSpace(codigo)) return "";
            var i = codigo.IndexOf('.');
            return (i > 0) ? codigo.Substring(0, i) : codigo;
        }
    
        /// <summary>
        /// Gestiona parada formateado.
        /// </summary>
        private static string CodigoParadaFormateado(string? codigo)
        {
            var c = CodigoSinDecimal(codigo);
            if (string.IsNullOrWhiteSpace(c)) return "";
            return $"P-{c}";
        }
    
        /// <summary>
        /// Normaliza codigo parada.
        /// </summary>
        private static string NormalizarCodigoParada(string? s)
        {
            if (string.IsNullOrWhiteSpace(s)) return "";
            s = s.Trim();
            s = s.Replace("P-", "", StringComparison.OrdinalIgnoreCase);
            return CodigoSinDecimal(s);
        }
    
        /// <summary>
        /// Gestiona palabras clave.
        /// </summary>
        private static string CapitalizarPalabrasClave(string s)
        {
            if (string.IsNullOrWhiteSpace(s)) return s;
    
            return s
                .Replace("parada", "Parada", StringComparison.OrdinalIgnoreCase)
                .Replace("paradas", "Paradas", StringComparison.OrdinalIgnoreCase)
                .Replace("línea", "Línea", StringComparison.OrdinalIgnoreCase)
                .Replace("linea", "Línea", StringComparison.OrdinalIgnoreCase)
                .Replace("líneas", "Líneas", StringComparison.OrdinalIgnoreCase)
                .Replace("lineas", "Líneas", StringComparison.OrdinalIgnoreCase);
        }
    
        /// <summary>
        /// 
        /// </summary>
        private static string Q(double v) => Math.Round(v, 5).ToString("0.00000", CultureInfo.InvariantCulture);
    
        /// <summary>
        /// Gestiona clave.
        /// </summary>
        private static string WalkKey(string provider, double lat1, double lon1, double lat2, double lon2)
            => $"{provider}|{Q(lat1)},{Q(lon1)}|{Q(lat2)},{Q(lon2)}";
    
        /// <summary>
        /// Gestiona to lat lng.
        /// </summary>
        private static List<double[]> LineaToLatLng(double[,] linea)
        {
            var res = new List<double[]>(linea.GetLength(0));
            for (int i = 0; i < linea.GetLength(0); i++)
                res.Add(new[] { linea[i, 0], linea[i, 1] });
            return res;
        }

        /// <summary>
        /// Devuelve el primer punto de una linea.
        /// </summary>
        private static double[]? ObtenerPrimerPunto(double[,]? linea)
        {
            if (linea is null || linea.GetLength(0) == 0)
                return null;

            return new[] { linea[0, 0], linea[0, 1] };
        }

        /// <summary>
        /// Devuelve el ultimo punto de una linea.
        /// </summary>
        private static double[]? ObtenerUltimoPunto(double[,]? linea)
        {
            if (linea is null || linea.GetLength(0) == 0)
                return null;

            var ultimo = linea.GetLength(0) - 1;
            return new[] { linea[ultimo, 0], linea[ultimo, 1] };
        }

        /// <summary>
        /// Sobrescribe el primer punto de una linea.
        /// </summary>
        private static void EstablecerPrimerPunto(double[,]? linea, double[]? punto)
        {
            if (linea is null || punto is null || linea.GetLength(0) == 0)
                return;

            linea[0, 0] = punto[0];
            linea[0, 1] = punto[1];
        }

        /// <summary>
        /// Obtiene la distancia entre dos puntos geograficos.
        /// </summary>
        private static double CalcularDistanciaEntrePuntos(double[]? origen, double[]? destino)
        {
            if (origen is null || destino is null || origen.Length < 2 || destino.Length < 2)
                return 0;

            return CalcularDistanciaMetros(origen[0], origen[1], destino[0], destino[1]);
        }

        /// <summary>
        /// Invalida cualquier pintado de ruta anterior y devuelve la nueva versión activa.
        /// </summary>
        private int IniciarNuevaVersionPintadoRuta()
            => Interlocked.Increment(ref _versionPintadoRuta);

        /// <summary>
        /// Lanza cancelación si el pintado ya no es el activo.
        /// </summary>
        private void ThrowIfPintadoObsoleto(int versionPintado)
        {
            if (versionPintado != Volatile.Read(ref _versionPintadoRuta))
                throw new OperationCanceledException();
        }

        /// <summary>
        /// Resuelve la parada fisica mas coherente para un codigo y una referencia espacial.
        /// </summary>
        private Parada ResolverParadaFisica(
            string? codigoParada,
            double[]? puntoReferencia = null,
            Parada? paradaPreferida = null)
        {
            if (!string.IsNullOrWhiteSpace(codigoParada))
            {
                var candidatas = RedBusServicio.Paradas
                    .Where(parada => parada.Codigo.Equals(codigoParada, StringComparison.OrdinalIgnoreCase))
                    .ToList();

                if (candidatas.Count == 1)
                    return candidatas[0];

                if (candidatas.Count > 1)
                {
                    if (paradaPreferida is not null &&
                        paradaPreferida.Codigo.Equals(codigoParada, StringComparison.OrdinalIgnoreCase))
                    {
                        var coincidenciaExacta = candidatas.FirstOrDefault(parada =>
                            Math.Abs(parada.Latitud - paradaPreferida.Latitud) < 1e-9 &&
                            Math.Abs(parada.Longitud - paradaPreferida.Longitud) < 1e-9);

                        if (coincidenciaExacta is not null)
                            return coincidenciaExacta;
                    }

                    if (puntoReferencia is not null)
                    {
                        return candidatas
                            .OrderBy(parada => CalcularDistanciaMetros(
                                parada.Latitud,
                                parada.Longitud,
                                puntoReferencia[0],
                                puntoReferencia[1]))
                            .First();
                    }

                    if (paradaPreferida is not null &&
                        paradaPreferida.Codigo.Equals(codigoParada, StringComparison.OrdinalIgnoreCase))
                    {
                        return paradaPreferida;
                    }

                    return candidatas[0];
                }
            }

            return paradaPreferida ?? new Parada();
        }

        /// <summary>
        /// Crea una linea minima entre dos puntos.
        /// </summary>
        private static double[,] ConstruirLineaDirecta(double[] origen, double[] destino)
        {
            return new[,]
            {
                { origen[0], origen[1] },
                { destino[0], destino[1] }
            };
        }
    
        /// <summary>
        /// Limpia rutas js.
        /// </summary>
        private async Task ClearRoutesJsAsync()
        {
            LimpiarParadasRutaVisibles();
            try { await JS.InvokeVoidAsync("rt.clearRoutes"); } catch { }
        }

        /// <summary>
        /// Limpia las paradas destacadas de la solución seleccionada.
        /// </summary>
        private async Task ClearRouteStopsJsAsync()
        {
            try { await JS.InvokeVoidAsync("rt.clearRouteStops"); } catch { }
        }

        /// <summary>
        /// Sincroniza los marcadores arrastrables de origen y destino con el estado actual.
        /// </summary>
        private async Task SincronizarMarcadoresOrigenDestinoJsAsync()
        {
            if (_coordenadasOrigen is not null)
            {
                try
                {
                    await JS.InvokeVoidAsync("rt.setEndpointMarker", "origin", _coordenadasOrigen[0], _coordenadasOrigen[1], new
                    {
                        color = "#22c55e",
                        title = "Origen",
                        size = 18
                    });
                }
                catch { }
            }
            else
            {
                try { await JS.InvokeVoidAsync("rt.clearEndpointMarkers", "origin"); } catch { }
            }

            if (_coordenadasDestino is not null)
            {
                try
                {
                    await JS.InvokeVoidAsync("rt.setEndpointMarker", "destination", _coordenadasDestino[0], _coordenadasDestino[1], new
                    {
                        color = "#ef4444",
                        title = "Destino",
                        size = 18
                    });
                }
                catch { }
            }
            else
            {
                try { await JS.InvokeVoidAsync("rt.clearEndpointMarkers", "destination"); } catch { }
            }
        }

        /// <summary>
        /// Limpia los marcadores arrastrables de origen y destino.
        /// </summary>
        private async Task ClearEndpointMarkersJsAsync()
        {
            try { await JS.InvokeVoidAsync("rt.clearEndpointMarkers"); } catch { }
        }
    
        /// <summary>
        /// Dibuja ruta js.
        /// </summary>
        private async Task DrawRouteJsAsync(
            string id,
            double[,] linea,
            string color,
            int weight,
            double opacity,
            string? dashArray = null,
            bool arrowEnd = false,
            string? lineCap = null,
            IReadOnlyList<object>? elevationSegments = null)
        {
            try
            {
                await JS.InvokeVoidAsync(
                    "rt.drawRoute",
                    id,
                    LineaToLatLng(linea),
                    new { color, weight, opacity, dashArray, arrowEnd, lineCap, elevationSegments }
                );
            }
            catch { }
        }
    
        /// <summary>
        /// Ajusta rutas js.
        /// </summary>
        private async Task FitRoutesJsAsync(
            IReadOnlyList<double[]>? coordenadas = null,
            double[]? centroPreferido = null)
        {
            try
            {
                if (coordenadas is null && centroPreferido is null)
                    await JS.InvokeVoidAsync("rt.fitRoutes");
                else
                    await JS.InvokeVoidAsync("rt.fitRoutes", coordenadas, centroPreferido);
            }
            catch { }
        }

        /// <summary>
        /// Dibuja las paradas destacadas de un tramo de la solución seleccionada.
        /// </summary>
        private async Task DrawRouteStopsJsAsync(
            string id,
            IReadOnlyList<object> stops,
            string fillColor,
            double radius = 8,
            string color = "#0f172a",
            int weight = 2,
            double opacity = 1.0,
            double fillOpacity = 1.0)
        {
            try
            {
                await JS.InvokeVoidAsync(
                    "rt.drawRouteStops",
                    id,
                    stops,
                    new { radius, color, fillColor, weight, opacity, fillOpacity });
            }
            catch { }
        }
    
        /// <summary>
        /// Formatea distancia.
        /// </summary>
        private string FormatearDistancia(double d)
        {
            if (d <= 0) return "-";
            if (d < 950) return $"{d:0.00} m";
            return $"{(d / 1000.0):0.00} km";
        }

        /// <summary>
        /// Formatea distancias para las tarjetas de alternativas con km a 3 decimales y metros truncados.
        /// </summary>
        private string FormatearDistanciaTarjeta(double d)
        {
            if (d <= 0) return "-";
            if (d < 1000) return $"{Math.Truncate(d):0} m";
            return $"{(d / 1000.0):0.000} km";
        }

        /// <summary>
        /// Formatea distancias para la ventana de detalle de ruta truncando los metros visibles.
        /// </summary>
        private string FormatearDistanciaDetalleRuta(double d)
        {
            if (d <= 0) return "-";
            if (d < 1000) return $"{Math.Truncate(d):0} m";
            return $"{(d / 1000.0):0.000} km";
        }

        /// <summary>
        /// Construye la lÃ­nea tÃ©cnica XYZ para el origen.
        /// </summary>
        private string FormatearResumenXYZOrigen()
        {
            if (_coordenadasOrigen is null)
                return string.Empty;

            var lon = _coordenadasOrigen[1].ToString("0.000000", CultureInfo.InvariantCulture);
            var lat = _coordenadasOrigen[0].ToString("0.000000", CultureInfo.InvariantCulture);
            var alt = _altitudOrigenResumenRuta is null
                ? "-"
                : $"{_altitudOrigenResumenRuta.Value.ToString("0.0", CultureInfo.InvariantCulture)} m";

            return $"O: Y.Lat = {lat}   X.Lon = {lon}   Z = {alt}";
        }

        /// <summary>
        /// Construye la lÃ­nea tÃ©cnica XYZ para el destino.
        /// </summary>
        private string FormatearResumenXYZDestino()
        {
            if (_coordenadasDestino is null)
                return string.Empty;

            var lon = _coordenadasDestino[1].ToString("0.000000", CultureInfo.InvariantCulture);
            var lat = _coordenadasDestino[0].ToString("0.000000", CultureInfo.InvariantCulture);
            var alt = _altitudDestinoResumenRuta is null
                ? "-"
                : $"{_altitudDestinoResumenRuta.Value.ToString("0.0", CultureInfo.InvariantCulture)} m";

            return $"D: Y.Lat = {lat}   X.Lon = {lon}   Z = {alt}";
        }

        private string FormatearResumenCallejeroOrigen()
        {
            if (string.IsNullOrWhiteSpace(_callejeroOrigenResumenRuta))
                return string.Empty;

            return $"O: {_callejeroOrigenResumenRuta}";
        }

        private string FormatearResumenCallejeroDestino()
        {
            if (string.IsNullOrWhiteSpace(_callejeroDestinoResumenRuta))
                return string.Empty;

            return $"D: {_callejeroDestinoResumenRuta}";
        }

        private static string FormatearLugarCallejeroResumen(LugarGeocodificadoCercano? cercano)
        {
            if (cercano is null)
                return string.Empty;

            var nombre = LimpiarTextoCallejeroResumen(cercano.Lugar.Nombre);
            if (string.IsNullOrWhiteSpace(nombre))
                nombre = LimpiarTextoCallejeroResumen(cercano.Lugar.Subtitulo);

            if (string.IsNullOrWhiteSpace(nombre))
                return string.Empty;

            var distancia = FormatearDistanciaCallejeroResumen(cercano.DistanciaMetros);
            if (string.IsNullOrWhiteSpace(distancia))
                return nombre;

            return $"{nombre} ({distancia})";
        }

        private static string LimpiarTextoCallejeroResumen(string texto)
        {
            if (string.IsNullOrWhiteSpace(texto))
                return string.Empty;

            texto = texto
                .Replace(" Â· ", " - ", StringComparison.Ordinal)
                .Replace("Â·", "-", StringComparison.Ordinal)
                .Replace("·", "-", StringComparison.Ordinal);

            return Regex.Replace(texto, @"\s+", " ").Trim();
        }

        private static string FormatearDistanciaCallejeroResumen(double distanciaMetros)
        {
            if (!double.IsFinite(distanciaMetros) || distanciaMetros < 0)
                return string.Empty;

            if (distanciaMetros < 1000)
                return $"{Math.Round(distanciaMetros):0} m";

            return $"{(distanciaMetros / 1000.0).ToString("0.000", CultureInfo.InvariantCulture)} km";
        }
    
        /// <summary>
        /// Formatea duracion.
        /// </summary>
        private string FormatearDuracion(double segundos)
        {
            if (segundos <= 0) return "-";
    
            int total = (int)Math.Round(segundos);
            var ts = TimeSpan.FromSeconds(total);
    
            if (ts.TotalHours >= 1)
                return $"{(int)ts.TotalHours}:{ts.Minutes:D2}:{ts.Seconds:D2}";
    
            return $"{ts.Minutes:D2}:{ts.Seconds:D2}";
        }

        private string FormatearTiempoCalculoRuta()
        {
            if (_tiempoUltimoCalculoRuta is not { } tiempo)
                return string.Empty;

            return $"{Math.Max(0, tiempo.TotalSeconds).ToString("0.00", CultureInfo.InvariantCulture)} s";
        }

        /// <summary>
        /// Construye los parametros actuales que afectan al ranking de rutas.
        /// </summary>
        private ParametrosPlanificadorRuta CrearParametrosPenalizacionActuales()
        {
            return new ParametrosPlanificadorRuta
            {
                VelocidadPieKmH = _velocidadPieKmH,
                VelocidadBusKmH = _velocidadBusKmH,
                FactorPenalizacionDistanciaPie = _factorPenalizacionDistanciaPie,
                ParametroPenalizacionDistanciaPieMetros = _parametroPenalizacionDistanciaPieMetros,
                PesoTransbordoRankingMinutos = _pesoTransbordoRankingMinutos,
                FactorEquilibradoAndarBus = _factorEquilibradoAndarBus
            };
        }

        /// <summary>
        /// Calcula la distancia peatonal corregida total de una alternativa.
        /// </summary>
        private double CalcularDistanciaCorregidaAlternativa(AlternativaRuta? alternativa)
        {
            if (alternativa is null)
                return 0;

            var parametros = CrearParametrosPenalizacionActuales();

            IEnumerable<double> distanciasPiePorTramo = alternativa.EsSoloAPie
                ? new[] { alternativa.DistanciaSoloAPie }
                : alternativa.DistanciasPiePorTramoAprox.Count > 0
                    ? alternativa.DistanciasPiePorTramoAprox
                    : new[]
                    {
                        alternativa.DistanciaPieInicioAprox,
                        alternativa.DistanciaPieTransbordosAprox,
                        alternativa.DistanciaPieFinAprox
                    };
            var distanciaCorregidaBase = UtilidadesTransitoDbus.CalcularDistanciaPieCorregidaTotalMetros(
                distanciasPiePorTramo,
                parametros);
            var esfuerzoExtra = _mostrarElevacionesRuta &&
                                double.IsFinite(alternativa.EsfuerzoExtraPendienteMetros)
                ? Math.Max(0, alternativa.EsfuerzoExtraPendienteMetros)
                : 0;

            return distanciaCorregidaBase + esfuerzoExtra;
        }

        private double CalcularCosteAndarEquilibradoSegundos(AlternativaRuta? alternativa)
        {
            if (alternativa is null)
                return double.PositiveInfinity;

            var distanciaCorregida = CalcularDistanciaCorregidaAlternativa(alternativa);
            if (!double.IsFinite(distanciaCorregida) || distanciaCorregida < 0)
                return double.PositiveInfinity;

            return EstimarDuracionPieSegundos(distanciaCorregida);
        }

        /// <summary>
        /// Calcula la aportacion de todos los tramos andando a la distancia equilibrada.
        /// </summary>
        private double CalcularDistanciaEquilibradaAndandoAlternativa(AlternativaRuta? alternativa)
        {
            if (alternativa is null)
                return 0;

            var distanciaAndando = CalcularDistanciaCorregidaAlternativa(alternativa);
            var factor = double.IsFinite(_factorEquilibradoAndarBus)
                ? Math.Max(0, _factorEquilibradoAndarBus)
                : FactorEquilibradoAndarBusPorDefecto;
            var distanciaEquilibrada = Math.Max(0, distanciaAndando) * factor;

            return double.IsFinite(distanciaEquilibrada)
                ? distanciaEquilibrada
                : 0;
        }

        private double CalcularScoreEquilibradoFactor(AlternativaRuta? alternativa)
        {
            if (alternativa is null || alternativa.EsSoloAPie || alternativa.AlternativaBus is null)
                return double.PositiveInfinity;

            var distanciaBus = Math.Max(0, alternativa.AlternativaBus.DistanciaTotalBus);
            var distanciaEquilibradaAndando = CalcularDistanciaEquilibradaAndandoAlternativa(alternativa);
            var penalizacionTransbordos = UtilidadesTransitoDbus.CalcularPenalizacionTransbordosRankingMetros(
                alternativa.NumeroTransbordosReales,
                CrearParametrosPenalizacionActuales());
            var score = distanciaBus + distanciaEquilibradaAndando + penalizacionTransbordos;
            return double.IsFinite(score) && score >= 0
                ? score
                : double.PositiveInfinity;
        }

        private string FormatearScoreEquilibradoAlternativa(AlternativaRuta? alternativa)
        {
            var score = CalcularScoreEquilibradoFactor(alternativa);
            if (!double.IsFinite(score))
                return string.Empty;

            return FormatearDistanciaTarjeta(score);
        }

        private async Task RecalcularRutaEquilibradaTrasCambioFactorAsync()
        {
            if (!CriterioPrioritarioEquilibradoActivo)
                return;

            if (_mapa is not null && _coordenadasOrigen is not null && _coordenadasDestino is not null)
            {
                _omitirRegistroHistorialEnSiguienteCalculo = true;
                await CalcularRutaCompletaAsync();
                return;
            }

            if (_opcionesRuta.Count == 0)
            {
                StateHasChanged();
                return;
            }

            OrdenarOpcionesRutaSegunCriterio();
            var indicePreferido = BuscarIndiceEquilibrado();
            if (indicePreferido < 0 || indicePreferido >= _opcionesRuta.Count)
            {
                StateHasChanged();
                return;
            }

            await CerrarTodasLasPreviewsAsync();
            _indiceAlternativaSeleccionada = indicePreferido;
            await CerrarPopupsAsync();
            ResetEstadoRuta(limpiarOpciones: false, limpiarMensaje: false);
            _mensajeRuta = "Representando ruta seleccionada...";
            StateHasChanged();
            await Task.Yield();
            await PintarOpcionRutaCompleta(_opcionesRuta[indicePreferido]);
            StateHasChanged();
        }

        /// <summary>
        /// Devuelve el coste penalizado interno de una alternativa en segundos.
        /// </summary>
        private double ObtenerCostePenalizadoAlternativa(AlternativaRuta? alternativa)
        {
            if (alternativa is null)
                return 0;

            if (alternativa.PuntuacionCoste > 0)
                return alternativa.PuntuacionCoste;

            var parametros = CrearParametrosPenalizacionActuales();
            var duracionReal = alternativa.DuracionTotalAproxSegundos;

            if (alternativa.EsSoloAPie)
            {
                return UtilidadesTransitoDbus.CalcularCosteConPenalizacionPieSegundos(
                    duracionReal,
                    parametros,
                    alternativa.DistanciaSoloAPie);
            }

            var distanciasPiePorTramo = alternativa.DistanciasPiePorTramoAprox.Count > 0
                ? alternativa.DistanciasPiePorTramoAprox.ToArray()
                : new[]
                {
                    alternativa.DistanciaPieInicioAprox,
                    alternativa.DistanciaPieTransbordosAprox,
                    alternativa.DistanciaPieFinAprox
                };

            return UtilidadesTransitoDbus.CalcularCosteConPenalizacionPieSegundos(
                duracionReal,
                parametros,
                distanciasPiePorTramo);
        }

        /// <summary>
        /// Devuelve un icono visual para el tipo de tramo.
        /// </summary>
        private static string IconoTramoRuta(string? tipo)
        {
            if (string.IsNullOrWhiteSpace(tipo))
                return "\u2022";

            if (tipo.StartsWith("Bus", StringComparison.OrdinalIgnoreCase))
                return "\U0001F68C";

            if (tipo.StartsWith("Espera en bus", StringComparison.OrdinalIgnoreCase) ||
                tipo.StartsWith("Continuidad en bus", StringComparison.OrdinalIgnoreCase))
                return "\u21BB";

            if (tipo.StartsWith("Espera", StringComparison.OrdinalIgnoreCase))
                return "\u231B";

            if (tipo.StartsWith("A pie", StringComparison.OrdinalIgnoreCase))
                return "\U0001F6B6";

            return "\U0001F4CD";
        }

        /// <summary>
        /// Devuelve la etiqueta visible del tramo dentro del detalle, agrupando los elementos que pertenecen al mismo transbordo.
        /// </summary>
        private static string ObtenerTipoVisibleDetalleTramo(IReadOnlyList<DetalleTramoRuta> tramos, int indice)
        {
            if (indice < 0 || indice >= tramos.Count)
                return string.Empty;

            var tramo = tramos[indice];
            if (EsTramoContinuidadEnBus(tramo))
                return "Continuidad en bus";
            if (EsTramoEsperaEnBus(tramo))
                return "Espera en bus";

            var transbordosPrevios = ContarTransbordosRealesDetalle(tramos, indice);

            if (EsTramoBus(tramo))
                return transbordosPrevios > 0 ? $"Bus (Transb.{transbordosPrevios})" : "Bus";

            var numeroTransbordo = EsTramoEntreBusesConTransbordoReal(tramos, indice)
                ? transbordosPrevios + 1
                : transbordosPrevios;
            if (numeroTransbordo <= 0 || !HayBusPosterior(tramos, indice))
                return tramo.Tipo;

            if (EsTramoEspera(tramo))
                return $"Espera (Transb.{numeroTransbordo})";

            if (EsTramoPie(tramo))
                return $"A pie (Transb.{numeroTransbordo})";

            return tramo.Tipo;
        }

        private static int ContarTransbordosRealesDetalle(IReadOnlyList<DetalleTramoRuta> tramos, int hastaIndiceExclusivo)
        {
            var total = 0;
            InfoVariante? busAnterior = null;

            for (var i = 0; i < hastaIndiceExclusivo && i < tramos.Count; i++)
            {
                if (!EsTramoBus(tramos[i]) || tramos[i].VarianteBus is null)
                    continue;

                if (busAnterior is not null &&
                    UtilidadesTransitoDbus.EsTransbordoReal(busAnterior, tramos[i].VarianteBus))
                {
                    total++;
                }

                busAnterior = tramos[i].VarianteBus;
            }

            return total;
        }

        private static bool EsTramoEntreBusesConTransbordoReal(IReadOnlyList<DetalleTramoRuta> tramos, int indice)
        {
            var anterior = BuscarVarianteBusAnterior(tramos, indice);
            var siguiente = BuscarVarianteBusPosterior(tramos, indice);
            return UtilidadesTransitoDbus.EsTransbordoReal(anterior, siguiente);
        }

        private static InfoVariante? BuscarVarianteBusAnterior(IReadOnlyList<DetalleTramoRuta> tramos, int indice)
        {
            for (var i = indice - 1; i >= 0; i--)
            {
                if (EsTramoBus(tramos[i]))
                    return tramos[i].VarianteBus;
            }

            return null;
        }

        private static InfoVariante? BuscarVarianteBusPosterior(IReadOnlyList<DetalleTramoRuta> tramos, int indice)
        {
            for (var i = indice + 1; i < tramos.Count; i++)
            {
                if (EsTramoBus(tramos[i]))
                    return tramos[i].VarianteBus;
            }

            return null;
        }

        /// <summary>
        /// Indica si el tramo representa un trayecto en bus.
        /// </summary>
        private static bool EsTramoBus(DetalleTramoRuta tramo)
            => tramo.Tipo.Equals("Bus", StringComparison.OrdinalIgnoreCase);

        /// <summary>
        /// Indica si el tramo representa una espera.
        /// </summary>
        private static bool EsTramoEspera(DetalleTramoRuta tramo)
            => tramo.Tipo.StartsWith("Espera", StringComparison.OrdinalIgnoreCase);

        /// <summary>
        /// Indica si el tramo representa una espera dentro del bus por continuidad circular.
        /// </summary>
        private static bool EsTramoEsperaEnBus(DetalleTramoRuta tramo)
            => tramo.Tipo.StartsWith("Espera en bus", StringComparison.OrdinalIgnoreCase);

        private static bool EsTramoContinuidadEnBus(DetalleTramoRuta tramo)
            => tramo.Tipo.StartsWith("Continuidad en bus", StringComparison.OrdinalIgnoreCase);

        /// <summary>
        /// Indica si el tramo representa una caminata.
        /// </summary>
        private static bool EsTramoPie(DetalleTramoRuta tramo)
            => tramo.Tipo.StartsWith("A pie", StringComparison.OrdinalIgnoreCase);

        /// <summary>
        /// Comprueba si después del tramo actual todavía queda otro bus por coger.
        /// </summary>
        private static bool HayBusPosterior(IReadOnlyList<DetalleTramoRuta> tramos, int indiceActual)
        {
            for (var i = indiceActual + 1; i < tramos.Count; i++)
            {
                if (EsTramoBus(tramos[i]))
                    return true;
            }

            return false;
        }

        private string FormatearDuracionBloqueBusDetalle(IReadOnlyList<DetalleTramoRuta> tramos)
        {
            var duracion = CalcularDuracionBloqueBusSegundos(tramos);
            return duracion is > 0 ? FormatearDuracionHorasMinutosReloj(duracion.Value) : "-";
        }

        private string FormatearDuracionBloqueBusResumen(IReadOnlyList<DetalleTramoRuta> tramos)
        {
            var duracion = CalcularDuracionBloqueBusSegundos(tramos);
            return duracion is > 0 ? FormatearDuracionHorasMinutosTexto(duracion.Value) : "-";
        }

        private static string FormatearDuracionHorasMinutosReloj(double segundos)
        {
            if (!double.IsFinite(segundos) || segundos <= 0)
                return "-";

            var totalMinutos = Math.Max(1, (int)Math.Round(segundos / 60.0));
            var horas = totalMinutos / 60;
            var minutos = totalMinutos % 60;

            if (horas <= 0)
                return $"00:{minutos:D2}";

            return minutos > 0
                ? $"{horas}:{minutos:D2}"
                : $"{horas}:00";
        }

        private static string FormatearDuracionHorasMinutosTexto(double segundos)
        {
            if (!double.IsFinite(segundos) || segundos <= 0)
                return "-";

            var totalMinutos = Math.Max(1, (int)Math.Round(segundos / 60.0));
            var horas = totalMinutos / 60;
            var minutos = totalMinutos % 60;

            if (horas <= 0)
                return $"{minutos} min";

            return minutos > 0
                ? $"{horas} h : {minutos:D2} m"
                : $"{horas} h";
        }

        private static double? CalcularDuracionBloqueBusSegundos(IReadOnlyList<DetalleTramoRuta> tramos)
        {
            var primerBus = -1;
            var ultimoBus = -1;

            for (var i = 0; i < tramos.Count; i++)
            {
                if (!EsTramoBus(tramos[i]))
                    continue;

                if (primerBus < 0)
                    primerBus = i;

                ultimoBus = i;
            }

            if (primerBus < 0 || ultimoBus < primerBus)
                return null;

            double total = 0;
            for (var i = primerBus; i <= ultimoBus; i++)
            {
                if (tramos[i].DuracionSegundos is not { } segundos ||
                    !double.IsFinite(segundos) ||
                    segundos <= 0)
                {
                    continue;
                }

                total += segundos;
            }

            return total;
        }

        /// <summary>
        /// Formatea la etiqueta visible de una alternativa.
        /// </summary>
        private string FormatearEtiquetaAlternativaTarjeta(AlternativaRuta alternativa, int indice)
        {
            return !string.IsNullOrWhiteSpace(alternativa.Etiqueta)
                ? alternativa.Etiqueta
                : $"Opcion {indice + 1}";
        }
    
    
        /// <summary>
        /// Estima duracion pie segundos.
        /// </summary>
        private double EstimarDuracionPieSegundos(double distanciaMetros)
        {
            double v = _velocidadPieKmH * 1000.0 / 3600.0;
            return distanciaMetros / Math.Max(0.1, v);
        }
    
        /// <summary>
        /// Estima duracion bus segundos.
        /// </summary>
        private double EstimarDuracionBusSegundos(double distanciaMetros)
        {
            double v = _velocidadBusKmH * 1000.0 / 3600.0;
            return distanciaMetros / Math.Max(0.1, v);
        }
    
        /// <summary>
        /// Restablece estado ruta.
        /// </summary>
        private void ResetEstadoRuta(bool limpiarOpciones = false, bool limpiarMensaje = true)
        {
            if (limpiarMensaje)
                _mensajeRuta = string.Empty;
            _hayRutaValida = false;
            _tramosRuta.Clear();
            _duracionTotalSegundos = 0;
            _alturaPendientePositivaSignificativaRutaMetros = 0.0;
            _distanciaPendientePositivaSignificativaRutaMetros = 0.0;
            _altitudOrigenResumenRuta = null;
            _altitudDestinoResumenRuta = null;
            _callejeroOrigenResumenRuta = null;
            _callejeroDestinoResumenRuta = null;
    
            _paradaOrigenSeleccionada = null;
            _paradaDestinoSeleccionada = null;
            _primerTransbordoseleccionado = null;
            _numeroTransbordos = 0;
            _distanciaAPieInicio = _distanciaAPieFin = _distanciaEnBus = 0;
            _paradaClicada = null;
    
            if (limpiarOpciones)
            {
                _opcionesRuta.Clear();
                _indiceAlternativaSeleccionada = 0;
                _tiempoUltimoCalculoRuta = null;
            }
        }
    
        // =========================================================
        // MODELOS internos
        // =========================================================
        private ElementoItinerario? _itinerarioClicado;
        private string? _itinerarioClicadoSubId; // id exacto pulsado (por si es subId)
    
        private sealed class CandidataParada
        {
            public Parada Parada { get; set; } = default!;
            public double DistEucl { get; set; }
            public double DistWalk { get; set; }
            public double TiempoWalk { get; set; }
        }
        private bool _zoomLimitsAplicados = false;
    
        // =========================================================
        // Ciclo de vida
        // =========================================================
}
