using System.Globalization;
using System.IO.Compression;
using System.Text;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Options;
using RutasDBUS.Modelos.Importacion;
using RutasDBUS.Modelos.RedBus;

namespace RutasDBUS.Servicios.Importacion;

public sealed class ServicioArchivoImportacionDbus
{
    private readonly IWebHostEnvironment _entorno;
    private readonly OpcionesRedBus _opciones;
    private readonly SemaphoreSlim _semCarga = new(1, 1);
    private ArchivoImportacionDbus? _archive;

    /// <summary>
    /// Inicializa una nueva instancia de la clase ServicioArchivoImportacionDbus.
    /// </summary>
    public ServicioArchivoImportacionDbus(
        IWebHostEnvironment entorno,
        IOptions<OpcionesRedBus> opciones)
    {
        _entorno = entorno;
        _opciones = opciones.Value ?? new OpcionesRedBus();
    }

    /// <summary>
    /// 
    /// </summary>
    public async Task<ArchivoImportacionDbus> ObtenerAsync(CancellationToken cancellationToken = default)
    {
        if (_archive is not null)
            return _archive;

        await _semCarga.WaitAsync(cancellationToken);
        try
        {
            if (_archive is not null)
                return _archive;

            var zipPath = ResolverRutaZip();
            if (zipPath is null || !File.Exists(zipPath))
                throw new FileNotFoundException("No se ha encontrado el ZIP de importaciÃ³n de dBUS.");

            _archive = await CargarArchivoAsync(zipPath, cancellationToken);
            return _archive;
        }
        finally
        {
            _semCarga.Release();
        }
    }

    public Task<ArchivoImportacionDbus> ObtenerDesdeRutaAsync(
        string zipPath,
        CancellationToken cancellationToken = default)
    {
        if (string.IsNullOrWhiteSpace(zipPath) || !File.Exists(zipPath))
            throw new FileNotFoundException("No se ha encontrado el ZIP de importacion de dBUS.", zipPath);

        return CargarArchivoAsync(zipPath, cancellationToken);
    }

    /// <summary>
    /// Resuelve zip camino.
    /// </summary>
    private string? ResolverRutaZip()
    {
        if (!string.IsNullOrWhiteSpace(_opciones.ArchivoDatos))
        {
            var configured = Path.Combine(
                _entorno.ContentRootPath,
                _opciones.ArchivoDatos.Replace('/', Path.DirectorySeparatorChar));

            if (File.Exists(configured))
                return configured;
        }

        var folder = Path.Combine(_entorno.ContentRootPath, "Modelos", "horarios");
        if (!Directory.Exists(folder))
            return null;

        return Directory.GetFiles(folder, "*.zip", SearchOption.TopDirectoryOnly)
            .Select(path => new FileInfo(path))
            .OrderByDescending(info => info.LastWriteTimeUtc)
            .FirstOrDefault()
            ?.FullName;
    }

    /// <summary>
    /// Carga el archivo.
    /// </summary>
    private async Task<ArchivoImportacionDbus> CargarArchivoAsync(string zipPath, CancellationToken ct)
    {
        using var fs = File.OpenRead(zipPath);
        using var zip = new ZipArchive(fs, ZipArchiveMode.Read, leaveOpen: false);

        var calendarios = await ReadCalendariosAsync(FindEntry(zip, "calendario.csv"), ct);
        var calendarioEx = await ReadCalendarioExcepcionesAsync(FindEntry(zip, "calendario_excepciones_puntuales.csv"), ct);
        var expediciones = await ReadExpedicionesAsync(FindEntry(zip, "expedicion.csv"), ct);
        var operadores = await ReadOperadoresAsync(FindEntry(zip, "operador.csv"), ct);
        var paradas = await ReadParadasAsync(FindEntry(zip, "parada.csv"), ct);
        var patrones = await ReadPatronesAsync(FindEntry(zip, "patron.csv"), ct);
        var rutas = await ReadRutasAsync(FindEntry(zip, "ruta.csv"), ct);
        var tarifas = await ReadTarifasAsync(FindEntry(zip, "tarifa.csv"), ct);
        var tiemposParada = await ReadTiemposParadaAsync(FindEntry(zip, "tiempo_parada.csv"), ct);
        var tiemposReales = await ReadTiemposRealesAsync(FindEntry(zip, "tiempo_real.csv"), ct);
        var transbordos = await ReadTransbordosAsync(FindEntry(zip, "transbordos.csv"), ct);
        var vehiculos = await ReadVehiculosAsync(FindEntry(zip, "vehiculos.csv"), ct);

        return new ArchivoImportacionDbus
        {
            RutaZip = zipPath,
            FechaArchivoUtc = File.GetLastWriteTimeUtc(zipPath),
            Calendarios = calendarios,
            CalendarioExcepciones = calendarioEx,
            Expediciones = expediciones,
            Operadores = operadores,
            Paradas = paradas,
            Patrones = patrones,
            Rutas = rutas,
            Tarifas = tarifas,
            TiemposParada = tiemposParada,
            TiemposReales = tiemposReales,
            Transbordos = transbordos,
            Vehiculos = vehiculos,
            ParadasPorId = paradas.ToDictionary(x => x.Id),
            RutasPorId = rutas.ToDictionary(x => x.Id, StringComparer.OrdinalIgnoreCase),
            ExpedicionesPorId = expediciones.ToDictionary(x => x.Id, StringComparer.OrdinalIgnoreCase),
            TiemposParadaPorExpedicion = tiemposParada
                .GroupBy(x => x.IdExp, StringComparer.OrdinalIgnoreCase)
                .ToDictionary(
                    g => g.Key,
                    g => g.OrderBy(x => x.SecPar).ToList(),
                    StringComparer.OrdinalIgnoreCase),
            PatronesPorId = patrones
                .GroupBy(x => x.Id, StringComparer.OrdinalIgnoreCase)
                .ToDictionary(
                    g => g.Key,
                    g => g.OrderBy(x => x.PtoSecuencia).ToList(),
                    StringComparer.OrdinalIgnoreCase),
            TransbordosPorRuta = transbordos
                .SelectMany(x => new[]
                {
                    new KeyValuePair<string, FilaTransbordoCsv>(x.IdRuta1, x),
                    new KeyValuePair<string, FilaTransbordoCsv>(x.IdRuta2, x)
                })
                .GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
                .ToDictionary(
                    g => g.Key,
                    g => g.Select(x => x.Value).ToList(),
                    StringComparer.OrdinalIgnoreCase)
        };
    }

    /// <summary>
    /// Busca entrada.
    /// </summary>
    private static ZipArchiveEntry? FindEntry(ZipArchive zip, string fileName)
        => zip.Entries.FirstOrDefault(entry =>
            entry.FullName.EndsWith("/" + fileName, StringComparison.OrdinalIgnoreCase) ||
            entry.FullName.Equals(fileName, StringComparison.OrdinalIgnoreCase));

    /// <summary>
    /// Lee csv rows.
    /// </summary>
    private static async Task<List<string[]>> ReadCsvRowsAsync(ZipArchiveEntry? entry, CancellationToken ct)
    {
        var rows = new List<string[]>();
        if (entry is null)
            return rows;

        var contenidoCsv = await ReadCsvTextAsync(entry, ct);
        using var reader = new StringReader(contenidoCsv);

        string? line;
        while ((line = await reader.ReadLineAsync()) is not null)
        {
            ct.ThrowIfCancellationRequested();
            if (string.IsNullOrWhiteSpace(line))
                continue;

            rows.Add(SplitCsvLine(line));
        }

        return rows;
    }

    /// <summary>
    /// Lee el contenido de un CSV intentando UTF-8 y cayendo a Windows-1252 cuando el archivo viene en esa codificación.
    /// </summary>
    private static async Task<string> ReadCsvTextAsync(ZipArchiveEntry entry, CancellationToken ct)
    {
        using var stream = entry.Open();
        using var memory = new MemoryStream();
        await stream.CopyToAsync(memory, ct);

        var bytes = memory.ToArray();
        var utf8Strict = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);

        try
        {
            return utf8Strict.GetString(bytes);
        }
        catch (DecoderFallbackException)
        {
            return Encoding.GetEncoding(1252).GetString(bytes);
        }
    }

    /// <summary>
    /// Gestiona csv linea.
    /// </summary>
    private static string[] SplitCsvLine(string line)
    {
        var values = new List<string>(16);
        var sb = new StringBuilder(line.Length);
        var inQuotes = false;

        for (var i = 0; i < line.Length; i++)
        {
            var c = line[i];

            if (c == '"')
            {
                if (inQuotes && i + 1 < line.Length && line[i + 1] == '"')
                {
                    sb.Append('"');
                    i++;
                }
                else
                {
                    inQuotes = !inQuotes;
                }

                continue;
            }

            if (c == ',' && !inQuotes)
            {
                values.Add(sb.ToString().Trim());
                sb.Clear();
                continue;
            }

            sb.Append(c);
        }

        values.Add(sb.ToString().Trim());
        return values.ToArray();
    }

    /// <summary>
    /// Gestiona of.
    /// </summary>
    private static int IndexOf(string[] header, string column)
    {
        for (var i = 0; i < header.Length; i++)
        {
            if (header[i].Equals(column, StringComparison.OrdinalIgnoreCase))
                return i;
        }

        return -1;
    }

    /// <summary>
    /// 
    /// </summary>
    private static string Get(string[] row, int index)
        => index >= 0 && index < row.Length ? row[index] : "";

    /// <summary>
    /// Analiza int.
    /// </summary>
    private static int ParseInt(string[] row, int index)
        => int.TryParse(Get(row, index), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) ? value : 0;

    /// <summary>
    /// Analiza nullable int.
    /// </summary>
    private static int? ParseNullableInt(string[] row, int index)
        => int.TryParse(Get(row, index), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) ? value : null;

    /// <summary>
    /// Analiza double.
    /// </summary>
    private static double ParseDouble(string[] row, int index)
        => double.TryParse(Get(row, index), NumberStyles.Any, CultureInfo.InvariantCulture, out var value) ? value : 0;

    /// <summary>
    /// Analiza nullable decimal.
    /// </summary>
    private static decimal? ParseNullableDecimal(string[] row, int index)
        => decimal.TryParse(Get(row, index), NumberStyles.Any, CultureInfo.InvariantCulture, out var value) ? value : null;

    /// <summary>
    /// Analiza date.
    /// </summary>
    private static DateOnly ParseDate(string value)
        => DateOnly.TryParseExact(value, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var date)
            ? date
            : default;

    /// <summary>
    /// Analiza tiempo.
    /// </summary>
    private static TimeOnly? ParseTime(string value)
        => TimeOnly.TryParseExact(value, "HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.None, out var time)
            ? time
            : null;

    /// <summary>
    /// Lee calendarios.
    /// </summary>
    private static async Task<List<FilaCalendarioCsv>> ReadCalendariosAsync(ZipArchiveEntry? entry, CancellationToken ct)
    {
        var rows = await ReadCsvRowsAsync(entry, ct);
        if (rows.Count == 0) return new();

        var h = rows[0];
        var res = new List<FilaCalendarioCsv>(Math.Max(0, rows.Count - 1));

        int iId = IndexOf(h, "ID");
        int iLun = IndexOf(h, "LUNES");
        int iMar = IndexOf(h, "MARTES");
        int iMie = IndexOf(h, "MIERCOLES");
        int iJue = IndexOf(h, "JUEVES");
        int iVie = IndexOf(h, "VIERNES");
        int iSab = IndexOf(h, "SABADO");
        int iDom = IndexOf(h, "DOMINGO");
        int iIni = IndexOf(h, "FECHA_INICIO");
        int iFin = IndexOf(h, "FECHA_FIN");

        foreach (var row in rows.Skip(1))
        {
            res.Add(new FilaCalendarioCsv(
                Get(row, iId),
                ParseInt(row, iLun),
                ParseInt(row, iMar),
                ParseInt(row, iMie),
                ParseInt(row, iJue),
                ParseInt(row, iVie),
                ParseInt(row, iSab),
                ParseInt(row, iDom),
                ParseDate(Get(row, iIni)),
                ParseDate(Get(row, iFin))));
        }

        return res;
    }

    /// <summary>
    /// Lee calendario excepciones.
    /// </summary>
    private static async Task<List<FilaCalendarioExcepcionCsv>> ReadCalendarioExcepcionesAsync(ZipArchiveEntry? entry, CancellationToken ct)
    {
        var rows = await ReadCsvRowsAsync(entry, ct);
        if (rows.Count == 0) return new();

        var h = rows[0];
        var res = new List<FilaCalendarioExcepcionCsv>(Math.Max(0, rows.Count - 1));

        int iId = IndexOf(h, "ID");
        int iFecha = IndexOf(h, "FECHA_EX");
        int iTipo = IndexOf(h, "TIPO_EXCEPCION");

        foreach (var row in rows.Skip(1))
        {
            res.Add(new FilaCalendarioExcepcionCsv(
                Get(row, iId),
                ParseDate(Get(row, iFecha)),
                ParseInt(row, iTipo)));
        }

        return res;
    }

    /// <summary>
    /// Lee expediciones.
    /// </summary>
    private static async Task<List<FilaExpedicionCsv>> ReadExpedicionesAsync(ZipArchiveEntry? entry, CancellationToken ct)
    {
        var rows = await ReadCsvRowsAsync(entry, ct);
        if (rows.Count == 0) return new();

        var h = rows[0];
        var res = new List<FilaExpedicionCsv>(Math.Max(0, rows.Count - 1));

        int iId = IndexOf(h, "ID");
        int iRuta = IndexOf(h, "ID_RUTA");
        int iDirDestino = IndexOf(h, "DIR_DESTINO");
        int iDireccion = IndexOf(h, "DIRECCION");
        int iCambio = IndexOf(h, "CAMBIO_EXP");
        int iPatron = IndexOf(h, "ID_PATRON_EXP");
        int iSalida = IndexOf(h, "H_SALIDA");
        int iCal = IndexOf(h, "ID_CAL");

        foreach (var row in rows.Skip(1))
        {
            res.Add(new FilaExpedicionCsv(
                Get(row, iId),
                Get(row, iRuta),
                Get(row, iDirDestino),
                ParseInt(row, iDireccion),
                Get(row, iCambio),
                Get(row, iPatron),
                ParseTime(Get(row, iSalida)),
                Get(row, iCal)));
        }

        return res;
    }

    /// <summary>
    /// Lee operadores.
    /// </summary>
    private static async Task<List<FilaOperadorCsv>> ReadOperadoresAsync(ZipArchiveEntry? entry, CancellationToken ct)
    {
        var rows = await ReadCsvRowsAsync(entry, ct);
        if (rows.Count == 0) return new();

        var h = rows[0];
        var res = new List<FilaOperadorCsv>(Math.Max(0, rows.Count - 1));

        int iId = IndexOf(h, "ID");
        int iNombre = IndexOf(h, "NOMBRE");
        int iTelefono = IndexOf(h, "TELEFONO");
        int iUrl = IndexOf(h, "URL");
        int iDescripcion = IndexOf(h, "DESCRIPCION");

        foreach (var row in rows.Skip(1))
        {
            res.Add(new FilaOperadorCsv(
                Get(row, iId),
                Get(row, iNombre),
                Get(row, iTelefono),
                Get(row, iUrl),
                Get(row, iDescripcion)));
        }

        return res;
    }

    /// <summary>
    /// Lee paradas.
    /// </summary>
    private static async Task<List<FilaParadaCsv>> ReadParadasAsync(ZipArchiveEntry? entry, CancellationToken ct)
    {
        var rows = await ReadCsvRowsAsync(entry, ct);
        if (rows.Count == 0) return new();

        var h = rows[0];
        var res = new List<FilaParadaCsv>(Math.Max(0, rows.Count - 1));

        int iId = IndexOf(h, "ID");
        int iCodigo = IndexOf(h, "CODIGO_SMS");
        int iNombre = IndexOf(h, "NOMBRE_PAR");
        int iLat = IndexOf(h, "LATITUD_Y_PAR");
        int iLon = IndexOf(h, "LONGITUD_X_PAR");
        int iLocalidad = IndexOf(h, "LOCALIDAD_PAR");
        int iZona = IndexOf(h, "ZONA_PAR");
        int iDescripcion = IndexOf(h, "DESCRIPCION_PAR");
        int iUrl = IndexOf(h, "URL_PAR");
        int iTipo = IndexOf(h, "TIPO_PAR");
        int iPadre = IndexOf(h, "ESTACION_PADRE");

        foreach (var row in rows.Skip(1))
        {
            res.Add(new FilaParadaCsv(
                ParseInt(row, iId),
                Get(row, iCodigo),
                Get(row, iNombre),
                ParseDouble(row, iLat),
                ParseDouble(row, iLon),
                Get(row, iLocalidad),
                Get(row, iZona),
                Get(row, iDescripcion),
                Get(row, iUrl),
                Get(row, iTipo),
                Get(row, iPadre)));
        }

        return res;
    }

    /// <summary>
    /// Lee patrones.
    /// </summary>
    private static async Task<List<FilaPatronCsv>> ReadPatronesAsync(ZipArchiveEntry? entry, CancellationToken ct)
    {
        var rows = await ReadCsvRowsAsync(entry, ct);
        if (rows.Count == 0) return new();

        var h = rows[0];
        var res = new List<FilaPatronCsv>(Math.Max(0, rows.Count - 1));

        int iId = IndexOf(h, "ID");
        int iLat = IndexOf(h, "PTO_LATITUD_Y");
        int iLon = IndexOf(h, "PTO_LONGITUD_X");
        int iSec = IndexOf(h, "PTO_SECUENCIA");
        int iDist = IndexOf(h, "DISTANCIA_RECORRIDA");

        foreach (var row in rows.Skip(1))
        {
            res.Add(new FilaPatronCsv(
                Get(row, iId),
                ParseDouble(row, iLat),
                ParseDouble(row, iLon),
                ParseInt(row, iSec),
                ParseDouble(row, iDist)));
        }

        return res;
    }

    /// <summary>
    /// Lee rutas.
    /// </summary>
    private static async Task<List<FilaRutaCsv>> ReadRutasAsync(ZipArchiveEntry? entry, CancellationToken ct)
    {
        var rows = await ReadCsvRowsAsync(entry, ct);
        if (rows.Count == 0) return new();

        var h = rows[0];
        var res = new List<FilaRutaCsv>(Math.Max(0, rows.Count - 1));

        int iId = IndexOf(h, "ID");
        int iAbreviatura = IndexOf(h, "ABREVIATURA");
        int iNombre = IndexOf(h, "NOMBRE");
        int iDescripcion = IndexOf(h, "DESCRIPCION");
        int iTipoVehiculo = IndexOf(h, "TIPO_VEHICULO");
        int iUrl = IndexOf(h, "URL");
        int iColor = IndexOf(h, "COLOR");

        foreach (var row in rows.Skip(1))
        {
            res.Add(new FilaRutaCsv(
                Get(row, iId),
                Get(row, iAbreviatura),
                Get(row, iNombre),
                Get(row, iDescripcion),
                Get(row, iTipoVehiculo),
                Get(row, iUrl),
                Get(row, iColor)));
        }

        return res;
    }

    /// <summary>
    /// Lee tarifas.
    /// </summary>
    private static async Task<List<FilaTarifaCsv>> ReadTarifasAsync(ZipArchiveEntry? entry, CancellationToken ct)
    {
        var rows = await ReadCsvRowsAsync(entry, ct);
        if (rows.Count == 0) return new();

        var h = rows[0];
        var res = new List<FilaTarifaCsv>(Math.Max(0, rows.Count - 1));

        int iId = IndexOf(h, "ID");
        int iTarRu = IndexOf(h, "ID_TAR_RU");
        int iParadaOrigen = IndexOf(h, "ID_PARADA_ORIGEN");
        int iParadaDestino = IndexOf(h, "ID_PARADA_DESTINO");
        int iZonaOrigen = IndexOf(h, "ID_ZONA_ORIGEN");
        int iZonaDestino = IndexOf(h, "ID_ZONA_DESTINO");
        int iPrecio = IndexOf(h, "PRECIO");
        int iTipoPago = IndexOf(h, "TIPO_PAGO");
        int iTiempoTransbordo = IndexOf(h, "TIEMPO_TRANSBORDO");
        int iTransbordos = IndexOf(h, "TRANSBORDOS");

        foreach (var row in rows.Skip(1))
        {
            res.Add(new FilaTarifaCsv(
                Get(row, iId),
                Get(row, iTarRu),
                Get(row, iParadaOrigen),
                Get(row, iParadaDestino),
                Get(row, iZonaOrigen),
                Get(row, iZonaDestino),
                ParseNullableDecimal(row, iPrecio),
                Get(row, iTipoPago),
                ParseNullableInt(row, iTiempoTransbordo),
                ParseNullableInt(row, iTransbordos)));
        }

        return res;
    }

    /// <summary>
    /// Lee tiempos parada.
    /// </summary>
    private static async Task<List<FilaTiempoParadaCsv>> ReadTiemposParadaAsync(ZipArchiveEntry? entry, CancellationToken ct)
    {
        var rows = await ReadCsvRowsAsync(entry, ct);
        if (rows.Count == 0) return new();

        var h = rows[0];
        var res = new List<FilaTiempoParadaCsv>(Math.Max(0, rows.Count - 1));

        int iExp = IndexOf(h, "ID_EXP");
        int iLlegada = IndexOf(h, "HORA_LLEGADA");
        int iSalida = IndexOf(h, "HORA_SALIDA");
        int iDistancia = IndexOf(h, "DISTANCIA_SIGUIENTE_PARADA");
        int iParada = IndexOf(h, "ID_TIEMPO_PAR");
        int iSec = IndexOf(h, "SEC_PAR");
        int iSubida = IndexOf(h, "TIPO_SUBIDA_PAR");
        int iBajada = IndexOf(h, "TIPO_BAJADA_PAR");
        int iDestino = IndexOf(h, "NUEVA_DIR_DESTINO_PAR");

        foreach (var row in rows.Skip(1))
        {
            res.Add(new FilaTiempoParadaCsv(
                Get(row, iExp),
                ParseTime(Get(row, iLlegada)),
                ParseTime(Get(row, iSalida)),
                ParseDouble(row, iDistancia),
                ParseInt(row, iParada),
                ParseInt(row, iSec),
                Get(row, iSubida),
                Get(row, iBajada),
                Get(row, iDestino)));
        }

        return res;
    }

    /// <summary>
    /// Lee tiempos reales.
    /// </summary>
    private static async Task<List<FilaTiempoRealCsv>> ReadTiemposRealesAsync(ZipArchiveEntry? entry, CancellationToken ct)
    {
        var rows = await ReadCsvRowsAsync(entry, ct);
        if (rows.Count == 0) return new();

        var h = rows[0];
        var res = new List<FilaTiempoRealCsv>(Math.Max(0, rows.Count - 1));

        int iIdRu = IndexOf(h, "ID_RU");
        int iServicio = IndexOf(h, "ID_SERVICIO");
        int iTrans = IndexOf(h, "ID_TRANS");
        int iLat = IndexOf(h, "LATITUD_Y");
        int iLon = IndexOf(h, "LONGITUD_X");
        int iHoraEmision = IndexOf(h, "HORA_EMISION");
        int iAntPar = IndexOf(h, "ID_ANT_PAR");
        int iFechaPaso = IndexOf(h, "FECHA_HORA_PASO");
        int iSalida = IndexOf(h, "H_SALIDA");
        int iDiff = IndexOf(h, "MINUTOS_DE_DIFERENCIA");
        int iTeorico = IndexOf(h, "TEORICO");

        foreach (var row in rows.Skip(1))
        {
            res.Add(new FilaTiempoRealCsv(
                Get(row, iIdRu),
                Get(row, iServicio),
                Get(row, iTrans),
                ParseDouble(row, iLat),
                ParseDouble(row, iLon),
                Get(row, iHoraEmision),
                Get(row, iAntPar),
                Get(row, iFechaPaso),
                ParseTime(Get(row, iSalida)),
                ParseNullableInt(row, iDiff),
                Get(row, iTeorico)));
        }

        return res;
    }

    /// <summary>
    /// Lee transbordos.
    /// </summary>
    private static async Task<List<FilaTransbordoCsv>> ReadTransbordosAsync(ZipArchiveEntry? entry, CancellationToken ct)
    {
        var rows = await ReadCsvRowsAsync(entry, ct);
        if (rows.Count == 0) return new();

        var h = rows[0];
        var res = new List<FilaTransbordoCsv>(Math.Max(0, rows.Count - 1));

        int iRuta1 = IndexOf(h, "ID_RUTA_1");
        int iRuta2 = IndexOf(h, "ID_RUTA_2");

        foreach (var row in rows.Skip(1))
        {
            res.Add(new FilaTransbordoCsv(Get(row, iRuta1), Get(row, iRuta2)));
        }

        return res;
    }

    /// <summary>
    /// Lee vehiculos.
    /// </summary>
    private static async Task<List<FilaVehiculoCsv>> ReadVehiculosAsync(ZipArchiveEntry? entry, CancellationToken ct)
    {
        var rows = await ReadCsvRowsAsync(entry, ct);
        if (rows.Count == 0) return new();

        var h = rows[0];
        var res = new List<FilaVehiculoCsv>(Math.Max(0, rows.Count - 1));

        int iId = IndexOf(h, "ID");
        int iTipo = IndexOf(h, "ID_TIPO");
        int iDescripcion = IndexOf(h, "DESCRIPCION_TIPO");
        int iCapacidad = IndexOf(h, "CAPACIDAD_MAXIMA");
        int iAsientos = IndexOf(h, "ASIENTOS");
        int iPuertas = IndexOf(h, "NUM_PUERTAS");
        int iAforo = IndexOf(h, "AFORO_ACTUAL");

        foreach (var row in rows.Skip(1))
        {
            res.Add(new FilaVehiculoCsv(
                Get(row, iId),
                Get(row, iTipo),
                Get(row, iDescripcion),
                ParseNullableInt(row, iCapacidad),
                ParseNullableInt(row, iAsientos),
                ParseNullableInt(row, iPuertas),
                ParseNullableInt(row, iAforo)));
        }

        return res;
    }
}
