using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using RutasDBUS.Modelos.Importacion;
using RutasDBUS.Modelos.RedBus;
using RutasDBUS.Servicios.Importacion;

namespace RutasDBUS.Servicios.RedBus;

public class RedBusServicio : IRedBusServicio
{
    private const string ColorVisualLinea40Argb = "FF9A7B00";
    private readonly OpcionesRedBus _opciones;
    private readonly ILogger<RedBusServicio> _logger;
    private readonly IServicioArchivoImportacionDbus _archiveService;
    private readonly CreadorRedTransporteDbus _networkBuilder;

    private readonly SemaphoreSlim _semCarga = new(1, 1);
    private bool _redCargada;
    private ArchivoImportacionDbus? _archivoCargado;

    private readonly List<Parada> _paradas = new();
    private readonly Dictionary<int, Trayecto> _trayectos = new();
    private readonly List<LineaBus> _lineas = new();

    private readonly List<InfoVariante> _infoVariantes = new();
    private readonly Dictionary<string, List<AparicionParada>> _indiceParadas =
        new(StringComparer.OrdinalIgnoreCase);

    private readonly Dictionary<string, List<string>> _paradasCercanas =
        new(StringComparer.OrdinalIgnoreCase);

    private const int EscalaGrid = 400;
    private Dictionary<(int cx, int cy), List<Parada>> _gridParadas = new();

    public IReadOnlyList<Parada> Paradas => _paradas;
    public IReadOnlyDictionary<int, Trayecto> Trayectos => _trayectos;
    public IReadOnlyList<LineaBus> Lineas => _lineas;

    /// <summary>
    /// Inicializa una nueva instancia de la clase RedBusServicio.
    /// </summary>
    public RedBusServicio(
        IOptions<OpcionesRedBus> opciones,
        ILogger<RedBusServicio> logger,
        IServicioArchivoImportacionDbus archiveService,
        CreadorRedTransporteDbus networkBuilder)
    {
        _opciones = opciones.Value ?? new OpcionesRedBus();
        _logger = logger;
        _archiveService = archiveService;
        _networkBuilder = networkBuilder;
    }

    /// <summary>
    /// Gestiona red.
    /// </summary>
    public async Task CargarRedAsync(CancellationToken cancellationToken = default)
    {
        ArchivoImportacionDbus archiveActual;
        try
        {
            archiveActual = await _archiveService.ObtenerAsync(cancellationToken);
        }
        catch (FileNotFoundException)
        {
            _logger.LogWarning("No se encontrÃ³ ningÃºn ZIP de horarios.");
            return;
        }

        if (_redCargada && ReferenceEquals(_archivoCargado, archiveActual)) return;

        await _semCarga.WaitAsync(cancellationToken);
        try
        {
            if (_redCargada && ReferenceEquals(_archivoCargado, archiveActual)) return;

            LimpiarTodo();

            ArchivoImportacionDbus archive;
            try
            {
                archive = await _archiveService.ObtenerAsync(cancellationToken);
            }
            catch (FileNotFoundException)
            {
                _logger.LogWarning("No se encontró ningún ZIP de horarios.");
                return;
            }

            var network = _networkBuilder.Build(archive);
            _paradas.AddRange(network.Paradas);
            foreach (var kvp in network.Trayectos)
                _trayectos[kvp.Key] = kvp.Value;
            foreach (var linea in network.Lineas)
            {
                AjustarColorVisualLinea(linea);
                _lineas.Add(linea);
            }

            if (_paradas.Count == 0 || _lineas.Count == 0)
            {
                _logger.LogWarning("No se han cargado suficientes datos desde el ZIP.");
                return;
            }

            ConstruirVariantesEIndice();
            ConstruirGridParadas();
            ConstruirParadasCercanasRapido(Math.Max(0, _opciones.DistanciaMaximaTransbordoMetros));

            _archivoCargado = archive;
            _redCargada = true;

            _logger.LogInformation("Paradas cargadas: {N}", _paradas.Count);
            _logger.LogInformation("Líneas cargadas: {N}", _lineas.Count);
            _logger.LogInformation("Trayectos cargados: {N}", _trayectos.Count);

            foreach (var p in _paradas.Take(10))
            {
                _logger.LogInformation(
                    "STOP {Codigo} | {Nombre} | lat={Lat} | lon={Lon}",
                    p.Codigo, p.Nombre, p.Latitud, p.Longitud);
            }
        }
        finally
        {
            _semCarga.Release();
        }
    }

    /// <summary>
    /// Gestiona todo.
    /// </summary>
    private void LimpiarTodo()
    {
        _paradas.Clear();
        _trayectos.Clear();
        _lineas.Clear();
        _infoVariantes.Clear();
        _indiceParadas.Clear();
        _paradasCercanas.Clear();
        _gridParadas.Clear();
    }

    private static void AjustarColorVisualLinea(LineaBus linea)
    {
        if (string.Equals(linea.Codigo, "40", StringComparison.OrdinalIgnoreCase))
            linea.ColorArgb = ColorVisualLinea40Argb;
    }

    /// <summary>
    /// 
    /// </summary>
    private static double Dist2(double lat1, double lon1, double lat2, double lon2)
    {
        double dLat = lat1 - lat2;
        double dLon = (lon1 - lon2) * Math.Cos((lat1 + lat2) / 2 * Math.PI / 180.0);
        return dLat * dLat + dLon * dLon;
    }

    // =========================================================
    // ÍNDICES
    // =========================================================

    /// <summary>
    /// Gestiona variantes e indice.
    /// </summary>
    private void ConstruirVariantesEIndice()
    {
        _infoVariantes.Clear();
        _indiceParadas.Clear();

        foreach (var linea in _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 (!_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 (_trayectos.TryGetValue(idTr, out var t))
                                info.CodigosParadas.Add(t.CodigoParadaDestino);
                        }

                        _infoVariantes.Add(info);

                        for (int i = 0; i < info.CodigosParadas.Count; i++)
                        {
                            string codigoParada = info.CodigosParadas[i];
                            if (string.IsNullOrWhiteSpace(codigoParada)) continue;

                            if (!_indiceParadas.TryGetValue(codigoParada, out var lista))
                            {
                                lista = new List<AparicionParada>(4);
                                _indiceParadas[codigoParada] = lista;
                            }

                            lista.Add(new AparicionParada
                            {
                                Variante = info,
                                IndiceParada = i
                            });
                        }
                    }
                }
            }
        }
    }

    // =========================================================
    // PARADAS CERCANAS
    // =========================================================

    /// <summary>
    /// 
    /// </summary>
    private static (int cx, int cy) Celda(double lat, double lon)
        => ((int)Math.Floor(lat * EscalaGrid), (int)Math.Floor(lon * EscalaGrid));

    /// <summary>
    /// Gestiona rejilla paradas.
    /// </summary>
    private void ConstruirGridParadas()
    {
        var dict = new Dictionary<(int, int), List<Parada>>();

        foreach (var p in _paradas)
        {
            var c = Celda(p.Latitud, p.Longitud);
            if (!dict.TryGetValue(c, out var list))
            {
                list = new List<Parada>(16);
                dict[c] = list;
            }
            list.Add(p);
        }

        _gridParadas = dict;
    }

    /// <summary>
    /// Gestiona paradas cercanas rapido.
    /// </summary>
    private void ConstruirParadasCercanasRapido(double maxDistMetros)
    {
        _paradasCercanas.Clear();

        if (maxDistMetros <= 0 || _paradas.Count == 0)
            return;

        double deg = maxDistMetros / 111_000.0;
        int step = (int)Math.Ceiling(deg * EscalaGrid);
        if (step < 1) step = 1;

        foreach (var p in _paradas)
        {
            var c0 = Celda(p.Latitud, p.Longitud);

            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;

                        double d = CalcularDistanciaMetros(p.Latitud, p.Longitud, q.Latitud, q.Longitud);
                        if (d <= maxDistMetros)
                            AgregarParadaCercana(p.Codigo, q.Codigo);
                    }
                }
            }
        }
    }

    /// <summary>
    /// Agrega parada cercana.
    /// </summary>
    private void AgregarParadaCercana(string codigoBase, string codigoCercana)
    {
        if (string.IsNullOrWhiteSpace(codigoBase) || string.IsNullOrWhiteSpace(codigoCercana))
            return;

        if (!_paradasCercanas.TryGetValue(codigoBase, out var lista))
        {
            lista = new List<string>(8);
            _paradasCercanas[codigoBase] = lista;
        }

        if (!lista.Contains(codigoCercana, StringComparer.OrdinalIgnoreCase))
            lista.Add(codigoCercana);
    }

    /// <summary>
    /// Calcula distancia metros.
    /// </summary>
    public double CalcularDistanciaMetros(double lat1, double lon1, double lat2, double lon2)
    {
        double d2 = Dist2(lat1, lon1, lat2, lon2);
        return Math.Sqrt(d2) * 111_000.0;
    }

    /// <summary>
    /// Busca parada mas cercana.
    /// </summary>
    public Parada? BuscarParadaMasCercana(double latitud, double longitud)
    {
        Parada? mejor = null;
        double mejorDistancia = double.MaxValue;

        foreach (var p in _paradas)
        {
            double d = Dist2(latitud, longitud, p.Latitud, p.Longitud);
            if (d < mejorDistancia)
            {
                mejorDistancia = d;
                mejor = p;
            }
        }

        return mejor;
    }

    /// <summary>
    /// Busca parada cercana en rango.
    /// </summary>
    public Parada? BuscarParadaCercanaEnRango(double latitud, double longitud, double maxDistMetros)
    {
        Parada? mejor = null;
        double mejorD2 = double.MaxValue;

        foreach (var p in _paradas)
        {
            double d2 = Dist2(latitud, longitud, p.Latitud, p.Longitud);
            if (d2 < mejorD2)
            {
                mejorD2 = d2;
                mejor = p;
            }
        }

        if (mejor is null)
            return null;

        double distanciaMetros = Math.Sqrt(mejorD2) * 111_000.0;
        return distanciaMetros <= maxDistMetros ? mejor : null;
    }

    /// <summary>
    /// Convierte color argb a css.
    /// </summary>
    public string ConvertirColorArgbACss(string argb)
    {
        if (string.IsNullOrWhiteSpace(argb))
            return "blue";

        argb = argb.Trim();

        if (argb.Length == 6)
            return "#" + argb;

        if (argb.Length == 8)
            return "#" + argb.Substring(2);

        return "blue";
    }

    // =========================================================
    // RUTADO BUS
    // =========================================================

    /// <summary>
    /// Busca ruta bus con filtro.
    /// </summary>
    private List<TramoBus>? BuscarRutaBusConFiltro(
        string codigoParadaOrigen,
        string codigoParadaDestino,
        Func<InfoVariante, bool> filtroVariante,
        out List<string> codigosParadasTransbordo)
    {
        codigosParadasTransbordo = new();

        if (!_indiceParadas.TryGetValue(codigoParadaOrigen, out var aparicionesOrigen))
            return null;
        if (!_indiceParadas.TryGetValue(codigoParadaDestino, out _))
            return null;

        var cola = new Queue<(InfoVariante Variante, int IndiceParada)>();
        var visitados = new HashSet<(InfoVariante Variante, int IndiceParada)>();
        var padre = new Dictionary<(InfoVariante Variante, int IndiceParada),
                                   (InfoVariante Variante, int IndiceParada)?>();

        (InfoVariante Variante, int IndiceParada)? objetivo = null;

        foreach (var aparicion in aparicionesOrigen)
        {
            if (!filtroVariante(aparicion.Variante))
                continue;

            var nodo = (aparicion.Variante, aparicion.IndiceParada);
            if (!visitados.Add(nodo)) continue;

            padre[nodo] = null;
            cola.Enqueue(nodo);
        }

        while (cola.Count > 0)
        {
            var actual = cola.Dequeue();
            var (variante, indice) = actual;

            string codigoParadaActual = variante.CodigosParadas[indice];

            if (codigoParadaActual == codigoParadaDestino)
            {
                objetivo = actual;
                break;
            }

            if (indice + 1 < variante.CodigosParadas.Count)
            {
                var siguiente = (variante, indice + 1);
                if (visitados.Add(siguiente))
                {
                    padre[siguiente] = actual;
                    cola.Enqueue(siguiente);
                }
            }

            if (_indiceParadas.TryGetValue(codigoParadaActual, out var aparicionesParada))
            {
                foreach (var ap in aparicionesParada)
                {
                    if (!filtroVariante(ap.Variante))
                        continue;

                    var siguiente = (ap.Variante, ap.IndiceParada);

                    if (ReferenceEquals(siguiente.Variante, variante) && siguiente.IndiceParada == indice)
                        continue;

                    if (visitados.Add(siguiente))
                    {
                        padre[siguiente] = actual;
                        cola.Enqueue(siguiente);
                    }
                }
            }

            if (_paradasCercanas.TryGetValue(codigoParadaActual, out var vecinas))
            {
                foreach (var codVecino in vecinas)
                {
                    if (!_indiceParadas.TryGetValue(codVecino, out var aparicionesVecino))
                        continue;

                    foreach (var ap in aparicionesVecino)
                    {
                        if (!filtroVariante(ap.Variante))
                            continue;

                        var siguiente = (ap.Variante, ap.IndiceParada);

                        if (visitados.Add(siguiente))
                        {
                            padre[siguiente] = actual;
                            cola.Enqueue(siguiente);
                        }
                    }
                }
            }
        }

        if (objetivo is null)
            return null;

        var nodosCamino = new List<(InfoVariante Variante, int IndiceParada)>();
        var nodoActual = objetivo.Value;

        while (true)
        {
            nodosCamino.Add(nodoActual);
            if (!padre.TryGetValue(nodoActual, out var anterior) || anterior is null)
                break;
            nodoActual = anterior.Value;
        }

        nodosCamino.Reverse();

        int numeroTransbordos;
        var tramos = ConstruirTramosYTransbordosDesdeCamino(
            nodosCamino,
            codigoParadaOrigen,
            codigoParadaDestino,
            out codigosParadasTransbordo,
            out numeroTransbordos);

        return tramos;
    }

    /// <summary>
    /// Gestiona tramos y transbordos desde camino.
    /// </summary>
    private static List<TramoBus> ConstruirTramosYTransbordosDesdeCamino(
        List<(InfoVariante Variante, int IndiceParada)> nodosCamino,
        string codigoParadaOrigen,
        string codigoParadaDestino,
        out List<string> codigosParadasTransbordo,
        out int numeroTransbordos)
    {
        codigosParadasTransbordo = new();

        for (int i = 1; i < nodosCamino.Count; i++)
        {
            var previo = nodosCamino[i - 1];
            var ahora = nodosCamino[i];

            if (!ReferenceEquals(previo.Variante, ahora.Variante))
            {
                string codigoParada = ahora.Variante.CodigosParadas[ahora.IndiceParada];
                if (!codigosParadasTransbordo.Contains(codigoParada) &&
                    codigoParada != codigoParadaOrigen &&
                    codigoParada != codigoParadaDestino)
                {
                    codigosParadasTransbordo.Add(codigoParada);
                }
            }
        }

        numeroTransbordos = codigosParadasTransbordo.Count;

        var tramos = new List<TramoBus>();
        if (nodosCamino.Count <= 1) return tramos;

        var varianteActual = nodosCamino[0].Variante;
        int indiceInicio = nodosCamino[0].IndiceParada;
        int ultimoIndice = nodosCamino[0].IndiceParada;

        for (int i = 1; i < nodosCamino.Count; i++)
        {
            var (variante, indice) = nodosCamino[i];

            if (ReferenceEquals(variante, varianteActual) && indice == ultimoIndice + 1)
            {
                ultimoIndice = indice;
                continue;
            }

            if (ultimoIndice > indiceInicio)
            {
                tramos.Add(new TramoBus
                {
                    Variante = varianteActual,
                    IndiceInicio = indiceInicio,
                    IndiceFin = ultimoIndice
                });
            }

            varianteActual = variante;
            indiceInicio = indice;
            ultimoIndice = indice;
        }

        if (ultimoIndice > indiceInicio)
        {
            tramos.Add(new TramoBus
            {
                Variante = varianteActual,
                IndiceInicio = indiceInicio,
                IndiceFin = ultimoIndice
            });
        }

        return tramos;
    }

    /// <summary>
    /// Busca ruta bus.
    /// </summary>
    public List<TramoBus>? BuscarRutaBus(
        string codigoParadaOrigen,
        string codigoParadaDestino,
        out List<string> codigosParadasTransbordo)
    {
        return BuscarRutaBusConFiltro(
            codigoParadaOrigen,
            codigoParadaDestino,
            filtroVariante: _ => true,
            codigosParadasTransbordo: out codigosParadasTransbordo);
    }

    /// <summary>
    /// Gestiona lineas bus.
    /// </summary>
    public List<double[,]> ConstruirLineasBus(
        IReadOnlyList<TramoBus> tramos,
        out double distanciaTotalBus)
    {
        distanciaTotalBus = 0;
        var resultado = new List<double[,]>();

        foreach (var tramo in tramos)
        {
            var info = tramo.Variante;
            var idsTrayectos = info.Variante.IdsTrayectos;
            var puntos = new List<double[]>();

            for (int indiceParada = tramo.IndiceInicio; indiceParada < tramo.IndiceFin; indiceParada++)
            {
                int indiceTrayecto = indiceParada;
                if (indiceTrayecto < 0 || indiceTrayecto >= idsTrayectos.Count)
                    continue;

                if (_trayectos.TryGetValue(idsTrayectos[indiceTrayecto], out var trayecto))
                {
                    foreach (var p in trayecto.Posiciones)
                    {
                        if (puntos.Count > 0)
                        {
                            var ultimo = puntos[^1];
                            if (Math.Abs(ultimo[0] - p[0]) < 1e-6 &&
                                Math.Abs(ultimo[1] - p[1]) < 1e-6)
                                continue;
                        }

                        puntos.Add(p);
                    }
                }
            }

            if (puntos.Count == 0) continue;

            var paradaInicio = ResolverParadaFisicaCercana(
                tramo.CodigoParadaInicio,
                puntos.Count > 0 ? puntos[0] : null);
            var paradaFin = ResolverParadaFisicaCercana(
                tramo.CodigoParadaFin,
                puntos.Count > 0 ? puntos[^1] : null);

            if (paradaInicio is not null)
                puntos[0] = new[] { paradaInicio.Latitud, paradaInicio.Longitud };

            if (paradaFin is not null)
                puntos[^1] = new[] { paradaFin.Latitud, paradaFin.Longitud };

            var linea = new double[puntos.Count, 2];
            for (int i = 0; i < puntos.Count; i++)
            {
                var lat = puntos[i][0];
                var lon = puntos[i][1];

                linea[i, 0] = lat;
                linea[i, 1] = lon;

                if (i > 0)
                {
                    var prev = puntos[i - 1];
                    distanciaTotalBus += CalcularDistanciaMetros(prev[0], prev[1], lat, lon);
                }
            }

            resultado.Add(linea);
        }

        return resultado;
    }

    /// <summary>
    /// Busca rutas bus alternativas.
    /// </summary>
    public IReadOnlyList<RutaBusAlternativa> BuscarRutasBusAlternativas(
        string codigoParadaOrigen,
        string codigoParadaDestino,
        int maxAlternativas = 3)
    {
        var alternativas = new List<RutaBusAlternativa>();
        if (maxAlternativas <= 0) return alternativas;

        var rutaPrincipal = BuscarRutaBusConFiltro(
            codigoParadaOrigen,
            codigoParadaDestino,
            filtroVariante: _ => true,
            codigosParadasTransbordo: out var transbordosPrincipal);

        if (rutaPrincipal is null || rutaPrincipal.Count == 0)
            return alternativas;

        double distanciaPrincipal;
        _ = ConstruirLineasBus(rutaPrincipal, out distanciaPrincipal);

        string FirmaRuta(List<TramoBus> tramos) =>
            string.Join(";", tramos.Select(t =>
                $"{t.Variante.Linea.Codigo}-{t.Variante.CodigoItinerario}-{NormalizarSentidoCodigo(t.Variante.Sentido?.Codigo)}-{t.Variante.Variante.Codigo}-{t.IndiceInicio}-{t.IndiceFin}"));

        var firmas = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        firmas.Add(FirmaRuta(rutaPrincipal));

        alternativas.Add(new RutaBusAlternativa
        {
            Tramos = rutaPrincipal,
            CodigosParadasTransbordo = transbordosPrincipal,
            NumeroTransbordos = Math.Max(0, transbordosPrincipal.Count),
            DistanciaTotalBus = distanciaPrincipal
        });

        if (alternativas.Count >= maxAlternativas)
            return alternativas;

        var variantesUsadas = rutaPrincipal
            .Select(t => t.Variante)
            .Distinct()
            .ToList();

        foreach (var varianteProhibida in variantesUsadas)
        {
            if (alternativas.Count >= maxAlternativas)
                break;

            var rutaAlt = BuscarRutaBusConFiltro(
                codigoParadaOrigen,
                codigoParadaDestino,
                filtroVariante: v => !ReferenceEquals(v, varianteProhibida),
                codigosParadasTransbordo: out var transbordosAlt);

            if (rutaAlt is null || rutaAlt.Count == 0)
                continue;

            var firmaAlt = FirmaRuta(rutaAlt);
            if (!firmas.Add(firmaAlt))
                continue;

            double distanciaAlt;
            _ = ConstruirLineasBus(rutaAlt, out distanciaAlt);

            alternativas.Add(new RutaBusAlternativa
            {
                Tramos = rutaAlt,
                CodigosParadasTransbordo = transbordosAlt,
                NumeroTransbordos = Math.Max(0, transbordosAlt.Count),
                DistanciaTotalBus = distanciaAlt
            });
        }

        return alternativas;
    }

    // =========================================================
    // HELPERS
    // =========================================================

    /// <summary>
    /// Resuelve la parada fisica mas cercana a un punto para un codigo concreto.
    /// </summary>
    private Parada? ResolverParadaFisicaCercana(string? codigoParada, double[]? puntoReferencia)
    {
        if (string.IsNullOrWhiteSpace(codigoParada))
            return null;

        var candidatas = _paradas
            .Where(parada => parada.Codigo.Equals(codigoParada, StringComparison.OrdinalIgnoreCase))
            .ToList();

        if (candidatas.Count == 0)
            return null;

        if (candidatas.Count == 1 || puntoReferencia is null || puntoReferencia.Length < 2)
            return candidatas[0];

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

    /// <summary>
    /// Normaliza sentido codigo.
    /// </summary>
    private static string NormalizarSentidoCodigo(string? codigo)
    {
        if (string.IsNullOrWhiteSpace(codigo)) return "?";

        var s = codigo.Trim();

        if (s.Equals("V", StringComparison.OrdinalIgnoreCase)) return "V";
        if (s.Equals("I", StringComparison.OrdinalIgnoreCase)) return "I";
        if (s.Equals("Vuelta", StringComparison.OrdinalIgnoreCase)) return "V";
        if (s.Equals("Ida", StringComparison.OrdinalIgnoreCase)) return "I";

        var c = char.ToUpperInvariant(s[0]);
        if (c == 'V') return "V";
        if (c == 'I') return "I";
        return "?";
    }
}
