using RutasDBUS.Modelos.Importacion;
using RutasDBUS.Modelos.RedBus;
using RutasDBUS.Servicios.RedBus;

namespace RutasDBUS.Servicios.Planificacion;

/// <summary>
/// Construye el indice temporal compartido a partir del ZIP y de la red visual.
/// </summary>
public sealed class CreadorIndiceTransitoDbus
{
    private const int EscalaGrid = 400;

    /// <summary>
    /// Construye el indice temporal de la red.
    /// </summary>
    public IndiceTransitoDbus Crear(
        IRedBusServicio redBusServicio,
        ArchivoImportacionDbus archivo,
        int distanciaMaximaTransferenciaMetros = 250)
    {
        var paradasPorCodigo = redBusServicio.Paradas
            .GroupBy(parada => parada.Codigo, StringComparer.OrdinalIgnoreCase)
            .ToDictionary(grupo => grupo.Key, grupo => grupo.First(), StringComparer.OrdinalIgnoreCase);

        var codigosPorIdParada = ConstruirCodigosPorIdParada(archivo, paradasPorCodigo);
        var variantes = ConstruirVariantes(redBusServicio);
        var variantesPorFirma = variantes.ToDictionary(
            variante => UtilidadesTransitoDbus.FirmaVariante(
                UtilidadesTransitoDbus.LineaCodigoStr(variante),
                variante.CodigoItinerario,
                UtilidadesTransitoDbus.NormalizarSentidoCodigo(variante.Sentido?.Codigo),
                variante.CodigosParadas),
            variante => variante,
            StringComparer.OrdinalIgnoreCase);

        var variantesPorFallback = variantes
            .GroupBy(CrearClaveFallbackVariante, StringComparer.OrdinalIgnoreCase)
            .ToDictionary(grupo => grupo.Key, grupo => grupo.First(), StringComparer.OrdinalIgnoreCase);

        var codigoLineaPorRuta = archivo.Rutas.ToDictionary(
            fila => fila.Id,
            fila => !string.IsNullOrWhiteSpace(fila.Abreviatura)
                ? UtilidadesTransitoDbus.CodigoSinDecimal(fila.Abreviatura)
                : UtilidadesTransitoDbus.CodigoSinDecimal(fila.Id),
            StringComparer.OrdinalIgnoreCase);

        var patrones = new Dictionary<string, PatronTransitoDbus>(StringComparer.OrdinalIgnoreCase);

        foreach (var expedicion in archivo.Expediciones)
        {
            if (!archivo.TiemposParadaPorExpedicion.TryGetValue(expedicion.Id, out var tiemposParada))
                continue;

            var tiemposOrdenados = tiemposParada.OrderBy(fila => fila.SecPar).ToList();
            var codigosParada = tiemposOrdenados
                .Select(fila => codigosPorIdParada.TryGetValue(fila.IdTiempoPar, out var codigo) ? codigo : "")
                .Where(codigo => !string.IsNullOrWhiteSpace(codigo))
                .ToList();

            if (codigosParada.Count < 2)
                continue;

            var codigoLinea = !string.IsNullOrWhiteSpace(expedicion.IdRuta) &&
                              codigoLineaPorRuta.TryGetValue(expedicion.IdRuta, out var codigo)
                ? codigo
                : UtilidadesTransitoDbus.CodigoSinDecimal(expedicion.IdRuta);
            var sentido = UtilidadesTransitoDbus.DireccionZipASentido(expedicion.Direccion);
            var codigoItinerario = UtilidadesTransitoDbus.ExtraerCodigoItinerario(expedicion.IdRuta, expedicion.IdPatronExp);
            var firmaVariante = UtilidadesTransitoDbus.FirmaVariante(codigoLinea, codigoItinerario, sentido, codigosParada);

            if (!variantesPorFirma.TryGetValue(firmaVariante, out var variante) &&
                !variantesPorFallback.TryGetValue(CrearClaveFallback(codigoLinea, codigoItinerario, sentido, codigosParada), out variante))
            {
                continue;
            }

            var clavePatron = $"{expedicion.IdRuta}|{expedicion.IdPatronExp}|{sentido}|{codigoItinerario}|{string.Join(">", codigosParada)}";
            if (!patrones.TryGetValue(clavePatron, out var patron))
            {
                patron = new PatronTransitoDbus
                {
                    Id = clavePatron,
                    IdPatronExp = expedicion.IdPatronExp ?? "",
                    IdRuta = expedicion.IdRuta ?? "",
                    CodigoLinea = codigoLinea,
                    NumeroLinea = UtilidadesTransitoDbus.ExtraerNumeroLinea(expedicion.IdRuta)
                        ?? UtilidadesTransitoDbus.ExtraerNumeroLinea(codigoLinea),
                    CodigoItinerario = codigoItinerario,
                    Sentido = sentido,
                    Destino = UtilidadesTransitoDbus.ResolverDestinoDescriptivo(expedicion.DirDestino, codigosParada, paradasPorCodigo),
                    Variante = variante,
                    CodigosParada = codigosParada,
                    DistanciaAcumuladaPorParadaMetros = ConstruirDistanciaAcumulada(variante, redBusServicio, paradasPorCodigo)
                };

                patrones[clavePatron] = patron;
            }

            var expedicionProgramada = ConstruirExpedicion(expedicion, tiemposOrdenados, patron, codigosPorIdParada);
            if (expedicionProgramada is not null)
                patron.Expediciones.Add(expedicionProgramada);
        }

        foreach (var patron in patrones.Values)
        {
            patron.Expediciones.Sort((izquierda, derecha) =>
            {
                var salidaIzquierda = izquierda.SalidasSegundos.Count > 0 ? izquierda.SalidasSegundos[0] : int.MaxValue;
                var salidaDerecha = derecha.SalidasSegundos.Count > 0 ? derecha.SalidasSegundos[0] : int.MaxValue;
                return salidaIzquierda.CompareTo(salidaDerecha);
            });
        }

        var patronesPorParada = new Dictionary<string, List<ReferenciaPatronParadaDbus>>(StringComparer.OrdinalIgnoreCase);
        foreach (var patron in patrones.Values)
        {
            for (var indice = 0; indice < patron.CodigosParada.Count; indice++)
            {
                var codigo = patron.CodigosParada[indice];
                if (!patronesPorParada.TryGetValue(codigo, out var lista))
                {
                    lista = new List<ReferenciaPatronParadaDbus>(4);
                    patronesPorParada[codigo] = lista;
                }

                lista.Add(new ReferenciaPatronParadaDbus
                {
                    Patron = patron,
                    IndiceParada = indice
                });
            }
        }

        var gridParadas = ConstruirGridParadas(redBusServicio.Paradas);
        var transferenciasPorParada = ConstruirTransferencias(redBusServicio.Paradas, gridParadas, distanciaMaximaTransferenciaMetros);

        return new IndiceTransitoDbus(
            paradasPorCodigo,
            patrones.Values.ToList(),
            patronesPorParada,
            transferenciasPorParada,
            gridParadas,
            EscalaGrid);
    }

    /// <summary>
    /// Construye el mapa de stop id interno a codigo de parada.
    /// </summary>
    private static Dictionary<int, string> ConstruirCodigosPorIdParada(
        ArchivoImportacionDbus archivo,
        IReadOnlyDictionary<string, Parada> paradasPorCodigo)
    {
        var resultado = new Dictionary<int, string>();

        foreach (var fila in archivo.Paradas)
        {
            var codigo = !string.IsNullOrWhiteSpace(fila.CodigoSms)
                ? UtilidadesTransitoDbus.NormalizarCodigoParada(fila.CodigoSms)
                : UtilidadesTransitoDbus.NormalizarCodigoParada(fila.Id.ToString());

            if (paradasPorCodigo.ContainsKey(codigo))
                resultado[fila.Id] = codigo;
        }

        return resultado;
    }

    /// <summary>
    /// Construye las variantes enriquecidas de la red visual.
    /// </summary>
    private static List<InfoVariante> ConstruirVariantes(IRedBusServicio redBusServicio)
    {
        var variantes = new List<InfoVariante>(2048);

        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 trayectoInicial))
                            continue;

                        var info = new InfoVariante
                        {
                            Linea = linea,
                            Sentido = sentido,
                            Variante = variante,
                            CodigoItinerario = itinerario.Codigo
                        };

                        info.CodigosParadas.Add(trayectoInicial.CodigoParadaOrigen);
                        foreach (var idTrayecto in variante.IdsTrayectos)
                        {
                            if (redBusServicio.Trayectos.TryGetValue(idTrayecto, out var trayecto))
                                info.CodigosParadas.Add(trayecto.CodigoParadaDestino);
                        }

                        variantes.Add(info);
                    }
                }
            }
        }

        return variantes;
    }

    /// <summary>
    /// Construye una expedicion programada alineada con el patron.
    /// </summary>
    private static ExpedicionTransitoDbus? ConstruirExpedicion(
        FilaExpedicionCsv expedicion,
        IReadOnlyList<FilaTiempoParadaCsv> tiemposOrdenados,
        PatronTransitoDbus patron,
        IReadOnlyDictionary<int, string> codigosPorIdParada)
    {
        if (tiemposOrdenados.Count != patron.CodigosParada.Count)
            return null;

        var llegadas = new int[tiemposOrdenados.Count];
        var salidas = new int[tiemposOrdenados.Count];
        var ultimoSegundo = -1;

        for (var indice = 0; indice < tiemposOrdenados.Count; indice++)
        {
            var tiempo = tiemposOrdenados[indice];
            if (!codigosPorIdParada.TryGetValue(tiempo.IdTiempoPar, out var codigoParada) ||
                !codigoParada.Equals(patron.CodigosParada[indice], StringComparison.OrdinalIgnoreCase))
            {
                return null;
            }

            var llegadaBase = (tiempo.HoraLlegada ?? tiempo.HoraSalida ?? TimeOnly.MinValue).ToTimeSpan();
            var salidaBase = (tiempo.HoraSalida ?? tiempo.HoraLlegada ?? TimeOnly.MinValue).ToTimeSpan();

            var llegadaSegundos = (int)llegadaBase.TotalSeconds;
            var salidaSegundos = (int)salidaBase.TotalSeconds;

            while (llegadaSegundos < ultimoSegundo)
                llegadaSegundos += 24 * 3600;

            while (salidaSegundos < llegadaSegundos)
                salidaSegundos += 24 * 3600;

            llegadas[indice] = llegadaSegundos;
            salidas[indice] = salidaSegundos;
            ultimoSegundo = salidaSegundos;
        }

        return new ExpedicionTransitoDbus
        {
            Id = expedicion.Id ?? "",
            IdCalendario = expedicion.IdCal ?? "",
            LlegadasSegundos = llegadas,
            SalidasSegundos = salidas
        };
    }

    /// <summary>
    /// Calcula la distancia acumulada por parada de una variante.
    /// </summary>
    private static IReadOnlyList<double> ConstruirDistanciaAcumulada(
        InfoVariante variante,
        IRedBusServicio redBusServicio,
        IReadOnlyDictionary<string, Parada> paradasPorCodigo)
    {
        var acumulada = new double[variante.CodigosParadas.Count];

        for (var indice = 0; indice < variante.CodigosParadas.Count - 1; indice++)
        {
            var distancia = 0.0;

            if (indice < variante.Variante.IdsTrayectos.Count &&
                redBusServicio.Trayectos.TryGetValue(variante.Variante.IdsTrayectos[indice], out var trayecto))
            {
                for (var p = 1; p < trayecto.Posiciones.Count; p++)
                {
                    distancia += UtilidadesTransitoDbus.CalcularDistanciaMetros(
                        trayecto.Posiciones[p - 1][0],
                        trayecto.Posiciones[p - 1][1],
                        trayecto.Posiciones[p][0],
                        trayecto.Posiciones[p][1]);
                }
            }
            else if (paradasPorCodigo.TryGetValue(variante.CodigosParadas[indice], out var paradaOrigen) &&
                     paradasPorCodigo.TryGetValue(variante.CodigosParadas[indice + 1], out var paradaDestino))
            {
                distancia = UtilidadesTransitoDbus.CalcularDistanciaMetros(
                    paradaOrigen.Latitud,
                    paradaOrigen.Longitud,
                    paradaDestino.Latitud,
                    paradaDestino.Longitud);
            }

            acumulada[indice + 1] = acumulada[indice] + distancia;
        }

        return acumulada;
    }

    /// <summary>
    /// Construye el grid espacial de paradas.
    /// </summary>
    private static Dictionary<(int cx, int cy), List<Parada>> ConstruirGridParadas(IReadOnlyList<Parada> paradas)
    {
        var resultado = new Dictionary<(int cx, int cy), List<Parada>>();

        foreach (var parada in paradas)
        {
            var celda = Celda(parada.Latitud, parada.Longitud, EscalaGrid);
            if (!resultado.TryGetValue(celda, out var lista))
            {
                lista = new List<Parada>(16);
                resultado[celda] = lista;
            }

            lista.Add(parada);
        }

        return resultado;
    }

    /// <summary>
    /// Construye las transferencias a pie entre paradas cercanas.
    /// </summary>
    private static Dictionary<string, List<TransferenciaPeatonalDbus>> ConstruirTransferencias(
        IReadOnlyList<Parada> paradas,
        IReadOnlyDictionary<(int cx, int cy), List<Parada>> gridParadas,
        int distanciaMaximaTransferenciaMetros)
    {
        var transferencias = new Dictionary<string, List<TransferenciaPeatonalDbus>>(StringComparer.OrdinalIgnoreCase);
        var deg = distanciaMaximaTransferenciaMetros / 111_000.0;
        var paso = Math.Max(1, (int)Math.Ceiling(deg * EscalaGrid));

        foreach (var parada in paradas)
        {
            var celdaOrigen = Celda(parada.Latitud, parada.Longitud, EscalaGrid);
            for (var dx = -paso; dx <= paso; dx++)
            {
                for (var dy = -paso; dy <= paso; dy++)
                {
                    if (!gridParadas.TryGetValue((celdaOrigen.cx + dx, celdaOrigen.cy + dy), out var vecinas))
                        continue;

                    foreach (var vecina in vecinas)
                    {
                        if (ReferenceEquals(parada, vecina))
                            continue;

                        var distancia = UtilidadesTransitoDbus.CalcularDistanciaMetros(
                            parada.Latitud,
                            parada.Longitud,
                            vecina.Latitud,
                            vecina.Longitud);

                        if (distancia > distanciaMaximaTransferenciaMetros)
                            continue;

                        if (!transferencias.TryGetValue(parada.Codigo, out var lista))
                        {
                            lista = new List<TransferenciaPeatonalDbus>(8);
                            transferencias[parada.Codigo] = lista;
                        }

                        if (lista.Any(x => x.CodigoParadaDestino.Equals(vecina.Codigo, StringComparison.OrdinalIgnoreCase)))
                            continue;

                        lista.Add(new TransferenciaPeatonalDbus
                        {
                            CodigoParadaDestino = vecina.Codigo,
                            DistanciaMetros = distancia
                        });
                    }
                }
            }
        }

        return transferencias;
    }

    /// <summary>
    /// Crea la clave de respaldo para una variante.
    /// </summary>
    private static string CrearClaveFallbackVariante(InfoVariante variante)
        => CrearClaveFallback(
            UtilidadesTransitoDbus.LineaCodigoStr(variante),
            variante.CodigoItinerario,
            UtilidadesTransitoDbus.NormalizarSentidoCodigo(variante.Sentido?.Codigo),
            variante.CodigosParadas);

    /// <summary>
    /// Crea una clave de respaldo por extremos y longitud.
    /// </summary>
    private static string CrearClaveFallback(string codigoLinea, int codigoItinerario, string sentido, IReadOnlyList<string> codigosParada)
        => codigosParada.Count == 0
            ? $"{codigoLinea}|{codigoItinerario}|{sentido}|0"
            : $"{codigoLinea}|{codigoItinerario}|{sentido}|{codigosParada[0]}|{codigosParada[^1]}|{codigosParada.Count}";

    private static (int cx, int cy) Celda(double latitud, double longitud, int escala)
        => ((int)Math.Floor(latitud * escala), (int)Math.Floor(longitud * escala));
}
