<?php

namespace App\Services;

use Illuminate\Support\Facades\Log;

class NinEncryptionService
{
    private ?string $encryptionKey;
    private ?string $privateKeyPem;

    public function __construct()
    {
        $this->encryptionKey = config('nin.encryption_key');

        $b64 = config('nin.private_key');
        $this->privateKeyPem = $b64 ? base64_decode($b64) : null;
    }

    /**
     * Decrypt NIN using AES-GCM.
     *
     * @param string $iv Base64-encoded initialization vector
     * @param string $ciphertext Base64-encoded ciphertext
     * @param string $tag Base64-encoded authentication tag
     * @return string Decrypted NIN
     * @throws \Exception if decryption fails
     */
    public function decrypt(string $iv, string $ciphertext, string $tag): string
    {
        // Validate encryption key is configured (deferred from constructor)
        if (empty($this->encryptionKey)) {
            Log::error('NIN_ENCRYPTION_KEY is not configured');
            throw new \RuntimeException('NIN_ENCRYPTION_KEY is not set in environment');
        }

        try {
            // Decode from Base64
            $ivDecoded = base64_decode($iv);
            $ciphertextDecoded = base64_decode($ciphertext);
            $tagDecoded = base64_decode($tag);

            // Validate decoded data
            if ($ivDecoded === false || $ciphertextDecoded === false || $tagDecoded === false) {
                throw new \Exception('Invalid Base64 encoding in encrypted data');
            }

            // Convert hex key to binary
            $keyBinary = hex2bin($this->encryptionKey);

            if ($keyBinary === false) {
                throw new \Exception('Invalid encryption key format');
            }

            // Decrypt using AES-256-GCM
            // For GCM mode, ciphertext and tag must be passed separately
            $decrypted = openssl_decrypt(
                $ciphertextDecoded,  // Only ciphertext (NOT concatenated with tag)
                'aes-256-gcm',
                $keyBinary,
                OPENSSL_RAW_DATA,
                $ivDecoded,
                $tagDecoded          // Tag passed separately as 6th parameter
            );

            if ($decrypted === false) {
                throw new \Exception('Decryption failed: ' . openssl_error_string());
            }

            return $decrypted;
        } catch (\Exception $e) {
            Log::error('NIN decryption failed: ' . $e->getMessage());
            throw new \Exception('Failed to decrypt NIN data');
        }
    }

    /**
     * Encrypt data using AES-GCM (for server-side encryption, e.g., bulk uploads).
     *
     * @param string $plaintext The data to encrypt (NIN/BVN)
     * @return array Returns ['iv' => base64, 'ciphertext' => base64, 'tag' => base64]
     * @throws \Exception if encryption fails
     */
    public function encrypt(string $plaintext): array
    {
        // Validate encryption key is configured
        if (empty($this->encryptionKey)) {
            Log::error('NIN_ENCRYPTION_KEY is not configured');
            throw new \RuntimeException('NIN_ENCRYPTION_KEY is not set in environment');
        }

        try {
            // Generate random IV (12 bytes for GCM)
            $iv = random_bytes(12);

            // Convert hex key to binary
            $keyBinary = hex2bin($this->encryptionKey);

            if ($keyBinary === false) {
                throw new \Exception('Invalid encryption key format');
            }

            // Encrypt using AES-256-GCM
            $tag = '';
            $ciphertext = openssl_encrypt(
                $plaintext,
                'aes-256-gcm',
                $keyBinary,
                OPENSSL_RAW_DATA,
                $iv,
                $tag
            );

            if ($ciphertext === false) {
                throw new \Exception('Encryption failed: ' . openssl_error_string());
            }

            // Return all components as base64-encoded strings
            return [
                'iv' => base64_encode($iv),
                'ciphertext' => base64_encode($ciphertext),
                'tag' => base64_encode($tag),
            ];
        } catch (\Exception $e) {
            Log::error('Encryption failed: ' . $e->getMessage());
            throw new \Exception('Failed to encrypt data');
        }
    }

    /**
     * Decrypt NIN that was RSA-OAEP encrypted by the frontend using the server public key.
     *
     * @param string $encryptedNin Base64-encoded RSA-OAEP ciphertext
     * @return string Plaintext NIN
     * @throws \RuntimeException|\Exception
     */
    public function decryptWithPrivateKey(string $encryptedNin): string
    {
        if (empty($this->privateKeyPem)) {
            Log::error('NIN_PRIVATE_KEY is not configured');
            throw new \RuntimeException('NIN_PRIVATE_KEY is not set in environment');
        }

        try {
            $ciphertext = base64_decode($encryptedNin, strict: true);

            if ($ciphertext === false) {
                throw new \Exception('Invalid Base64 encoding in encrypted_nin');
            }

            $privateKey = openssl_pkey_get_private($this->privateKeyPem);

            if ($privateKey === false) {
                throw new \Exception('Failed to load RSA private key: ' . openssl_error_string());
            }

            $decrypted = '';
            $result = openssl_private_decrypt($ciphertext, $decrypted, $privateKey, OPENSSL_PKCS1_OAEP_PADDING);

            if (!$result) {
                throw new \Exception('RSA decryption failed: ' . openssl_error_string());
            }

            return $decrypted;
        } catch (\Exception $e) {
            Log::error('RSA NIN decryption failed: ' . $e->getMessage());
            throw new \Exception('Failed to decrypt NIN data');
        }
    }

    /**
     * Return the RSA public key (PEM) derived from the configured private key.
     * Exposed via the public API so the frontend can encrypt without a shared secret.
     *
     * @return string PEM-encoded public key
     * @throws \RuntimeException
     */
    public function getPublicKey(): string
    {
        if (empty($this->privateKeyPem)) {
            throw new \RuntimeException('NIN_PRIVATE_KEY is not configured');
        }

        $privateKey = openssl_pkey_get_private($this->privateKeyPem);

        if ($privateKey === false) {
            throw new \RuntimeException('Failed to load RSA private key');
        }

        $details = openssl_pkey_get_details($privateKey);

        return $details['key'];
    }

    /**
     * Validate NIN format (11 digits).
     *
     * @param string $nin
     * @return bool
     */
    public function validateNinFormat(string $nin): bool
    {
        return preg_match('/^\d{11}$/', $nin) === 1;
    }
}

