using RutasDBUS.Modelos.Planificacion;
using RutasDBUS.Modelos.RedBus;

namespace RutasDBUS.Servicios.Planificacion;

/// <summary>
/// Busca rutas de bus mediante rondas sobre patrones y expediciones.
/// </summary>
public sealed class BuscadorRutasTemporalesDbus
{
    private const int MaxEstadosPorParada = 4;
    private const int MaxContinuidadesCircularesExtra = 2;

    /// <summary>
    /// Busca alternativas de bus entre grupos de paradas candidatas.
    /// </summary>
    public IReadOnlyList<ResultadoBusquedaRutaBusDbus> BuscarAlternativas(
        IndiceTransitoDbus indice,
        IReadOnlyList<ParadaCandidata> candidatasOrigen,
        IReadOnlyList<ParadaCandidata> candidatasDestino,
        DateTime instanteSalida,
        ParametrosPlanificadorRuta parametros,
        IReadOnlySet<string>? calendariosActivos,
        CancellationToken cancellationToken = default)
    {
        var origenPorCodigo = candidatasOrigen
            .GroupBy(candidata => candidata.Parada.Codigo, StringComparer.OrdinalIgnoreCase)
            .ToDictionary(grupo => grupo.Key, grupo => grupo.OrderBy(x => x.TiempoAPie).First(), StringComparer.OrdinalIgnoreCase);
        var destinoPorCodigo = candidatasDestino
            .GroupBy(candidata => candidata.Parada.Codigo, StringComparer.OrdinalIgnoreCase)
            .ToDictionary(grupo => grupo.Key, grupo => grupo.OrderBy(x => x.TiempoAPie).First(), StringComparer.OrdinalIgnoreCase);

        var rondas = new List<Dictionary<string, EstadoBusquedaParadaDbus>>();
        var rondaInicial = new Dictionary<string, EstadoBusquedaParadaDbus>(StringComparer.OrdinalIgnoreCase);

        foreach (var candidata in candidatasOrigen.OrderBy(x => x.TiempoAPie))
        {
            var estado = new EstadoBusquedaParadaDbus
            {
                CodigoParada = candidata.Parada.Codigo,
                Llegada = UtilidadesTransitoDbus.AddSecondsSafe(instanteSalida, candidata.TiempoAPie),
                DistanciaCaminadaTotal = candidata.DistanciaAPie,
                DistanciaPieTransbordos = 0,
                TiempoEsperaInicialSegundos = 0,
                TiempoEsperaTransbordosSegundos = 0,
                TransbordosReales = 0,
                BusesTomados = 0,
                Movimiento = new MovimientoAccesoDbus
                {
                    DistanciaMetros = candidata.DistanciaAPie,
                    DuracionSegundos = candidata.TiempoAPie
                }
            };

            IntentarActualizarEstado(rondaInicial, estado);
        }

        rondas.Add(rondaInicial);
        var marcadas = new HashSet<string>(rondaInicial.Values.Select(estado => estado.CodigoParada), StringComparer.OrdinalIgnoreCase);
        var maximoBuses = ResolverMaximoBuses(parametros);
        var codigosParadaRelevantes = CrearFiltroParadasRelevantes(indice, candidatasDestino, parametros);

        for (var ronda = 1; ronda <= maximoBuses && marcadas.Count > 0; ronda++)
        {
            cancellationToken.ThrowIfCancellationRequested();

            var estadosActuales = new Dictionary<string, EstadoBusquedaParadaDbus>(StringComparer.OrdinalIgnoreCase);
            var estadosPreviosPorParada = IndexarEstadosPorParada(rondas[ronda - 1].Values);
            var patronesMarcados = new HashSet<PatronTransitoDbus>();

            foreach (var codigoParada in marcadas)
            {
                if (!indice.PatronesPorParada.TryGetValue(codigoParada, out var referencias))
                    continue;

                foreach (var referencia in referencias)
                {
                    if (PatronPermitido(referencia.Patron, parametros))
                        patronesMarcados.Add(referencia.Patron);
                }
            }

            var bloqueoEstados = new object();
            Parallel.ForEach(patronesMarcados, new ParallelOptions
            {
                CancellationToken = cancellationToken,
                MaxDegreeOfParallelism = Math.Clamp(Environment.ProcessorCount, 1, 4)
            }, patron =>
            {
                var estadosLocales = new Dictionary<string, EstadoBusquedaParadaDbus>(StringComparer.OrdinalIgnoreCase);

                if (parametros.UsarHorariosReales)
                {
                    var cacheExpediciones = new Dictionary<string, (ExpedicionTransitoDbus? expedicion, DateTime? salida)>(StringComparer.Ordinal);
                    EscanearPatronConHorarios(
                        patron,
                        estadosPreviosPorParada,
                        estadosLocales,
                        instanteSalida.Date,
                        parametros,
                        calendariosActivos,
                        cacheExpediciones,
                        codigosParadaRelevantes,
                        cancellationToken);
                }
                else
                {
                    EscanearPatronAproximado(
                        patron,
                        estadosPreviosPorParada,
                        estadosLocales,
                        parametros,
                        codigosParadaRelevantes,
                        cancellationToken);
                }

                if (estadosLocales.Count == 0)
                    return;

                lock (bloqueoEstados)
                {
                    foreach (var estado in estadosLocales.Values)
                        IntentarActualizarEstado(estadosActuales, estado);
                }
            });

            RelajarTransferencias(indice, estadosActuales, parametros, codigosParadaRelevantes, cancellationToken);
            PodarEstadosPorParada(estadosActuales);
            rondas.Add(estadosActuales);
            marcadas = new HashSet<string>(estadosActuales.Values.Select(estado => estado.CodigoParada), StringComparer.OrdinalIgnoreCase);
        }

        return ConstruirResultados(rondas, origenPorCodigo, destinoPorCodigo, instanteSalida, parametros);
    }

    /// <summary>
    /// Calcula el maximo de buses a considerar.
    /// </summary>
    private static int ResolverMaximoBuses(ParametrosPlanificadorRuta parametros)
        => Math.Max(2, Math.Max(0, parametros.MaxTransbordosPermitidos) + 1 + MaxContinuidadesCircularesExtra);

    /// <summary>
    /// Recorre un patron en modo horario real.
    /// </summary>
    private static void EscanearPatronConHorarios(
        PatronTransitoDbus patron,
        IReadOnlyDictionary<string, List<EstadoBusquedaParadaDbus>> estadosPrevios,
        IDictionary<string, EstadoBusquedaParadaDbus> estadosActuales,
        DateTime fechaBase,
        ParametrosPlanificadorRuta parametros,
        IReadOnlySet<string>? calendariosActivos,
        IDictionary<string, (ExpedicionTransitoDbus? expedicion, DateTime? salida)> cacheExpediciones,
        IReadOnlySet<string>? codigosParadaRelevantes,
        CancellationToken cancellationToken)
    {
        if (calendariosActivos is null || calendariosActivos.Count == 0)
            return;

        for (var indiceSubida = 0; indiceSubida < patron.CodigosParada.Count - 1; indiceSubida++)
        {
            cancellationToken.ThrowIfCancellationRequested();

            var codigoParada = patron.CodigosParada[indiceSubida];
            foreach (var estadoPrevio in ObtenerEstadosEnParada(estadosPrevios, codigoParada))
            {
                var expedicion = EncontrarPrimeraExpedicionValida(
                    patron,
                    indiceSubida,
                    estadoPrevio,
                    fechaBase,
                    parametros,
                    calendariosActivos,
                    cacheExpediciones,
                    out var esTransbordoReal,
                    out var salida);

                if (expedicion is null || salida is null)
                    continue;

                var transbordosReales = estadoPrevio.TransbordosReales + (esTransbordoReal ? 1 : 0);
                if (!TransbordosEnRangoParcial(transbordosReales, parametros))
                    continue;

                var esperaSegundos = Math.Max(0, (salida.Value - estadoPrevio.Llegada).TotalSeconds);
                var esperaInicial = estadoPrevio.TiempoEsperaInicialSegundos + (!esTransbordoReal ? esperaSegundos : 0);
                var esperaTransbordos = estadoPrevio.TiempoEsperaTransbordosSegundos + (esTransbordoReal ? esperaSegundos : 0);

                for (var indiceBajada = indiceSubida + 1; indiceBajada < patron.CodigosParada.Count; indiceBajada++)
                {
                    if (!ParadaEsRelevante(patron.CodigosParada[indiceBajada], codigosParadaRelevantes))
                        continue;

                    var llegada = ObtenerFechaDesdeSegundos(fechaBase, expedicion.LlegadasSegundos[indiceBajada]);
                    var nuevoEstado = new EstadoBusquedaParadaDbus
                    {
                        CodigoParada = patron.CodigosParada[indiceBajada],
                        Llegada = llegada,
                        DistanciaCaminadaTotal = estadoPrevio.DistanciaCaminadaTotal,
                        DistanciaPieTransbordos = estadoPrevio.DistanciaPieTransbordos,
                        TiempoEsperaInicialSegundos = esperaInicial,
                        TiempoEsperaTransbordosSegundos = esperaTransbordos,
                        TransbordosReales = transbordosReales,
                        BusesTomados = estadoPrevio.BusesTomados + 1,
                        EstadoAnterior = estadoPrevio,
                        Movimiento = new MovimientoBusDbus
                        {
                            Patron = patron,
                            IndiceSubida = indiceSubida,
                            IndiceBajada = indiceBajada,
                            Salida = salida.Value,
                            Llegada = llegada
                        }
                    };

                    IntentarActualizarEstado(estadosActuales, nuevoEstado);
                }
            }
        }
    }

    /// <summary>
    /// Recorre un patron en modo aproximado sin horarios.
    /// </summary>
    private static void EscanearPatronAproximado(
        PatronTransitoDbus patron,
        IReadOnlyDictionary<string, List<EstadoBusquedaParadaDbus>> estadosPrevios,
        IDictionary<string, EstadoBusquedaParadaDbus> estadosActuales,
        ParametrosPlanificadorRuta parametros,
        IReadOnlySet<string>? codigosParadaRelevantes,
        CancellationToken cancellationToken)
    {
        for (var indiceSubida = 0; indiceSubida < patron.CodigosParada.Count - 1; indiceSubida++)
        {
            cancellationToken.ThrowIfCancellationRequested();

            var codigoParada = patron.CodigosParada[indiceSubida];
            foreach (var estadoPrevio in ObtenerEstadosEnParada(estadosPrevios, codigoParada))
            {
                if (!PuedeTomarPatronDesdeEstado(estadoPrevio, patron, out var esTransbordoReal))
                    continue;

                var salida = esTransbordoReal
                    ? estadoPrevio.Llegada.AddMinutes(parametros.MinutosPorTransbordo)
                    : estadoPrevio.Llegada;
                var transbordosReales = estadoPrevio.TransbordosReales + (esTransbordoReal ? 1 : 0);
                if (!TransbordosEnRangoParcial(transbordosReales, parametros))
                    continue;

                for (var indiceBajada = indiceSubida + 1; indiceBajada < patron.CodigosParada.Count; indiceBajada++)
                {
                    if (!ParadaEsRelevante(patron.CodigosParada[indiceBajada], codigosParadaRelevantes))
                        continue;

                    var distanciaBus = patron.DistanciaAcumuladaPorParadaMetros[indiceBajada] - patron.DistanciaAcumuladaPorParadaMetros[indiceSubida];
                    var duracionBus = UtilidadesTransitoDbus.EstimarDuracionBusSegundos(distanciaBus, parametros) +
                                      ((indiceBajada - indiceSubida) * Math.Max(0, parametros.SegundosPorParadaBus));
                    var llegada = UtilidadesTransitoDbus.AddSecondsSafe(salida, duracionBus);
                    var nuevoEstado = new EstadoBusquedaParadaDbus
                    {
                        CodigoParada = patron.CodigosParada[indiceBajada],
                        Llegada = llegada,
                        DistanciaCaminadaTotal = estadoPrevio.DistanciaCaminadaTotal,
                        DistanciaPieTransbordos = estadoPrevio.DistanciaPieTransbordos,
                        TiempoEsperaInicialSegundos = estadoPrevio.TiempoEsperaInicialSegundos,
                        TiempoEsperaTransbordosSegundos = estadoPrevio.TiempoEsperaTransbordosSegundos + (esTransbordoReal ? parametros.MinutosPorTransbordo * 60.0 : 0),
                        TransbordosReales = transbordosReales,
                        BusesTomados = estadoPrevio.BusesTomados + 1,
                        EstadoAnterior = estadoPrevio,
                        Movimiento = new MovimientoBusDbus
                        {
                            Patron = patron,
                            IndiceSubida = indiceSubida,
                            IndiceBajada = indiceBajada,
                            Salida = salida,
                            Llegada = llegada
                        }
                    };

                    IntentarActualizarEstado(estadosActuales, nuevoEstado);
                }
            }
        }
    }

    /// <summary>
    /// Propaga transferencias peatonales dentro de la misma ronda.
    /// </summary>
    private static void RelajarTransferencias(
        IndiceTransitoDbus indice,
        IDictionary<string, EstadoBusquedaParadaDbus> estadosActuales,
        ParametrosPlanificadorRuta parametros,
        IReadOnlySet<string>? codigosParadaRelevantes,
        CancellationToken cancellationToken)
    {
        var cola = new Queue<EstadoBusquedaParadaDbus>(estadosActuales.Values);
        while (cola.Count > 0)
        {
            cancellationToken.ThrowIfCancellationRequested();

            var estado = cola.Dequeue();
            if (!indice.TransferenciasPorParada.TryGetValue(estado.CodigoParada, out var transferencias))
                continue;

            foreach (var transferencia in transferencias)
            {
                if (!ParadaEsRelevante(transferencia.CodigoParadaDestino, codigosParadaRelevantes))
                    continue;

                var duracion = UtilidadesTransitoDbus.EstimarDuracionPieSegundos(transferencia.DistanciaMetros, parametros);
                var nuevoEstado = new EstadoBusquedaParadaDbus
                {
                    CodigoParada = transferencia.CodigoParadaDestino,
                    Llegada = UtilidadesTransitoDbus.AddSecondsSafe(estado.Llegada, duracion),
                    DistanciaCaminadaTotal = estado.DistanciaCaminadaTotal + transferencia.DistanciaMetros,
                    DistanciaPieTransbordos = estado.DistanciaPieTransbordos + transferencia.DistanciaMetros,
                    TiempoEsperaInicialSegundos = estado.TiempoEsperaInicialSegundos,
                    TiempoEsperaTransbordosSegundos = estado.TiempoEsperaTransbordosSegundos,
                    TransbordosReales = estado.TransbordosReales,
                    BusesTomados = estado.BusesTomados,
                    EstadoAnterior = estado,
                    Movimiento = new MovimientoPieTransbordoDbus
                    {
                        DistanciaMetros = transferencia.DistanciaMetros,
                        DuracionSegundos = duracion
                    }
                };

                if (IntentarActualizarEstado(estadosActuales, nuevoEstado))
                    cola.Enqueue(nuevoEstado);
            }
        }
    }

    /// <summary>
    /// Encuentra la primera expedicion utilizable desde una parada del patron.
    /// </summary>
    private static ExpedicionTransitoDbus? EncontrarPrimeraExpedicionValida(
        PatronTransitoDbus patron,
        int indiceSubida,
        EstadoBusquedaParadaDbus estadoPrevio,
        DateTime fechaBase,
        ParametrosPlanificadorRuta parametros,
        IReadOnlySet<string> calendariosActivos,
        IDictionary<string, (ExpedicionTransitoDbus? expedicion, DateTime? salida)> cacheExpediciones,
        out bool esTransbordoReal,
        out DateTime? salida)
    {
        salida = null;
        esTransbordoReal = false;

        if (!PuedeTomarPatronDesdeEstado(estadoPrevio, patron, out esTransbordoReal))
            return null;

        var margenTransbordo = esTransbordoReal
            ? Math.Clamp(parametros.MargenTransbordoSegundos, 0, 1800)
            : 0;
        var primeraSalidaAceptable = estadoPrevio.Llegada.AddSeconds(margenTransbordo);
        var claveCache = CrearClaveExpedicion(patron, indiceSubida, fechaBase, primeraSalidaAceptable);

        if (!cacheExpediciones.TryGetValue(claveCache, out var cacheado))
        {
            cacheado = EncontrarPrimeraExpedicionDesde(
                patron,
                indiceSubida,
                fechaBase,
                calendariosActivos,
                primeraSalidaAceptable);
            cacheExpediciones[claveCache] = cacheado;
        }

        if (cacheado.expedicion is null || cacheado.salida is null)
            return null;

        var esperaCacheadaSegundos = Math.Max(0, (cacheado.salida.Value - estadoPrevio.Llegada).TotalSeconds);
        if (!EsEsperaValida(estadoPrevio, esperaCacheadaSegundos, esTransbordoReal, parametros))
            return null;

        salida = cacheado.salida;
        return cacheado.expedicion;
    }

    private static (ExpedicionTransitoDbus? expedicion, DateTime? salida) EncontrarPrimeraExpedicionDesde(
        PatronTransitoDbus patron,
        int indiceSubida,
        DateTime fechaBase,
        IReadOnlySet<string> calendariosActivos,
        DateTime primeraSalidaAceptable)
    {
        var expediciones = patron.Expediciones;
        if (expediciones.Count == 0)
            return (null, null);

        var segundosObjetivo = (int)Math.Ceiling((primeraSalidaAceptable - fechaBase.Date).TotalSeconds);
        var indiceInicial = BuscarPrimerIndiceSalidaDesde(expediciones, indiceSubida, segundosObjetivo);
        if (indiceInicial < 0)
            return (null, null);

        for (var indiceExpedicion = indiceInicial; indiceExpedicion < expediciones.Count; indiceExpedicion++)
        {
            var expedicion = expediciones[indiceExpedicion];
            if (!calendariosActivos.Contains(expedicion.IdCalendario))
                continue;

            var salidaExpedicion = ObtenerFechaDesdeSegundos(fechaBase, expedicion.SalidasSegundos[indiceSubida]);
            if (salidaExpedicion < primeraSalidaAceptable)
                continue;

            return (expedicion, salidaExpedicion);
        }

        return (null, null);
    }

    private static int BuscarPrimerIndiceSalidaDesde(
        IReadOnlyList<ExpedicionTransitoDbus> expediciones,
        int indiceSubida,
        int segundosObjetivo)
    {
        var izquierda = 0;
        var derecha = expediciones.Count - 1;
        var encontrado = -1;

        while (izquierda <= derecha)
        {
            var medio = izquierda + ((derecha - izquierda) / 2);
            var salida = expediciones[medio].SalidasSegundos[indiceSubida];
            if (salida >= segundosObjetivo)
            {
                encontrado = medio;
                derecha = medio - 1;
            }
            else
            {
                izquierda = medio + 1;
            }
        }

        return encontrado;
    }

    private static string CrearClaveExpedicion(
        PatronTransitoDbus patron,
        int indiceSubida,
        DateTime fechaBase,
        DateTime primeraSalidaAceptable)
    {
        var ticksSegundo = primeraSalidaAceptable.Ticks - primeraSalidaAceptable.Ticks % TimeSpan.TicksPerSecond;
        return $"{patron.Id}|{indiceSubida}|{fechaBase:yyyyMMdd}|{ticksSegundo}";
    }

    /// <summary>
    /// Comprueba si una espera respeta las restricciones configuradas.
    /// </summary>
    private static bool EsEsperaValida(
        EstadoBusquedaParadaDbus estadoPrevio,
        double esperaSegundos,
        bool esTransbordoReal,
        ParametrosPlanificadorRuta parametros)
    {
        if (estadoPrevio.BusesTomados == 0 &&
            parametros.MaxEsperaInicialMinutos > 0 &&
            esperaSegundos > parametros.MaxEsperaInicialMinutos * 60.0)
        {
            return false;
        }

        if (esTransbordoReal &&
            parametros.MaxEsperaTransbordoMinutos > 0 &&
            esperaSegundos > parametros.MaxEsperaTransbordoMinutos * 60.0)
        {
            return false;
        }

        var esperaTotal = estadoPrevio.TiempoEsperaInicialSegundos + estadoPrevio.TiempoEsperaTransbordosSegundos + esperaSegundos;
        if (parametros.MaxEsperaTotalMinutos > 0 &&
            esperaTotal > parametros.MaxEsperaTotalMinutos * 60.0)
        {
            return false;
        }

        return true;
    }

    private static bool TransbordosEnRangoParcial(int transbordosReales, ParametrosPlanificadorRuta parametros)
    {
        if (!parametros.ActivarRangoTransbordos)
            return true;

        var maximo = Math.Max(parametros.MinTransbordosPermitidos, parametros.MaxTransbordosPermitidos);
        return transbordosReales <= maximo;
    }

    private static HashSet<string>? CrearFiltroParadasRelevantes(
        IndiceTransitoDbus indice,
        IReadOnlyList<ParadaCandidata> candidatasDestino,
        ParametrosPlanificadorRuta parametros)
    {
        if (!parametros.ActivarRangoTransbordos)
            return null;

        var maximoTransbordos = Math.Max(parametros.MinTransbordosPermitidos, parametros.MaxTransbordosPermitidos);
        if (maximoTransbordos > 1)
            return null;

        var relevantes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        var subidasHaciaDestino = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

        foreach (var destino in candidatasDestino)
        {
            relevantes.Add(destino.Parada.Codigo);
            if (!indice.PatronesPorParada.TryGetValue(destino.Parada.Codigo, out var referenciasDestino))
                continue;

            foreach (var referencia in referenciasDestino)
            {
                var patron = referencia.Patron;
                if (!PatronPermitido(patron, parametros))
                    continue;

                var indiceDestino = referencia.IndiceParada;
                if (indiceDestino <= 0 || indiceDestino >= patron.CodigosParada.Count)
                    continue;

                for (var indiceSubida = 0; indiceSubida < indiceDestino; indiceSubida++)
                    subidasHaciaDestino.Add(patron.CodigosParada[indiceSubida]);
            }
        }

        foreach (var codigoSubida in subidasHaciaDestino)
            relevantes.Add(codigoSubida);

        foreach (var transferenciaOrigen in indice.TransferenciasPorParada)
        {
            if (transferenciaOrigen.Value.Any(transferencia =>
                    subidasHaciaDestino.Contains(transferencia.CodigoParadaDestino)))
            {
                relevantes.Add(transferenciaOrigen.Key);
            }
        }

        foreach (var referenciaPorParada in indice.PatronesPorParada)
        {
            if (EsPivoteCircular(referenciaPorParada.Value))
                relevantes.Add(referenciaPorParada.Key);
        }

        return relevantes;
    }

    private static bool ParadaEsRelevante(string codigoParada, IReadOnlySet<string>? codigosParadaRelevantes)
        => codigosParadaRelevantes is null || codigosParadaRelevantes.Contains(codigoParada);

    private static bool EsPivoteCircular(IReadOnlyList<ReferenciaPatronParadaDbus> referencias)
    {
        for (var i = 0; i < referencias.Count; i++)
        {
            var izquierda = referencias[i].Patron;
            for (var j = i + 1; j < referencias.Count; j++)
            {
                if (EsContinuidadCircular(izquierda, referencias[j].Patron))
                    return true;
            }
        }

        return false;
    }

    private static bool EsContinuidadCircular(PatronTransitoDbus primero, PatronTransitoDbus segundo)
    {
        return string.Equals(primero.CodigoLinea, segundo.CodigoLinea, StringComparison.OrdinalIgnoreCase) &&
               primero.CodigoItinerario == segundo.CodigoItinerario &&
               !string.Equals(
                   UtilidadesTransitoDbus.NormalizarSentidoCodigo(primero.Sentido),
                   UtilidadesTransitoDbus.NormalizarSentidoCodigo(segundo.Sentido),
                   StringComparison.OrdinalIgnoreCase);
    }

    private static bool EsTransbordoRealDesdeEstado(EstadoBusquedaParadaDbus estadoPrevio, PatronTransitoDbus patron)
    {
        if (estadoPrevio.BusesTomados == 0)
            return false;

        if (HayTransbordoPeatonalDesdeUltimoBus(estadoPrevio))
            return true;

        var movimientoBusAnterior = BuscarMovimientoBusAnterior(estadoPrevio);
        return movimientoBusAnterior is null ||
               UtilidadesTransitoDbus.EsTransbordoReal(movimientoBusAnterior.Patron.Variante, patron.Variante);
    }

    private static bool PuedeTomarPatronDesdeEstado(
        EstadoBusquedaParadaDbus estadoPrevio,
        PatronTransitoDbus patron,
        out bool esTransbordoReal)
    {
        esTransbordoReal = EsTransbordoRealDesdeEstado(estadoPrevio, patron);
        if (estadoPrevio.BusesTomados == 0 || esTransbordoReal)
            return true;

        return ContarContinuidadesNoRealesConsecutivas(estadoPrevio) < MaxContinuidadesCircularesExtra;
    }

    private static bool HayTransbordoPeatonalDesdeUltimoBus(EstadoBusquedaParadaDbus estado)
    {
        var cursor = estado;
        while (cursor is not null)
        {
            if (cursor.Movimiento is MovimientoBusDbus)
                return false;

            if (cursor.Movimiento is MovimientoPieTransbordoDbus)
                return true;

            cursor = cursor.EstadoAnterior;
        }

        return false;
    }

    private static int ContarContinuidadesNoRealesConsecutivas(EstadoBusquedaParadaDbus estado)
    {
        var continuidades = 0;
        MovimientoBusDbus? busPosterior = null;
        var cursor = estado;

        while (cursor is not null)
        {
            if (cursor.Movimiento is MovimientoPieTransbordoDbus)
                return continuidades;

            if (cursor.Movimiento is MovimientoBusDbus bus)
            {
                if (busPosterior is not null)
                {
                    if (UtilidadesTransitoDbus.EsTransbordoReal(bus.Patron.Variante, busPosterior.Patron.Variante))
                        return continuidades;

                    continuidades++;
                }

                busPosterior = bus;
            }

            cursor = cursor.EstadoAnterior;
        }

        return continuidades;
    }

    private static MovimientoBusDbus? BuscarMovimientoBusAnterior(EstadoBusquedaParadaDbus? estado)
    {
        var cursor = estado;
        while (cursor is not null)
        {
            if (cursor.Movimiento is MovimientoBusDbus bus)
                return bus;

            cursor = cursor.EstadoAnterior;
        }

        return null;
    }

    /// <summary>
    /// Convierte rondas exactas en rutas candidatas deduplicadas.
    /// </summary>
    private static IReadOnlyList<ResultadoBusquedaRutaBusDbus> ConstruirResultados(
        IReadOnlyList<Dictionary<string, EstadoBusquedaParadaDbus>> rondas,
        IReadOnlyDictionary<string, ParadaCandidata> origenPorCodigo,
        IReadOnlyDictionary<string, ParadaCandidata> destinoPorCodigo,
        DateTime instanteSalida,
        ParametrosPlanificadorRuta parametros)
    {
        var mejores = new Dictionary<string, ResultadoBusquedaRutaBusDbus>(StringComparer.OrdinalIgnoreCase);

        foreach (var estadosRonda in rondas.Skip(1))
        {
            var estadosPorParada = IndexarEstadosPorParada(estadosRonda.Values);
            foreach (var destino in destinoPorCodigo.Values)
            {
                foreach (var estadoDestino in ObtenerEstadosEnParada(estadosPorParada, destino.Parada.Codigo))
                {
                    var tramosBus = NormalizarTramosBus(ConstruirTramosBus(estadoDestino));
                    if (tramosBus.Count == 0)
                        continue;

                    if (UtilidadesTransitoDbus.TieneContinuidadesNoRealesConsecutivasExcesivas(
                            tramosBus,
                            MaxContinuidadesCircularesExtra))
                    {
                        continue;
                    }

                    var numeroTransbordos = UtilidadesTransitoDbus.ContarTransbordosReales(tramosBus);
                    if (parametros.ActivarRangoTransbordos)
                    {
                        var minimo = Math.Min(parametros.MinTransbordosPermitidos, parametros.MaxTransbordosPermitidos);
                        var maximo = Math.Max(parametros.MinTransbordosPermitidos, parametros.MaxTransbordosPermitidos);
                        if (numeroTransbordos < minimo || numeroTransbordos > maximo)
                            continue;
                    }

                    var primeraParada = tramosBus[0].CodigoParadaInicio;
                    if (!origenPorCodigo.TryGetValue(primeraParada, out var origen))
                        continue;

                    var distanciaTotalBus = CalcularDistanciaBusEstado(estadoDestino);
                    var ruta = new RutaBusAlternativa
                    {
                        Tramos = tramosBus,
                        NumeroTransbordos = numeroTransbordos,
                        CodigosParadasTransbordo = ConstruirCodigosTransbordo(tramosBus),
                        DistanciaTotalBus = distanciaTotalBus
                    };

                    var duracionEstimada = Math.Max(0, (estadoDestino.Llegada - instanteSalida).TotalSeconds) + destino.TiempoAPie;
                    var distanciasPieTransbordos = ObtenerDistanciasPieTransbordos(estadoDestino);
                    var distanciasPiePorTramo = ConstruirDistanciasPiePorTramo(
                        origen.DistanciaAPie,
                        distanciasPieTransbordos,
                        destino.DistanciaAPie);
                    var puntuacionCoste = UtilidadesTransitoDbus.CalcularCosteConPenalizacionesRankingSegundos(
                        duracionEstimada,
                        parametros,
                        numeroTransbordos,
                        distanciasPiePorTramo.ToArray());

                    var resultado = new ResultadoBusquedaRutaBusDbus
                    {
                        ParadaOrigen = origen.Parada,
                        ParadaDestino = destino.Parada,
                        DistanciaPieInicio = origen.DistanciaAPie,
                        TiempoPieInicio = origen.TiempoAPie,
                        DistanciaPieFin = destino.DistanciaAPie,
                        TiempoPieFin = destino.TiempoAPie,
                        DistanciaPieTransbordos = estadoDestino.DistanciaPieTransbordos,
                        DistanciasPieTransbordosPorTramo = distanciasPieTransbordos,
                        TiempoEsperaInicialSegundos = estadoDestino.TiempoEsperaInicialSegundos,
                        TiempoEsperaTransbordosSegundos = estadoDestino.TiempoEsperaTransbordosSegundos,
                        DuracionEstimadaSegundos = duracionEstimada,
                        PuntuacionCoste = puntuacionCoste,
                        RutaBus = ruta
                    };

                    var firma = UtilidadesTransitoDbus.FirmaRutaBus(ruta);
                    if (!mejores.TryGetValue(firma, out var actual) ||
                        ResultadoEsMejor(resultado, actual))
                    {
                        mejores[firma] = resultado;
                    }
                }
            }
        }

        return SeleccionarPoolBusqueda(
            mejores.Values,
            Math.Max(12, Math.Max(1, parametros.MaxAlternativasBusPorPar) * 8),
            parametros);
    }

    /// <summary>
    /// Conserva candidatas buenas por coste y un cupo por menor caminata.
    /// </summary>
    private static List<ResultadoBusquedaRutaBusDbus> SeleccionarPoolBusqueda(
        IEnumerable<ResultadoBusquedaRutaBusDbus> candidatas,
        int maxPorCoste,
        ParametrosPlanificadorRuta parametros)
    {
        var lista = candidatas.ToList();
        if (lista.Count == 0)
            return new();

        var seleccionadas = new List<ResultadoBusquedaRutaBusDbus>();
        var firmas = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        var limiteCoste = Math.Max(1, maxPorCoste);
        var limiteAndar = Math.Max(8, limiteCoste / 2);
        var limiteEquilibrado = Math.Max(8, limiteCoste / 2);

        Agregar(lista
            .OrderBy(x => x.PuntuacionCoste)
            .ThenBy(x => x.DuracionEstimadaSegundos)
            .ThenBy(x => x.RutaBus.NumeroTransbordos)
            .ThenBy(x => x.DistanciaPieTotal)
            .Take(limiteCoste));

        Agregar(lista
            .OrderBy(x => x.DistanciaPieTotal)
            .ThenBy(x => x.DuracionEstimadaSegundos)
            .ThenBy(x => x.PuntuacionCoste)
            .ThenBy(x => x.RutaBus.NumeroTransbordos)
            .Take(limiteAndar));

        Agregar(lista
            .OrderBy(x => CalcularScoreBusquedaEquilibrado(x, parametros))
            .ThenBy(x => x.DuracionEstimadaSegundos)
            .ThenBy(x => x.RutaBus.NumeroTransbordos)
            .ThenBy(x => x.PuntuacionCoste)
            .Take(limiteEquilibrado));

        return seleccionadas
            .OrderBy(x => x.PuntuacionCoste)
            .ThenBy(x => x.DuracionEstimadaSegundos)
            .ThenBy(x => x.RutaBus.NumeroTransbordos)
            .ThenBy(x => x.DistanciaPieTotal)
            .ToList();

        void Agregar(IEnumerable<ResultadoBusquedaRutaBusDbus> origen)
        {
            foreach (var candidata in origen)
            {
                var firma = UtilidadesTransitoDbus.FirmaRutaBus(candidata.RutaBus);
                if (firmas.Add(firma))
                    seleccionadas.Add(candidata);
            }
        }
    }

    private static double CalcularScoreBusquedaEquilibrado(
        ResultadoBusquedaRutaBusDbus resultado,
        ParametrosPlanificadorRuta parametros)
    {
        var distanciaBus = Math.Max(0, resultado.RutaBus.DistanciaTotalBus);
        var distanciaAndando = UtilidadesTransitoDbus.CalcularDistanciaPieCorregidaTotalMetros(
            ConstruirDistanciasPiePorTramo(
                resultado.DistanciaPieInicio,
                resultado.DistanciasPieTransbordosPorTramo,
                resultado.DistanciaPieFin),
            parametros);
        var factor = double.IsFinite(parametros.FactorEquilibradoAndarBus)
            ? Math.Max(0, parametros.FactorEquilibradoAndarBus)
            : 6.0;
        var penalizacionTransbordos = UtilidadesTransitoDbus.CalcularPenalizacionTransbordosRankingMetros(
            UtilidadesTransitoDbus.ContarTransbordosReales(resultado.RutaBus.Tramos),
            parametros);

        var score = distanciaBus + (distanciaAndando * factor) + penalizacionTransbordos;
        return double.IsFinite(score) && score >= 0
            ? score
            : double.PositiveInfinity;
    }

    private static List<double> ObtenerDistanciasPieTransbordos(EstadoBusquedaParadaDbus estadoDestino)
    {
        var distancias = new List<double>();
        var cursor = estadoDestino;
        while (cursor is not null)
        {
            if (cursor.Movimiento is MovimientoPieTransbordoDbus movimiento &&
                double.IsFinite(movimiento.DistanciaMetros) &&
                movimiento.DistanciaMetros > 0)
            {
                distancias.Add(movimiento.DistanciaMetros);
            }

            cursor = cursor.EstadoAnterior;
        }

        distancias.Reverse();
        return distancias;
    }

    private static List<double> ConstruirDistanciasPiePorTramo(
        double distanciaInicio,
        IEnumerable<double> distanciasTransbordos,
        double distanciaFin)
    {
        var distancias = new List<double>();
        AgregarDistancia(distanciaInicio);
        foreach (var distancia in distanciasTransbordos)
            AgregarDistancia(distancia);
        AgregarDistancia(distanciaFin);
        return distancias;

        void AgregarDistancia(double distancia)
        {
            if (double.IsFinite(distancia) && distancia > 0)
                distancias.Add(distancia);
        }
    }

    private static double CalcularDistanciaBusEstado(EstadoBusquedaParadaDbus estadoDestino)
    {
        var distancia = 0.0;
        var cursor = estadoDestino;
        while (cursor is not null)
        {
            if (cursor.Movimiento is MovimientoBusDbus movimiento)
                distancia += CalcularDistanciaBusPatron(movimiento.Patron, movimiento.IndiceSubida, movimiento.IndiceBajada);

            cursor = cursor.EstadoAnterior;
        }

        return double.IsFinite(distancia) && distancia > 0
            ? distancia
            : 0;
    }

    private static double CalcularDistanciaBusPatron(PatronTransitoDbus patron, int indiceInicio, int indiceFin)
    {
        if (indiceInicio < 0 ||
            indiceFin <= indiceInicio ||
            indiceFin >= patron.DistanciaAcumuladaPorParadaMetros.Count)
        {
            return 0;
        }

        var distancia = patron.DistanciaAcumuladaPorParadaMetros[indiceFin] -
                        patron.DistanciaAcumuladaPorParadaMetros[indiceInicio];
        return double.IsFinite(distancia) && distancia > 0
            ? distancia
            : 0;
    }

    /// <summary>
    /// Reconstruye los tramos de bus de una ruta.
    /// </summary>
    private static List<TramoBus> ConstruirTramosBus(EstadoBusquedaParadaDbus estadoDestino)
    {
        var movimientos = new Stack<MovimientoBusDbus>();
        var cursor = estadoDestino;

        while (cursor is not null)
        {
            if (cursor.Movimiento is MovimientoBusDbus movimientoBus)
                movimientos.Push(movimientoBus);

            cursor = cursor.EstadoAnterior;
        }

        var tramos = new List<TramoBus>(movimientos.Count);
        while (movimientos.Count > 0)
        {
            var movimiento = movimientos.Pop();
            tramos.Add(new TramoBus
            {
                Variante = movimiento.Patron.Variante,
                IndiceInicio = movimiento.IndiceSubida,
                IndiceFin = movimiento.IndiceBajada
            });
        }

        return tramos;
    }

    /// <summary>
    /// Fusiona tramos consecutivos que realmente pertenecen a la misma variante.
    /// </summary>
    private static List<TramoBus> NormalizarTramosBus(List<TramoBus> tramosOriginales)
    {
        if (tramosOriginales.Count <= 1)
            return tramosOriginales;

        var resultado = new List<TramoBus>(tramosOriginales.Count);
        foreach (var tramo in tramosOriginales)
        {
            if (!tramo.EsValido)
                continue;

            if (resultado.Count == 0)
            {
                resultado.Add(tramo);
                continue;
            }

            var ultimo = resultado[^1];
            if (SonMismaVariante(ultimo, tramo) && tramo.IndiceInicio <= ultimo.IndiceFin)
            {
                ultimo.IndiceFin = Math.Max(ultimo.IndiceFin, tramo.IndiceFin);
                continue;
            }

            if (SonMismaVariante(ultimo, tramo) && tramo.IndiceInicio >= ultimo.IndiceFin)
            {
                ultimo.IndiceFin = Math.Max(ultimo.IndiceFin, tramo.IndiceFin);
                continue;
            }

            resultado.Add(tramo);
        }

        return resultado;
    }

    /// <summary>
    /// Indica si dos tramos recorren la misma variante operativa.
    /// </summary>
    private static bool SonMismaVariante(TramoBus izquierda, TramoBus derecha)
    {
        return string.Equals(
                   UtilidadesTransitoDbus.LineaCodigoStr(izquierda.Variante),
                   UtilidadesTransitoDbus.LineaCodigoStr(derecha.Variante),
                   StringComparison.OrdinalIgnoreCase) &&
               izquierda.Variante.CodigoItinerario == derecha.Variante.CodigoItinerario &&
               string.Equals(
                   UtilidadesTransitoDbus.NormalizarSentidoCodigo(izquierda.Variante.Sentido?.Codigo),
                   UtilidadesTransitoDbus.NormalizarSentidoCodigo(derecha.Variante.Sentido?.Codigo),
                   StringComparison.OrdinalIgnoreCase) &&
               izquierda.Variante.Variante.Codigo == derecha.Variante.Variante.Codigo;
    }

    /// <summary>
    /// Reconstruye las paradas de transbordo visibles.
    /// </summary>
    private static List<string> ConstruirCodigosTransbordo(IReadOnlyList<TramoBus> tramos)
    {
        var codigos = new List<string>();
        for (var indice = 1; indice < tramos.Count; indice++)
        {
            if (!UtilidadesTransitoDbus.EsTransbordoReal(tramos[indice - 1], tramos[indice]))
                continue;

            var codigo = tramos[indice].CodigoParadaInicio;
            if (!codigos.Contains(codigo, StringComparer.OrdinalIgnoreCase))
                codigos.Add(codigo);
        }

        return codigos;
    }

    /// <summary>
    /// Decide si un patron entra en los filtros del usuario.
    /// </summary>
    private static bool PatronPermitido(PatronTransitoDbus patron, ParametrosPlanificadorRuta parametros)
    {
        if (parametros.ActivarRangoLineas)
        {
            if (patron.NumeroLinea is null)
                return false;

            if (parametros.LineasPermitidas.Count == 0 ||
                !parametros.LineasPermitidas.Contains(patron.NumeroLinea.Value))
            {
                return false;
            }
        }

        if (parametros.ActivarRangoItinerarios)
        {
            var minimoItinerario = Math.Min(parametros.ItinerarioMin, parametros.ItinerarioMax);
            var maximoItinerario = Math.Max(parametros.ItinerarioMin, parametros.ItinerarioMax);
            if (patron.CodigoItinerario < minimoItinerario || patron.CodigoItinerario > maximoItinerario)
                return false;
        }

        return true;
    }

    /// <summary>
    /// Intenta almacenar un estado si mejora al existente.
    /// </summary>
    private static bool IntentarActualizarEstado(
        IDictionary<string, EstadoBusquedaParadaDbus> estados,
        EstadoBusquedaParadaDbus nuevoEstado)
    {
        var clave = CrearClaveEstado(nuevoEstado);
        if (!estados.TryGetValue(clave, out var actual) ||
            EstadoEsMejor(nuevoEstado, actual))
        {
            estados[clave] = nuevoEstado;
            return true;
        }

        return false;
    }

    private static void PodarEstadosPorParada(IDictionary<string, EstadoBusquedaParadaDbus> estados)
    {
        var conservar = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

        foreach (var grupo in estados.GroupBy(par => par.Value.CodigoParada, StringComparer.OrdinalIgnoreCase))
        {
            var seleccionados = new List<KeyValuePair<string, EstadoBusquedaParadaDbus>>(MaxEstadosPorParada);
            var lista = grupo.ToList();

            Agregar(lista
                .OrderBy(par => par.Value.Llegada)
                .ThenBy(par => par.Value.TiempoEsperaTotalSegundos)
                .ThenBy(par => par.Value.DistanciaCaminadaTotal)
                .First());

            Agregar(lista
                .OrderBy(par => par.Value.DistanciaCaminadaTotal)
                .ThenBy(par => par.Value.Llegada)
                .ThenBy(par => par.Value.TiempoEsperaTotalSegundos)
                .First());

            foreach (var representanteContexto in lista
                         .GroupBy(par => CrearContextoBusEstado(par.Value), StringComparer.OrdinalIgnoreCase)
                         .Select(contexto => contexto
                             .OrderBy(par => par.Value.Llegada)
                             .ThenBy(par => par.Value.TiempoEsperaTotalSegundos)
                             .ThenBy(par => par.Value.DistanciaCaminadaTotal)
                             .First())
                         .OrderBy(par => par.Value.Llegada)
                         .ThenBy(par => par.Value.DistanciaCaminadaTotal))
            {
                Agregar(representanteContexto);
                if (seleccionados.Count >= MaxEstadosPorParada)
                    break;
            }

            foreach (var candidato in lista
                         .OrderBy(par => par.Value.Llegada)
                         .ThenBy(par => par.Value.TiempoEsperaTotalSegundos)
                         .ThenBy(par => par.Value.DistanciaCaminadaTotal))
            {
                Agregar(candidato);
                if (seleccionados.Count >= MaxEstadosPorParada)
                    break;
            }

            foreach (var seleccionado in seleccionados)
                conservar.Add(seleccionado.Key);

            void Agregar(KeyValuePair<string, EstadoBusquedaParadaDbus> candidato)
            {
                if (seleccionados.Count >= MaxEstadosPorParada)
                    return;

                if (seleccionados.Any(par => string.Equals(par.Key, candidato.Key, StringComparison.OrdinalIgnoreCase)))
                    return;

                seleccionados.Add(candidato);
            }
        }

        var eliminar = estados.Keys
            .Where(clave => !conservar.Contains(clave))
            .ToList();

        foreach (var clave in eliminar)
            estados.Remove(clave);
    }

    private static IEnumerable<EstadoBusquedaParadaDbus> ObtenerEstadosEnParada(
        IReadOnlyDictionary<string, List<EstadoBusquedaParadaDbus>> estados,
        string codigoParada)
    {
        return estados.TryGetValue(codigoParada, out var lista)
            ? lista
            : Enumerable.Empty<EstadoBusquedaParadaDbus>();
    }

    private static Dictionary<string, List<EstadoBusquedaParadaDbus>> IndexarEstadosPorParada(
        IEnumerable<EstadoBusquedaParadaDbus> estados)
    {
        var indice = new Dictionary<string, List<EstadoBusquedaParadaDbus>>(StringComparer.OrdinalIgnoreCase);
        foreach (var estado in estados)
        {
            if (!indice.TryGetValue(estado.CodigoParada, out var lista))
            {
                lista = new List<EstadoBusquedaParadaDbus>();
                indice[estado.CodigoParada] = lista;
            }

            lista.Add(estado);
        }

        return indice;
    }

    private static string CrearClaveEstado(EstadoBusquedaParadaDbus estado)
    {
        return $"{estado.CodigoParada}|b:{estado.BusesTomados}|ctx:{CrearContextoBusEstado(estado)}";
    }

    private static string CrearContextoBusEstado(EstadoBusquedaParadaDbus estado)
    {
        var movimientoBus = BuscarMovimientoBusAnterior(estado);
        if (movimientoBus is null)
            return "sin-bus";

        var variante = movimientoBus.Patron.Variante;
        var sentido = UtilidadesTransitoDbus.NormalizarSentidoCodigo(variante.Sentido?.Codigo);
        return $"{UtilidadesTransitoDbus.LineaCodigoStr(variante)}.{variante.CodigoItinerario}-{sentido}|v:{variante.Variante.Codigo}";
    }

    /// <summary>
    /// Compara dos estados de parada.
    /// </summary>
    private static bool EstadoEsMejor(EstadoBusquedaParadaDbus candidato, EstadoBusquedaParadaDbus actual)
    {
        var comparacionLlegada = candidato.Llegada.CompareTo(actual.Llegada);
        if (comparacionLlegada != 0)
            return comparacionLlegada < 0;

        var comparacionEspera = candidato.TiempoEsperaTotalSegundos.CompareTo(actual.TiempoEsperaTotalSegundos);
        if (comparacionEspera != 0)
            return comparacionEspera < 0;

        var comparacionTransbordos = candidato.TransbordosReales.CompareTo(actual.TransbordosReales);
        if (comparacionTransbordos != 0)
            return comparacionTransbordos < 0;

        return candidato.DistanciaCaminadaTotal < actual.DistanciaCaminadaTotal;
    }

    /// <summary>
    /// Compara dos resultados ya construidos.
    /// </summary>
    private static bool ResultadoEsMejor(ResultadoBusquedaRutaBusDbus candidato, ResultadoBusquedaRutaBusDbus actual)
    {
        var comparacionCoste = candidato.PuntuacionCoste.CompareTo(actual.PuntuacionCoste);
        if (comparacionCoste != 0)
            return comparacionCoste < 0;

        var comparacionDuracion = candidato.DuracionEstimadaSegundos.CompareTo(actual.DuracionEstimadaSegundos);
        if (comparacionDuracion != 0)
            return comparacionDuracion < 0;

        var comparacionTrans = candidato.RutaBus.NumeroTransbordos.CompareTo(actual.RutaBus.NumeroTransbordos);
        if (comparacionTrans != 0)
            return comparacionTrans < 0;

        return candidato.DistanciaPieTotal < actual.DistanciaPieTotal;
    }

    /// <summary>
    /// Obtiene una fecha a partir de segundos desde el inicio del dia.
    /// </summary>
    private static DateTime ObtenerFechaDesdeSegundos(DateTime fechaBase, int segundos)
        => fechaBase.Date.AddSeconds(segundos);
}

/// <summary>
/// Estado de una parada en la busqueda por rondas.
/// </summary>
public sealed class EstadoBusquedaParadaDbus
{
    /// <summary>
    /// Codigo de la parada.
    /// </summary>
    public string CodigoParada { get; init; } = "";

    /// <summary>
    /// Momento de llegada a la parada.
    /// </summary>
    public DateTime Llegada { get; init; }

    /// <summary>
    /// Distancia total caminada hasta este punto.
    /// </summary>
    public double DistanciaCaminadaTotal { get; init; }

    /// <summary>
    /// Distancia caminada solo en transbordos.
    /// </summary>
    public double DistanciaPieTransbordos { get; init; }

    /// <summary>
    /// Espera inicial acumulada.
    /// </summary>
    public double TiempoEsperaInicialSegundos { get; init; }

    /// <summary>
    /// Espera acumulada en transbordos.
    /// </summary>
    public double TiempoEsperaTransbordosSegundos { get; init; }

    /// <summary>
    /// Numero de buses tomados hasta llegar aqui.
    /// </summary>
    public int BusesTomados { get; init; }

    /// <summary>
    /// Numero de transbordos reales acumulados hasta llegar aqui.
    /// </summary>
    public int TransbordosReales { get; init; }

    /// <summary>
    /// Estado previo del que cuelga la reconstruccion.
    /// </summary>
    public EstadoBusquedaParadaDbus? EstadoAnterior { get; init; }

    /// <summary>
    /// Movimiento aplicado para llegar a este estado.
    /// </summary>
    public MovimientoBusquedaDbus? Movimiento { get; init; }

    /// <summary>
    /// Espera total acumulada.
    /// </summary>
    public double TiempoEsperaTotalSegundos => TiempoEsperaInicialSegundos + TiempoEsperaTransbordosSegundos;
}

/// <summary>
/// Movimiento abstracto dentro de la reconstruccion.
/// </summary>
public abstract class MovimientoBusquedaDbus
{
}

/// <summary>
/// Movimiento inicial a pie desde el origen.
/// </summary>
public sealed class MovimientoAccesoDbus : MovimientoBusquedaDbus
{
    /// <summary>
    /// Distancia del acceso.
    /// </summary>
    public double DistanciaMetros { get; init; }

    /// <summary>
    /// Duracion del acceso.
    /// </summary>
    public double DuracionSegundos { get; init; }
}

/// <summary>
/// Movimiento a pie entre dos paradas.
/// </summary>
public sealed class MovimientoPieTransbordoDbus : MovimientoBusquedaDbus
{
    /// <summary>
    /// Distancia del transbordo.
    /// </summary>
    public double DistanciaMetros { get; init; }

    /// <summary>
    /// Duracion del transbordo.
    /// </summary>
    public double DuracionSegundos { get; init; }
}

/// <summary>
/// Movimiento en bus sobre un patron concreto.
/// </summary>
public sealed class MovimientoBusDbus : MovimientoBusquedaDbus
{
    /// <summary>
    /// Patron recorrido.
    /// </summary>
    public PatronTransitoDbus Patron { get; init; } = default!;

    /// <summary>
    /// Indice de subida.
    /// </summary>
    public int IndiceSubida { get; init; }

    /// <summary>
    /// Indice de bajada.
    /// </summary>
    public int IndiceBajada { get; init; }

    /// <summary>
    /// Hora de salida del tramo.
    /// </summary>
    public DateTime Salida { get; init; }

    /// <summary>
    /// Hora de llegada del tramo.
    /// </summary>
    public DateTime Llegada { get; init; }
}

/// <summary>
/// Resultado intermedio del buscador temporal.
/// </summary>
public sealed class ResultadoBusquedaRutaBusDbus
{
    /// <summary>
    /// Parada de acceso inicial.
    /// </summary>
    public Parada ParadaOrigen { get; init; } = default!;

    /// <summary>
    /// Parada de salida final del bus.
    /// </summary>
    public Parada ParadaDestino { get; init; } = default!;

    /// <summary>
    /// Distancia a pie inicial.
    /// </summary>
    public double DistanciaPieInicio { get; init; }

    /// <summary>
    /// Tiempo a pie inicial.
    /// </summary>
    public double TiempoPieInicio { get; init; }

    /// <summary>
    /// Distancia a pie final.
    /// </summary>
    public double DistanciaPieFin { get; init; }

    /// <summary>
    /// Tiempo a pie final.
    /// </summary>
    public double TiempoPieFin { get; init; }

    /// <summary>
    /// Distancia peatonal en transbordos.
    /// </summary>
    public double DistanciaPieTransbordos { get; init; }

    /// <summary>
    /// Distancias peatonales de cada transbordo, sin agregarlas antes de penalizarlas.
    /// </summary>
    public IReadOnlyList<double> DistanciasPieTransbordosPorTramo { get; init; } = Array.Empty<double>();

    /// <summary>
    /// Espera inicial acumulada.
    /// </summary>
    public double TiempoEsperaInicialSegundos { get; init; }

    /// <summary>
    /// Espera acumulada en transbordos.
    /// </summary>
    public double TiempoEsperaTransbordosSegundos { get; init; }

    /// <summary>
    /// Duracion estimada total desde el origen real.
    /// </summary>
    public double DuracionEstimadaSegundos { get; init; }

    /// <summary>
    /// Coste interno con penalizacion progresiva de caminata.
    /// </summary>
    public double PuntuacionCoste { get; init; }

    /// <summary>
    /// Ruta de bus reconstruida.
    /// </summary>
    public RutaBusAlternativa RutaBus { get; init; } = default!;

    /// <summary>
    /// Distancia peatonal total.
    /// </summary>
    public double DistanciaPieTotal => DistanciaPieInicio + DistanciaPieTransbordos + DistanciaPieFin;
}
