using System.Globalization;
using System.Text.Json;
using RutasDBUS.Modelos.Historial;

namespace RutasDBUS.Servicios.Historial;

/// <summary>
/// Mantiene un historial compartido de escenarios de origen/destino para toda la app.
/// </summary>
public sealed class ServicioHistorialRutasCompartido : IHistorialRutasCompartidoService
{
    private const int MaxItemsCompartidos = 100;
    private const int IntentosAccesoFichero = 5;
    private static readonly JsonSerializerOptions JsonOptions = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        WriteIndented = true
    };

    private readonly SemaphoreSlim _mutex = new(1, 1);
    private readonly string _rutaFichero;

    public ServicioHistorialRutasCompartido(IWebHostEnvironment env)
    {
        _rutaFichero = ResolverRutaFichero(env);
    }

    public async Task<IReadOnlyList<RegistroHistorialRuta>> ObtenerAsync(CancellationToken ct = default)
    {
        await _mutex.WaitAsync(ct);
        try
        {
            var items = await LeerInternoAsync(ct);
            return items
                .OrderByDescending(x => x.FechaActualizacionUtc)
                .Select(Copiar)
                .ToList();
        }
        finally
        {
            _mutex.Release();
        }
    }

    public async Task<RegistroHistorialRuta> GuardarAsync(RegistroHistorialRuta registro, CancellationToken ct = default)
    {
        ArgumentNullException.ThrowIfNull(registro);

        await _mutex.WaitAsync(ct);
        try
        {
            var items = await LeerInternoAsync(ct);
            var clave = CalcularClaveEscenario(registro);
            var ahora = DateTime.UtcNow;
            var fechaCreacionEntrada = registro.FechaCreacionUtc != default
                ? registro.FechaCreacionUtc
                : ahora;
            var fechaActualizacionEntrada = registro.FechaActualizacionUtc != default
                ? registro.FechaActualizacionUtc
                : fechaCreacionEntrada;

            var existente = items.FirstOrDefault(x =>
                string.Equals(CalcularClaveEscenario(x), clave, StringComparison.OrdinalIgnoreCase));

            if (existente is null)
            {
                existente = Copiar(registro);
                existente.Id = string.IsNullOrWhiteSpace(existente.Id)
                    ? Guid.NewGuid().ToString("N")
                    : existente.Id;
                existente.FechaCreacionUtc = fechaCreacionEntrada;
                existente.FechaActualizacionUtc = fechaActualizacionEntrada;
                items.Insert(0, existente);
            }
            else
            {
                existente.OrigenTexto = registro.OrigenTexto;
                existente.DestinoTexto = registro.DestinoTexto;
                existente.OrigenLatitud = registro.OrigenLatitud;
                existente.OrigenLongitud = registro.OrigenLongitud;
                existente.DestinoLatitud = registro.DestinoLatitud;
                existente.DestinoLongitud = registro.DestinoLongitud;
                existente.UsarHorariosTeoricos = registro.UsarHorariosTeoricos;
                existente.UsarHoraSimulada = registro.UsarHoraSimulada;
                existente.FechaSimulada = registro.FechaSimulada;
                existente.HoraSimulada = registro.HoraSimulada;
                existente.CriterioPrioritarioRuta = registro.CriterioPrioritarioRuta;
                existente.PreferenciaEtiquetaRuta = registro.PreferenciaEtiquetaRuta;
                existente.FactorEquilibradoAndarBus = registro.FactorEquilibradoAndarBus;
                existente.ActivarFiltroLineas = registro.ActivarFiltroLineas;
                existente.LineasPermitidas = NormalizarLineas(registro.LineasPermitidas).ToList();

                if (registro.FechaCreacionUtc != default)
                    existente.FechaCreacionUtc = registro.FechaCreacionUtc;
                if (registro.FechaActualizacionUtc != default)
                    existente.FechaActualizacionUtc = registro.FechaActualizacionUtc;
            }

            var finales = items
                .OrderByDescending(x => x.FechaActualizacionUtc)
                .Take(MaxItemsCompartidos)
                .ToList();

            await EscribirInternoAsync(finales, ct);
            return Copiar(existente);
        }
        finally
        {
            _mutex.Release();
        }
    }

    public async Task<bool> EliminarAsync(string id, CancellationToken ct = default)
    {
        if (string.IsNullOrWhiteSpace(id))
            return false;

        await _mutex.WaitAsync(ct);
        try
        {
            var items = await LeerInternoAsync(ct);
            var eliminados = items.RemoveAll(x => string.Equals(x.Id, id, StringComparison.OrdinalIgnoreCase));
            if (eliminados == 0)
                return false;

            await EscribirInternoAsync(items, ct);
            return true;
        }
        finally
        {
            _mutex.Release();
        }
    }

    private async Task<List<RegistroHistorialRuta>> LeerInternoAsync(CancellationToken ct)
    {
        if (!File.Exists(_rutaFichero))
            return new List<RegistroHistorialRuta>();

        return await EjecutarConReintentosAsync(async () =>
        {
            await using var stream = new FileStream(
                _rutaFichero,
                FileMode.Open,
                FileAccess.Read,
                FileShare.ReadWrite | FileShare.Delete,
                4096,
                useAsync: true);

            var items = await JsonSerializer.DeserializeAsync<List<RegistroHistorialRuta>>(stream, JsonOptions, ct);
            return items ?? new List<RegistroHistorialRuta>();
        }, ct);
    }

    private async Task EscribirInternoAsync(List<RegistroHistorialRuta> items, CancellationToken ct)
    {
        var carpeta = Path.GetDirectoryName(_rutaFichero);
        if (!string.IsNullOrWhiteSpace(carpeta))
            Directory.CreateDirectory(carpeta);

        await EjecutarConReintentosAsync(async () =>
        {
            var temporal = Path.Combine(
                carpeta ?? Path.GetTempPath(),
                $"{Path.GetFileName(_rutaFichero)}.{Guid.NewGuid():N}.tmp");

            try
            {
                await using (var stream = new FileStream(
                                 temporal,
                                 FileMode.CreateNew,
                                 FileAccess.Write,
                                 FileShare.None,
                                 4096,
                                 useAsync: true))
                {
                    await JsonSerializer.SerializeAsync(stream, items, JsonOptions, ct);
                }

                File.Move(temporal, _rutaFichero, overwrite: true);
            }
            finally
            {
                TryDelete(temporal);
            }
        }, ct);
    }

    private static async Task<T> EjecutarConReintentosAsync<T>(Func<Task<T>> accion, CancellationToken ct)
    {
        for (var intento = 1; ; intento++)
        {
            try
            {
                return await accion();
            }
            catch (IOException) when (intento < IntentosAccesoFichero)
            {
                await Task.Delay(40 * intento, ct);
            }
            catch (UnauthorizedAccessException) when (intento < IntentosAccesoFichero)
            {
                await Task.Delay(40 * intento, ct);
            }
        }
    }

    private static async Task EjecutarConReintentosAsync(Func<Task> accion, CancellationToken ct)
    {
        await EjecutarConReintentosAsync(async () =>
        {
            await accion();
            return true;
        }, ct);
    }

    private static void TryDelete(string ruta)
    {
        try
        {
            if (File.Exists(ruta))
                File.Delete(ruta);
        }
        catch
        {
        }
    }

    private static string ResolverRutaFichero(IWebHostEnvironment env)
    {
        foreach (var carpeta in EnumerarCarpetasCandidatas(env))
        {
            if (TryEnsureWritableDirectory(carpeta))
                return Path.Combine(carpeta, "historial-rutas-compartido.json");
        }

        return Path.Combine(env.ContentRootPath, "Data", "historial-rutas-compartido.json");
    }

    private static IEnumerable<string> EnumerarCarpetasCandidatas(IWebHostEnvironment env)
    {
        yield return Path.Combine(env.ContentRootPath, "Data");

        var commonAppData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
        if (!string.IsNullOrWhiteSpace(commonAppData))
            yield return Path.Combine(commonAppData, "RutasDBUS");

        var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
        if (!string.IsNullOrWhiteSpace(localAppData))
            yield return Path.Combine(localAppData, "RutasDBUS");

        yield return Path.Combine(Path.GetTempPath(), "RutasDBUS");
    }

    private static bool TryEnsureWritableDirectory(string carpeta)
    {
        try
        {
            Directory.CreateDirectory(carpeta);
            var rutaPrueba = Path.Combine(carpeta, $".write-test-{Guid.NewGuid():N}.tmp");

            using (var stream = new FileStream(
                       rutaPrueba,
                       FileMode.CreateNew,
                       FileAccess.Write,
                       FileShare.None,
                       256,
                       FileOptions.DeleteOnClose))
            {
                stream.WriteByte(1);
            }

            if (File.Exists(rutaPrueba))
                File.Delete(rutaPrueba);

            return true;
        }
        catch
        {
            return false;
        }
    }

    private static RegistroHistorialRuta Copiar(RegistroHistorialRuta item)
        => new()
        {
            Id = item.Id,
            OrigenTexto = item.OrigenTexto,
            DestinoTexto = item.DestinoTexto,
            OrigenLatitud = item.OrigenLatitud,
            OrigenLongitud = item.OrigenLongitud,
            DestinoLatitud = item.DestinoLatitud,
            DestinoLongitud = item.DestinoLongitud,
            UsarHorariosTeoricos = item.UsarHorariosTeoricos,
            UsarHoraSimulada = item.UsarHoraSimulada,
            FechaSimulada = item.FechaSimulada,
            HoraSimulada = item.HoraSimulada,
            CriterioPrioritarioRuta = item.CriterioPrioritarioRuta,
            PreferenciaEtiquetaRuta = item.PreferenciaEtiquetaRuta,
            FactorEquilibradoAndarBus = item.FactorEquilibradoAndarBus,
            ActivarFiltroLineas = item.ActivarFiltroLineas,
            LineasPermitidas = NormalizarLineas(item.LineasPermitidas).ToList(),
            FechaCreacionUtc = item.FechaCreacionUtc,
            FechaActualizacionUtc = item.FechaActualizacionUtc
        };

    private static string CalcularClaveEscenario(RegistroHistorialRuta item)
    {
        var origenLat = Math.Round(item.OrigenLatitud, 6).ToString("0.000000", CultureInfo.InvariantCulture);
        var origenLon = Math.Round(item.OrigenLongitud, 6).ToString("0.000000", CultureInfo.InvariantCulture);
        var destinoLat = Math.Round(item.DestinoLatitud, 6).ToString("0.000000", CultureInfo.InvariantCulture);
        var destinoLon = Math.Round(item.DestinoLongitud, 6).ToString("0.000000", CultureInfo.InvariantCulture);
        var fecha = item.UsarHoraSimulada ? item.FechaSimulada?.Trim() ?? string.Empty : string.Empty;
        var hora = item.UsarHoraSimulada ? item.HoraSimulada?.Trim() ?? string.Empty : string.Empty;
        var criterio = NormalizarTextoClave(item.CriterioPrioritarioRuta);
        if (string.IsNullOrWhiteSpace(criterio))
            criterio = NormalizarTextoClave(item.PreferenciaEtiquetaRuta);
        var factorEquilibrado = item.FactorEquilibradoAndarBus is { } factor && double.IsFinite(factor)
            ? Math.Max(0, factor).ToString("0.######", CultureInfo.InvariantCulture)
            : string.Empty;
        var lineas = item.ActivarFiltroLineas
            ? string.Join(",", NormalizarLineas(item.LineasPermitidas))
            : string.Empty;

        return string.Join("|",
            origenLat,
            origenLon,
            destinoLat,
            destinoLon,
            item.UsarHorariosTeoricos ? "1" : "0",
            item.UsarHoraSimulada ? "1" : "0",
            fecha,
            hora,
            criterio,
            factorEquilibrado,
            item.ActivarFiltroLineas ? "1" : "0",
            lineas);
    }

    private static IReadOnlyList<int> NormalizarLineas(IEnumerable<int>? lineas)
        => (lineas ?? Array.Empty<int>())
            .Where(linea => linea > 0)
            .Distinct()
            .OrderBy(linea => linea)
            .ToList();

    private static string NormalizarTextoClave(string? texto)
        => texto?.Trim().ToUpperInvariant() ?? string.Empty;
}
