using System.Globalization;
using System.Text.RegularExpressions;
using RutasDBUS.Modelos.Planificacion;
using RutasDBUS.Modelos.RedBus;

namespace RutasDBUS.Servicios.Planificacion;

/// <summary>
/// Agrupa utilidades comunes del motor de planificacion.
/// </summary>
internal static class UtilidadesTransitoDbus
{
    /// <summary>
    /// Quita prefijos y decimales de un codigo numerico.
    /// </summary>
    public static string CodigoSinDecimal(string? codigo)
    {
        if (string.IsNullOrWhiteSpace(codigo))
            return "";

        codigo = codigo.Trim();
        if (codigo.StartsWith("P-", StringComparison.OrdinalIgnoreCase))
            codigo = codigo[2..];

        var index = codigo.IndexOf('.');
        if (index > 0)
            codigo = codigo[..index];

        return codigo.Trim();
    }

    /// <summary>
    /// Normaliza el codigo de parada al formato usado por la app.
    /// </summary>
    public static string NormalizarCodigoParada(string? codigo)
        => CodigoSinDecimal(codigo);

    /// <summary>
    /// Convierte la direccion del CSV a un sentido normalizado.
    /// </summary>
    public static string DireccionZipASentido(int direccion)
        => direccion switch
        {
            1 => "I",
            2 => "V",
            0 => "I",
            _ => "?"
        };

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

        var texto = codigo.Trim();
        if (texto.Equals("I", StringComparison.OrdinalIgnoreCase) ||
            texto.Equals("Ida", StringComparison.OrdinalIgnoreCase))
        {
            return "I";
        }

        if (texto.Equals("V", StringComparison.OrdinalIgnoreCase) ||
            texto.Equals("Vuelta", StringComparison.OrdinalIgnoreCase))
        {
            return "V";
        }

        return char.ToUpperInvariant(texto[0]) switch
        {
            'I' => "I",
            'V' => "V",
            _ => "?"
        };
    }

    /// <summary>
    /// Extrae el codigo de itinerario a partir de la ruta y del patron.
    /// </summary>
    public static int ExtraerCodigoItinerario(string? idRuta, string? idPatronExp)
    {
        if (string.IsNullOrWhiteSpace(idPatronExp))
            return 0;

        var patron = idPatronExp.Trim();
        var ruta = CodigoSinDecimal(idRuta);

        if (!string.IsNullOrWhiteSpace(ruta) &&
            patron.StartsWith(ruta, StringComparison.OrdinalIgnoreCase))
        {
            patron = patron[ruta.Length..];
        }

        patron = patron.Trim();
        if (patron.Length >= 4)
        {
            var sinDir = patron[..^2].TrimStart('0');
            if (int.TryParse(sinDir, NumberStyles.Integer, CultureInfo.InvariantCulture, out var directo))
                return directo;
        }

        var match = Regex.Match(patron, @"(\d{1,3})");
        return match.Success &&
               int.TryParse(match.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var valor)
            ? valor
            : 0;
    }

    /// <summary>
    /// Extrae el codigo de variante desde el identificador del patron.
    /// </summary>
    public static int ExtraerCodigoVariante(string? idPatronExp)
    {
        if (string.IsNullOrWhiteSpace(idPatronExp))
            return 0;

        var texto = idPatronExp.Trim();
        if (int.TryParse(texto, NumberStyles.Integer, CultureInfo.InvariantCulture, out var directo))
            return directo;

        var match = Regex.Match(texto, @"(\d+)$");
        return match.Success &&
               int.TryParse(match.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var valor)
            ? valor
            : 0;
    }

    /// <summary>
    /// Resuelve el instante efectivo de salida.
    /// </summary>
    public static DateTime ResolverInstanteSalida(ParametrosPlanificadorRuta parametros)
    {
        var fecha = parametros.FechaSalida ?? DateOnly.FromDateTime(DateTime.Now);
        var hora = parametros.HoraSalida ?? TimeOnly.FromDateTime(DateTime.Now);
        return fecha.ToDateTime(hora);
    }

    /// <summary>
    /// Suma segundos evitando cambios inutiles cuando la duracion es cero.
    /// </summary>
    public static DateTime AddSecondsSafe(DateTime fecha, double segundos)
        => segundos <= 0 ? fecha : fecha.AddSeconds(segundos);

    /// <summary>
    /// Obtiene el codigo textual de una linea.
    /// </summary>
    public static string LineaCodigoStr(InfoVariante variante)
        => variante.Linea.Codigo?.ToString() ?? "?";

    /// <summary>
    /// Indica si pasar de un tramo al siguiente exige cambiar realmente de bus.
    /// </summary>
    public static bool EsTransbordoReal(TramoBus? anterior, TramoBus? siguiente)
    {
        if (anterior?.Variante is null || siguiente?.Variante is null)
            return false;

        return EsTransbordoReal(anterior.Variante, siguiente.Variante);
    }

    public static bool EsTransbordoReal(InfoVariante? anterior, InfoVariante? siguiente)
    {
        if (anterior is null || siguiente is null)
            return false;

        return !string.Equals(
                   LineaCodigoStr(anterior),
                   LineaCodigoStr(siguiente),
                   StringComparison.OrdinalIgnoreCase) ||
               anterior.CodigoItinerario != siguiente.CodigoItinerario;
    }

    /// <summary>
    /// Cuenta solo los cambios que obligan a bajarse y subir a otro bus.
    /// </summary>
    public static int ContarTransbordosReales(IReadOnlyList<TramoBus>? tramos)
    {
        if (tramos is null || tramos.Count <= 1)
            return 0;

        var total = 0;
        for (var indice = 1; indice < tramos.Count; indice++)
        {
            if (EsTransbordoReal(tramos[indice - 1], tramos[indice]))
                total++;
        }

        return total;
    }

    /// <summary>
    /// Detecta rutas que encadenan demasiadas continuidades de la misma linea sin transbordo real.
    /// </summary>
    public static bool TieneContinuidadesNoRealesConsecutivasExcesivas(
        IReadOnlyList<TramoBus>? tramos,
        int maxContinuidadesConsecutivas)
    {
        if (tramos is null || tramos.Count <= 1)
            return false;

        var maximo = Math.Max(0, maxContinuidadesConsecutivas);
        var consecutivas = 0;
        for (var indice = 1; indice < tramos.Count; indice++)
        {
            if (EsTransbordoReal(tramos[indice - 1], tramos[indice]))
            {
                consecutivas = 0;
                continue;
            }

            consecutivas++;
            if (consecutivas > maximo)
                return true;
        }

        return false;
    }

    /// <summary>
    /// Extrae el numero de linea desde codigos numericos o alfanumericos como TB6.
    /// </summary>
    public static int? ExtraerNumeroLinea(string? codigoLinea)
    {
        if (string.IsNullOrWhiteSpace(codigoLinea))
            return null;

        var texto = CodigoSinDecimal(codigoLinea);
        if (int.TryParse(texto, NumberStyles.Integer, CultureInfo.InvariantCulture, out var directo))
            return directo;

        var match = Regex.Match(texto, @"(\d+)");
        return match.Success &&
               int.TryParse(match.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var valor)
            ? valor
            : null;
    }

    /// <summary>
    /// Comprueba si un destino descriptivo merece mostrarse tal cual al usuario.
    /// </summary>
    public static bool DestinoDescriptivoEsValido(string? destino)
    {
        if (string.IsNullOrWhiteSpace(destino))
            return false;

        var texto = destino.Trim();
        return !texto.Equals("N.A.", StringComparison.OrdinalIgnoreCase) &&
               !texto.Equals("N/A", StringComparison.OrdinalIgnoreCase) &&
               !texto.Equals("NA", StringComparison.OrdinalIgnoreCase) &&
               !texto.Equals("-", StringComparison.OrdinalIgnoreCase) &&
               !texto.Equals("?", StringComparison.OrdinalIgnoreCase);
    }

    /// <summary>
    /// Resuelve un destino visible usando el texto original o, si no sirve, la ultima parada conocida.
    /// </summary>
    public static string ResolverDestinoDescriptivo(
        string? destinoOriginal,
        IReadOnlyList<string> codigosParada,
        IReadOnlyDictionary<string, Parada> paradasPorCodigo)
    {
        if (DestinoDescriptivoEsValido(destinoOriginal))
            return destinoOriginal!.Trim();

        if (codigosParada.Count > 0 &&
            paradasPorCodigo.TryGetValue(codigosParada[^1], out var ultimaParada) &&
            !string.IsNullOrWhiteSpace(ultimaParada.Nombre))
        {
            return ultimaParada.Nombre.Trim();
        }

        return "";
    }

    /// <summary>
    /// Calcula la distancia entre dos coordenadas con Haversine.
    /// </summary>
    public static double CalcularDistanciaMetros(double lat1, double lon1, double lat2, double lon2)
    {
        const double radioTierra = 6371000.0;
        static double ToRad(double valor) => valor * Math.PI / 180.0;

        var dLat = ToRad(lat2 - lat1);
        var dLon = ToRad(lon2 - lon1);

        var a =
            Math.Sin(dLat / 2) * Math.Sin(dLat / 2) +
            Math.Cos(ToRad(lat1)) * Math.Cos(ToRad(lat2)) *
            Math.Sin(dLon / 2) * Math.Sin(dLon / 2);

        var c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a));
        return radioTierra * c;
    }

    /// <summary>
    /// Estima la duracion andando.
    /// </summary>
    public static double EstimarDuracionPieSegundos(double distanciaMetros, ParametrosPlanificadorRuta parametros)
    {
        var velocidad = parametros.VelocidadPieKmH * 1000.0 / 3600.0;
        return distanciaMetros / Math.Max(0.1, velocidad);
    }

    /// <summary>
    /// Calcula la distancia peatonal corregida para coste interno sin reducir nunca la distancia real.
    /// </summary>
    public static double CalcularDistanciaPieCorregidaMetros(double distanciaMetros, ParametrosPlanificadorRuta parametros)
    {
        if (!double.IsFinite(distanciaMetros) || distanciaMetros <= 0)
            return 0;

        var factor = double.IsFinite(parametros.FactorPenalizacionDistanciaPie)
            ? parametros.FactorPenalizacionDistanciaPie
            : 1.6;
        var parametroBaseMetros = double.IsFinite(parametros.ParametroPenalizacionDistanciaPieMetros)
            ? Math.Abs(parametros.ParametroPenalizacionDistanciaPieMetros)
            : 100.0;

        if (parametroBaseMetros <= 0)
            parametroBaseMetros = 1.0;

        var corregida = Math.Pow(distanciaMetros / parametroBaseMetros, factor) * parametroBaseMetros;
        return Math.Max(distanciaMetros, corregida);
    }

    /// <summary>
    /// Calcula un coste interno que penaliza de forma progresiva la caminata sin cambiar el tiempo real.
    /// </summary>
    public static double CalcularCosteConPenalizacionPieSegundos(
        double duracionRealSegundos,
        ParametrosPlanificadorRuta parametros,
        params double[] distanciasPieMetros)
    {
        var coste = Math.Max(0, duracionRealSegundos);

        foreach (var distancia in distanciasPieMetros)
        {
            if (!double.IsFinite(distancia) || distancia <= 0)
                continue;

            var distanciaCorregida = CalcularDistanciaPieCorregidaMetros(distancia, parametros);
            var excesoDistancia = Math.Max(0, distanciaCorregida - distancia);
            coste += EstimarDuracionPieSegundos(excesoDistancia, parametros);
        }

        return coste;
    }

    /// <summary>
    /// Calcula la penalizacion interna por transbordos reales sin cambiar el tiempo visible.
    /// </summary>
    public static double CalcularPenalizacionTransbordosRankingSegundos(
        int numeroTransbordosReales,
        ParametrosPlanificadorRuta parametros)
    {
        if (numeroTransbordosReales <= 0)
            return 0;

        var pesoMinutos = double.IsFinite(parametros.PesoTransbordoRankingMinutos)
            ? parametros.PesoTransbordoRankingMinutos
            : 4.0;

        if (pesoMinutos <= 0)
            return 0;

        return numeroTransbordosReales * pesoMinutos * 60.0;
    }

    /// <summary>
    /// Calcula el coste interno aplicando preferencias de caminata y transbordos reales.
    /// </summary>
    public static double CalcularCosteConPenalizacionesRankingSegundos(
        double duracionRealSegundos,
        ParametrosPlanificadorRuta parametros,
        int numeroTransbordosReales,
        params double[] distanciasPieMetros)
    {
        return CalcularCosteConPenalizacionPieSegundos(duracionRealSegundos, parametros, distanciasPieMetros)
             + CalcularPenalizacionTransbordosRankingSegundos(numeroTransbordosReales, parametros);
    }

    /// <summary>
    /// Estima la duracion en bus.
    /// </summary>
    public static double EstimarDuracionBusSegundos(double distanciaMetros, ParametrosPlanificadorRuta parametros)
    {
        var velocidad = parametros.VelocidadBusKmH * 1000.0 / 3600.0;
        return distanciaMetros / Math.Max(0.1, velocidad);
    }

    /// <summary>
    /// Crea una firma estable de una variante.
    /// </summary>
    public static string FirmaVariante(string codigoLinea, int codigoItinerario, string sentido, IReadOnlyList<string> codigosParada)
        => $"{codigoLinea}|{codigoItinerario}|{sentido}|{string.Join(">", codigosParada)}";

    /// <summary>
    /// Crea una firma estable de una ruta de bus.
    /// </summary>
    public static string FirmaRutaBus(RutaBusAlternativa alternativa)
        => string.Join(";", alternativa.Tramos.Select(tramo =>
            $"{LineaCodigoStr(tramo.Variante)}-{tramo.Variante.CodigoItinerario}-{NormalizarSentidoCodigo(tramo.Variante.Sentido?.Codigo)}-{tramo.Variante.Variante.Codigo}-{tramo.IndiceInicio}-{tramo.IndiceFin}"));
}
