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

namespace RutasDBUS.Servicios.RedBus;

public sealed class CreadorRedTransporteDbus
{
    /// <summary>
    /// 
    /// </summary>
    public RedTransporteDbus Build(ArchivoImportacionDbus archive)
    {
        var paradas = new List<Parada>(archive.Paradas.Count);
        var trayectos = new Dictionary<int, Trayecto>();
        var lineas = new List<LineaBus>(archive.Rutas.Count);

        var paradaById = new Dictionary<int, Parada>();

        foreach (var row in archive.Paradas)
        {
            var (lat, lon) = ConvertirCoordenadaZipALatLon(row.LongitudXPar, row.LatitudYPar);
            if (!EsLatLonValida(lat, lon))
                continue;

            var codigo = !string.IsNullOrWhiteSpace(row.CodigoSms)
                ? NormalizarCodigoParada(row.CodigoSms)
                : NormalizarCodigoParada(row.Id.ToString(CultureInfo.InvariantCulture));

            var parada = new Parada
            {
                Codigo = codigo,
                Nombre = row.NombrePar ?? "",
                NombreCorto = !string.IsNullOrWhiteSpace(row.DescripcionPar) ? row.DescripcionPar : (row.NombrePar ?? ""),
                Abreviatura = row.DescripcionPar ?? "",
                PreZona = "",
                PostZona = "",
                ZonaSalida = "",
                Altura = 0,
                Latitud = lat,
                Longitud = lon
            };

            paradas.Add(parada);
            paradaById[row.Id] = parada;
        }

        var lineaByRutaId = new Dictionary<string, LineaBus>(StringComparer.OrdinalIgnoreCase);
        foreach (var row in archive.Rutas)
        {
            var linea = new LineaBus
            {
                IdRuta = row.Id ?? "",
                Codigo = !string.IsNullOrWhiteSpace(row.Abreviatura)
                    ? CodigoSinDecimal(row.Abreviatura)
                    : CodigoSinDecimal(row.Id),
                Nombre = !string.IsNullOrWhiteSpace(row.Nombre) ? row.Nombre : (row.Descripcion ?? ""),
                ColorArgb = NormalizarColorArgb(row.Color),
                CodigoItinerarioPorDefecto = 0
            };

            lineas.Add(linea);
            lineaByRutaId[row.Id ?? ""] = linea;
        }

        var stopSequenceByPattern = archive.Expediciones
            .Where(exp => !string.IsNullOrWhiteSpace(exp.IdPatronExp))
            .Select(exp => new
            {
                exp.IdPatronExp,
                Sequence = archive.TiemposParadaPorExpedicion.TryGetValue(exp.Id, out var seq)
                    ? seq.Select(x => x.IdTiempoPar).ToList()
                    : new List<int>()
            })
            .Where(x => x.Sequence.Count >= 2)
            .GroupBy(x => x.IdPatronExp, StringComparer.OrdinalIgnoreCase)
            .ToDictionary(
                g => g.Key,
                g => g.First().Sequence,
                StringComparer.OrdinalIgnoreCase);

        var variantesTemp = new Dictionary<string, VarianteTemporal>(StringComparer.OrdinalIgnoreCase);
        foreach (var exp in archive.Expediciones)
        {
            if (!lineaByRutaId.TryGetValue(exp.IdRuta, out var linea))
                continue;

            if (!stopSequenceByPattern.TryGetValue(exp.IdPatronExp, out var stopIds) || stopIds.Count < 2)
                continue;

            var sentido = DireccionZipASentido(exp.Direccion);
            var codigoItinerario = ExtraerCodigoItinerario(exp.IdRuta, exp.IdPatronExp);
            var codigoVariante = ExtraerCodigoVariante(exp.IdPatronExp);
            var key = $"{exp.IdRuta}|{codigoItinerario}|{sentido}|{exp.IdPatronExp}";

            if (!variantesTemp.ContainsKey(key))
            {
                variantesTemp[key] = new VarianteTemporal
                {
                    RutaId = exp.IdRuta,
                    CodigoItinerario = codigoItinerario,
                    Sentido = sentido,
                    CodigoVariante = codigoVariante,
                    IdPatron = exp.IdPatronExp,
                    Destino = exp.DirDestino ?? "",
                    StopIds = stopIds
                };
            }
        }

        var nextTrayectoId = 1;
        foreach (var temp in variantesTemp.Values)
        {
            if (!lineaByRutaId.TryGetValue(temp.RutaId, out var linea))
                continue;

            var itinerario = linea.Itinerarios.FirstOrDefault(x => x.Codigo == temp.CodigoItinerario);
            if (itinerario is null)
            {
                itinerario = new Itinerario { Codigo = temp.CodigoItinerario };
                linea.Itinerarios.Add(itinerario);
            }

            var sentido = itinerario.Sentidos.FirstOrDefault(x =>
                string.Equals(NormalizarSentidoCodigo(x.Codigo), temp.Sentido, StringComparison.OrdinalIgnoreCase));

            if (sentido is null)
            {
                sentido = new Sentido { Codigo = temp.Sentido };
                itinerario.Sentidos.Add(sentido);
            }

            var variante = new Variante { Codigo = temp.CodigoVariante };
            var stopIdsValidos = temp.StopIds.Where(paradaById.ContainsKey).ToList();
            var codigosParadas = stopIdsValidos
                .Select(id => paradaById[id].Codigo)
                .Where(x => !string.IsNullOrWhiteSpace(x))
                .ToList();

            if (codigosParadas.Count < 2)
                continue;

            var patronLatLon = archive.PatronesPorId.TryGetValue(temp.IdPatron, out var puntosPatron)
                ? puntosPatron
                    .OrderBy(x => x.PtoSecuencia)
                    .Select(p =>
                    {
                        var (lat, lon) = ConvertirCoordenadaZipALatLon(p.PtoLongitudX, p.PtoLatitudY);
                        return new[] { lat, lon };
                    })
                    .Where(p => EsLatLonValida(p[0], p[1]))
                    .ToList()
                : new List<double[]>();

            var stopLatLon = stopIdsValidos
                .Select(id => new[] { paradaById[id].Latitud, paradaById[id].Longitud })
                .ToList();

            var segmentos = ConstruirSegmentosDelPatron(stopLatLon, patronLatLon);

            for (var i = 0; i < codigosParadas.Count - 1; i++)
            {
                var codigoOrigen = codigosParadas[i];
                var codigoDestino = codigosParadas[i + 1];

                var puntos = (i < segmentos.Count && segmentos[i].Count >= 2)
                    ? segmentos[i]
                    : new List<double[]>
                    {
                        new[] { stopLatLon[i][0], stopLatLon[i][1] },
                        new[] { stopLatLon[i + 1][0], stopLatLon[i + 1][1] }
                    };

                trayectos[nextTrayectoId] = new Trayecto
                {
                    Id = nextTrayectoId,
                    CodigoParadaOrigen = codigoOrigen,
                    CodigoParadaDestino = codigoDestino,
                    Posiciones = puntos
                };

                variante.IdsTrayectos.Add(nextTrayectoId);
                nextTrayectoId++;
            }

            if (variante.IdsTrayectos.Count > 0)
                sentido.Variantes.Add(variante);
        }

        foreach (var linea in lineas)
        {
            linea.Itinerarios = linea.Itinerarios
                .Where(it => it.Sentidos.Any(s => s.Variantes.Any()))
                .OrderBy(it => it.Codigo)
                .ToList();

            foreach (var itinerario in linea.Itinerarios)
            {
                itinerario.Sentidos = itinerario.Sentidos
                    .Where(s => s.Variantes.Any())
                    .OrderBy(s => NormalizarSentidoCodigo(s.Codigo))
                    .ToList();
            }
        }

        lineas.RemoveAll(x => x.Itinerarios.Count == 0);

        return new RedTransporteDbus
        {
            Paradas = paradas,
            Trayectos = trayectos,
            Lineas = lineas
        };
    }

    private sealed class VarianteTemporal
    {
        public string RutaId { get; set; } = "";
        public int CodigoItinerario { get; set; }
        public string Sentido { get; set; } = "";
        public int CodigoVariante { get; set; }
        public string IdPatron { get; set; } = "";
        public string Destino { get; set; } = "";
        public List<int> StopIds { get; set; } = new();
    }

    /// <summary>
    /// Gestiona segmentos del patron.
    /// </summary>
    private static List<List<double[]>> ConstruirSegmentosDelPatron(
        List<double[]> stopLatLon,
        List<double[]> patronLatLon)
    {
        var res = new List<List<double[]>>();

        if (stopLatLon.Count < 2)
            return res;

        if (patronLatLon.Count < 2)
        {
            for (var i = 0; i < stopLatLon.Count - 1; i++)
            {
                res.Add(new List<double[]>
                {
                    new[] { stopLatLon[i][0], stopLatLon[i][1] },
                    new[] { stopLatLon[i + 1][0], stopLatLon[i + 1][1] }
                });
            }

            return res;
        }

        var indices = CalcularIndicesParadasEnPatron(stopLatLon, patronLatLon);

        for (var i = 0; i < indices.Count - 1; i++)
        {
            var a = indices[i];
            var b = Math.Max(a, indices[i + 1]);
            var segmento = new List<double[]>();

            for (var j = a; j <= b && j < patronLatLon.Count; j++)
                segmento.Add(new[] { patronLatLon[j][0], patronLatLon[j][1] });

            if (segmento.Count == 0)
            {
                segmento.Add(new[] { stopLatLon[i][0], stopLatLon[i][1] });
                segmento.Add(new[] { stopLatLon[i + 1][0], stopLatLon[i + 1][1] });
            }
            else if (segmento.Count == 1)
            {
                segmento.Insert(0, new[] { stopLatLon[i][0], stopLatLon[i][1] });
                segmento.Add(new[] { stopLatLon[i + 1][0], stopLatLon[i + 1][1] });
            }
            else
            {
                segmento[0] = new[] { stopLatLon[i][0], stopLatLon[i][1] };
                segmento[^1] = new[] { stopLatLon[i + 1][0], stopLatLon[i + 1][1] };
            }

            res.Add(QuitarDuplicadosConsecutivos(segmento));
        }

        return res;
    }

    /// <summary>
    /// Calcula para cada parada el indice del punto del patron que mejor la representa,
    /// forzando una progresion monotona para no perder detalle entre paradas consecutivas.
    /// </summary>
    private static List<int> CalcularIndicesParadasEnPatron(
        List<double[]> stopLatLon,
        List<double[]> patronLatLon)
    {
        var indices = new List<int>(stopLatLon.Count);
        var ultimoIndicePatron = patronLatLon.Count - 1;

        for (var i = 0; i < stopLatLon.Count; i++)
        {
            var minIdx = indices.Count == 0
                ? 0
                : Math.Min(ultimoIndicePatron, indices[^1] + 1);

            var paradasRestantes = stopLatLon.Count - i - 1;
            var maxIdx = Math.Max(minIdx, ultimoIndicePatron - paradasRestantes);

            indices.Add(BuscarIndicePatronMasCercano(stopLatLon[i], patronLatLon, minIdx, maxIdx));
        }

        return indices;
    }

    /// <summary>
    /// Busca el punto del patron mas cercano a una parada dentro de un rango valido.
    /// </summary>
    private static int BuscarIndicePatronMasCercano(
        double[] paradaLatLon,
        List<double[]> patronLatLon,
        int minIdx,
        int maxIdx)
    {
        var inicio = Math.Clamp(minIdx, 0, patronLatLon.Count - 1);
        var fin = Math.Clamp(maxIdx, inicio, patronLatLon.Count - 1);

        var mejorIdx = inicio;
        var mejorDist = double.MaxValue;

        for (var j = inicio; j <= fin; j++)
        {
            var dist = Dist2(paradaLatLon[0], paradaLatLon[1], patronLatLon[j][0], patronLatLon[j][1]);
            if (dist < mejorDist)
            {
                mejorDist = dist;
                mejorIdx = j;
            }
        }

        return mejorIdx;
    }

    /// <summary>
    /// Quita duplicados consecutivos.
    /// </summary>
    private static List<double[]> QuitarDuplicadosConsecutivos(List<double[]> puntos)
    {
        var res = new List<double[]>(puntos.Count);
        foreach (var punto in puntos)
        {
            if (res.Count == 0)
            {
                res.Add(punto);
                continue;
            }

            var ultimo = res[^1];
            if (Math.Abs(ultimo[0] - punto[0]) < 1e-9 && Math.Abs(ultimo[1] - punto[1]) < 1e-9)
                continue;

            res.Add(punto);
        }

        return res;
    }

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

    /// <summary>
    /// Gestiona lat lon valida.
    /// </summary>
    private static bool EsLatLonValida(double lat, double lon)
        => lat >= 42.0 && lat <= 44.5 && lon >= -3.5 && lon <= 0.5;

    private const double OffsetLat = -0.0018141507864726236;
    private const double OffsetLon = -0.0013070642408454791;

    /// <summary>
    /// 
    /// </summary>
    private static (double lat, double lon) ConvertirCoordenadaZipALatLon(double x, double y)
    {
        if (x == 0 || y == 0)
            return (0, 0);

        var (lat, lon) = Utm30ToLatLonWgs84(x, y);
        return (lat + OffsetLat, lon + OffsetLon);
    }

    /// <summary>
    /// 
    /// </summary>
    private static (double lat, double lon) Utm30ToLatLonWgs84(double easting, double northing)
    {
        const int zone = 30;
        const double a = 6378137.0;
        const double eccSquared = 0.0066943799901413165;
        const double k0 = 0.9996;

        var x = easting - 500000.0;
        var y = northing;
        var longOrigin = (zone - 1) * 6 - 180 + 3;
        var eccPrimeSquared = eccSquared / (1 - eccSquared);
        var m = y / k0;
        var mu = m / (a * (1 - eccSquared / 4.0 - 3.0 * eccSquared * eccSquared / 64.0 - 5.0 * Math.Pow(eccSquared, 3) / 256.0));
        var e1 = (1.0 - Math.Sqrt(1.0 - eccSquared)) / (1.0 + Math.Sqrt(1.0 - eccSquared));

        var phi1Rad =
            mu
            + (3.0 * e1 / 2.0 - 27.0 * Math.Pow(e1, 3) / 32.0) * Math.Sin(2.0 * mu)
            + (21.0 * e1 * e1 / 16.0 - 55.0 * Math.Pow(e1, 4) / 32.0) * Math.Sin(4.0 * mu)
            + (151.0 * Math.Pow(e1, 3) / 96.0) * Math.Sin(6.0 * mu)
            + (1097.0 * Math.Pow(e1, 4) / 512.0) * Math.Sin(8.0 * mu);

        var n1 = a / Math.Sqrt(1.0 - eccSquared * Math.Sin(phi1Rad) * Math.Sin(phi1Rad));
        var t1 = Math.Tan(phi1Rad) * Math.Tan(phi1Rad);
        var c1 = eccPrimeSquared * Math.Cos(phi1Rad) * Math.Cos(phi1Rad);
        var r1 = a * (1.0 - eccSquared) / Math.Pow(1.0 - eccSquared * Math.Sin(phi1Rad) * Math.Sin(phi1Rad), 1.5);
        var d = x / (n1 * k0);

        var latRad =
            phi1Rad
            - (n1 * Math.Tan(phi1Rad) / r1) *
            (
                d * d / 2.0
                - (5.0 + 3.0 * t1 + 10.0 * c1 - 4.0 * c1 * c1 - 9.0 * eccPrimeSquared) * Math.Pow(d, 4) / 24.0
                + (61.0 + 90.0 * t1 + 298.0 * c1 + 45.0 * t1 * t1 - 252.0 * eccPrimeSquared - 3.0 * c1 * c1) * Math.Pow(d, 6) / 720.0
            );

        var lonRad =
            (
                d
                - (1.0 + 2.0 * t1 + c1) * Math.Pow(d, 3) / 6.0
                + (5.0 - 2.0 * c1 + 28.0 * t1 - 3.0 * c1 * c1 + 8.0 * eccPrimeSquared + 24.0 * t1 * t1) * Math.Pow(d, 5) / 120.0
            ) / Math.Cos(phi1Rad);

        return (latRad * 180.0 / Math.PI, longOrigin + lonRad * 180.0 / Math.PI);
    }

    /// <summary>
    /// Gestiona sin decimal.
    /// </summary>
    private static string CodigoSinDecimal(string? codigo)
    {
        if (string.IsNullOrWhiteSpace(codigo)) return "";
        codigo = codigo.Trim();

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

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

        return codigo.Trim();
    }

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

        codigo = codigo.Trim();
        codigo = codigo.Replace("P-", "", StringComparison.OrdinalIgnoreCase);

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

        return codigo.Trim();
    }

    /// <summary>
    /// Normaliza color argb.
    /// </summary>
    private static string NormalizarColorArgb(string? color)
    {
        if (string.IsNullOrWhiteSpace(color))
            return "FF2563EB";

        color = color.Trim().TrimStart('#');

        if (color.Length == 6)
            return "FF" + color.ToUpperInvariant();

        if (color.Length == 8)
            return color.ToUpperInvariant();

        return "FF2563EB";
    }

    /// <summary>
    /// Gestiona zip a sentido.
    /// </summary>
    private static string DireccionZipASentido(int direccion) =>
        direccion switch
        {
            1 => "I",
            2 => "V",
            0 => "I",
            _ => "?"
        };

    /// <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]);
        return c switch
        {
            'V' => "V",
            'I' => "I",
            _ => "?"
        };
    }

    /// <summary>
    /// Gestiona codigo variante.
    /// </summary>
    private static int ExtraerCodigoVariante(string? idPatronExp)
    {
        if (string.IsNullOrWhiteSpace(idPatronExp))
            return 0;

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

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

    /// <summary>
    /// Gestiona codigo itinerario.
    /// </summary>
    private 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 value))
                return value;
        }

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