using System.Security.Cryptography;
using System.Text;
using Application.Abstractions.Crypto;
using Microsoft.Extensions.Options;
using SharedKernel;

namespace Infrastructure.Crypto;

/// <summary>
/// AES-256-GCM 기반 필드 단위 암호화기. KeyVersion 별 키 묶음 지원.
///
/// 출력 포맷 (Base64):
/// |   12 bytes   |   N bytes   |   16 bytes   |
/// |    nonce     | ciphertext  |     tag      |
///
/// nonce 는 매 암호화마다 RandomNumberGenerator 로 생성. 동일 평문이라도 매번 다른 암호문 반환 (probabilistic).
/// 검색이 필요한 컬럼은 별도 SHA-256 해시 컬럼(Hash)을 함께 저장.
/// </summary>
public sealed class AesGcmFieldEncryptor : IFieldEncryptor
{
    private const int NonceSize = 12;
    private const int TagSize = 16;
    private const int KeySize = 32; // AES-256

    private readonly Dictionary<int, byte[]> _keys;

    public int CurrentKeyVersion { get; }

    public AesGcmFieldEncryptor(IOptions<AppSettings> options)
    {
        ArgumentNullException.ThrowIfNull(options);
        var section = options.Value.Encryption;

        if (section is null || section.Keys is null || section.Keys.Count == 0)
        {
            throw new InvalidOperationException("Encryption.Keys 가 설정되지 않았습니다. appsettings 에 V1 키 (Base64 32바이트) 를 추가하세요.");
        }

        _keys = new Dictionary<int, byte[]>(section.Keys.Count);

        foreach (var kvp in section.Keys)
        {
            // 키 이름 형식: "V1", "V2", ... -> 정수 추출
            if (!kvp.Key.StartsWith('V') || !int.TryParse(kvp.Key.AsSpan(1), out var version))
            {
                throw new InvalidOperationException($"Encryption.Keys 의 키 이름은 'V{{int}}' 형식이어야 합니다. 현재: '{kvp.Key}'");
            }

            byte[] keyBytes;

            try
            {
                keyBytes = Convert.FromBase64String(kvp.Value);
            }
            catch (FormatException ex)
            {
                throw new InvalidOperationException($"Encryption.Keys.{kvp.Key} 값이 유효한 Base64 가 아닙니다.", ex);
            }

            if (keyBytes.Length != KeySize)
            {
                throw new InvalidOperationException($"Encryption.Keys.{kvp.Key} 키 길이가 {KeySize} 바이트가 아닙니다. AES-256 키만 허용.");
            }

            _keys[version] = keyBytes;
        }

        CurrentKeyVersion = section.CurrentKeyVersion;

        if (!_keys.ContainsKey(CurrentKeyVersion))
        {
            throw new InvalidOperationException($"Encryption.CurrentKeyVersion={CurrentKeyVersion} 에 해당하는 키가 Encryption.Keys 에 없습니다.");
        }
    }

    public string Encrypt(string plaintext)
    {
        ArgumentNullException.ThrowIfNull(plaintext);

        var key = _keys[CurrentKeyVersion];
        var plainBytes = Encoding.UTF8.GetBytes(plaintext);

        var nonce = new byte[NonceSize];
        RandomNumberGenerator.Fill(nonce);

        var cipher = new byte[plainBytes.Length];
        var tag = new byte[TagSize];

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

        // [nonce(12) | cipher(N) | tag(16)] 결합 -> Base64
        var combined = new byte[NonceSize + cipher.Length + TagSize];
        Buffer.BlockCopy(nonce, 0, combined, 0, NonceSize);
        Buffer.BlockCopy(cipher, 0, combined, NonceSize, cipher.Length);
        Buffer.BlockCopy(tag, 0, combined, NonceSize + cipher.Length, TagSize);

        return Convert.ToBase64String(combined);
    }

    public string Decrypt(string ciphertextBase64, int keyVersion)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(ciphertextBase64);

        if (!_keys.TryGetValue(keyVersion, out var key))
        {
            throw new InvalidOperationException($"KeyVersion={keyVersion} 에 해당하는 키가 없습니다. (키 회전 시 과거 키도 Encryption.Keys 에 유지 필요)");
        }

        var combined = Convert.FromBase64String(ciphertextBase64);

        if (combined.Length < NonceSize + TagSize)
        {
            throw new CryptographicException("암호문 길이가 너무 짧습니다.");
        }

        var nonce = new byte[NonceSize];
        var tag = new byte[TagSize];
        var cipher = new byte[combined.Length - NonceSize - TagSize];

        Buffer.BlockCopy(combined, 0, nonce, 0, NonceSize);
        Buffer.BlockCopy(combined, NonceSize, cipher, 0, cipher.Length);
        Buffer.BlockCopy(combined, NonceSize + cipher.Length, tag, 0, TagSize);

        var plain = new byte[cipher.Length];

        using var aes = new AesGcm(key, TagSize);
        aes.Decrypt(nonce, cipher, tag, plain);

        return Encoding.UTF8.GetString(plain);
    }

    public string Hash(string plaintext)
    {
        ArgumentNullException.ThrowIfNull(plaintext);

        var bytes = Encoding.UTF8.GetBytes(plaintext);
        var hash = SHA256.HashData(bytes);

        return Convert.ToHexString(hash);
    }
}
