using System.Security.Cryptography;
using System.Text;
using System.Collections.Concurrent;
using ApiDenuncias.Configuration;
using Azure;
using Azure.Identity;
using Azure.Security.KeyVault.Keys.Cryptography;
using Microsoft.Extensions.Options;
using MySqlConnector;

namespace ApiDenuncias.Services;

public sealed class EnvelopeEncryptionKeyProvider : IEnvelopeEncryptionKeyProvider
{
    private readonly KeyVaultOptions _keyVaultOptions;
    private readonly IConfiguration _configuration;
    private readonly MySqlConnectionStringProvider _connectionStringProvider;
    private readonly ILogger<EnvelopeEncryptionKeyProvider> _logger;
    private readonly SemaphoreSlim _unwrapGate = new(1, 1);
    private readonly ConcurrentDictionary<DateOnly, CachedEncryptionKey> _cache = [];

    public EnvelopeEncryptionKeyProvider(
        IOptions<KeyVaultOptions> keyVaultOptions,
        IConfiguration configuration,
        MySqlConnectionStringProvider connectionStringProvider,
        ILogger<EnvelopeEncryptionKeyProvider> logger)
    {
        _keyVaultOptions = keyVaultOptions.Value;
        _configuration = configuration;
        _connectionStringProvider = connectionStringProvider;
        _logger = logger;
    }

    public async ValueTask<EncryptionKeyMaterial> GetCurrentKeyAsync(CancellationToken cancellationToken = default)
    {
        if (!_keyVaultOptions.Enabled)
        {
            var today = DateOnly.FromDateTime(DateTime.UtcNow);
            return CreateLocalDevelopmentKey(today);
        }

        using var timeout = CreateKeyOperationTimeout(cancellationToken);
        try
        {
            var row = await LoadCurrentKeyRowAsync(timeout.Token);
            if (!string.Equals(row.Status, "active", StringComparison.OrdinalIgnoreCase))
            {
                throw new EncryptionKeyUnavailableException(
                    $"No hay clave activa para hoy ({row.KeyDate:yyyy-MM-dd}). Estado actual: {row.Status}.");
            }

            return await GetOrUnwrapAsync(row, timeout.Token);
        }
        catch (OperationCanceledException ex)
        {
            throw new EncryptionKeyUnavailableException(
                $"Timeout cargando la clave diaria activa desde encryption_keys/Key Vault tras {_keyVaultOptions.EncryptionKeyTimeoutSeconds} segundos.",
                ex);
        }
    }

    public async ValueTask<EncryptionKeyMaterial> GetKeyForDateAsync(DateOnly keyDate, CancellationToken cancellationToken = default)
    {
        if (!_keyVaultOptions.Enabled)
        {
            return CreateLocalDevelopmentKey(keyDate);
        }

        using var timeout = CreateKeyOperationTimeout(cancellationToken);
        try
        {
            var row = await LoadKeyRowAsync(keyDate, timeout.Token);
            if (string.Equals(row.Status, "purged", StringComparison.OrdinalIgnoreCase))
            {
                _cache.TryRemove(keyDate, out _);
                throw new EncryptedDataPurgedException(keyDate);
            }

            if (!string.Equals(row.Status, "active", StringComparison.OrdinalIgnoreCase))
            {
                throw new EncryptionKeyUnavailableException(
                    $"La clave diaria {keyDate:yyyy-MM-dd} no esta activa. Estado actual: {row.Status}.");
            }

            return await GetOrUnwrapAsync(row, timeout.Token);
        }
        catch (OperationCanceledException ex)
        {
            throw new EncryptionKeyUnavailableException(
                $"Timeout cargando la clave diaria {keyDate:yyyy-MM-dd} desde encryption_keys/Key Vault tras {_keyVaultOptions.EncryptionKeyTimeoutSeconds} segundos.",
                ex);
        }
    }

    private CancellationTokenSource CreateKeyOperationTimeout(CancellationToken cancellationToken)
    {
        var timeoutSeconds = Math.Clamp(_keyVaultOptions.EncryptionKeyTimeoutSeconds, 5, 120);
        var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
        timeout.CancelAfter(TimeSpan.FromSeconds(timeoutSeconds));
        return timeout;
    }

    private async Task<EncryptionKeyRow> LoadCurrentKeyRowAsync(CancellationToken cancellationToken)
    {
        const string sql = """
            SELECT key_date, key_version, edek, status
            FROM encryption_keys
            WHERE key_date = CURDATE()
            LIMIT 1;
            """;

        return await LoadKeyRowCoreAsync(sql, keyDate: null, cancellationToken);
    }

    private async Task<EncryptionKeyRow> LoadKeyRowAsync(DateOnly keyDate, CancellationToken cancellationToken)
    {
        const string sql = """
            SELECT key_date, key_version, edek, status
            FROM encryption_keys
            WHERE key_date = @keyDate
            LIMIT 1;
            """;

        return await LoadKeyRowCoreAsync(sql, keyDate, cancellationToken);
    }

    private async Task<EncryptionKeyRow> LoadKeyRowCoreAsync(
        string sql,
        DateOnly? keyDate,
        CancellationToken cancellationToken)
    {
        await using var connection = await OpenConnectionAsync(cancellationToken);
        await using var command = new MySqlCommand(sql, connection);
        if (keyDate.HasValue)
        {
            command.Parameters.AddWithValue("@keyDate", keyDate.Value.ToDateTime(TimeOnly.MinValue));
        }

        await using var reader = await command.ExecuteReaderAsync(cancellationToken);
        if (!await reader.ReadAsync(cancellationToken))
        {
            var dateMessage = keyDate.HasValue
                ? $"para {keyDate.Value:yyyy-MM-dd}"
                : "para hoy";
            throw new EncryptionKeyUnavailableException(
                $"No existe fila en encryption_keys {dateMessage}. Ejecutar la Function App de claves antes de arrancar la API.");
        }

        return new EncryptionKeyRow(
            DateOnly.FromDateTime(reader.GetDateTime("key_date")),
            reader.GetString("key_version"),
            reader.GetString("edek"),
            reader.GetString("status"));
    }

    private async Task<MySqlConnection> OpenConnectionAsync(CancellationToken cancellationToken)
    {
        var connectionString = await _connectionStringProvider.GetConnectionStringAsync(cancellationToken);
        var connection = new MySqlConnection(connectionString);
        await connection.OpenAsync(cancellationToken);

        await using var timeZoneCommand = new MySqlCommand("SET time_zone = '+00:00';", connection);
        await timeZoneCommand.ExecuteNonQueryAsync(cancellationToken);

        return connection;
    }

    private async Task<EncryptionKeyMaterial> GetOrUnwrapAsync(
        EncryptionKeyRow row,
        CancellationToken cancellationToken)
    {
        var edekHash = ComputeSha256Hex(row.Edek);

        try
        {
            await _unwrapGate.WaitAsync(cancellationToken);
        }
        catch (OperationCanceledException ex)
        {
            throw new EncryptionKeyUnavailableException(
                $"No se ha podido obtener el bloqueo de carga de clave en {_keyVaultOptions.EncryptionKeyTimeoutSeconds} segundos.",
                ex);
        }

        try
        {
            if (_cache.TryGetValue(row.KeyDate, out var cached) &&
                string.Equals(cached.KeyVersion, row.KeyVersion, StringComparison.Ordinal) &&
                string.Equals(cached.EdekHash, edekHash, StringComparison.Ordinal))
            {
                return cached.ToMaterial();
            }

            _logger.LogInformation(
                "Cargando DEK diaria desde Key Vault. KeyDate={KeyDate}; KeyVersion={KeyVersion}",
                row.KeyDate,
                row.KeyVersion);

            var dek = await UnwrapDekAsync(row, cancellationToken);
            var cachedKey = new CachedEncryptionKey(
                row.KeyDate,
                row.KeyVersion,
                edekHash,
                dek);
            _cache[row.KeyDate] = cachedKey;

            _logger.LogInformation(
                "DEK diaria cargada en memoria desde encryption_keys. KeyDate={KeyDate}; KeyVersion={KeyVersion}",
                row.KeyDate,
                row.KeyVersion);

            return cachedKey.ToMaterial();
        }
        finally
        {
            _unwrapGate.Release();
        }
    }

    private static async Task<byte[]> UnwrapDekAsync(
        EncryptionKeyRow row,
        CancellationToken cancellationToken)
    {
        byte[] encryptedDek;
        try
        {
            encryptedDek = Convert.FromBase64String(row.Edek.Trim());
        }
        catch (FormatException ex)
        {
            throw new EncryptionKeyUnavailableException(
                $"La eDEK de encryption_keys para {row.KeyDate:yyyy-MM-dd} no es Base64 valida.",
                ex);
        }

        try
        {
            var cryptographyClient = new CryptographyClient(new Uri(row.KeyVersion), new DefaultAzureCredential());
            var response = await cryptographyClient.UnwrapKeyAsync(
                KeyWrapAlgorithm.RsaOaep,
                encryptedDek,
                cancellationToken);

            var dek = response.Key;
            if (dek.Length != 32)
            {
                throw new EncryptionKeyUnavailableException(
                    $"La DEK de {row.KeyDate:yyyy-MM-dd} no tiene 32 bytes tras unwrapKey.");
            }

            return dek.ToArray();
        }
        catch (RequestFailedException ex)
        {
            throw new EncryptionKeyUnavailableException(
                $"Key Vault no ha podido hacer unwrapKey para {row.KeyDate:yyyy-MM-dd}. Status={ex.Status}; Error={ex.ErrorCode}.",
                ex);
        }
        catch (OperationCanceledException ex)
        {
            throw new EncryptionKeyUnavailableException(
                $"Timeout obteniendo la DEK diaria {row.KeyDate:yyyy-MM-dd} desde encryption_keys/Key Vault. Revisa acceso a Key Vault, permisos unwrapKey y conectividad.",
                ex);
        }
    }

    private EncryptionKeyMaterial CreateLocalDevelopmentKey(DateOnly keyDate)
    {
        var configuredLocalKey = _configuration["Encryption:LocalDevelopmentKey"];
        if (string.IsNullOrWhiteSpace(configuredLocalKey))
        {
            throw new EncryptionKeyUnavailableException(
                "Key Vault esta deshabilitado y no se ha configurado Encryption:LocalDevelopmentKey.");
        }

        return new EncryptionKeyMaterial(
            keyDate,
            NormalizeKey(configuredLocalKey),
            "local-development",
            EncryptionKeyMaterial.EnvelopeScheme);
    }

    private static byte[] NormalizeKey(string secretValue)
    {
        var trimmed = secretValue.Trim();

        try
        {
            var base64Key = Convert.FromBase64String(trimmed);
            if (base64Key.Length is 16 or 24 or 32)
            {
                return base64Key.Length == 32 ? base64Key : SHA256.HashData(base64Key);
            }
        }
        catch (FormatException)
        {
            // Si no es base64, derivamos una clave estable desde el valor textual.
        }

        return SHA256.HashData(Encoding.UTF8.GetBytes(trimmed));
    }

    private static string ComputeSha256Hex(string value)
        => Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(value))).ToLowerInvariant();

    private sealed record EncryptionKeyRow(
        DateOnly KeyDate,
        string KeyVersion,
        string Edek,
        string Status);

    private sealed record CachedEncryptionKey(
        DateOnly KeyDate,
        string KeyVersion,
        string EdekHash,
        byte[] Key)
    {
        public EncryptionKeyMaterial ToMaterial()
            => new(KeyDate, Key.ToArray(), KeyVersion, EncryptionKeyMaterial.EnvelopeScheme);
    }
}
