using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Options;
using RutasDBUS.Modelos.Importacion;
using RutasDBUS.Modelos.RedBus;

namespace RutasDBUS.Servicios.Importacion;

public sealed class ServicioArchivoImportacionDbusMonitorizado : IServicioArchivoImportacionDbus
{
    private const long MaxModeloSubidoBytes = 250L * 1024 * 1024;

    private readonly IWebHostEnvironment _entorno;
    private readonly IOptions<OpcionesRedBus> _opciones;
    private readonly SemaphoreSlim _semaforo = new(1, 1);

    private ArchivoImportacionDbus? _archivo;
    private string? _rutaZipCargada;
    private DateTime _fechaZipCargadaUtc;
    private string? _carpetaDatos;

    public ServicioArchivoImportacionDbusMonitorizado(
        IWebHostEnvironment entorno,
        IOptions<OpcionesRedBus> opciones)
    {
        _entorno = entorno;
        _opciones = opciones;
    }

    public async Task<ArchivoImportacionDbus> ObtenerAsync(CancellationToken cancellationToken = default)
    {
        var rutaZip = ResolverRutaZip();
        if (rutaZip is null || !File.Exists(rutaZip))
            throw new FileNotFoundException("No se ha encontrado el ZIP de importacion de dBUS.");

        var infoZip = new FileInfo(rutaZip);
        if (_archivo is not null &&
            string.Equals(_rutaZipCargada, infoZip.FullName, StringComparison.OrdinalIgnoreCase) &&
            _fechaZipCargadaUtc == infoZip.LastWriteTimeUtc)
        {
            return _archivo;
        }

        await _semaforo.WaitAsync(cancellationToken);
        try
        {
            if (_archivo is not null &&
                string.Equals(_rutaZipCargada, infoZip.FullName, StringComparison.OrdinalIgnoreCase) &&
                _fechaZipCargadaUtc == infoZip.LastWriteTimeUtc)
            {
                return _archivo;
            }

            var importador = new ServicioArchivoImportacionDbus(_entorno, _opciones);
            _archivo = await importador.ObtenerDesdeRutaAsync(infoZip.FullName, cancellationToken);
            _rutaZipCargada = infoZip.FullName;
            _fechaZipCargadaUtc = infoZip.LastWriteTimeUtc;
            return _archivo;
        }
        finally
        {
            _semaforo.Release();
        }
    }

    public Task<IReadOnlyList<ModeloHorarioDbusInfo>> ListarModelosAsync(
        CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();

        var rutaActiva = ResolverRutaZip();
        var modelos = EnumerarRutasModelos()
            .Select(modelo => CrearInfoModelo(modelo.Ruta, modelo.Origen, rutaActiva))
            .OrderByDescending(modelo => modelo.Activo)
            .ThenByDescending(modelo => modelo.FechaUtc)
            .ThenBy(modelo => modelo.NombreArchivo, StringComparer.OrdinalIgnoreCase)
            .ToList();

        return Task.FromResult<IReadOnlyList<ModeloHorarioDbusInfo>>(modelos);
    }

    public async Task<ModeloHorarioDbusInfo?> ObtenerModeloActivoAsync(
        CancellationToken cancellationToken = default)
    {
        var modelos = await ListarModelosAsync(cancellationToken);
        return modelos.FirstOrDefault(modelo => modelo.Activo);
    }

    public async Task SeleccionarModeloAsync(
        string idModelo,
        CancellationToken cancellationToken = default)
    {
        if (string.IsNullOrWhiteSpace(idModelo))
            throw new ArgumentException("No se ha indicado el modelo de horarios.", nameof(idModelo));

        var modelo = EnumerarRutasModelos()
            .FirstOrDefault(x => string.Equals(CrearIdModelo(x.Ruta), idModelo, StringComparison.OrdinalIgnoreCase));

        if (string.IsNullOrWhiteSpace(modelo.Ruta) || !File.Exists(modelo.Ruta))
            throw new FileNotFoundException("No se ha encontrado el modelo de horarios seleccionado.");

        await ValidarModeloAsync(modelo.Ruta, cancellationToken);
        await SeleccionarRutaModeloAsync(modelo.Ruta, cancellationToken);
    }

    public async Task<ModeloHorarioDbusInfo> GuardarModeloSubidoAsync(
        string nombreArchivo,
        Stream contenido,
        CancellationToken cancellationToken = default)
    {
        if (contenido is null)
            throw new ArgumentNullException(nameof(contenido));

        var nombreSeguro = CrearNombreArchivoSeguro(nombreArchivo);
        var carpetaDestino = ResolverCarpetaModelosSubidos(crear: true);
        var rutaDestino = ResolverRutaDestinoUnica(carpetaDestino, nombreSeguro);

        await CopiarStreamLimitadoAsync(contenido, rutaDestino, MaxModeloSubidoBytes, cancellationToken);

        try
        {
            await ValidarModeloAsync(rutaDestino, cancellationToken);
        }
        catch
        {
            try
            {
                if (File.Exists(rutaDestino))
                    File.Delete(rutaDestino);
            }
            catch
            {
            }

            throw;
        }

        await SeleccionarRutaModeloAsync(rutaDestino, cancellationToken);

        return CrearInfoModelo(rutaDestino, "Subido", rutaDestino);
    }

    private async Task ValidarModeloAsync(string rutaZip, CancellationToken cancellationToken)
    {
        var importador = new ServicioArchivoImportacionDbus(_entorno, _opciones);
        await importador.ObtenerDesdeRutaAsync(rutaZip, cancellationToken);
    }

    private async Task SeleccionarRutaModeloAsync(string rutaZip, CancellationToken cancellationToken)
    {
        var rutaCompleta = Path.GetFullPath(rutaZip);
        if (!File.Exists(rutaCompleta))
            throw new FileNotFoundException("No se ha encontrado el modelo de horarios seleccionado.", rutaCompleta);

        await _semaforo.WaitAsync(cancellationToken);
        try
        {
            GuardarRutaActiva(rutaCompleta);
            InvalidarCacheArchivo();
        }
        finally
        {
            _semaforo.Release();
        }
    }

    private void InvalidarCacheArchivo()
    {
        _archivo = null;
        _rutaZipCargada = null;
        _fechaZipCargadaUtc = default;
    }

    private string? ResolverRutaZip()
    {
        var rutaActiva = LeerRutaActiva();
        if (!string.IsNullOrWhiteSpace(rutaActiva) && File.Exists(rutaActiva))
            return Path.GetFullPath(rutaActiva);

        var opciones = _opciones.Value ?? new OpcionesRedBus();
        if (!string.IsNullOrWhiteSpace(opciones.ArchivoDatos))
        {
            var configurado = Path.Combine(
                _entorno.ContentRootPath,
                opciones.ArchivoDatos.Replace('/', Path.DirectorySeparatorChar));

            if (File.Exists(configurado))
                return Path.GetFullPath(configurado);
        }

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

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

    private IEnumerable<(string Ruta, string Origen)> EnumerarRutasModelos()
    {
        var vistos = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

        foreach (var ruta in EnumerarZips(Path.Combine(_entorno.ContentRootPath, "Modelos", "Horarios")))
        {
            if (vistos.Add(ruta))
                yield return (ruta, "Incluido");
        }

        foreach (var ruta in EnumerarZips(ResolverCarpetaModelosSubidos(crear: false)))
        {
            if (vistos.Add(ruta))
                yield return (ruta, "Subido");
        }
    }

    private static IEnumerable<string> EnumerarZips(string carpeta)
    {
        if (string.IsNullOrWhiteSpace(carpeta) || !Directory.Exists(carpeta))
            yield break;

        foreach (var ruta in Directory.GetFiles(carpeta, "*.zip", SearchOption.TopDirectoryOnly))
            yield return Path.GetFullPath(ruta);
    }

    private ModeloHorarioDbusInfo CrearInfoModelo(string ruta, string origen, string? rutaActiva)
    {
        var info = new FileInfo(ruta);
        return new ModeloHorarioDbusInfo(
            CrearIdModelo(info.FullName),
            info.Name,
            CrearRutaMostrable(info.FullName),
            origen,
            info.Length,
            info.LastWriteTimeUtc,
            !string.IsNullOrWhiteSpace(rutaActiva) &&
            string.Equals(Path.GetFullPath(rutaActiva), info.FullName, StringComparison.OrdinalIgnoreCase));
    }

    private static string CrearIdModelo(string ruta)
    {
        var rutaNormalizada = Path.GetFullPath(ruta).ToUpperInvariant();
        var hash = SHA256.HashData(Encoding.UTF8.GetBytes(rutaNormalizada));
        return Convert.ToHexString(hash);
    }

    private string CrearRutaMostrable(string ruta)
    {
        try
        {
            var relativa = Path.GetRelativePath(_entorno.ContentRootPath, ruta);
            return relativa.StartsWith("..", StringComparison.Ordinal)
                ? ruta
                : relativa.Replace('\\', '/');
        }
        catch
        {
            return ruta;
        }
    }

    private string ResolverCarpetaDatos()
    {
        if (!string.IsNullOrWhiteSpace(_carpetaDatos))
            return _carpetaDatos;

        var candidatas = new[]
        {
            Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "RutasDBUS"),
            Path.Combine(_entorno.ContentRootPath, "App_Data"),
            Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "RutasDBUS"),
            Path.Combine(Path.GetTempPath(), "RutasDBUS")
        };

        foreach (var candidata in candidatas.Where(x => !string.IsNullOrWhiteSpace(x)))
        {
            try
            {
                Directory.CreateDirectory(candidata);
                var prueba = Path.Combine(candidata, $".write-test-{Guid.NewGuid():N}.tmp");
                File.WriteAllText(prueba, "ok", Encoding.UTF8);
                File.Delete(prueba);
                _carpetaDatos = candidata;
                return _carpetaDatos;
            }
            catch
            {
            }
        }

        throw new IOException("No se ha encontrado una carpeta escribible para guardar modelos de horarios.");
    }

    private string ResolverCarpetaModelosSubidos(bool crear)
    {
        var carpeta = Path.Combine(ResolverCarpetaDatos(), "ModelosHorarios");
        if (crear)
            Directory.CreateDirectory(carpeta);
        return carpeta;
    }

    private string RutaEstadoActivo => Path.Combine(ResolverCarpetaDatos(), "modelo-horarios-activo.json");

    private string? LeerRutaActiva()
    {
        try
        {
            if (!File.Exists(RutaEstadoActivo))
                return null;

            var json = File.ReadAllText(RutaEstadoActivo, Encoding.UTF8);
            var estado = JsonSerializer.Deserialize<EstadoModeloHorario>(json);
            return estado?.Ruta;
        }
        catch
        {
            return null;
        }
    }

    private void GuardarRutaActiva(string ruta)
    {
        var estado = new EstadoModeloHorario { Ruta = Path.GetFullPath(ruta) };
        var json = JsonSerializer.Serialize(estado, new JsonSerializerOptions { WriteIndented = true });
        File.WriteAllText(RutaEstadoActivo, json, Encoding.UTF8);
    }

    private static string CrearNombreArchivoSeguro(string nombreArchivo)
    {
        var nombre = Path.GetFileName(nombreArchivo);
        if (string.IsNullOrWhiteSpace(nombre))
            nombre = "modelo_dbus.zip";

        if (!string.Equals(Path.GetExtension(nombre), ".zip", StringComparison.OrdinalIgnoreCase))
            throw new InvalidOperationException("Solo se pueden subir archivos .zip.");

        var baseNombre = Path.GetFileNameWithoutExtension(nombre);
        if (string.IsNullOrWhiteSpace(baseNombre))
            baseNombre = "modelo_dbus";

        var invalidos = Path.GetInvalidFileNameChars();
        var limpio = new string(baseNombre
            .Select(ch => invalidos.Contains(ch) ? '_' : ch)
            .ToArray())
            .Trim();

        if (string.IsNullOrWhiteSpace(limpio))
            limpio = "modelo_dbus";

        return limpio + ".zip";
    }

    private static string ResolverRutaDestinoUnica(string carpeta, string nombreArchivo)
    {
        var ruta = Path.Combine(carpeta, nombreArchivo);
        if (!File.Exists(ruta))
            return ruta;

        var baseNombre = Path.GetFileNameWithoutExtension(nombreArchivo);
        var extension = Path.GetExtension(nombreArchivo);
        var marca = DateTime.Now.ToString("yyyyMMdd_HHmmss", CultureInfo.InvariantCulture);

        for (var i = 1; i <= 999; i++)
        {
            var candidato = Path.Combine(carpeta, $"{baseNombre}_{marca}_{i}{extension}");
            if (!File.Exists(candidato))
                return candidato;
        }

        return Path.Combine(carpeta, $"{baseNombre}_{Guid.NewGuid():N}{extension}");
    }

    private static async Task CopiarStreamLimitadoAsync(
        Stream origen,
        string rutaDestino,
        long maxBytes,
        CancellationToken cancellationToken)
    {
        var total = 0L;

        try
        {
            await using var destino = new FileStream(
                rutaDestino,
                FileMode.CreateNew,
                FileAccess.Write,
                FileShare.None,
                bufferSize: 81920,
                useAsync: true);

            var buffer = new byte[81920];
            while (true)
            {
                var leidos = await origen.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken);
                if (leidos == 0)
                    break;

                total += leidos;
                if (total > maxBytes)
                    throw new InvalidOperationException("El ZIP supera el tamano maximo permitido.");

                await destino.WriteAsync(buffer.AsMemory(0, leidos), cancellationToken);
            }
        }
        catch
        {
            try
            {
                if (File.Exists(rutaDestino))
                    File.Delete(rutaDestino);
            }
            catch
            {
            }

            throw;
        }
    }

    private sealed class EstadoModeloHorario
    {
        public string? Ruta { get; set; }
    }
}
