using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using ApiDenuncias.Helpers;
using GestionaDenuncias.Shared.Models;
using ApiDenuncias.Services;
using Microsoft.AspNetCore.DataProtection;

namespace ApiDenuncias.Services;

public sealed class EncryptedDenunciaStore : IDenunciaStore, IFilteredDenunciaStore
{
    private const string DataProtectionStringPrefix = "enc:v1:";
    private const string KeyVaultStringPrefix = "enc:v2:";
    private static readonly byte[] DataProtectionBytesPrefix = Encoding.ASCII.GetBytes("enc:v1:");
    private static readonly byte[] KeyVaultBytesPrefix = Encoding.ASCII.GetBytes("enc:v2:");
    private static readonly byte[] KeyVaultRawBytesPrefix = Encoding.ASCII.GetBytes("enc:v3:");
    private const int AesGcmNonceSize = 12;
    private const int AesGcmTagSize = 16;
    private static readonly PropertyInfo[] ComplaintProperties = typeof(DenunciasGestiona)
        .GetProperties(BindingFlags.Public | BindingFlags.Instance)
        .Where(property => property.CanRead && property.CanWrite)
        .ToArray();

    private readonly MySqlDenunciaStore _inner;
    private readonly IEnvelopeEncryptionKeyProvider _envelopeKeyProvider;
    private readonly IEncryptionKeyProvider _legacyKeyProvider;
    private readonly IDataProtector _protector;
    private readonly ILogger<EncryptedDenunciaStore> _logger;

    public EncryptedDenunciaStore(
        MySqlDenunciaStore inner,
        IEnvelopeEncryptionKeyProvider envelopeKeyProvider,
        IEncryptionKeyProvider legacyKeyProvider,
        IDataProtectionProvider dataProtectionProvider,
        ILogger<EncryptedDenunciaStore> logger)
    {
        _inner = inner;
        _envelopeKeyProvider = envelopeKeyProvider;
        _legacyKeyProvider = legacyKeyProvider;
        _protector = dataProtectionProvider.CreateProtector("ApiDenuncias.DatabaseSensitiveData.v1");
        _logger = logger;
    }

    public Task EnsureSchemaAsync(CancellationToken cancellationToken = default)
        => _inner.EnsureSchemaAsync(cancellationToken);

    public async Task<List<DenunciasGestiona>> GetAllDenunciasAsync(CancellationToken cancellationToken = default)
    {
        var denuncias = await _inner.GetAllDenunciasAsync(cancellationToken);
        return await UnprotectComplaintsAsync(denuncias, skipPurgedRows: true, cancellationToken);
    }

    public async Task<List<DenunciasGestiona>> GetDenunciasByScopeAsync(
        DenunciaListScope scope,
        CancellationToken cancellationToken = default)
    {
        var denuncias = await _inner.GetDenunciasByScopeAsync(scope, cancellationToken);
        return await UnprotectComplaintsAsync(denuncias, skipPurgedRows: true, cancellationToken);
    }

    public async Task<List<DenunciasGestiona>> GetDenunciasByIdsAsync(
        IReadOnlyCollection<int> denunciaIds,
        CancellationToken cancellationToken = default)
    {
        var denuncias = await _inner.GetDenunciasByIdsAsync(denunciaIds, cancellationToken);
        return await UnprotectComplaintsAsync(denuncias, skipPurgedRows: true, cancellationToken);
    }

    public async Task<List<DenunciasGestiona>> GetDenunciasByIdsAsync(
        IReadOnlyCollection<int> denunciaIds,
        DenunciaListScope scope,
        CancellationToken cancellationToken = default)
    {
        var denuncias = await _inner.GetDenunciasByIdsAsync(denunciaIds, scope, cancellationToken);
        return await UnprotectComplaintsAsync(denuncias, skipPurgedRows: true, cancellationToken);
    }

    private async Task<List<DenunciasGestiona>> UnprotectComplaintsAsync(
        List<DenunciasGestiona> denuncias,
        bool skipPurgedRows,
        CancellationToken cancellationToken)
    {
        var result = new List<DenunciasGestiona>(denuncias.Count);
        var requestKeyCache = new Dictionary<DateOnly, byte[]>();
        foreach (var denuncia in denuncias)
        {
            try
            {
                result.Add(await UnprotectComplaintAsync(denuncia, requestKeyCache, cancellationToken));
            }
            catch (EncryptedDataPurgedException ex) when (skipPurgedRows)
            {
                _logger.LogWarning(
                    "Se omite la denuncia {DenunciaId} en el listado porque sus datos estan purgados para la clave diaria {KeyDate}.",
                    denuncia.Id_Denuncia,
                    ex.KeyDate);
            }
        }

        return result;
    }

    public async Task<List<FicherosDenuncias>> GetAllFicherosAsync(CancellationToken cancellationToken = default)
    {
        var ficheros = await _inner.GetAllFicherosAsync(cancellationToken);
        return await UnprotectAttachmentsAsync(ficheros, skipPurgedRows: true, cancellationToken);
    }

    public async Task<List<FicherosDenuncias>> GetFicherosByDenunciaIdsAsync(
        IReadOnlyCollection<int> denunciaIds,
        CancellationToken cancellationToken = default)
    {
        var ficheros = await _inner.GetFicherosByDenunciaIdsAsync(denunciaIds, cancellationToken);
        return await UnprotectAttachmentsAsync(ficheros, skipPurgedRows: true, cancellationToken);
    }

    public async Task<List<FicherosDenuncias>> GetFicherosByDenunciaAsync(int denunciaId, CancellationToken cancellationToken = default)
    {
        var ficheros = await _inner.GetFicherosByDenunciaAsync(denunciaId, cancellationToken);
        return await UnprotectAttachmentsAsync(ficheros, skipPurgedRows: false, cancellationToken);
    }

    public Task<List<FicherosDenuncias>> GetFicherosMetadataByDenunciaAsync(
        int denunciaId,
        CancellationToken cancellationToken = default)
        => _inner.GetFicherosMetadataByDenunciaAsync(denunciaId, cancellationToken);

    public async Task<List<GestionaUploadHistoryEntry>> GetGestionaUploadHistoryAsync(CancellationToken cancellationToken = default)
    {
        var history = await _inner.GetGestionaUploadHistoryAsync(cancellationToken);
        var knownIds = history
            .Select(item => item.DenunciaId)
            .ToHashSet();

        List<GestionaUploadHistoryEntry> fallbackHistory = [];
        try
        {
            var storedComplaints = await _inner.GetDenunciasByScopeAsync(DenunciaListScope.InGestiona, cancellationToken);
            var fallbackComplaints = await UnprotectComplaintsAsync(storedComplaints, skipPurgedRows: true, cancellationToken);
            fallbackHistory = fallbackComplaints
                .Where(complaint => complaint.FechaSubidaAGestiona != DateTime.MinValue &&
                                    !knownIds.Contains(complaint.Id_Denuncia))
                .Select(complaint => new GestionaUploadHistoryEntry(
                    0,
                    complaint.Id_Denuncia,
                    complaint.Expediente_Gestiona ?? string.Empty,
                    complaint.ExpedienteGestionaMostrable,
                    complaint.UltimaSubidaGestionaTipoMostrable,
                    ExtractGroupCode(complaint.UltimoGrupoAsignadoGestionaMostrable),
                    "No registrado",
                    complaint.FechaSubidaAGestiona,
                    complaint.NombreDenuncia ?? string.Empty,
                    complaint.ArchivoElegido ?? string.Empty))
                .ToList();
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "No se ha podido completar el historico de Gestiona con denuncias antiguas descifrables.");
        }

        return history
            .Concat(fallbackHistory)
            .OrderByDescending(item => item.UploadedAtUtc)
            .ThenByDescending(item => item.Id)
            .ToList();
    }

    private async Task<List<FicherosDenuncias>> UnprotectAttachmentsAsync(
        List<FicherosDenuncias> ficheros,
        bool skipPurgedRows,
        CancellationToken cancellationToken)
    {
        var result = new List<FicherosDenuncias>(ficheros.Count);
        var requestKeyCache = new Dictionary<DateOnly, byte[]>();
        foreach (var fichero in ficheros)
        {
            try
            {
                result.Add(await UnprotectAttachmentAsync(fichero, requestKeyCache, cancellationToken));
            }
            catch (EncryptedDataPurgedException ex) when (skipPurgedRows)
            {
                _logger.LogWarning(
                    "Se omite el adjunto {AttachmentId} de la denuncia {DenunciaId} en el listado porque sus datos estan purgados para la clave diaria {KeyDate}.",
                    fichero.Id_Fichero,
                    fichero.Id_Denuncia,
                    ex.KeyDate);
            }
        }

        return result;
    }

    public async Task<DenunciasGestiona?> GetDenunciaByIdAsync(int denunciaId, CancellationToken cancellationToken = default)
    {
        var denuncia = await _inner.GetDenunciaByIdAsync(denunciaId, cancellationToken);
        return denuncia is null ? null : await UnprotectComplaintAsync(denuncia, [], cancellationToken);
    }

    public async Task UpsertDenunciaAsync(DenunciasGestiona denuncia, CancellationToken cancellationToken = default)
    {
        var key = await _envelopeKeyProvider.GetCurrentKeyAsync(cancellationToken);
        await _inner.UpsertDenunciaAsync(ProtectComplaint(denuncia, key), cancellationToken);
    }

    public async Task UpsertFicherosAsync(IEnumerable<FicherosDenuncias> ficheros, CancellationToken cancellationToken = default)
    {
        var key = await _envelopeKeyProvider.GetCurrentKeyAsync(cancellationToken);
        await _inner.UpsertFicherosAsync(ficheros.Select(fichero => ProtectAttachment(fichero, key)).ToArray(), cancellationToken);
    }

    public Task AddGestionaUploadHistoryAsync(
        GestionaUploadHistoryCreateRequest request,
        string username,
        CancellationToken cancellationToken = default)
        => _inner.AddGestionaUploadHistoryAsync(request, username, cancellationToken);

    public Task MarkFicherosAsUploadedAsync(
        int denunciaId,
        IEnumerable<string> fileNames,
        DateTime uploadedAtUtc,
        CancellationToken cancellationToken = default)
        => _inner.MarkFicherosAsUploadedAsync(denunciaId, fileNames, uploadedAtUtc, cancellationToken);

    private DenunciasGestiona ProtectComplaint(DenunciasGestiona source, EncryptionKeyMaterial key)
    {
        var protectedComplaint = TransformComplaint(
            ToPersistentComplaint(source),
            value => ProtectString(value, key.Key));

        protectedComplaint.KeyDate = key.KeyDate;
        protectedComplaint.EncryptionScheme = key.Scheme;
        protectedComplaint.EncryptedAtUtc = DateTime.UtcNow;
        return protectedComplaint;
    }

    private async Task<DenunciasGestiona> UnprotectComplaintAsync(
        DenunciasGestiona source,
        Dictionary<DateOnly, byte[]> requestKeyCache,
        CancellationToken cancellationToken)
    {
        var key = await ResolveReadKeyAsync(source.KeyDate, requestKeyCache, cancellationToken);
        var decrypted = TransformComplaint(source, value => UnprotectString(value, key, source.KeyDate));
        var rebuilt = RebuildComplaintFromPayload(decrypted);
        rebuilt.KeyDate = source.KeyDate;
        rebuilt.EncryptionScheme = source.EncryptionScheme;
        rebuilt.EncryptedAtUtc = source.EncryptedAtUtc;
        return rebuilt;
    }

    private static DenunciasGestiona ToPersistentComplaint(DenunciasGestiona source)
    {
        return new DenunciasGestiona
        {
            // Permanentes tecnicos y de trazabilidad.
            Id_RegistroDenuncia = source.Id_RegistroDenuncia,
            Id_Denuncia = source.Id_Denuncia,
            Fecha = source.Fecha,
            Expediente_Gestiona = source.Expediente_Gestiona,
            CodigoExpedienteGestiona = source.CodigoExpedienteGestiona,
            Id_Persona_Gestiona = source.Id_Persona_Gestiona,
            Etiqueta = source.Etiqueta,
            Estado = source.Estado,
            Confidencial = source.Confidencial,
            EsActualizacion = source.EsActualizacion,
            ProcedureId = source.ProcedureId,
            GroupId = source.GroupId,
            NombreDenuncia = source.NombreDenuncia,
            EstadoDenuncia = source.EstadoDenuncia,
            ArchivoElegido = source.ArchivoElegido,
            FechaSubidaAGestiona = source.FechaSubidaAGestiona,
            UltimaSubidaGestionaTipo = source.UltimaSubidaGestionaTipo,
            UltimoGrupoAsignadoGestiona = source.UltimoGrupoAsignadoGestiona,
            EnGestiona = source.EnGestiona,
            EnRechazada = source.EnRechazada,

            // Payload temporal cifrado. Los campos funcionales se derivan de aqui al leer.
            CamposFormularioJson = source.CamposFormularioJson,
            TextoOriginalReport = source.TextoOriginalReport
        };
    }

    private static DenunciasGestiona RebuildComplaintFromPayload(DenunciasGestiona stored)
    {
        var rebuilt = TryParseStoredReport(stored) ?? new DenunciasGestiona();

        if (rebuilt.Id_Denuncia == 0)
        {
            rebuilt.Id_Denuncia = stored.Id_Denuncia;
        }

        if (rebuilt.Fecha == DateTime.MinValue)
        {
            rebuilt.Fecha = stored.Fecha;
        }

        if (string.IsNullOrWhiteSpace(rebuilt.CamposFormularioJson))
        {
            rebuilt.CamposFormularioJson = stored.CamposFormularioJson;
        }

        rebuilt.TextoOriginalReport = stored.TextoOriginalReport;
        ApplyPersistentTechnicalFields(rebuilt, stored);
        return rebuilt;
    }

    private static string ExtractGroupCode(string? value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return string.Empty;
        }

        var trimmed = value.Trim();
        var code = new string(trimmed.TakeWhile(char.IsDigit).ToArray());
        return string.IsNullOrWhiteSpace(code) ? trimmed : code;
    }

    private static DenunciasGestiona? TryParseStoredReport(DenunciasGestiona stored)
    {
        if (string.IsNullOrWhiteSpace(stored.TextoOriginalReport))
        {
            return null;
        }

        try
        {
            return ReportParser.ParseReport(stored.TextoOriginalReport);
        }
        catch
        {
            return null;
        }
    }

    private static void ApplyPersistentTechnicalFields(DenunciasGestiona target, DenunciasGestiona stored)
    {
        target.Id_RegistroDenuncia = stored.Id_RegistroDenuncia;
        target.Id_Denuncia = stored.Id_Denuncia == 0 ? target.Id_Denuncia : stored.Id_Denuncia;
        target.Expediente_Gestiona = stored.Expediente_Gestiona;
        target.CodigoExpedienteGestiona = stored.CodigoExpedienteGestiona;
        target.Id_Persona_Gestiona = stored.Id_Persona_Gestiona;
        target.Etiqueta = stored.Etiqueta;
        target.Estado = stored.Estado;
        target.Confidencial = stored.Confidencial || target.Confidencial;
        target.EsActualizacion = stored.EsActualizacion;
        target.ProcedureId = stored.ProcedureId;
        target.GroupId = stored.GroupId;
        target.NombreDenuncia = stored.NombreDenuncia;
        target.EstadoDenuncia = stored.EstadoDenuncia;
        target.ArchivoElegido = stored.ArchivoElegido;
        target.FechaSubidaAGestiona = stored.FechaSubidaAGestiona;
        target.UltimaSubidaGestionaTipo = stored.UltimaSubidaGestionaTipo;
        target.UltimoGrupoAsignadoGestiona = stored.UltimoGrupoAsignadoGestiona;
        target.EnGestiona = stored.EnGestiona;
        target.EnRechazada = stored.EnRechazada;
    }

    private static DenunciasGestiona TransformComplaint(DenunciasGestiona source, Func<string, string> transformString)
    {
        var target = new DenunciasGestiona();

        foreach (var property in ComplaintProperties)
        {
            var value = property.GetValue(source);
            if (property.PropertyType == typeof(string))
            {
                property.SetValue(target, transformString((string?)value ?? string.Empty));
            }
            else
            {
                property.SetValue(target, value);
            }
        }

        return target;
    }

    private FicherosDenuncias ProtectAttachment(FicherosDenuncias source, EncryptionKeyMaterial key)
    {
        var content = source.Fichero ?? [];
        var hash = string.IsNullOrWhiteSpace(source.ContentSha256)
            ? ComputeSha256Hex(content)
            : source.ContentSha256.Trim().ToLowerInvariant();

        return new FicherosDenuncias
        {
            Id_Fichero = source.Id_Fichero,
            Id_Tipo = source.Id_Tipo,
            Descripcion = ProtectString(source.Descripcion ?? string.Empty, key.Key),
            Fecha = source.Fecha,
            Observaciones = ProtectString(source.Observaciones ?? string.Empty, key.Key),
            Id_Denuncia = source.Id_Denuncia,
            NombreFichero = source.NombreFichero,
            Fichero = ProtectBytes(content, key.Key),
            Subido = source.Subido,
            FechaSubida = source.FechaSubida,
            ContentSha256 = hash,
            KeyDate = key.KeyDate,
            EncryptionScheme = key.Scheme,
            EncryptedAtUtc = DateTime.UtcNow
        };
    }

    private async Task<FicherosDenuncias> UnprotectAttachmentAsync(
        FicherosDenuncias source,
        Dictionary<DateOnly, byte[]> requestKeyCache,
        CancellationToken cancellationToken)
    {
        var key = await ResolveReadKeyAsync(source.KeyDate, requestKeyCache, cancellationToken);
        return new FicherosDenuncias
        {
            Id_Fichero = source.Id_Fichero,
            Id_Tipo = source.Id_Tipo,
            Descripcion = UnprotectString(source.Descripcion ?? string.Empty, key, source.KeyDate),
            Fecha = source.Fecha,
            Observaciones = UnprotectString(source.Observaciones ?? string.Empty, key, source.KeyDate),
            Id_Denuncia = source.Id_Denuncia,
            NombreFichero = source.NombreFichero,
            Fichero = UnprotectBytes(source.Fichero ?? [], key, source.KeyDate),
            Subido = source.Subido,
            FechaSubida = source.FechaSubida,
            ContentSha256 = source.ContentSha256,
            KeyDate = source.KeyDate,
            EncryptionScheme = source.EncryptionScheme,
            EncryptedAtUtc = source.EncryptedAtUtc
        };
    }

    private async Task<byte[]> ResolveReadKeyAsync(
        DateOnly? keyDate,
        Dictionary<DateOnly, byte[]> requestKeyCache,
        CancellationToken cancellationToken)
    {
        if (keyDate.HasValue)
        {
            if (requestKeyCache.TryGetValue(keyDate.Value, out var cached))
            {
                return cached;
            }

            var key = await _envelopeKeyProvider.GetKeyForDateAsync(keyDate.Value, cancellationToken);
            var copy = key.CopyKey();
            requestKeyCache[keyDate.Value] = copy;
            return copy;
        }

        return await _legacyKeyProvider.GetKeyAsync(cancellationToken);
    }

    private string ProtectString(string value, byte[] key)
    {
        if (string.IsNullOrWhiteSpace(value) ||
            value.StartsWith(KeyVaultStringPrefix, StringComparison.Ordinal) ||
            value.StartsWith(DataProtectionStringPrefix, StringComparison.Ordinal))
        {
            return value;
        }

        var encrypted = EncryptBytes(Encoding.UTF8.GetBytes(value), key);
        return KeyVaultStringPrefix + Convert.ToBase64String(encrypted);
    }

    private string UnprotectString(string value, byte[] key, DateOnly? keyDate = null)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return value;
        }

        if (value.StartsWith(KeyVaultStringPrefix, StringComparison.Ordinal))
        {
            try
            {
                var encrypted = Convert.FromBase64String(value[KeyVaultStringPrefix.Length..]);
                return Encoding.UTF8.GetString(DecryptBytes(encrypted, key));
            }
            catch (Exception ex)
            {
                if (keyDate.HasValue)
                {
                    throw CreatePurgedDataException(keyDate.Value, ex);
                }

                return value;
            }
        }

        if (!value.StartsWith(DataProtectionStringPrefix, StringComparison.Ordinal))
        {
            return value;
        }

        try
        {
            return _protector.Unprotect(value[DataProtectionStringPrefix.Length..]);
        }
        catch
        {
            return value;
        }
    }

    private byte[] ProtectBytes(byte[] value, byte[] key)
    {
        if (value.Length == 0 ||
            StartsWith(value, KeyVaultBytesPrefix) ||
            StartsWith(value, KeyVaultRawBytesPrefix) ||
            StartsWith(value, DataProtectionBytesPrefix))
        {
            return value;
        }

        var encrypted = EncryptBytes(value, key);
        return [.. KeyVaultRawBytesPrefix, .. encrypted];
    }

    private byte[] UnprotectBytes(byte[] value, byte[] key, DateOnly? keyDate = null)
    {
        if (value.Length == 0)
        {
            return value;
        }

        if (StartsWith(value, KeyVaultRawBytesPrefix))
        {
            try
            {
                return DecryptBytes(value[KeyVaultRawBytesPrefix.Length..], key);
            }
            catch (Exception ex)
            {
                if (keyDate.HasValue)
                {
                    throw CreatePurgedDataException(keyDate.Value, ex);
                }

                return value;
            }
        }

        if (StartsWith(value, KeyVaultBytesPrefix))
        {
            try
            {
                var base64 = Encoding.ASCII.GetString(value, KeyVaultBytesPrefix.Length, value.Length - KeyVaultBytesPrefix.Length);
                return DecryptBytes(Convert.FromBase64String(base64), key);
            }
            catch (Exception ex)
            {
                if (keyDate.HasValue)
                {
                    throw CreatePurgedDataException(keyDate.Value, ex);
                }

                return value;
            }
        }

        if (!StartsWith(value, DataProtectionBytesPrefix))
        {
            return value;
        }

        try
        {
            var base64 = Encoding.ASCII.GetString(value, DataProtectionBytesPrefix.Length, value.Length - DataProtectionBytesPrefix.Length);
            return _protector.Unprotect(Convert.FromBase64String(base64));
        }
        catch
        {
            return value;
        }
    }

    private static byte[] EncryptBytes(byte[] plainBytes, byte[] key)
    {
        var nonce = RandomNumberGenerator.GetBytes(AesGcmNonceSize);
        var cipherBytes = new byte[plainBytes.Length];
        var tag = new byte[AesGcmTagSize];

        using var aes = new AesGcm(key, AesGcmTagSize);
        aes.Encrypt(nonce, plainBytes, cipherBytes, tag);

        return [.. nonce, .. tag, .. cipherBytes];
    }

    private static byte[] DecryptBytes(byte[] encryptedBytes, byte[] key)
    {
        if (encryptedBytes.Length < AesGcmNonceSize + AesGcmTagSize)
        {
            throw new CryptographicException("Payload cifrado invalido.");
        }

        var nonce = encryptedBytes.AsSpan(0, AesGcmNonceSize);
        var tag = encryptedBytes.AsSpan(AesGcmNonceSize, AesGcmTagSize);
        var cipherBytes = encryptedBytes.AsSpan(AesGcmNonceSize + AesGcmTagSize);
        var plainBytes = new byte[cipherBytes.Length];

        using var aes = new AesGcm(key, AesGcmTagSize);
        aes.Decrypt(nonce, cipherBytes, tag, plainBytes);

        return plainBytes;
    }

    private static bool StartsWith(byte[] value, byte[] prefix)
    {
        if (value.Length < prefix.Length)
        {
            return false;
        }

        for (var i = 0; i < prefix.Length; i++)
        {
            if (value[i] != prefix[i])
            {
                return false;
            }
        }

        return true;
    }

    private static string ComputeSha256Hex(byte[] content)
        => Convert.ToHexString(SHA256.HashData(content)).ToLowerInvariant();

    private static EncryptedDataPurgedException CreatePurgedDataException(DateOnly keyDate, Exception innerException)
        => new(keyDate, innerException);
}
