using System.Globalization;
using RutasDBUS.Modelos.Geocodificacion;
using RutasDBUS.Modelos.RedBus;

namespace RutasDBUS.Components.Pages;

public partial class PlanificadorRutas
{
    private enum CampoBusquedaMapa
    {
        Origen,
        Destino
    }

    private sealed class SugerenciaBusquedaMapa
    {
        public string Id { get; init; } = "";
        public string TextoEntrada { get; init; } = "";
        public string Titulo { get; init; } = "";
        public string Subtitulo { get; init; } = "";
        public string Tipo { get; init; } = "";
        public string Fuente { get; init; } = "";
        public double Latitud { get; init; }
        public double Longitud { get; init; }
    }

    private readonly List<SugerenciaBusquedaMapa> _sugerenciasBusquedaOrigen = new();
    private readonly List<SugerenciaBusquedaMapa> _sugerenciasBusquedaDestino = new();
    private CampoBusquedaMapa? _campoBusquedaActivo;
    private SugerenciaBusquedaMapa? _ubicacionOrigenSeleccionada;
    private SugerenciaBusquedaMapa? _ubicacionDestinoSeleccionada;
    private int _versionSugerenciasBusqueda;
    private CancellationTokenSource? _ctsSugerenciasBusqueda;

    private IReadOnlyList<SugerenciaBusquedaMapa> SugerenciasOrigenVisibles =>
        _campoBusquedaActivo == CampoBusquedaMapa.Origen
            ? _sugerenciasBusquedaOrigen
            : Array.Empty<SugerenciaBusquedaMapa>();

    private IReadOnlyList<SugerenciaBusquedaMapa> SugerenciasDestinoVisibles =>
        _campoBusquedaActivo == CampoBusquedaMapa.Destino
            ? _sugerenciasBusquedaDestino
            : Array.Empty<SugerenciaBusquedaMapa>();

    private Task ActualizarSugerenciasOrigenAsync()
        => ActualizarSugerenciasBusquedaAsync(CampoBusquedaMapa.Origen);

    private Task ActualizarSugerenciasDestinoAsync()
        => ActualizarSugerenciasBusquedaAsync(CampoBusquedaMapa.Destino);

    private Task MostrarSugerenciasOrigenAsync()
        => ActualizarSugerenciasBusquedaAsync(CampoBusquedaMapa.Origen);

    private Task MostrarSugerenciasDestinoAsync()
        => ActualizarSugerenciasBusquedaAsync(CampoBusquedaMapa.Destino);

    private async Task ActualizarSugerenciasBusquedaAsync(CampoBusquedaMapa campo)
    {
        _campoBusquedaActivo = campo;
        InvalidarUbicacionSeleccionadaSiEditada(campo);

        var texto = ObtenerTextoBusqueda(campo);
        var version = ++_versionSugerenciasBusqueda;

        _ctsSugerenciasBusqueda?.Cancel();
        _ctsSugerenciasBusqueda = new CancellationTokenSource();
        var ct = _ctsSugerenciasBusqueda.Token;

        if (string.IsNullOrWhiteSpace(texto) || texto.Trim().Length < 2)
        {
            AsignarSugerenciasBusqueda(campo, Array.Empty<SugerenciaBusquedaMapa>());
            StateHasChanged();
            return;
        }

        try
        {
            await Task.Delay(120, ct);
            var sugerencias = await CrearSugerenciasBusquedaAsync(texto, 20, ct);

            if (ct.IsCancellationRequested || version != _versionSugerenciasBusqueda)
                return;

            AsignarSugerenciasBusqueda(campo, sugerencias);
            StateHasChanged();
        }
        catch (OperationCanceledException)
        {
        }
        catch
        {
            AsignarSugerenciasBusqueda(campo, Array.Empty<SugerenciaBusquedaMapa>());
            StateHasChanged();
        }
    }

    private async Task SeleccionarSugerenciaBusquedaAsync(
        CampoBusquedaMapa campo,
        SugerenciaBusquedaMapa sugerencia)
    {
        if (campo == CampoBusquedaMapa.Origen)
        {
            _textoBusquedaOrigen = sugerencia.TextoEntrada;
            _coordenadasOrigen = new[] { sugerencia.Latitud, sugerencia.Longitud };
            _ubicacionOrigenSeleccionada = sugerencia;
            _sugerenciasBusquedaOrigen.Clear();
        }
        else
        {
            _textoBusquedaDestino = sugerencia.TextoEntrada;
            _coordenadasDestino = new[] { sugerencia.Latitud, sugerencia.Longitud };
            _ubicacionDestinoSeleccionada = sugerencia;
            _sugerenciasBusquedaDestino.Clear();
        }

        _campoBusquedaActivo = null;
        _mensajeRuta = string.Empty;
        _hayRutaValida = false;

        await SincronizarMarcadoresOrigenDestinoJsAsync();
        await FitRoutesJsAsync(new List<double[]>
        {
            new[] { sugerencia.Latitud, sugerencia.Longitud }
        });
        StateHasChanged();
    }

    private async Task<SugerenciaBusquedaMapa?> ResolverUbicacionBusquedaAsync(
        CampoBusquedaMapa campo,
        CancellationToken cancellationToken = default)
    {
        var texto = ObtenerTextoBusqueda(campo);
        if (string.IsNullOrWhiteSpace(texto))
            return null;

        var seleccion = ObtenerUbicacionSeleccionada(campo);
        if (seleccion is not null &&
            string.Equals(texto.Trim(), seleccion.TextoEntrada, StringComparison.OrdinalIgnoreCase))
        {
            return seleccion;
        }

        var parada = ResolverParadaDesdeTexto(texto);
        if (parada is not null)
            return CrearSugerenciaParada(parada);

        var sugerencias = await CrearSugerenciasBusquedaAsync(texto, 1, cancellationToken);
        return sugerencias.FirstOrDefault();
    }

    private async Task<IReadOnlyList<SugerenciaBusquedaMapa>> CrearSugerenciasBusquedaAsync(
        string texto,
        int maxResultados,
        CancellationToken cancellationToken)
    {
        var paradas = BuscarSugerenciasParadas(texto, 5);
        var lugares = await GeocodificadorLocal.BuscarAsync(texto, Math.Max(6, maxResultados), cancellationToken);

        return paradas
            .Concat(lugares.Select(CrearSugerenciaLugar))
            .GroupBy(sugerencia => NormalizarClaveSugerencia(sugerencia), StringComparer.Ordinal)
            .Select(grupo => grupo.First())
            .Take(Math.Clamp(maxResultados, 1, 20))
            .ToList();
    }

    private IReadOnlyList<SugerenciaBusquedaMapa> BuscarSugerenciasParadas(string texto, int maxResultados)
    {
        var consulta = NormalizeForSearch(texto);
        var tokens = consulta
            .Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);

        if (tokens.Length == 0)
            return Array.Empty<SugerenciaBusquedaMapa>();

        return RedBusServicio.Paradas
            .Select(parada => new
            {
                Parada = parada,
                Puntuacion = PuntuarParada(parada, consulta, tokens)
            })
            .Where(x => x.Puntuacion < int.MaxValue)
            .OrderBy(x => x.Puntuacion)
            .ThenBy(x => x.Parada.Nombre, StringComparer.CurrentCultureIgnoreCase)
            .Take(Math.Clamp(maxResultados, 1, 12))
            .Select(x => CrearSugerenciaParada(x.Parada))
            .ToList();
    }

    private static int PuntuarParada(Parada parada, string consulta, IReadOnlyList<string> tokens)
    {
        var codigo = NormalizeForSearch(CodigoParadaFormateado(parada.Codigo));
        var codigoRaw = NormalizeForSearch(parada.Codigo);
        var nombre = NormalizeForSearch(parada.Nombre);
        var nombreCorto = NormalizeForSearch(parada.NombreCorto);
        var abreviatura = NormalizeForSearch(parada.Abreviatura);
        var texto = $"{codigo} {codigoRaw} {nombre} {nombreCorto} {abreviatura}";

        foreach (var token in tokens)
        {
            if (!texto.Contains(token, StringComparison.Ordinal))
                return int.MaxValue;
        }

        if (codigo.Equals(consulta, StringComparison.Ordinal) ||
            codigoRaw.Equals(consulta, StringComparison.Ordinal))
        {
            return 0;
        }

        if (nombre.Equals(consulta, StringComparison.Ordinal))
            return 8;

        if (nombre.StartsWith(consulta, StringComparison.Ordinal) ||
            nombreCorto.StartsWith(consulta, StringComparison.Ordinal) ||
            abreviatura.StartsWith(consulta, StringComparison.Ordinal))
        {
            return 15;
        }

        return 45 + Math.Min(40, nombre.Length / 3);
    }

    private static SugerenciaBusquedaMapa CrearSugerenciaParada(Parada parada)
    {
        var codigo = CodigoParadaFormateado(parada.Codigo);
        var texto = $"{parada.Nombre} ({codigo})";

        return new SugerenciaBusquedaMapa
        {
            Id = "parada:" + parada.Codigo,
            TextoEntrada = texto,
            Titulo = texto,
            Subtitulo = "Parada DBUS",
            Tipo = "Parada",
            Fuente = "DBUS",
            Latitud = parada.Latitud,
            Longitud = parada.Longitud
        };
    }

    private static SugerenciaBusquedaMapa CrearSugerenciaLugar(LugarGeocodificado lugar)
    {
        return new SugerenciaBusquedaMapa
        {
            Id = lugar.Id,
            TextoEntrada = lugar.Nombre,
            Titulo = lugar.Nombre,
            Subtitulo = string.IsNullOrWhiteSpace(lugar.Subtitulo)
                ? $"{lugar.Tipo} · {lugar.Fuente}"
                : $"{lugar.Subtitulo} · {lugar.Fuente}",
            Tipo = lugar.Tipo,
            Fuente = lugar.Fuente,
            Latitud = lugar.Latitud,
            Longitud = lugar.Longitud
        };
    }

    private string ObtenerTextoBusqueda(CampoBusquedaMapa campo)
        => campo == CampoBusquedaMapa.Origen
            ? _textoBusquedaOrigen
            : _textoBusquedaDestino;

    private SugerenciaBusquedaMapa? ObtenerUbicacionSeleccionada(CampoBusquedaMapa campo)
        => campo == CampoBusquedaMapa.Origen
            ? _ubicacionOrigenSeleccionada
            : _ubicacionDestinoSeleccionada;

    private void InvalidarUbicacionSeleccionadaSiEditada(CampoBusquedaMapa campo)
    {
        var texto = ObtenerTextoBusqueda(campo).Trim();
        var seleccion = ObtenerUbicacionSeleccionada(campo);
        if (seleccion is null ||
            string.Equals(texto, seleccion.TextoEntrada, StringComparison.OrdinalIgnoreCase))
        {
            return;
        }

        if (campo == CampoBusquedaMapa.Origen)
            _ubicacionOrigenSeleccionada = null;
        else
            _ubicacionDestinoSeleccionada = null;
    }

    private void AsignarSugerenciasBusqueda(
        CampoBusquedaMapa campo,
        IReadOnlyList<SugerenciaBusquedaMapa> sugerencias)
    {
        var lista = campo == CampoBusquedaMapa.Origen
            ? _sugerenciasBusquedaOrigen
            : _sugerenciasBusquedaDestino;

        lista.Clear();
        lista.AddRange(sugerencias);
    }

    private static string NormalizarClaveSugerencia(SugerenciaBusquedaMapa sugerencia)
    {
        var lat = sugerencia.Latitud.ToString("0.000000", CultureInfo.InvariantCulture);
        var lon = sugerencia.Longitud.ToString("0.000000", CultureInfo.InvariantCulture);
        return $"{NormalizeForSearch(sugerencia.Titulo)}|{lat}|{lon}";
    }
}
