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 enum ProveedorCaminata
        {
            Osrm = 1,     // v1
            Valhalla = 2  // v2
        }
    
        private ProveedorCaminata _walkProvider = ProveedorCaminata.Valhalla; // por defecto v2
    
        private bool _mostrarBuscadorMapa = false;
    
        /// <summary>
        /// Alterna buscador mapa.
        /// </summary>
        private void ToggleBuscadorMapa()
        {
            _mostrarBuscadorMapa = !_mostrarBuscadorMapa;
        }
    
        /// <summary>
        /// Quita todos itinerarios.
        /// </summary>
        private async Task QuitarTodosItinerarios()
        {
            await CerrarTodasLasPreviewsAsync();
    
            // cerrar popup de itinerario al ocultar todo
            _itinerarioClicado = null;
            _itinerarioClicadoSubId = null;
    
            StateHasChanged();
        }
    
        /// <summary>
        /// Actualiza colores itinerarios activos.
        /// </summary>
        private async Task RefrescarColoresItinerariosActivosAsync()
        {
            // Claves activas (26.12-I, 26.12-V, ...)
            var clavesActivas = _itPreviewIdsPorClave
                .Where(kvp => kvp.Value is not null && kvp.Value.Count > 0)
                .Select(kvp => kvp.Key)
                .ToHashSet(StringComparer.OrdinalIgnoreCase);
    
            // Quitamos todos los previews de itinerarios
            await QuitarTodosItinerarios();
    
            // Repintamos exactamente los sentidos que estaban activos
            foreach (var it in _itItems)
            {
                if (it?.Variantes is null || it.Variantes.Count == 0) continue;
    
                foreach (var v in it.Variantes)
                {
                    var clave = ItClave(v);
                    if (!clavesActivas.Contains(clave)) continue;
    
                    // Pintar SOLO este sentido (como item temporal de 1 variante)
                    var temp = new ElementoItinerario
                    {
                        Id = it.Id,
                        Text = it.Text,
                        Variantes = new List<InfoVariante> { v }
                    };
    
                    await PintarItItemAsync(temp);
                }
            }
    
            StateHasChanged();
        }
    /// <summary>
    /// Gestiona cambio modo color itinerarios.
    /// </summary>
    private async Task OnCambioModoColorItinerarios(ChangeEventArgs e)
    {
        _usarColorRealLineaEnItinerarios = e?.Value is bool b && b;
        await RefrescarColoresItinerariosActivosAsync();
        await PersistirConfiguracionAsync();
    }
    
        // true = usar color real del JSON (comportamiento actual)
    // false = usar color secuencial por itinerario
    private bool _usarColorRealLineaEnItinerarios = false;
    
    /// <summary>
    /// Gestiona color itinerario vista previa.
    /// </summary>
    private string ColorItinerarioPreview(InfoVariante v, string? itItemId = null)
    {
        // ✅ Modo actual: color real desde JSON
        if (_usarColorRealLineaEnItinerarios)
            return ColorCssDesdeVariante(v);
    
        // ✅ Modo alternativo: color "secuencial/estable" desde paleta
        // Clave estable por itinerario (línea + itinerario + sentido)
        // así cada itinerario mantiene su color aunque repintes
        var key = itItemId;
        if (string.IsNullOrWhiteSpace(key))
        {
            var linea = v.Linea?.Codigo?.ToString() ?? "?";
            var itin = v.CodigoItinerario.ToString(CultureInfo.InvariantCulture);
            var sentido = NormalizarSentidoCodigo(v.Sentido?.Codigo);
            key = $"{linea}.{itin}-{sentido}";
        }
    
        return ColorForKey(key);
    }
    
        private int _selectItKey = 0;
    
        /// <summary>
        /// Obtiene variante real clicada.
        /// </summary>
        private InfoVariante? GetVarianteRealClicada()
    {
        if (_itinerarioClicado is null || _itinerarioClicado.Variantes is null || _itinerarioClicado.Variantes.Count == 0)
            return null;
    
        // Si solo hay una variante, es esa
        if (_itinerarioClicado.Variantes.Count == 1)
            return _itinerarioClicado.Variantes[0];
    
        // Si hay varias (caso IV), mirar el subId real pulsado (..._I / ..._V)
        var subId = _itinerarioClicadoSubId ?? "";
    
        string? sentidoBuscado = null;
        if (subId.EndsWith("_I", StringComparison.OrdinalIgnoreCase))
            sentidoBuscado = "I";
        else if (subId.EndsWith("_V", StringComparison.OrdinalIgnoreCase))
            sentidoBuscado = "V";
    
        if (sentidoBuscado is not null)
        {
            var v = _itinerarioClicado.Variantes.FirstOrDefault(x =>
                string.Equals(NormalizarSentidoCodigo(x.Sentido?.Codigo), sentidoBuscado, StringComparison.OrdinalIgnoreCase));
    
            if (v is not null)
                return v;
        }
    
        // Fallback defensivo (no debería hacer falta normalmente)
        return _itinerarioClicado.Variantes.FirstOrDefault();
    }
    
        /// <summary>
        /// Calcula distancia variante metros.
        /// </summary>
        private double CalcularDistanciaVarianteMetros(InfoVariante v)
        {
            if (v is null) return 0;
    
            var linea = ConstruirGeometriaCompletaVariante(v);
            if (linea is null) return 0;
    
            double total = 0;
            int n = linea.GetLength(0);
    
            for (int i = 1; i < n; i++)
            {
                total += CalcularDistanciaMetros(
                    linea[i - 1, 0], linea[i - 1, 1],
                    linea[i, 0], linea[i, 1]
                );
            }
    
            return total;
        }
    
    /// <summary>
    /// Formatea distancia variante.
    /// </summary>
    private string FormatearDistanciaVariante(InfoVariante? v)
    {
        if (v is null) return "-";
    
        var d = CalcularDistanciaVarianteMetros(v);
        if (d <= 0) return "-";
    
        return FormatearDistancia(d); // reutiliza tu helper actual
    }
    
    
        /// <summary>
        /// Gestiona sincronizar seleccion itinerario con filtro.
        /// </summary>
        private void SincronizarSeleccionItinerarioConFiltro()
    {
        var filtrados = _itItemsFiltrados.ToList();
    
        if (filtrados.Count == 0)
        {
            _itSeleccionadoId = null;
            return;
        }
    
        // Si el seleccionado actual sigue visible, lo mantenemos
        if (!string.IsNullOrWhiteSpace(_itSeleccionadoId) &&
            filtrados.Any(x => x.Id.Equals(_itSeleccionadoId, StringComparison.OrdinalIgnoreCase)))
        {
            return;
        }
    
        // Si ya no está en el filtro, seleccionar el primero visible
        _itSeleccionadoId = filtrados[0].Id;
    }
    
    
    
    /// <summary>
    /// Pinta it filtro.
    /// </summary>
    private async Task PintarItFiltro()
    {
        foreach (var it in _itItemsFiltrados)
        {
            if (it is null) continue;
            await PintarItItemAsync(it); // ✅ pintará solo I/V que falten
        }
    }
    
    /// <summary>
    /// Pinta it elemento.
    /// </summary>
    private async Task PintarItItemAsync(ElementoItinerario it)
    {
        if (it is null) return;
    
        for (int i = 0; i < it.Variantes.Count; i++)
        {
            var v = it.Variantes[i];
            if (v is null) continue;
    
            var clave = ItClave(v);
    
            // ✅ Si ESTE sentido ya está pintado, saltar solo este, no todo el item
            if (_itPreviewIdsPorClave.TryGetValue(clave, out var set) && set.Count > 0)
                continue;
    
            var full = ConstruirGeometriaCompletaVariante(v);
            if (full is null) continue;
    
            var sentido = NormalizarSentidoCodigo(v.Sentido?.Codigo); // "I" o "V"
            var subId = (it.Variantes.Count == 1) ? it.Id : $"{it.Id}_{sentido}";
    
            var color = ColorItinerarioPreview(v, it.Id);
            var dash = (sentido == "V") ? "10 10" : null;
    
            await JS.InvokeVoidAsync("rt.drawPreviewLine", subId, LineaToLatLng(full), new
            {
                color,
                weight = 4,
                opacity = 0.90,
                dashArray = dash,
                arrowEnd = true,
                arrowBackPx = 26
            });
    
            await JS.InvokeVoidAsync("rt.toggleItStops", subId, BuildStopsForVariante(v), new
            {
                radius = 7,
                color = "#0f172a",
                fillColor = color,
                weight = 2,
                opacity = 1.0,
                fillOpacity = 1.0
            });
    
            RegistrarPreview(clave, subId);
        }
    }
    
        private Parada? _paradaPrimerClickPendiente;
    
        // Detecta si el click cae realmente sobre el círculo de una parada (en píxeles)
        /// <summary>
        /// Gestiona detectar parada hit.
        /// </summary>
        private async Task<Parada?> DetectarParadaHitAsync(double clickLat, double clickLon)
        {
            // ✅ Si las paradas están ocultas, no hay hit ni popup
            if (!_mostrarParadas)
                return null;
            var maxDist = await RadioClickParadaMetrosAsync(clickLat);
    
            // Candidatea por distancia (rápido)
            var parada = RedBusServicio.BuscarParadaCercanaEnRango(clickLat, clickLon, maxDistMetros: 40);
            if (parada is null) return null;
    
            // Validación final: dentro del círculo en pantalla
            var hit = await JS.InvokeAsync<bool>(
                "rt.isClickInsideCirclePx",
                clickLat, clickLon,
                parada.Latitud, parada.Longitud,
                7 // radio (5) + margen (2)
            );
    
            return hit ? parada : null;
        }
    
        // Tu popup actual, pero en método reutilizable (click simple)
        /// <summary>
        /// Muestra popup parada.
        /// </summary>
        private async Task MostrarPopupParadaAsync(Parada paradaDetectada)
        {
            var myToken = Interlocked.Increment(ref _paradaPopupToken);
    
            _paradaClicada = null;
            _paradaElevG = null;
    
            // limpiar horarios del popup
            _nextDeps.Clear();
    
            StateHasChanged();
    
            double? elevG = null;
    
            try
            {
                var kIdee = ClaveElevacionIdee(paradaDetectada.Latitud, paradaDetectada.Longitud);
    
                if (_elevIdeeCache.TryGetValue(kIdee, out var g))
                {
                    elevG = g;
                }
                else
                {
                    await ObtenerElevacionesIdeePorLoteAsync(
                        new List<(double lat, double lon)> { (paradaDetectada.Latitud, paradaDetectada.Longitud) },
                        CancellationToken.None);

                    elevG = _elevIdeeCache.TryGetValue(
                        ClaveElevacionIdee(paradaDetectada.Latitud, paradaDetectada.Longitud),
                        out g) ? g : (double?)null;
                }
            }
            catch
            {
                elevG = null;
            }
    
            if (myToken != _paradaPopupToken) return;
    
            _paradaElevG = elevG;
            _paradaClicada = paradaDetectada;
    
            // ==========================
            // HORARIOS (ZIP) -> usando tu AhoraLocal() (real o simulada)
            // ==========================
            if (_usarHorariosTeoricos)
            {
                await EnsureHorariosLoadedAsync();
    
                if (_horariosOk)
                {
                    try
                    {
                        var now = AhoraLocal();
                        var fecha = DateOnly.FromDateTime(now);
                        var hora = TimeOnly.FromDateTime(now);
    
                        // tu parada usa Codigo (ej "34" o "P-34"). El servicio normaliza.
                        var deps = Horarios.GetNextDeparturesByCodigo(
                            paradaDetectada.Codigo,
                            fecha,
                            hora,
                            take: null,
                            includePast: true);
    
                        _nextDeps = ConstruirListaHorariosConActual(deps, hora);
                        _scrollHorariosPendiente = true;
                    }
                    catch (Exception ex)
                    {
                        _horariosOk = false;
                        _horariosError = "Error leyendo horarios: " + ex.Message;
                    }
                }
            }
    
            StateHasChanged();
        }
    
    
    
        /// <summary>
        /// Gestiona toggle horarios teoricos.
        /// </summary>
        private async Task OnToggleHorariosTeoricos(ChangeEventArgs e)
        {
            var usabaHorarios = _usarHorariosTeoricos;
            _usarHorariosTeoricos = e?.Value is bool b && b;

            if (_usarHorariosTeoricos && !usabaHorarios)
                _segundosPorParadaBusManual = Math.Max(0, _segundosPorParadaBus);

            SincronizarSegundosPorParadaConHorarios();
    
            if (_usarHorariosTeoricos)
            {
                await EnsureHorariosLoadedAsync();
    
                // si hay popup de parada abierto, refrescar inmediatamente
                if (_paradaClicada is not null && _horariosOk)
                {
                    try
                    {
                        var now = AhoraLocal();
                        var fecha = DateOnly.FromDateTime(now);
                        var hora = TimeOnly.FromDateTime(now);
    
                        var deps = Horarios.GetNextDeparturesByCodigo(
                            _paradaClicada.Codigo,
                            fecha,
                            hora,
                            take: null,
                            includePast: true);
    
                        _nextDeps = ConstruirListaHorariosConActual(deps, hora);
                        _scrollHorariosPendiente = true;
                    }
                    catch (Exception ex)
                    {
                        _horariosOk = false;
                        _horariosError = "Error leyendo horarios: " + ex.Message;
                }
            }

            await PersistirConfiguracionAsync();
        }
            else
            {
                _nextDeps.Clear();
            }
    
            StateHasChanged();
        }
    
        /// <summary>
        /// Asegura que horarios la carga este disponible.
        /// </summary>
        private async Task EnsureHorariosLoadedAsync()
        {
            if (_horariosOk) return;
    
            _horariosError = null;
    
            try
            {
                // Carga el ZIP de importación resuelto por la capa compartida de importación.
                await Horarios.AsegurarCargaAsync(Env.ContentRootPath);
                _horariosOk = true;
            }
            catch (Exception ex)
            {
                _horariosOk = false;
                _horariosError = ex.Message;
            }
        }
    
    
    
        private bool _mostrarParadas = true;
    
        /// <summary>
        /// Alterna paradas.
        /// </summary>
        private async Task ToggleParadasAsync()
        {
            _mostrarParadas = !_mostrarParadas;
    
            if (_mapa is null) { StateHasChanged(); return; }
    
            if (_mostrarParadas)
            {
                // vuelve a pintar paradas (y también origen/destino si están)
                await DibujarParadasAsync();
            }
            else
            {
                // oculta paradas (pero deja rutas, previews, etc.)
                await OcultarParadasAsync();
            }

            await PersistirConfiguracionAsync();
    
            StateHasChanged();
        }
    
        /// <summary>
        /// Oculta paradas.
        /// </summary>
        private async Task OcultarParadasAsync()
        {
            if (_mapa is null) return;
    
            // Esto borra TODOS los puntos pintados con DisplayPointsFromArray,
            // así que luego reponemos origen/destino si existen.
            await _mapa.Geometric.DisplayPointsFromArray.deleteAllPoints();
        }
    
    
    
        private int OpacidadMapaPorcentaje => _nivelBrilloMapa switch
        {
            0 => 0,
            1 => 25,
            2 => 50,
            3 => 75,
            4 => 90,
            _ => 0
        };
    
        // ✅ Límite de transbordos
        // ✅ Transbordos: máximo y rango (mín..máx)
        private bool _activarRangoTransbordos = true;
        private int _minTransbordosPermitidos = 0;
        private int _maxTransbordosPermitidos = 3;
    
        /// <summary>
        /// Indica si esta popup puntos abierto.
        /// </summary>
        private bool EstaPopupPuntosAbierto(DetalleTramoRuta tramo)
        {
            // Abierto si el popup está mostrando ESTE tramo y no está minimizado
            return _tramoElevDetalle == tramo && !_elevPopupMinimizado;
        }
    
        /// <summary>
        /// Alterna puntos.
        /// </summary>
        private async Task TogglePuntosAsync(DetalleTramoRuta tramo)
        {
            if (tramo is null) return;
    
            // Si ya está abierto para este tramo => cerrar (igual que "- Iti")
            if (_tramoElevDetalle == tramo && !_elevPopupMinimizado)
            {
                await CerrarPopupElevAsync();
                return;
            }
    
            // Si estaba abierto para otro tramo, limpiamos puntos antes de cambiar
            if (_tramoElevDetalle is not null && _tramoElevDetalle != tramo)
            {
                try { await JS.InvokeVoidAsync("rt.clearElevPoints", "elev"); } catch { }
            }
    
            // Abrir para este tramo
            _elevPopupMinimizado = false;
            _tramoElevDetalle = tramo;
    
            await EnsureElevacionTramoAsync(tramo);
            await PintarPuntosElevacionAsync(tramo);
    
            StateHasChanged();
        }
    
        private static readonly string[] _palette =
    {
        "#2563eb", "#16a34a", "#dc2626", "#7c3aed", "#ea580c",
        "#0891b2", "#db2777", "#65a30d", "#0f766e", "#b45309",
        "#4f46e5", "#be123c", "#15803d", "#c026d3", "#0284c7",
        "#ca8a04"
    };
        /// <summary>
        /// Gestiona stable hash.
        /// </summary>
        private static int StableHash(string s)
        {
            unchecked
            {
                int h = 23;
                foreach (var ch in s)
                    h = (h * 31) + ch;
                return h;
            }
        }
    
        /// <summary>
        /// Gestiona color for clave.
        /// </summary>
        private static string ColorForKey(string key)
        {
            var idx = Math.Abs(StableHash(key)) % _palette.Length;
            return _palette[idx];
        }
    
    
    private string _itFiltro = "";
    private string ItFiltro
    {
        get => _itFiltro;
        set
        {
            value ??= "";
    
            if (string.Equals(_itFiltro, value, StringComparison.Ordinal))
                return;
    
            var filtroAnterior = _itFiltro;
            _itFiltro = value;
    
            // Si se limpia el filtro (X nativa o borrar), selecciona el primero visible
            if (!string.IsNullOrWhiteSpace(filtroAnterior) && string.IsNullOrWhiteSpace(_itFiltro))
            {
                _itSeleccionadoId = _itItemsFiltrados.FirstOrDefault()?.Id;
            }
            else
            {
                SincronizarSeleccionItinerarioConFiltro();
            }
    
            // Fuerza reconstrucción visual del select
            _selectItKey++;
        }
    }
        private IEnumerable<ElementoItinerario> _itItemsFiltrados =>
            string.IsNullOrWhiteSpace(_itFiltro)
                ? _itItems
                : _itItems.Where(x => MatchFiltro(x, _itFiltro));
    
    
    
    
    /// <summary>
    /// Indica si match filtro.
    /// </summary>
    private static bool MatchFiltro(ElementoItinerario? item, string? filtro)
    {
        var text = item?.TextoBusqueda;
        if (string.IsNullOrWhiteSpace(filtro)) return true;
        if (string.IsNullOrWhiteSpace(text)) return false;
    
        var haystack = NormalizeForSearch(text);
    
        // varios patrones separados por ; o ,
        var parts = filtro.Split(new[] { ';', ',' }, StringSplitOptions.RemoveEmptyEntries);
    
        foreach (var raw in parts)
        {
            var f = raw.Trim();
            if (f.Length == 0) continue;
    
            // 1) Regex explícita: re:...
            if (f.StartsWith("re:", StringComparison.OrdinalIgnoreCase))
            {
                var rx = f.Substring(3).Trim();
                if (rx.Length == 0) continue;
    
                try
                {
                    if (Regex.IsMatch(haystack, rx, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant))
                        return true;
                }
                catch
                {
                    // regex inválida -> ignorar patrón
                }
    
                continue;
            }
    
            // Definición de "token"
            const string TOKEN_CHARS = @"[\p{L}\p{N}.]";
            string leftBoundary = $@"(?<!{TOKEN_CHARS})";
    
            // 2) Wildcards: * y ?
            if (f.Contains('*') || f.Contains('?'))
            {
                var pat = NormalizeForSearch(f);
    
                // Presuponer SIEMPRE * final si no está
                if (!pat.EndsWith("*", StringComparison.Ordinal))
                    pat += "*";
    
                bool leadStar = pat.StartsWith("*", StringComparison.Ordinal);
                bool tailStar = pat.EndsWith("*", StringComparison.Ordinal);
    
                // ✅ Importante: quitar extremos en el patrón ORIGINAL,
                // no en el regex ya escapado (evita romper cosas como ".*")
                var rawInner = pat.Trim('*');
    
                // Si el patrón era solo "*" / "**" / "***", coincide con todo
                // (si llevaba ?, no entra aquí porque rawInner no quedará vacío)
                if (rawInner.Length == 0)
                    return true;
    
                // Convertir wildcard -> regex
                var inner = Regex.Escape(rawInner)
                    .Replace(@"\*", ".*")
                    .Replace(@"\?", ".");
    
                var rx =
                    (leadStar ? "" : leftBoundary) +
                    inner +
                    (tailStar ? "" : $@"(?!{TOKEN_CHARS})");
    
                try
                {
                    if (Regex.IsMatch(haystack, rx, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant))
                        return true;
                }
                catch
                {
                    // patrón inválido -> ignorar
                }
    
                continue;
            }
    
            // 3) SIN wildcard: por defecto "empieza por" (equivale a añadir * al final)
            var needle = NormalizeForSearch(f);
    
            // --- caso A: solo dígitos -> token numérico que EMPIEZA por needle ---
            // Se interpreta como búsqueda de línea lógica:
            // - líneas numéricas normales por prefijo
            // - taxibús TB6/TB7 por su número real 6/7
            // - líneas B por su identificador 100 exacto (B1 -> 101)
            if (Regex.IsMatch(needle, @"^\d+$"))
            {
                if (CoincideFiltroNumericoItinerario(item, needle))
                    return true;
    
                continue;
            }
    
            // --- caso B: ".5" fracción -> mantener lógica ---
            if (Regex.IsMatch(needle, @"^\.\d+$"))
            {
                var rxFrac = $@"(?<=\p{{N}}){Regex.Escape(needle)}(?!\p{{N}})";
                if (Regex.IsMatch(haystack, rxFrac, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant))
                    return true;
    
                continue;
            }
    
            // --- caso C: token general que EMPIEZA por needle ---
            var rxTokenPrefix = $@"{leftBoundary}{Regex.Escape(needle)}{TOKEN_CHARS}*";
            if (Regex.IsMatch(haystack, rxTokenPrefix, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant))
                return true;
        }
    
        return false;
    }

    /// <summary>
    /// Evalúa búsquedas numéricas sobre la línea lógica del itinerario.
    /// </summary>
    private static bool CoincideFiltroNumericoItinerario(ElementoItinerario? item, string needle)
    {
        if (item?.Variantes is null || item.Variantes.Count == 0)
            return false;

        foreach (var variante in item.Variantes)
        {
            var codigoLinea = variante?.Linea?.Codigo?.ToString()?.Trim() ?? "";
            if (string.IsNullOrWhiteSpace(codigoLinea))
                continue;

            var codigoNormalizado = UtilidadesTransitoDbus.CodigoSinDecimal(codigoLinea);

            if (Regex.IsMatch(codigoNormalizado, @"^\d+$") &&
                codigoNormalizado.StartsWith(needle, StringComparison.OrdinalIgnoreCase))
            {
                return true;
            }

            var matchTb = Regex.Match(codigoNormalizado, @"^TB(\d+)$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
            if (matchTb.Success &&
                matchTb.Groups[1].Value.StartsWith(needle, StringComparison.OrdinalIgnoreCase))
            {
                return true;
            }

            var esBuho = Regex.IsMatch(codigoNormalizado, @"^B\d+$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
            if (esBuho &&
                int.TryParse(variante?.Linea?.IdRuta, NumberStyles.Integer, CultureInfo.InvariantCulture, out var idRutaBuho) &&
                idRutaBuho.ToString(CultureInfo.InvariantCulture) == needle)
            {
                return true;
            }
        }

        return false;
    }
        // Normaliza: minúsculas + sin tildes (para búsquedas “humanas”)
        /// <summary>
        /// Gestiona normalize for busqueda.
        /// </summary>
        private static string NormalizeForSearch(string s)
        {
            s = s.Trim().ToLowerInvariant();
            const char marcadorEnye = '\uE000';
            s = s.Replace('\u00f1', marcadorEnye);
    
            // quitar diacríticos
            var formD = s.Normalize(NormalizationForm.FormD);
            var sb = new System.Text.StringBuilder(formD.Length);
            foreach (var ch in formD)
            {
                var uc = CharUnicodeInfo.GetUnicodeCategory(ch);
                if (uc != UnicodeCategory.NonSpacingMark)
                    sb.Append(ch);
            }
            return sb
                .ToString()
                .Normalize(NormalizationForm.FormC)
                .Replace(marcadorEnye, '\u00f1');
        }
    
    
    
    
    
        private sealed class ElementoItinerario
        {
            public string Id { get; set; } = "";
            public string Text { get; set; } = "";
            public List<string> SearchAliases { get; set; } = new();
            public string TextoBusqueda =>
                SearchAliases.Count == 0
                    ? Text
                    : $"{Text} {string.Join(" ", SearchAliases)}";
    
            // ✅ Puede contener 1 (solo Ida o solo Vuelta) o 2 (Ida+Vuelta)
            public List<InfoVariante> Variantes { get; set; } = new();
        }
    
    
        /// <summary>
        /// Gestiona codigo linea as int.
        /// </summary>
        private static int CodigoLineaAsInt(InfoVariante v)
    {
        // v.Linea.Codigo puede ser int/decimal/string según tu modelo.
        // Lo pasamos a string y extraemos la parte numérica inicial.
        var numero = CodigoLineaAsInt(v?.Linea);
        return numero;
    }

        private static int CodigoLineaAsInt(LineaBus? linea)
        {
            if (linea is null)
                return int.MaxValue;

            if (int.TryParse(linea.IdRuta, NumberStyles.Integer, CultureInfo.InvariantCulture, out var idRuta))
                return idRuta;

            var numero = UtilidadesTransitoDbus.ExtraerNumeroLinea(linea.Codigo);
            return numero ?? int.MaxValue;
        }

        /// <summary>
        /// Obtiene el identificador numerico real de la ruta para ordenar el filtro.
        /// </summary>
        private static int IdRutaAsInt(InfoVariante? variante)
        {
            if (variante?.Linea is null)
                return int.MaxValue;

            return int.TryParse(variante.Linea.IdRuta, out var idRuta)
                ? idRuta
                : CodigoLineaAsInt(variante);
        }
    
    
    
        /// <summary>
        /// Construye un texto de trayecto usando la primera y la ultima parada de la variante.
        /// </summary>
        private string ConstruirTextoRecorridoVariante(InfoVariante? variante)
        {
            if (variante?.CodigosParadas is null || variante.CodigosParadas.Count == 0)
                return "";

            string NombreParada(string codigo)
            {
                if (_paradaByCode.TryGetValue(codigo, out var parada) && !string.IsNullOrWhiteSpace(parada.Nombre))
                    return parada.Nombre.Trim();

                return CodigoParadaFormateado(codigo);
            }

            var origen = NombreParada(variante.CodigosParadas[0]);
            var destino = NombreParada(variante.CodigosParadas[^1]);

            if (string.IsNullOrWhiteSpace(origen))
                return destino;

            if (string.IsNullOrWhiteSpace(destino))
                return origen;

            return string.Equals(origen, destino, StringComparison.OrdinalIgnoreCase)
                ? origen
                : $"{origen} -> {destino}";
        }

        /// <summary>
        /// Obtiene nombres alternativos de expediciones para que el buscador encuentre el itinerario por destino.
        /// </summary>
        private List<string> ObtenerAliasesBusquedaItinerario(
            InfoVariante? variante,
            Dictionary<string, List<string>> cache)
        {
            if (variante is null)
                return new List<string>();

            var sentido = NormalizarSentidoCodigo(variante.Sentido?.Codigo);
            var key = string.Join("|",
                variante.Linea.IdRuta ?? "",
                variante.Linea.Codigo ?? "",
                variante.CodigoItinerario.ToString(CultureInfo.InvariantCulture),
                sentido);

            if (cache.TryGetValue(key, out var cached))
                return cached;

            try
            {
                cached = Horarios
                    .GetAliasBusquedaItinerario(
                        variante.Linea.IdRuta,
                        variante.Linea.Codigo,
                        variante.CodigoItinerario,
                        sentido)
                    .Where(x => !string.IsNullOrWhiteSpace(x))
                    .Distinct(StringComparer.OrdinalIgnoreCase)
                    .ToList();
            }
            catch
            {
                cached = new List<string>();
            }

            cache[key] = cached;
            return cached;
        }

        /// <summary>
        /// Construye itinerarios dropdown.
        /// </summary>
        private void BuildItinerariosDropdown()
        {
            EnsureBusIndexBuilt();
    
            var grupos = _infoVariantes
                .GroupBy(v => new
                {
                    Linea = v.Linea.Codigo?.ToString() ?? "?",
                    Itin = v.CodigoItinerario
                })
                .OrderBy(g =>
                {
                    var any = g.FirstOrDefault();
                    return any is null ? int.MaxValue : IdRutaAsInt(any);
                })
                .ThenBy(g => g.Key.Linea, StringComparer.OrdinalIgnoreCase)
                .ThenBy(g => g.Key.Itin)
                .ToList();
    
            var list = new List<ElementoItinerario>(grupos.Count * 2);
            var aliasesCache = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
    
            foreach (var g in grupos)
            {
                var linea = g.Key.Linea;
                var itin = g.Key.Itin;
    
                // 1) Elegimos UNA variante por sentido (determinista)
                InfoVariante? ida = g
                    .Where(v => NormalizarSentidoCodigo(v.Sentido?.Codigo) == "I")
                    .OrderBy(v => v.Variante.Codigo)
                    .FirstOrDefault();
    
                InfoVariante? vuelta = g
                    .Where(v => NormalizarSentidoCodigo(v.Sentido?.Codigo) == "V")
                    .OrderBy(v => v.Variante.Codigo)
                    .FirstOrDefault();
    
                var nombreLinea = (ida?.Linea.Nombre ?? vuelta?.Linea.Nombre ?? "");
    
                // 2) ✅ Si existe Ida: añadir item I
                if (ida is not null)
                {
                    var varCod = ida.Variante.Codigo.ToString() ?? "?";
                    var id = $"it_{linea}_{itin}_I_{varCod}";
                    var trayectoIda = ConstruirTextoRecorridoVariante(ida);
                    var textVisible = string.IsNullOrWhiteSpace(trayectoIda)
                        ? $"{linea}.{itin}-I — {ida.Linea.Nombre}"
                        : $"{linea}.{itin}-I — {trayectoIda}";
                    var text = $"{linea}.{itin}-I — {ida.Linea.Nombre}";
    
                    text = textVisible;

                    list.Add(new ElementoItinerario
                    {
                        Id = id,
                        Text = text,
                        SearchAliases = ObtenerAliasesBusquedaItinerario(ida, aliasesCache),
                        Variantes = new List<InfoVariante> { ida }
                    });
                }
    
                // 3) ✅ Si existe Vuelta: añadir item V
                if (vuelta is not null)
                {
                    var varCod = vuelta.Variante.Codigo.ToString() ?? "?";
                    var id = $"it_{linea}_{itin}_V_{varCod}";
                    var trayectoVuelta = ConstruirTextoRecorridoVariante(vuelta);
                    var textVisible = string.IsNullOrWhiteSpace(trayectoVuelta)
                        ? $"{linea}.{itin}-V — {vuelta.Linea.Nombre}"
                        : $"{linea}.{itin}-V — {trayectoVuelta}";
                    var text = $"{linea}.{itin}-V — {vuelta.Linea.Nombre}";
    
                    text = textVisible;

                    list.Add(new ElementoItinerario
                    {
                        Id = id,
                        Text = text,
                        SearchAliases = ObtenerAliasesBusquedaItinerario(vuelta, aliasesCache),
                        Variantes = new List<InfoVariante> { vuelta }
                    });
                }
    
                // 4) ✅ Si existen ambos: añadir item combinado IV
                if (ida is not null && vuelta is not null)
                {
                    var id = $"it_{linea}_{itin}_IV";
                    var text = $"{linea}.{itin} — {nombreLinea} (I+V)";
    
                    list.Add(new ElementoItinerario
                    {
                        Id = id,
                        Text = text,
                        SearchAliases = new[] { ida, vuelta }
                            .SelectMany(v => ObtenerAliasesBusquedaItinerario(v, aliasesCache))
                            .Distinct(StringComparer.OrdinalIgnoreCase)
                            .ToList(),
                        Variantes = new List<InfoVariante> { ida, vuelta }
                    });
                }
            }
    
            _itItems = list;
    
            _itSeleccionadoId ??= _itItems.FirstOrDefault()?.Id;
        }
    
    
    
        private List<ElementoItinerario> _itItems = new();
        private string? _itSeleccionadoId;
    
    
        private int _paradaPopupToken = 0;
    
    
    /// <summary>
    /// Gestiona lineas de alternativa.
    /// </summary>
    private string LineasDeAlternativa(AlternativaRuta alt)
    {
        if (alt is null || alt.EsSoloAPie || alt.AlternativaBus?.Tramos is null)
            return "";

        return TextoLineasDeTramos(alt.AlternativaBus.Tramos);
    }

    private static string TextoLineasDeTramos(IReadOnlyList<TramoBus>? tramos)
    {
        if (tramos is null || tramos.Count == 0)
            return "";

        var sb = new StringBuilder();
        string? ultimoCodigoMostrado = null;
        TramoBus? tramoAnteriorEnSecuencia = null;

        foreach (var tramo in tramos)
        {
            var codigo = CodigoLineaItinerarioSentido(tramo);
            if (string.IsNullOrWhiteSpace(codigo))
                continue;

            var repetidoConsecutivo = string.Equals(codigo, ultimoCodigoMostrado, StringComparison.OrdinalIgnoreCase);
            if (!repetidoConsecutivo)
            {
                if (sb.Length > 0)
                {
                    var separador = tramoAnteriorEnSecuencia is not null &&
                                    !UtilidadesTransitoDbus.EsTransbordoReal(tramoAnteriorEnSecuencia, tramo)
                        ? " + "
                        : ", ";
                    sb.Append(separador);
                }

                sb.Append(codigo);
                ultimoCodigoMostrado = codigo;
            }

            tramoAnteriorEnSecuencia = tramo;
        }

        return sb.ToString();
    }

    private static List<string> CompactarCodigosLineaConsecutivos(IEnumerable<string?> codigos)
    {
        var resultado = new List<string>();
        string? ultimoCodigoMostrado = null;

        foreach (var codigo in codigos)
        {
            if (string.IsNullOrWhiteSpace(codigo))
                continue;

            if (string.Equals(codigo, ultimoCodigoMostrado, StringComparison.OrdinalIgnoreCase))
                continue;

            resultado.Add(codigo);
            ultimoCodigoMostrado = codigo;
        }

        return resultado;
    }
    
    
    
    
    
    
        private HashSet<string> _previewLineasActivas = new();
        /// <summary>
        /// Indica si esta vista previa activa.
        /// </summary>
        private bool EstaPreviewActiva(string id)
        {
            return _previewLineasActivas.Contains(id);
        }
    
        // --- “doble click” por software ---
        private CancellationTokenSource? _ctsDobleClick;
        private (double lat, double lon)? _primerClickPendiente;
    
        /// <summary>
        /// 
        /// </summary>
        private bool Cerca(double lat1, double lon1, double lat2, double lon2, double maxMetros)
        => CalcularDistanciaMetros(lat1, lon1, lat2, lon2) <= maxMetros;
    
    
    
    
    
        // ajusta a gusto
        private const int DBLCLICK_MS = 320;         // ventana de doble click
        private const double DBLCLICK_MAX_METROS = 18; // tolerancia espacial (para no exigir pixel-perfect)
    
    
        private bool _elevPopupMinimizado = false;
    
    
        private const string _versionApp = "(v-20260714a)";
    
    
        private double? _paradaElevG;
    
    
    
        private DetalleTramoRuta? _tramoElevDetalle;
    
    
        /// <summary>
        /// Gestiona cerrar popups.
        /// </summary>
        private async Task CerrarPopupsAsync()
        {
            _itinerarioClicado = null;
    
            _paradaClicada = null;
            _popupInspectorVisible = false;
    
            if (_tramoElevDetalle is not null)
            {
                _tramoElevDetalle = null;
                try { await JS.InvokeVoidAsync("rt.clearElevPoints", "elev"); } catch { }
            }
        }
    
    
    
        /// <summary>
        /// Pinta puntos elevacion.
        /// </summary>
        private async Task PintarPuntosElevacionAsync(DetalleTramoRuta tramo)
        {  
            if (tramo?.MuestrasElevacion is null || tramo.MuestrasElevacion.Count == 0)
            {
                try { await JS.InvokeVoidAsync("rt.clearElevPoints", "elev"); } catch { }
                return;
            }
    
            var pts = tramo.MuestrasElevacion.Select((p, i) => new
            {
                lat = p.Latitud,
                lon = p.Longitud,
                // etiqueta: Ini/Fin o índice
                label = (i == 0) ? "Ini" : (i == tramo.MuestrasElevacion.Count - 1) ? "Fin" : p.Indice.ToString()
            }).ToList();
    
            try
            {
                await JS.InvokeVoidAsync("rt.drawElevPoints", "elev", pts, new
                {
                    radius = 3,
                    opacity = 0.9,
                    fillOpacity = 0.9,
                    showLabels = true,
                    labelEvery = 1 
                });
            }
            catch { }
        }
    
    
    
    
        // =========================================================
        //  CACHES (walking + bus filtrado)
        // =========================================================
    
        private static readonly ConcurrentDictionary<string, (double[,]? line, double dist, double dur)> _walkCache =
            new(StringComparer.OrdinalIgnoreCase);
    
        // Bus cache (filtrado por rango): key = o|d|min|max|maxAlts
        private static readonly ConcurrentDictionary<string, IReadOnlyList<RutaBusAlternativa>> _busFilteredCache =
            new(StringComparer.OrdinalIgnoreCase);
    
        // =========================================================
        //  INDICES “BUS GRAPH” (construidos desde Lineas/Trayectos)
        // =========================================================
    
        private static readonly object _busIndexLock = new();
        private static bool _busIndexBuilt = false;
    
        private static List<InfoVariante> _infoVariantes = new();
        private static Dictionary<string, List<AparicionParada>> _indiceParadas = new(StringComparer.OrdinalIgnoreCase);
        private static Dictionary<string, List<string>> _paradasCercanas = new(StringComparer.OrdinalIgnoreCase);
    
        private static Dictionary<string, Parada> _paradaByCode = new(StringComparer.OrdinalIgnoreCase);
    
        // Grid de paradas (para “paradas cerca del punto”)
        private static Dictionary<(int cx, int cy), List<Parada>> _gridParadas = new();
        private static int _gridScale = 400; // ~ 250-300m en lat
    
        /// <summary>
        /// 
        /// </summary>
        private static (int cx, int cy) Cell(double lat, double lon, int scale)
            => ((int)Math.Floor(lat * scale), (int)Math.Floor(lon * scale));

        private static void InvalidarBusIndex()
        {
            lock (_busIndexLock)
            {
                _busIndexBuilt = false;
                _infoVariantes = new List<InfoVariante>();
                _indiceParadas = new Dictionary<string, List<AparicionParada>>(StringComparer.OrdinalIgnoreCase);
                _paradasCercanas = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
                _paradaByCode = new Dictionary<string, Parada>(StringComparer.OrdinalIgnoreCase);
                _gridParadas = new Dictionary<(int cx, int cy), List<Parada>>();
            }
        }
    
    
    
        /// <summary>
        /// Gestiona cerrar popup elev.
        /// </summary>
        private async Task CerrarPopupElevAsync()
        {
            _elevPopupMinimizado = false;
    
            _tramoElevDetalle = null;
            try { await JS.InvokeVoidAsync("rt.clearElevPoints", "elev"); } catch { }
        }
    
        /// <summary>
        /// Obtiene texto itinerario clicado.
        /// </summary>
        private string GetTextoItinerarioClicado()
        {
            if (_itinerarioClicado is null)
                return "";
    
            var vReal = GetVarianteRealClicada();
            if (vReal is null)
                return _itinerarioClicado.Text;
    
            var linea = vReal.Linea?.Codigo?.ToString() ?? "?";
            var itin = vReal.CodigoItinerario;
            var sentido = NormalizarSentidoCodigo(vReal.Sentido?.Codigo); // I / V
            var nombre = vReal.Linea?.Nombre ?? "";
            var trayecto = ConstruirTextoRecorridoVariante(vReal);

            if (!string.IsNullOrWhiteSpace(trayecto))
                return $"{linea}.{itin}-{sentido} — {trayecto}";
    
            // ✅ Siempre devuelve el texto del sentido pinchado, no "I+V"
            return $"{linea}.{itin}-{sentido} — {nombre}";
        }
    
        /// <summary>
        /// Asegura que bus indice built este disponible.
        /// </summary>
        private void EnsureBusIndexBuilt()
        {
            if (_busIndexBuilt) return;
    
            lock (_busIndexLock)
            {
                if (_busIndexBuilt) return;
    
                // 1) map parada por código
                _paradaByCode = RedBusServicio.Paradas
                    .GroupBy(p => p.Codigo, StringComparer.OrdinalIgnoreCase)
                    .ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
    
                // 2) construir InfoVariante + indiceParadas
                _infoVariantes = new List<InfoVariante>(4096);
                _indiceParadas = new Dictionary<string, List<AparicionParada>>(StringComparer.OrdinalIgnoreCase);
    
                foreach (var linea in RedBusServicio.Lineas)
                {
                    foreach (var itinerario in linea.Itinerarios)
                    {
                        foreach (var sentido in itinerario.Sentidos)
                        {
                            foreach (var variante in sentido.Variantes)
                            {
                                if (variante.IdsTrayectos.Count == 0) continue;
    
                                if (!RedBusServicio.Trayectos.TryGetValue(variante.IdsTrayectos[0], out var t0))
                                    continue;
    
                                var info = new InfoVariante
                                {
                                    Linea = linea,
                                    Sentido = sentido,
                                    Variante = variante,
                                    CodigoItinerario = itinerario.Codigo
                                };
    
                                info.CodigosParadas.Add(t0.CodigoParadaOrigen);
    
                                foreach (var idTr in variante.IdsTrayectos)
                                {
                                    if (RedBusServicio.Trayectos.TryGetValue(idTr, out var t))
                                        info.CodigosParadas.Add(t.CodigoParadaDestino);
                                }
    
                                _infoVariantes.Add(info);
    
                                for (int i = 0; i < info.CodigosParadas.Count; i++)
                                {
                                    var cod = info.CodigosParadas[i];
                                    if (!_indiceParadas.TryGetValue(cod, out var lista))
                                    {
                                        lista = new List<AparicionParada>(4);
                                        _indiceParadas[cod] = lista;
                                    }
    
                                    lista.Add(new AparicionParada { Variante = info, IndiceParada = i });
                                }
                            }
                        }
                    }
                }
    
                // 3) grid de paradas (para query en radio)
                var dict = new Dictionary<(int, int), List<Parada>>();
                foreach (var p in RedBusServicio.Paradas)
                {
                    var c = Cell(p.Latitud, p.Longitud, _gridScale);
                    if (!dict.TryGetValue(c, out var list))
                    {
                        list = new List<Parada>(16);
                        dict[c] = list;
                    }
                    list.Add(p);
                }
                _gridParadas = dict;
    
                // 4) paradas cercanas para transbordo andando (sin O(n^2))
                _paradasCercanas = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
                const double MAX_TRANSBORDO = 250;
                double deg = MAX_TRANSBORDO / 111_000.0;
                int step = (int)Math.Ceiling(deg * _gridScale);
                if (step < 1) step = 1;
    
                foreach (var p in RedBusServicio.Paradas)
                {
                    var c0 = Cell(p.Latitud, p.Longitud, _gridScale);
    
                    for (int dx = -step; dx <= step; dx++)
                    {
                        for (int dy = -step; dy <= step; dy++)
                        {
                            var c = (c0.cx + dx, c0.cy + dy);
                            if (!_gridParadas.TryGetValue(c, out var list)) continue;
    
                            foreach (var q in list)
                            {
                                if (ReferenceEquals(p, q)) continue;
    
                                var d = CalcularDistanciaMetros(p.Latitud, p.Longitud, q.Latitud, q.Longitud);
                                if (d <= MAX_TRANSBORDO)
                                {
                                    AddCercana(p.Codigo, q.Codigo);
                                }
                            }
                        }
                    }
                }
    
                void AddCercana(string a, string b)
                {
                    if (!_paradasCercanas.TryGetValue(a, out var l))
                    {
                        l = new List<string>(8);
                        _paradasCercanas[a] = l;
                    }
                    if (!l.Contains(b, StringComparer.OrdinalIgnoreCase))
                        l.Add(b);
                }
    
                _busIndexBuilt = true;
            }
        }
    
        /// <summary>
        /// Consulta paradas en radio.
        /// </summary>
        private List<Parada> QueryParadasEnRadio(double lat, double lon, int radioMetros)
        {
            double deg = Math.Max(0.0005, radioMetros / 111_000.0);
            int step = (int)Math.Ceiling(deg * _gridScale);
            if (step < 1) step = 1;
    
            var c0 = Cell(lat, lon, _gridScale);
    
            var res = new List<Parada>(128);
            for (int dx = -step; dx <= step; dx++)
            {
                for (int dy = -step; dy <= step; dy++)
                {
                    var c = (c0.cx + dx, c0.cy + dy);
                    if (_gridParadas.TryGetValue(c, out var list))
                        res.AddRange(list);
                }
            }
            return res;
        }
}
