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.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
{
        private readonly record struct ResumenEsfuerzoElevacion(
            double DistanciaTotalMetros,
            double DistanciaPendientePositivaMetros,
            double AlturaPendientePositivaMetros,
            double EsfuerzoPositivo,
            double EsfuerzoTotal,
            bool HayEsfuerzoPositivo,
            bool HayEsfuerzo)
        {
            public double EsfuerzoExtraMetros =>
                Math.Max(0, EsfuerzoPositivo - DistanciaPendientePositivaMetros);
        }

        /// <summary>
        /// Asegura que elevacion tramo este disponible.
        /// </summary>
        private async Task EnsureElevacionTramoAsync(DetalleTramoRuta tramo)
        {
            if (tramo is null) return;
    
            // Si ya está calculado, nada
            if (tramo.MuestrasElevacion is not null && tramo.MuestrasElevacion.Count > 0) return;
    
            // Necesitamos la geometría
            if (tramo.Linea is null || tramo.Linea.GetLength(0) <= 0) return;
    
            if (tramo.ElevacionCargando) return;
    
            tramo.ElevacionCargando = true;
            StateHasChanged();
    
            try
            {
                var ctNone = CancellationToken.None;
    
                // Samples bajo demanda usando la fuente G.
                var samples = await ConstruirMuestrasElevacionAsync(tramo.Linea, ctNone);
                tramo.MuestrasElevacion = samples;

                var muestrasIdee = samples
                    .Where(x => x.ElevacionIdee is not null)
                    .Select(x => x.ElevacionIdee!.Value)
                    .ToList();

                tramo.ElevInicio = samples.FirstOrDefault()?.ElevacionIdee;
                tramo.ElevFin = samples.LastOrDefault()?.ElevacionIdee;
                tramo.ElevMin = muestrasIdee.Count > 0 ? muestrasIdee.Min() : null;
                tramo.ElevMax = muestrasIdee.Count > 0 ? muestrasIdee.Max() : null;
            }
            catch
            {
                // silencioso: la elevación es informativa
                tramo.MuestrasElevacion ??= new List<MuestraElevacion>();
            }
            finally
            {
                tramo.ElevacionCargando = false;
                StateHasChanged();
            }
        }

        private async Task ToggleElevacionesRutaAsync(ChangeEventArgs e)
        {
            var mostrarElevaciones = e.Value switch
            {
                bool valor => valor,
                string texto when bool.TryParse(texto, out var valor) => valor,
                _ => !_mostrarElevacionesRuta
            };

            if (_mostrarElevacionesRuta == mostrarElevaciones)
                return;

            _mostrarElevacionesRuta = mostrarElevaciones;
            await PersistirConfiguracionAsync();
            if (_opcionesRuta.Count == 0)
            {
                StateHasChanged();
                return;
            }

            _pintandoElevacionesRuta = true;
            _mensajeRuta = _mostrarElevacionesRuta
                ? "Aplicando pendientes..."
                : "Recalculando sin pendientes...";
            StateHasChanged();
            await Task.Yield();

            try
            {
                await RecalcularRankingTrasCambioEsfuerzoPendienteAsync(forzarRepintado: true);
            }
            finally
            {
                _pintandoElevacionesRuta = false;
                StateHasChanged();
            }
        }

        private async Task CambiarUmbralElevacionRutaAsync(ChangeEventArgs e, bool esSubida)
        {
            var actual = esSubida
                ? _umbralPendienteSubidaRuta
                : _umbralPendienteBajadaRuta;
            var nuevo = ParseUmbralPendienteRuta(e.Value, actual);

            if (esSubida)
                _umbralPendienteSubidaRuta = nuevo;
            else
                _umbralPendienteBajadaRuta = nuevo;

            await PersistirConfiguracionAsync();

            if (esSubida && _mostrarElevacionesRuta)
            {
                await RecalcularRankingTrasCambioEsfuerzoPendienteAsync();
            }
            else if (_mostrarElevacionesRuta)
                await RepintarRutaSeleccionadaPorElevacionesAsync("Aplicando pendientes...");
            else
                StateHasChanged();
        }

        private async Task RecalcularRankingTrasCambioEsfuerzoPendienteAsync(bool forzarRepintado = false)
        {
            if (_opcionesRuta.Count == 0)
            {
                StateHasChanged();
                return;
            }

            var opcionSeleccionadaAnterior = _indiceAlternativaSeleccionada >= 0 &&
                                             _indiceAlternativaSeleccionada < _opcionesRuta.Count
                ? _opcionesRuta[_indiceAlternativaSeleccionada]
                : null;

            await CalcularEsfuerzoExtraPendienteAlternativasAsync(_opcionesRuta, CancellationToken.None);
            OrdenarOpcionesRutaSegunCriterio();

            var nuevoIndice = CriterioPrioritarioTiempoActivo && opcionSeleccionadaAnterior is not null
                ? _opcionesRuta.IndexOf(opcionSeleccionadaAnterior)
                : BuscarIndicePreferidoPorCriterio(_criterioPrioritarioRuta);
            if (nuevoIndice < 0 || nuevoIndice >= _opcionesRuta.Count)
                nuevoIndice = 0;

            var cambioSeleccion = _indiceAlternativaSeleccionada != nuevoIndice ||
                                  !ReferenceEquals(opcionSeleccionadaAnterior, _opcionesRuta[nuevoIndice]);
            _indiceAlternativaSeleccionada = nuevoIndice;

            if (forzarRepintado || _mostrarElevacionesRuta || cambioSeleccion)
            {
                await RepintarRutaSeleccionadaPorElevacionesAsync(_mostrarElevacionesRuta
                    ? "Aplicando pendientes..."
                    : "Representando ruta seleccionada...");
            }
            else
            {
                StateHasChanged();
            }
        }

        private async Task CambiarUmbralAlturaAlertaPendienteRutaAsync(ChangeEventArgs e)
        {
            _umbralAlturaAlertaPendienteRutaMetros = ParseUmbralAlturaAlertaPendienteRutaMetros(
                e.Value,
                _umbralAlturaAlertaPendienteRutaMetros);

            await PersistirConfiguracionAsync();
            StateHasChanged();
        }

        private async Task RepintarRutaSeleccionadaPorElevacionesAsync(string mensaje)
        {
            if (_opcionesRuta.Count == 0 ||
                _indiceAlternativaSeleccionada < 0 ||
                _indiceAlternativaSeleccionada >= _opcionesRuta.Count)
            {
                StateHasChanged();
                return;
            }

            ReiniciarResumenPendientesRuta();
            _pintandoElevacionesRuta = true;
            _mensajeRuta = mensaje;
            StateHasChanged();
            await Task.Yield();

            try
            {
                await CerrarPopupsAsync();
                await PintarOpcionRutaCompleta(_opcionesRuta[_indiceAlternativaSeleccionada]);
            }
            finally
            {
                _pintandoElevacionesRuta = false;
                StateHasChanged();
            }
        }

        private static double ParseUmbralPendienteRuta(object? valor, double actual)
        {
            var texto = Convert.ToString(valor, CultureInfo.InvariantCulture)?.Replace(',', '.');
            if (!double.TryParse(texto, NumberStyles.Float, CultureInfo.InvariantCulture, out var nuevo) ||
                !double.IsFinite(nuevo))
            {
                return NormalizarUmbralPendienteRuta(actual);
            }

            return NormalizarUmbralPendienteRuta(nuevo);
        }

        private static double NormalizarUmbralPendienteRuta(double valor)
            => double.IsFinite(valor)
                ? Math.Clamp(Math.Abs(valor), 0.0, 100.0)
                : UmbralPendienteSubidaRutaPorDefecto;

        private static double ParseUmbralAlturaAlertaPendienteRutaMetros(object? valor, double actual)
        {
            var texto = Convert.ToString(valor, CultureInfo.InvariantCulture)?.Replace(',', '.');
            if (!double.TryParse(texto, NumberStyles.Float, CultureInfo.InvariantCulture, out var nuevo) ||
                !double.IsFinite(nuevo))
            {
                return NormalizarUmbralAlturaAlertaPendienteRutaMetros(actual);
            }

            return NormalizarUmbralAlturaAlertaPendienteRutaMetros(nuevo);
        }

        private static double NormalizarUmbralAlturaAlertaPendienteRutaMetros(double valor)
            => double.IsFinite(valor)
                ? Math.Clamp(Math.Abs(valor), 0.0, 1000.0)
                : UmbralAlturaAlertaPendienteRutaMetrosPorDefecto;

        private async Task DrawWalkingRouteJsAsync(
            string id,
            double[,] linea,
            int weight,
            int versionPintado,
            bool arrowEnd = true)
        {
            var resultadoElevacion = await ConstruirSegmentosElevacionMapaAsync(linea, CancellationToken.None);
            ThrowIfPintadoObsoleto(versionPintado);

            if (_mostrarElevacionesRuta)
            {
                _alturaPendientePositivaSignificativaRutaMetros +=
                    resultadoElevacion.AlturaPositivaSignificativaMetros;
                _distanciaPendientePositivaSignificativaRutaMetros +=
                    resultadoElevacion.DistanciaPositivaSignificativaMetros;
            }

            await DrawRouteJsAsync(
                id,
                linea,
                "#dc2626",
                weight,
                0.9,
                dashArray: DashArrayPiePunteado,
                arrowEnd: arrowEnd,
                lineCap: "round",
                elevationSegments: resultadoElevacion.Segmentos);
        }

        private async Task<(
            List<object>? Segmentos,
            double AlturaPositivaSignificativaMetros,
            double DistanciaPositivaSignificativaMetros)>
            ConstruirSegmentosElevacionMapaAsync(double[,] linea, CancellationToken ct)
        {
            if (!_mostrarElevacionesRuta || linea.GetLength(0) < 2)
                return (null, 0.0, 0.0);
             
            var muestras = await ConstruirMuestrasElevacionAsync(linea, ct);
            if (muestras.Count < 2)
                return (null, 0.0, 0.0);

            var segmentos = new List<object>();
            var hayPendienteCalculada = false;
            var totalesPendientePositiva = CalcularTotalesPendientePositivaSignificativa(muestras);

            for (var i = 1; i < muestras.Count; i++)
            {
                ct.ThrowIfCancellationRequested();

                var anterior = muestras[i - 1];
                var actual = muestras[i];
                var distancia = CalcularDistanciaMetros(
                    anterior.Latitud,
                    anterior.Longitud,
                    actual.Latitud,
                    actual.Longitud);

                if (distancia <= 0.5)
                    continue;

                var elevAnterior = ElevacionPreferidaMapa(anterior);
                var elevActual = ElevacionPreferidaMapa(actual);
                double? pendiente = null;

                if (elevAnterior is not null && elevActual is not null)
                {
                    var diferenciaAltura = elevActual.Value - elevAnterior.Value;
                    pendiente = diferenciaAltura / distancia * 100.0;
                    hayPendienteCalculada = true;
                }

                var (categoria, color) = ClasificarPendienteMapa(pendiente);
                segmentos.Add(new
                {
                    coords = new[]
                    {
                        new[] { anterior.Latitud, anterior.Longitud },
                        new[] { actual.Latitud, actual.Longitud }
                    },
                    color,
                    category = categoria,
                    slope = pendiente,
                    distance = distancia,
                    elevationStart = elevAnterior,
                    elevationEnd = elevActual,
                    label = pendiente is null
                        ? "-"
                        : pendiente.Value.ToString("+0.0;-0.0;0.0", CultureInfo.InvariantCulture) + " %"
                });
            }

            return (
                hayPendienteCalculada && segmentos.Count > 0 ? segmentos : null,
                totalesPendientePositiva.AlturaMetros,
                totalesPendientePositiva.DistanciaMetros);
        }

        private (double DistanciaMetros, double AlturaMetros)
            CalcularTotalesPendientePositivaSignificativa(IReadOnlyList<MuestraElevacion>? muestras)
        {
            var resumen = CalcularResumenEsfuerzoElevacion(muestras);
            return (resumen.DistanciaPendientePositivaMetros, resumen.AlturaPendientePositivaMetros);
        }

        private ResumenEsfuerzoElevacion CalcularResumenEsfuerzoElevacion(
            IReadOnlyList<MuestraElevacion>? muestras)
        {
            if (muestras is null || muestras.Count < 2)
                return default;

            var distanciaTotal = 0.0;
            var distanciaPendientePositiva = 0.0;
            var alturaPendientePositiva = 0.0;
            var esfuerzoPositivo = 0.0;
            var esfuerzoTotal = 0.0;
            var hayEsfuerzoPositivo = false;
            var hayEsfuerzo = false;
            var umbralSubida = NormalizarUmbralPendienteRuta(_umbralPendienteSubidaRuta);

            for (var i = 1; i < muestras.Count; i++)
            {
                var anterior = muestras[i - 1];
                var actual = muestras[i];
                var distancia = CalcularDistanciaMetros(
                    anterior.Latitud,
                    anterior.Longitud,
                    actual.Latitud,
                    actual.Longitud);
                if (!double.IsFinite(distancia) || distancia <= 0)
                    continue;

                distanciaTotal += distancia;
                if (anterior.ElevacionIdee is null || actual.ElevacionIdee is null)
                    continue;

                var diferenciaAltura = actual.ElevacionIdee.Value - anterior.ElevacionIdee.Value;
                var pendiente = diferenciaAltura / distancia * 100.0;
                var esfuerzo = CalcularEsfuerzoElevacion(distancia, pendiente);
                if (esfuerzo is null)
                    continue;

                esfuerzoTotal += esfuerzo.Value;
                hayEsfuerzo = true;

                if (pendiente < umbralSubida)
                    continue;

                distanciaPendientePositiva += distancia;
                alturaPendientePositiva += Math.Max(0, diferenciaAltura);
                esfuerzoPositivo += esfuerzo.Value;
                hayEsfuerzoPositivo = true;
            }

            return new ResumenEsfuerzoElevacion(
                distanciaTotal,
                distanciaPendientePositiva,
                alturaPendientePositiva,
                esfuerzoPositivo,
                esfuerzoTotal,
                hayEsfuerzoPositivo,
                hayEsfuerzo);
        }

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

            if (distanciaMetros < 950)
                return $"{distanciaMetros:0.00} m";

            return $"{(distanciaMetros / 1000.0):0.000} km";
        }

        private void ReiniciarResumenPendientesRuta()
        {
            _alturaPendientePositivaSignificativaRutaMetros = 0.0;
            _distanciaPendientePositivaSignificativaRutaMetros = 0.0;
        }

        private static double? ElevacionPreferidaMapa(MuestraElevacion muestra)
            => muestra.ElevacionIdee;

        private (string Categoria, string Color) ClasificarPendienteMapa(double? pendientePorcentaje)
        {
            if (pendientePorcentaje is null || !double.IsFinite(pendientePorcentaje.Value))
                return ("sin-datos", "#94a3b8");

            var umbralSubida = NormalizarUmbralPendienteRuta(_umbralPendienteSubidaRuta);
            var umbralBajada = NormalizarUmbralPendienteRuta(_umbralPendienteBajadaRuta);
            if (pendientePorcentaje.Value >= umbralSubida)
                return ("subida", "#000000");

            if (pendientePorcentaje.Value <= -umbralBajada)
                return ("bajada", "#2563eb");

            return ("llano", "#dc2626");
        }
    
    
    
    
    
        /// <summary>
        /// Pinta opcion ruta completa.
        /// </summary>
        private async Task PintarOpcionRutaCompleta(AlternativaRuta opcion)
        {
            var versionPintado = IniciarNuevaVersionPintadoRuta();
            var mensajeAnterior = _mensajeRuta;
            ReiniciarResumenPendientesRuta();

            try
            {
                await ClearRoutesJsAsync();
                ThrowIfPintadoObsoleto(versionPintado);

                if (opcion.EsSoloAPie)
                {
                    await PintarRutaSoloAPie(opcion.LineaSoloAPie, opcion.DistanciaSoloAPie, versionPintado: versionPintado);
                }
                else
                {
                    await PintarRutaDesdeAlternativa(opcion.OrigenParada, opcion.DestinoParada, opcion.AlternativaBus!, versionPintado);
                    ThrowIfPintadoObsoleto(versionPintado);
                    await PintarParadasSolucionSeleccionadaAsync(opcion, versionPintado);
                }

                ThrowIfPintadoObsoleto(versionPintado);
                await ActualizarResumenTecnicoRutaAsync(CancellationToken.None);
                ThrowIfPintadoObsoleto(versionPintado);
                await CerrarPopupsAsync();

                ThrowIfPintadoObsoleto(versionPintado);
                await FitRoutesJsAsync(centroPreferido: _coordenadasOrigen);
                ThrowIfPintadoObsoleto(versionPintado);
                StateHasChanged();
            }
            catch (OperationCanceledException)
            {
                if (string.IsNullOrWhiteSpace(_mensajeRuta))
                    _mensajeRuta = mensajeAnterior;

                StateHasChanged();
            }
            catch (Exception ex)
            {
                _mensajeRuta = $"No se ha podido representar la ruta seleccionada: {ex.Message}";
                _hayRutaValida = false;
                StateHasChanged();
            }
        }

        /// <summary>
        /// Pinta las paradas recorridas por la solución seleccionada con el color de cada tramo de bus.
        /// </summary>
        private async Task PintarParadasSolucionSeleccionadaAsync(AlternativaRuta opcion, int versionPintado)
        {
            await ClearRouteStopsJsAsync();
            LimpiarParadasRutaVisibles();
            ThrowIfPintadoObsoleto(versionPintado);

            var tramos = opcion.AlternativaBus?.Tramos;
            if (opcion.EsSoloAPie || tramos is null || tramos.Count == 0)
                return;

            for (var indice = 0; indice < tramos.Count; indice++)
            {
                var tramo = tramos[indice];
                if (tramo?.Variante is null)
                    continue;

                var paradasTramo = BuildParadasForTramoBus(tramo);
                if (paradasTramo.Count == 0)
                    continue;

                var stops = BuildStopsForTramoBus(tramo);

                var id = $"route_stops_{indice}_{tramo.Variante.Linea.Codigo}_{tramo.Variante.CodigoItinerario}_{NormalizarSentidoCodigo(tramo.Variante.Sentido?.Codigo)}";
                var color = ColorCssDesdeVariante(tramo.Variante);

                await DrawRouteStopsJsAsync(id, stops, color, radius: 8, weight: 2, opacity: 1.0, fillOpacity: 1.0);
                RegistrarParadasRutaVisibles(id, paradasTramo);
                ThrowIfPintadoObsoleto(versionPintado);
            }
        }
    
        /// <summary>
        /// Pinta ruta solo a pie.
        /// </summary>
        private async Task PintarRutaSoloAPie(double[,]? lineaPre = null, double distPre = 0, int versionPintado = 0)
        {
            if (_mapa is null || _coordenadasOrigen is null || _coordenadasDestino is null) return;
    
            ResetEstadoRuta(limpiarMensaje: false);
            ThrowIfPintadoObsoleto(versionPintado);
    
            double[,]? linea = lineaPre;
            double distancia = distPre;
    
            if (linea is null)
            {
                var w = await WalkCached(_coordenadasOrigen[0], _coordenadasOrigen[1], _coordenadasDestino[0], _coordenadasDestino[1]);
                linea = w.line;
                distancia = w.dist;
                ThrowIfPintadoObsoleto(versionPintado);
            }
    
            if (linea is null)
            {
                _mensajeRuta = "No he podido calcular una ruta a pie entre los puntos seleccionados.";
                _hayRutaValida = false;
                StateHasChanged();
                return;
            }
    
            // ✅ duración por velocidad configurada
            double duracion = (distancia > 0) ? EstimarDuracionPieSegundos(distancia) : 0;
    
            _distanciaAPieInicio = distancia;
            _duracionTotalSegundos = duracion;
    
            // ✅ Guardamos el tramo andando SIN calcular elevación
            _tramosRuta.Add(new DetalleTramoRuta
            {
                Tipo = "A pie",
                Descripcion = CapitalizarPalabrasClave("Camina desde tu punto de origen hasta tu destino final."),
                DistanciaMetros = distancia,
                DuracionSegundos = duracion,
    
                // clave para lazy elevation
                Linea = linea,
    
                // elevación aún no
                ElevInicio = null,
                ElevFin = null,
                ElevMin = null,
                ElevMax = null,
                MuestrasElevacion = null
            });
    
            ThrowIfPintadoObsoleto(versionPintado);
            await DrawWalkingRouteJsAsync("walk_full", linea, 5, versionPintado);
            ThrowIfPintadoObsoleto(versionPintado);
    
            _mensajeRuta = CapitalizarPalabrasClave(
                $"Puedes realizar todo el recorrido andando: {FormatearDistancia(distancia)} aproximadamente."
            );
    
            _hayRutaValida = true;
            ThrowIfPintadoObsoleto(versionPintado);
            StateHasChanged();
        }
    
    
    /// <summary>
    /// Pinta ruta desde alternativa.
    /// </summary>
    private async Task PintarRutaDesdeAlternativa(
        Parada paradaOrigenCandidata,
        Parada paradaDestinoCandidata,
        RutaBusAlternativa alternativa,
        int versionPintado)
    {
        if (_mapa is null || _coordenadasOrigen is null || _coordenadasDestino is null) return;
    
        ResetEstadoRuta(limpiarMensaje: false);
        ThrowIfPintadoObsoleto(versionPintado);
    
        var tramosBus = alternativa.Tramos;
        var lineasBus = RedBusServicio.ConstruirLineasBus(tramosBus, out var distanciaBusTotal);
    
        Parada paradaInicioBus = paradaOrigenCandidata;
        Parada paradaFinBus = paradaDestinoCandidata;
    
        if (tramosBus.Count > 0 && lineasBus.Count > 0)
        {
            var primerTramo = tramosBus.First();
            var ultimoTramo = tramosBus.Last();
            paradaInicioBus = ResolverParadaFisica(
                primerTramo.CodigoParadaInicio,
                ObtenerPrimerPunto(lineasBus[0]),
                paradaOrigenCandidata);
            paradaFinBus = ResolverParadaFisica(
                ultimoTramo.CodigoParadaFin,
                ObtenerUltimoPunto(lineasBus[^1]),
                paradaDestinoCandidata);
        }
    
        _paradaOrigenSeleccionada = paradaInicioBus;
        _paradaDestinoSeleccionada = paradaFinBus;
    
        _numeroTransbordos = CalcularTransbordosReales(alternativa);
    
        _primerTransbordoseleccionado = null;
        if (alternativa.CodigosParadasTransbordo is not null && alternativa.CodigosParadasTransbordo.Count > 0)
        {
            var codigoPrimera = alternativa.CodigosParadasTransbordo[0];
            _primerTransbordoseleccionado = ResolverParadaFisica(codigoPrimera);
        }
    
        _distanciaAPieInicio = 0;
        _distanciaAPieFin = 0;
        _distanciaEnBus = 0;
        _duracionTotalSegundos = 0;
    
        var now = AhoraLocal();
        var tCursor = now;
    
        if (_usarHorariosTeoricos)
        {
            await EnsureHorariosLoadedAsync();
            ThrowIfPintadoObsoleto(versionPintado);
        }
    
        // -----------------------
        // Pie 1
        // -----------------------
        var w1 = await WalkCached(_coordenadasOrigen[0], _coordenadasOrigen[1], paradaInicioBus.Latitud, paradaInicioBus.Longitud);
        ThrowIfPintadoObsoleto(versionPintado);
        if (w1.line is not null)
        {
            _distanciaAPieInicio = w1.dist;
    
            var durPie1 = (w1.dist > 0) ? EstimarDuracionPieSegundos(w1.dist) : 0;
            var tIni = tCursor;
            tCursor = AddSecondsSafe(tCursor, durPie1);
    
            _duracionTotalSegundos += durPie1;
    
            _tramosRuta.Add(new DetalleTramoRuta
            {
                Tipo = "A pie",
                Descripcion = CapitalizarPalabrasClave(
                    $"{HHmm(tIni)} -> {HHmm(tCursor)} - Desde tu punto de origen hasta la Parada \"{paradaInicioBus.Nombre}\" ({CodigoParadaFormateado(paradaInicioBus.Codigo)})."
                ),
                DistanciaMetros = w1.dist,
                DuracionSegundos = durPie1,
                Linea = w1.line
            });
    
            ThrowIfPintadoObsoleto(versionPintado);
            await DrawWalkingRouteJsAsync("walk_1", w1.line, 4, versionPintado);
            ThrowIfPintadoObsoleto(versionPintado);
        }
    
        // -----------------------
        // Bus
        // -----------------------
        _distanciaEnBus = distanciaBusTotal;
        var lineasUsadasVisuales = new List<string>(Math.Max(1, tramosBus.Count));
    
        Parada paradaUltimaBus = paradaInicioBus;
        var ultimoPuntoDibujado = w1.line is not null
            ? ObtenerUltimoPunto(w1.line)
            : new[] { paradaInicioBus.Latitud, paradaInicioBus.Longitud };
    
        for (int i = 0; i < tramosBus.Count; i++)
        {
            var tramo = tramosBus[i];
            var esTransbordoReal = i > 0 && UtilidadesTransitoDbus.EsTransbordoReal(tramosBus[i - 1], tramo);
            var infoVariante = tramo.Variante;
            var lineaGeom = lineasBus[i];
    
            string codParadaIni = infoVariante.CodigosParadas[tramo.IndiceInicio];
            string codParadaFin = infoVariante.CodigosParadas[tramo.IndiceFin];
    
            var puntoInicioTramo = ObtenerPrimerPunto(lineaGeom);
            var puntoFinTramo = ObtenerUltimoPunto(lineaGeom);
    
            var pIniTramo = ResolverParadaFisica(codParadaIni, puntoInicioTramo, i == 0 ? paradaInicioBus : paradaUltimaBus);
            var pFinTramo = ResolverParadaFisica(codParadaFin, puntoFinTramo, i == tramosBus.Count - 1 ? paradaFinBus : paradaUltimaBus);
            var distanciaUnion = CalcularDistanciaEntrePuntos(ultimoPuntoDibujado, puntoInicioTramo);

            if (ultimoPuntoDibujado is not null && puntoInicioTramo is not null && distanciaUnion > 0)
            {
                if (distanciaUnion <= UmbralCosturaVisualMetros)
                {
                    EstablecerPrimerPunto(lineaGeom, ultimoPuntoDibujado);
                    puntoInicioTramo = ultimoPuntoDibujado;
                }
                else if (!esTransbordoReal && i > 0 && distanciaUnion <= UmbralUnionPeatonalAutomaticaMetros)
                {
                    EstablecerPrimerPunto(lineaGeom, ultimoPuntoDibujado);
                    puntoInicioTramo = ultimoPuntoDibujado;
                }
                else if ((i == 0 || esTransbordoReal) && distanciaUnion <= UmbralUnionPeatonalAutomaticaMetros)
                {
                    var union = await WalkCached(
                        ultimoPuntoDibujado[0],
                        ultimoPuntoDibujado[1],
                        puntoInicioTramo[0],
                        puntoInicioTramo[1]);
                    ThrowIfPintadoObsoleto(versionPintado);

                    var lineaUnion = union.line ?? ConstruirLineaDirecta(ultimoPuntoDibujado, puntoInicioTramo);
                    var distanciaUnionReal = union.dist > 0 ? union.dist : distanciaUnion;
                    var duracionUnion = EstimarDuracionPieSegundos(distanciaUnionReal);
                    var tIniWalkUnion = tCursor;
                    tCursor = AddSecondsSafe(tCursor, duracionUnion);

                    _duracionTotalSegundos += duracionUnion;
                    if (i == 0)
                        _distanciaAPieInicio += distanciaUnionReal;

                    _tramosRuta.Add(new DetalleTramoRuta
                    {
                        Tipo = i == 0 ? "A pie (Ajuste acceso)" : "A pie (Transbordo)",
                        Descripcion = CapitalizarPalabrasClave(
                            i == 0
                                ? $"{HHmm(tIniWalkUnion)} -> {HHmm(tCursor)} - Camina desde el final del acceso peatonal hasta la Parada \"{pIniTramo.Nombre}\" ({CodigoParadaFormateado(pIniTramo.Codigo)})."
                                : $"{HHmm(tIniWalkUnion)} -> {HHmm(tCursor)} - Camina desde la Parada \"{paradaUltimaBus.Nombre}\" ({CodigoParadaFormateado(paradaUltimaBus.Codigo)}) hasta \"{pIniTramo.Nombre}\" ({CodigoParadaFormateado(pIniTramo.Codigo)})."
                        ),
                        DistanciaMetros = distanciaUnionReal,
                        DuracionSegundos = duracionUnion,
                        Linea = lineaUnion
                    });

                    ThrowIfPintadoObsoleto(versionPintado);
                    await DrawWalkingRouteJsAsync($"walk_union_{i}", lineaUnion, 4, versionPintado);
                    ThrowIfPintadoObsoleto(versionPintado);
                    ultimoPuntoDibujado = ObtenerUltimoPunto(lineaUnion);
                }
            }

            var distanciaTransferenciaPendiente = CalcularDistanciaEntrePuntos(ultimoPuntoDibujado, puntoInicioTramo);
            if (i > 0 &&
                esTransbordoReal &&
                ultimoPuntoDibujado is not null &&
                puntoInicioTramo is not null &&
                distanciaTransferenciaPendiente > UmbralUnionPeatonalAutomaticaMetros)
            {
                _mensajeRuta = "La alternativa seleccionada requiere un transbordo a pie demasiado largo. Recalcula la ruta para elegir otra combinacion.";
                StateHasChanged();
                return;
            }

            if (i > 0 &&
                esTransbordoReal &&
                ultimoPuntoDibujado is not null &&
                puntoInicioTramo is not null &&
                distanciaTransferenciaPendiente > UmbralCosturaVisualMetros)
            {
                var wt = await WalkCached(
                    ultimoPuntoDibujado[0],
                    ultimoPuntoDibujado[1],
                    puntoInicioTramo[0],
                    puntoInicioTramo[1]);
                ThrowIfPintadoObsoleto(versionPintado);

                var lineaTransferencia = wt.line ?? ConstruirLineaDirecta(ultimoPuntoDibujado, puntoInicioTramo);
                var distanciaTransferenciaReal = wt.dist > 0 ? wt.dist : distanciaTransferenciaPendiente;
                var duracionTransferencia = EstimarDuracionPieSegundos(distanciaTransferenciaReal);
                var tIniWalkTransfer = tCursor;
                tCursor = AddSecondsSafe(tCursor, duracionTransferencia);

                _duracionTotalSegundos += duracionTransferencia;

                _tramosRuta.Add(new DetalleTramoRuta
                {
                    Tipo = "A pie (Transbordo)",
                    Descripcion = CapitalizarPalabrasClave(
                        $"{HHmm(tIniWalkTransfer)} -> {HHmm(tCursor)} - Camina desde la Parada \"{paradaUltimaBus.Nombre}\" ({CodigoParadaFormateado(paradaUltimaBus.Codigo)}) hasta \"{pIniTramo.Nombre}\" ({CodigoParadaFormateado(pIniTramo.Codigo)})."
                    ),
                    DistanciaMetros = distanciaTransferenciaReal,
                    DuracionSegundos = duracionTransferencia,
                    Linea = lineaTransferencia
                });

                ThrowIfPintadoObsoleto(versionPintado);
                await DrawWalkingRouteJsAsync($"walk_transfer_real_{i}", lineaTransferencia, 4, versionPintado);
                ThrowIfPintadoObsoleto(versionPintado);

                ultimoPuntoDibujado = ObtenerUltimoPunto(lineaTransferencia);
                if (ultimoPuntoDibujado is not null)
                {
                    EstablecerPrimerPunto(lineaGeom, ultimoPuntoDibujado);
                    puntoInicioTramo = ultimoPuntoDibujado;
                }
            }
    
            // -----------------------
            // Transbordo andando
            // -----------------------
    if (false && i > 0 && ultimoPuntoDibujado is null)
    {
        if (!paradaUltimaBus.Codigo.Equals(codParadaIni, StringComparison.OrdinalIgnoreCase))
        {
            var wt = await WalkCached(
                paradaUltimaBus.Latitud, paradaUltimaBus.Longitud,
                pIniTramo.Latitud, pIniTramo.Longitud);
            ThrowIfPintadoObsoleto(versionPintado);
    
            if (wt.line is not null)
            {
                var durTrans = (wt.dist > 0) ? EstimarDuracionPieSegundos(wt.dist) : 0;
                var tIniWalk = tCursor;
                tCursor = AddSecondsSafe(tCursor, durTrans);
    
                _duracionTotalSegundos += durTrans;
    
                _tramosRuta.Add(new DetalleTramoRuta
                {
                    Tipo = "A pie (Transbordo)",
                    Descripcion = CapitalizarPalabrasClave(
                        $"{HHmm(tIniWalk)} -> {HHmm(tCursor)} - Camina desde la Parada \"{paradaUltimaBus.Nombre}\" ({CodigoParadaFormateado(paradaUltimaBus.Codigo)}) hasta \"{pIniTramo.Nombre}\" ({CodigoParadaFormateado(pIniTramo.Codigo)})."
                    ),
                    DistanciaMetros = wt.dist,
                    DuracionSegundos = durTrans,
                    Linea = wt.line
                });
    
                ThrowIfPintadoObsoleto(versionPintado);
                await DrawWalkingRouteJsAsync($"walk_transfer_{i}", wt.line, 4, versionPintado);
                ThrowIfPintadoObsoleto(versionPintado);
            }
        }
    }
    
            // -----------------------
            // Intentar horario real
            // -----------------------
            string codLineaItinSentido = CodigoLineaItinerarioSentido(tramo);
            string codLineaItinSentidoMostrado = codLineaItinSentido;
            string lineaCod = LineaCodigoStr(infoVariante);
            int itin = infoVariante.CodigoItinerario;
            string sentido = NormalizarSentidoCodigo(infoVariante.Sentido?.Codigo);
    
            DateTime? depT = null;
            DateTime? arrT = null;
            double distanciaTramo = 0;
            for (int p = 1; p < lineaGeom.GetLength(0); p++)
            {
                distanciaTramo += CalcularDistanciaMetros(
                    lineaGeom[p - 1, 0], lineaGeom[p - 1, 1],
                    lineaGeom[p, 0], lineaGeom[p, 1]);
            }
    
            if (_usarHorariosTeoricos && _horariosOk)
            {
                var primeraSalidaAceptable = esTransbordoReal
                    ? tCursor.AddSeconds(Math.Clamp(_margenTransbordoSegundos, 0, 1800))
                    : tCursor;
                var fecha = DateOnly.FromDateTime(primeraSalidaAceptable);
                var horaMin = TimeOnly.FromDateTime(primeraSalidaAceptable);
    
                if (Horarios.TryGetNextBusSegment(
                    codParadaIni,
                    codParadaFin,
                    lineaCod,
                    itin,
                    sentido,
                    fecha,
                    horaMin,
                    out var dep,
                    out var arr,
                    out var expIdReal))
                {
                    depT = dep;
                    arrT = arr;

                    if (!string.IsNullOrWhiteSpace(expIdReal))
                    {
                        var codigoReal = Horarios.GetLineaItinerarioSentidoText(expIdReal, lineaCod);
                        if (!string.IsNullOrWhiteSpace(codigoReal) && !codigoReal.Contains('?'))
                            codLineaItinSentidoMostrado = codigoReal;
                    }
                }
            }
    
            // -----------------------
            // Si hay horario real: usarlo
            // Si no: NO inventar horas
            // -----------------------
            if (depT is not null && arrT is not null)
            {
                void AgregarContinuidadCircularEnBus(DateTime inicio, DateTime fin, double duracionSegundos)
                {
                    _tramosRuta.Add(new DetalleTramoRuta
                    {
                        Tipo = duracionSegundos > 1 ? "Espera en bus" : "Continuidad en bus",
                        Descripcion = CapitalizarPalabrasClave(
                            $"{HHmm(inicio)} -> {HHmm(fin)} - continuidad circular de la linea {codLineaItinSentidoMostrado}; Parada \"{pIniTramo.Nombre}\" ({CodigoParadaFormateado(pIniTramo.Codigo)})."
                        ),
                        DistanciaMetros = 0,
                        DuracionSegundos = Math.Max(0, duracionSegundos)
                    });
                }

                if (depT.Value > tCursor)
                {
                    var wait = (depT.Value - tCursor).TotalSeconds;
                    if (wait > 1)
                    {
                        var tIniWait = tCursor;
                        tCursor = depT.Value;
                        var esperaEnBus = i > 0 && !esTransbordoReal;
    
                        _duracionTotalSegundos += wait;
    
                        if (esperaEnBus)
                        {
                            AgregarContinuidadCircularEnBus(tIniWait, tCursor, wait);
                        }
                        else
                        {
                            _tramosRuta.Add(new DetalleTramoRuta
                            {
                                Tipo = "Espera",
                                Descripcion = CapitalizarPalabrasClave(
                                    $"{HHmm(tIniWait)} -> {HHmm(tCursor)} - En \"{pIniTramo.Nombre}\" ({CodigoParadaFormateado(pIniTramo.Codigo)})."
                                ),
                                DistanciaMetros = 0,
                                DuracionSegundos = wait
                            });
                        }
                    }
                }
                else if (i > 0 && !esTransbordoReal && Math.Abs((depT.Value - tCursor).TotalSeconds) <= 1)
                {
                    AgregarContinuidadCircularEnBus(tCursor, tCursor, 0);
                }
    
                var durBus = (arrT.Value - depT.Value).TotalSeconds;
                if (durBus < 0) durBus = 0;
    
                _duracionTotalSegundos += durBus;
                tCursor = arrT.Value;
    
                _tramosRuta.Add(new DetalleTramoRuta
                {
                    Tipo = "Bus",
                    Descripcion = CapitalizarPalabrasClave(
                        $"{HHmm(depT.Value)} -> {HHmm(arrT.Value)} - Linea {codLineaItinSentidoMostrado} ({infoVariante.Linea.Nombre})."
                    ),
                    DistanciaMetros = distanciaTramo,
                    DuracionSegundos = durBus,
                    VarianteBus = infoVariante
                });
            }
            else
            {
                int paradasTramo = ContarParadasRecorridas(tramo);
                double extraParadasSeg = paradasTramo * Math.Max(0, _segundosPorParadaBus);
                double duracionBusSegundos = EstimarDuracionBusSegundos(distanciaTramo) + extraParadasSeg;
    
                _duracionTotalSegundos += duracionBusSegundos;
                tCursor = AddSecondsSafe(tCursor, duracionBusSegundos);
    
                _tramosRuta.Add(new DetalleTramoRuta
                {
                    Tipo = "Bus",
                    Descripcion = CapitalizarPalabrasClave(
                        $"Linea {codLineaItinSentidoMostrado} ({infoVariante.Linea.Nombre})."
                    ),
                    DistanciaMetros = distanciaTramo,
                    DuracionSegundos = duracionBusSegundos,
                    VarianteBus = infoVariante
                });
            }

            lineasUsadasVisuales.Add(codLineaItinSentidoMostrado);
    
            string colorCss = RedBusServicio.ConvertirColorArgbACss(infoVariante.Linea.ColorArgb);
            var fullId = $"full_{infoVariante.Linea.Codigo}_{infoVariante.CodigoItinerario}_{NormalizarSentidoCodigo(infoVariante.Sentido?.Codigo)}_{infoVariante.Variante.Codigo}_{i}_{tramo.IndiceInicio}_{tramo.IndiceFin}";
            var dashBus = string.Equals(NormalizarSentidoCodigo(infoVariante.Sentido?.Codigo), "V", StringComparison.OrdinalIgnoreCase)
                ? DashArrayBusVuelta
                : null;
            ThrowIfPintadoObsoleto(versionPintado);
            await DrawRouteJsAsync(fullId, lineaGeom, colorCss, 5, 0.9, dashArray: dashBus, arrowEnd: true);
            ThrowIfPintadoObsoleto(versionPintado);
    
            paradaUltimaBus = pFinTramo;
            ultimoPuntoDibujado = ObtenerUltimoPunto(lineaGeom) ?? new[] { pFinTramo.Latitud, pFinTramo.Longitud };
        }
    
        // -----------------------
        // Pie 2
        // -----------------------
        var w2 = await WalkCached(paradaFinBus.Latitud, paradaFinBus.Longitud, _coordenadasDestino[0], _coordenadasDestino[1]);
        ThrowIfPintadoObsoleto(versionPintado);
    
        if (w2.line is not null)
        {
            var inicioPieFinal = ObtenerPrimerPunto(w2.line);
            var distanciaUnionFinal = CalcularDistanciaEntrePuntos(ultimoPuntoDibujado, inicioPieFinal);
            if (ultimoPuntoDibujado is not null && inicioPieFinal is not null && distanciaUnionFinal > 0)
            {
                if (distanciaUnionFinal <= UmbralCosturaVisualMetros)
                {
                    EstablecerPrimerPunto(w2.line, ultimoPuntoDibujado);
                }
                else if (distanciaUnionFinal <= UmbralUnionPeatonalAutomaticaMetros)
                {
                    var unionFinal = await WalkCached(
                        ultimoPuntoDibujado[0],
                        ultimoPuntoDibujado[1],
                        inicioPieFinal[0],
                        inicioPieFinal[1]);
                    ThrowIfPintadoObsoleto(versionPintado);

                    var lineaUnionFinal = unionFinal.line ?? ConstruirLineaDirecta(ultimoPuntoDibujado, inicioPieFinal);
                    var distanciaUnionFinalReal = unionFinal.dist > 0 ? unionFinal.dist : distanciaUnionFinal;
                    var duracionUnionFinal = EstimarDuracionPieSegundos(distanciaUnionFinalReal);
                    var tIniUnionFinal = tCursor;
                    tCursor = AddSecondsSafe(tCursor, duracionUnionFinal);

                    _duracionTotalSegundos += duracionUnionFinal;
                    _distanciaAPieFin += distanciaUnionFinalReal;

                    _tramosRuta.Add(new DetalleTramoRuta
                    {
                        Tipo = "A pie (Ajuste salida)",
                        Descripcion = CapitalizarPalabrasClave(
                            $"{HHmm(tIniUnionFinal)} -> {HHmm(tCursor)} - Camina desde el punto real de bajada hasta la Parada \"{paradaFinBus.Nombre}\" ({CodigoParadaFormateado(paradaFinBus.Codigo)})."
                        ),
                        DistanciaMetros = distanciaUnionFinalReal,
                        DuracionSegundos = duracionUnionFinal,
                        Linea = lineaUnionFinal
                    });

                    ThrowIfPintadoObsoleto(versionPintado);
                    await DrawWalkingRouteJsAsync("walk_union_final", lineaUnionFinal, 4, versionPintado);
                    ThrowIfPintadoObsoleto(versionPintado);
                    EstablecerPrimerPunto(w2.line, ObtenerUltimoPunto(lineaUnionFinal));
                }
            }

            if (w2.dist > 0)
                _distanciaAPieFin += w2.dist;
    
            var durPie2 = (w2.dist > 0) ? EstimarDuracionPieSegundos(w2.dist) : 0;
            var tIni2 = tCursor;
            tCursor = AddSecondsSafe(tCursor, durPie2);
    
            _duracionTotalSegundos += durPie2;
    
            _tramosRuta.Add(new DetalleTramoRuta
            {
                Tipo = "A pie",
                Descripcion = CapitalizarPalabrasClave(
                    $"{HHmm(tIni2)} -> {HHmm(tCursor)} - Desde la Parada \"{paradaFinBus.Nombre}\" ({CodigoParadaFormateado(paradaFinBus.Codigo)}) hasta tu destino."
                ),
                DistanciaMetros = w2.dist,
                DuracionSegundos = durPie2,
                Linea = w2.line
            });
    
            ThrowIfPintadoObsoleto(versionPintado);
            await DrawWalkingRouteJsAsync("walk_2", w2.line, 4, versionPintado);
            ThrowIfPintadoObsoleto(versionPintado);
        }
    
        var lineasUsadas = lineasUsadasVisuales.Count > 0
            ? CompactarCodigosLineaConsecutivos(lineasUsadasVisuales)
            : CompactarCodigosLineaConsecutivos(tramosBus.Select(t => CodigoLineaItinerarioSentido(t)));
    
        string txtLineas = (lineasUsadas.Count == 0)
            ? ""
            : $" Lineas: {string.Join(", ", lineasUsadas)}.";
    
        int tramosCount = tramosBus?.Count ?? 0;
        int trans = UtilidadesTransitoDbus.ContarTransbordosReales(tramosBus);
    
        _mensajeRuta = CapitalizarPalabrasClave(
            $"Inicio {HHmm(now)}. Camina {FormatearDistanciaDetalleRuta(_distanciaAPieInicio)} hasta \"{_paradaOrigenSeleccionada?.Nombre}\" ({CodigoParadaFormateado(_paradaOrigenSeleccionada?.Codigo)}). " +
            $"Luego viaja en bus {FormatearDistanciaDetalleRuta(_distanciaEnBus)}" +
            (tramosCount > 0 ? $" en {tramosCount} tramo(s)" : "") +
            (trans == 0 ? "." : $" con {trans} transbordo(s).") +
            txtLineas + " " +
            $"Finalmente camina {FormatearDistanciaDetalleRuta(_distanciaAPieFin)} hasta tu destino."
        );
    
        _hayRutaValida = true;
        ThrowIfPintadoObsoleto(versionPintado);
        StateHasChanged();
    }
    
    
    
    
    
        /// <summary>
        /// Recalcula con parametros.
        /// </summary>
        private async Task RecalcularConParametros()
        {
            if (_coordenadasOrigen is null || _coordenadasDestino is null) return;
            await PersistirConfiguracionAsync();
    
            if (_mapa is not null)
            {
                try { await _mapa.Geometric.DisplayPolylinesFromArray.deleteConnectors(); } catch { }
                await ClearRoutesJsAsync();
            }
            await CerrarPopupsAsync();
    
            await CalcularRutaCompletaAsync();
            StateHasChanged();
        }
    
        /// <summary>
        /// Reinicia seleccion.
        /// </summary>
        private async Task ReiniciarSeleccion()
        {
            await CerrarTodasLasPreviewsAsync();
    
            await CerrarPopupsAsync();
    
            _ctsCalculo?.Cancel();
            IniciarNuevaVersionPintadoRuta();
    
            _coordenadasOrigen = null;
            _coordenadasDestino = null;
            _textoBusquedaOrigen = string.Empty;
            _textoBusquedaDestino = string.Empty;
            _ubicacionOrigenSeleccionada = null;
            _ubicacionDestinoSeleccionada = null;
            _sugerenciasBusquedaOrigen.Clear();
            _sugerenciasBusquedaDestino.Clear();
            _campoBusquedaActivo = null;
    
            _mensajeRuta = string.Empty;
            _hayRutaValida = false;
            _paradaOrigenSeleccionada = null;
            _paradaDestinoSeleccionada = null;
            _primerTransbordoseleccionado = null;
            _numeroTransbordos = 0;
            _distanciaAPieInicio = _distanciaAPieFin = _distanciaEnBus = 0;
            _paradaClicada = null;
            _tramosRuta.Clear();
            _duracionTotalSegundos = 0;
            _opcionesRuta.Clear();
            _indiceAlternativaSeleccionada = 0;
            _panelRutasMovilContraido = false;
    
            if (_mapa != null)
            {
                await ClearRoutesJsAsync();
                await ClearEndpointMarkersJsAsync();
                try { await _mapa.Geometric.DisplayPolylinesFromArray.deleteConnectors(); } catch { }
    
                await _mapa.Geometric.DisplayPointsFromArray.deleteAllPoints();
                if (_mostrarParadas)
                    await DibujarParadasAsync();
                else
                    await OcultarParadasAsync(); // (repone origen/destino si hubiera, aquí ya no hay, pero ok)
    
            }
    
            StateHasChanged();
        }
    
        // =========================================================
        // Distancia rápida
        // =========================================================
        /// <summary>
        /// Calcula distancia metros.
        /// </summary>
        private static double CalcularDistanciaMetros(double lat1, double lon1, double lat2, double lon2)
        {
            const double R = 6371000.0; // metros
            static double ToRad(double x) => x * Math.PI / 180.0;
    
            var dLat = ToRad(lat2 - lat1);
            var dLon = ToRad(lon2 - lon1);
    
            var a =
                Math.Sin(dLat / 2) * Math.Sin(dLat / 2) +
                Math.Cos(ToRad(lat1)) * Math.Cos(ToRad(lat2)) *
                Math.Sin(dLon / 2) * Math.Sin(dLon / 2);
    
            var c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a));
            return R * c;
        }
}
