<?php

namespace App\Services;

use App\Models\CaCertificate;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Str;

class OpenSslService
{
    /**
     * Generate Root and Intermediate CA certificates.
     */
    public function setupCa()
    {
        if (CaCertificate::count() > 0) {
            return false;
        }

        $rootConfig = Config::get('openssl.ca_root');
        $int4096Config = Config::get('openssl.ca_4096');
        $int2048Config = Config::get('openssl.ca_2048');

        // Create a basic temporary openssl config for CA extensions
        $configContent = "[req]\ndistinguished_name = req\n[v3_ca]\nsubjectKeyIdentifier = hash\nauthorityKeyIdentifier = keyid:always,issuer\nbasicConstraints = critical, CA:true\nkeyUsage = critical, digitalSignature, cRLSign, keyCertSign";
        $configFile = tempnam(sys_get_temp_dir(), 'ca_conf_');
        file_put_contents($configFile, $configContent);

        try {
            // Root CA (4096-bit)
            $rootKey = openssl_pkey_new([
                'private_key_bits' => 4096,
                'private_key_type' => OPENSSL_KEYTYPE_RSA,
                'config' => $configFile
            ]);
            if (!$rootKey) throw new \Exception('Failed to generate Root Key: ' . openssl_error_string());

            $rootCsr = openssl_csr_new($rootConfig, $rootKey, ['digest_alg' => 'sha256', 'config' => $configFile]);
            if (!$rootCsr) throw new \Exception('Failed to generate Root CSR: ' . openssl_error_string());

            // Generate a random serial
            $serial = $this->generateSerialNumber();

            $rootCert = openssl_csr_sign($rootCsr, null, $rootKey, 10950, [
                'digest_alg' => 'sha256',
                'x509_extensions' => 'v3_ca',
                'config' => $configFile,
            ], $serial);
            if (!$rootCert) throw new \Exception('Failed to sign Root Cert: ' . openssl_error_string());
            
            if (!openssl_x509_export($rootCert, $rootCertPem)) throw new \Exception('Failed to export Root Cert');
            if (!openssl_pkey_export($rootKey, $rootKeyPem, null, ['config' => $configFile])) throw new \Exception('Failed to export Root Key');

            $rootDetails = openssl_x509_parse($rootCertPem);
            
            // Prefer serialNumberHex if available (PHP 8.0+)
            $serialHex = isset($rootDetails['serialNumberHex']) 
                ? $this->formatHex($rootDetails['serialNumberHex'])
                : $this->formatSerialToHex($rootDetails['serialNumber']);

            CaCertificate::create([
                'ca_type' => 'root',
                'cert_content' => $rootCertPem,
                'key_content' => $rootKeyPem,
                'serial_number' => $serialHex,
                'common_name' => $rootDetails['subject']['CN'] ?? 'Root CA',
                'organization' => $rootDetails['subject']['O'] ?? null,
                'valid_from' => date('Y-m-d H:i:s', $rootDetails['validFrom_time_t']),
                'valid_to' => date('Y-m-d H:i:s', $rootDetails['validTo_time_t']),
            ]);

            // Intermediate CA 4096-bit
            $int4096Key = openssl_pkey_new([
                'private_key_bits' => 4096,
                'private_key_type' => OPENSSL_KEYTYPE_RSA,
                'config' => $configFile
            ]);
            if (!$int4096Key) throw new \Exception('Failed to generate Int-4096 Key: ' . openssl_error_string());

            $int4096Csr = openssl_csr_new($int4096Config, $int4096Key, ['digest_alg' => 'sha256', 'config' => $configFile]);
            if (!$int4096Csr) throw new \Exception('Failed to generate Int-4096 CSR: ' . openssl_error_string());

            $int4096Cert = openssl_csr_sign($int4096Csr, $rootCert, $rootKey, 10950, [
                'digest_alg' => 'sha256',
                'x509_extensions' => 'v3_ca',
                'config' => $configFile,
            ], $this->generateSerialNumber());
            if (!$int4096Cert) throw new \Exception('Failed to sign Int-4096 Cert: ' . openssl_error_string());

            if (!openssl_x509_export($int4096Cert, $int4096CertPem)) throw new \Exception('Failed to export Int-4096 Cert');
            if (!openssl_pkey_export($int4096Key, $int4096KeyPem, null, ['config' => $configFile])) throw new \Exception('Failed to export Int-4096 Key');

            $int4096Details = openssl_x509_parse($int4096CertPem);
            $serialHex4096 = isset($int4096Details['serialNumberHex']) 
                ? $this->formatHex($int4096Details['serialNumberHex'])
                : $this->formatSerialToHex($int4096Details['serialNumber']);

            CaCertificate::create([
                'ca_type' => 'intermediate_4096', 
                'cert_content' => $int4096CertPem, 
                'key_content' => $int4096KeyPem,
                'serial_number' => $serialHex4096,
                'common_name' => $int4096Details['subject']['CN'] ?? 'Intermediate CA 4096',
                'organization' => $int4096Details['subject']['O'] ?? null,
                'valid_from' => date('Y-m-d H:i:s', $int4096Details['validFrom_time_t']),
                'valid_to' => date('Y-m-d H:i:s', $int4096Details['validTo_time_t']),
            ]);

            // Intermediate CA 2048-bit
            $int2048Key = openssl_pkey_new([
                'private_key_bits' => 2048,
                'private_key_type' => OPENSSL_KEYTYPE_RSA,
                'config' => $configFile
            ]);
            if (!$int2048Key) throw new \Exception('Failed to generate Int-2048 Key: ' . openssl_error_string());

            $int2048Csr = openssl_csr_new($int2048Config, $int2048Key, ['digest_alg' => 'sha256', 'config' => $configFile]);
            if (!$int2048Csr) throw new \Exception('Failed to generate Int-2048 CSR: ' . openssl_error_string());

            $int2048Cert = openssl_csr_sign($int2048Csr, $rootCert, $rootKey, 10950, [
                'digest_alg' => 'sha256',
                'x509_extensions' => 'v3_ca',
                'config' => $configFile,
            ], $this->generateSerialNumber());
            if (!$int2048Cert) throw new \Exception('Failed to sign Int-2048 Cert: ' . openssl_error_string());

            if (!openssl_x509_export($int2048Cert, $int2048CertPem)) throw new \Exception('Failed to export Int-2048 Cert');
            if (!openssl_pkey_export($int2048Key, $int2048KeyPem, null, ['config' => $configFile])) throw new \Exception('Failed to export Int-2048 Key');

            $int2048Details = openssl_x509_parse($int2048CertPem);
            $serialHex2048 = isset($int2048Details['serialNumberHex']) 
                ? $this->formatHex($int2048Details['serialNumberHex'])
                : $this->formatSerialToHex($int2048Details['serialNumber']);

            CaCertificate::create([
                'ca_type' => 'intermediate_2048', 
                'cert_content' => $int2048CertPem, 
                'key_content' => $int2048KeyPem,
                'serial_number' => $serialHex2048,
                'common_name' => $int2048Details['subject']['CN'] ?? 'Intermediate CA 2048',
                'organization' => $int2048Details['subject']['O'] ?? null,
                'valid_from' => date('Y-m-d H:i:s', $int2048Details['validFrom_time_t']),
                'valid_to' => date('Y-m-d H:i:s', $int2048Details['validTo_time_t']),
            ]);

            return true;
        } finally {
            if (file_exists($configFile)) unlink($configFile);
        }
    }

    /**
     * Generate a domain certificate (Leaf).
     */
    public function generateLeaf($data)
    {
        $keyBits = $data['key_bits'] ?? 2048;
        $issuerType = (int)$keyBits === 4096 ? 'intermediate_4096' : 'intermediate_2048';
        
        $intermediate = CaCertificate::where('ca_type', $issuerType)->first();
        if (!$intermediate) {
            throw new \Exception("Intermediate CA ({$issuerType}) not found. Please setup CA first.");
        }

        $dn = [
            "countryName" => $data['country'],
            "stateOrProvinceName" => $data['state'],
            "localityName" => $data['locality'],
            "organizationName" => $data['organization'],
            "commonName" => $data['common_name']
        ];

        $cn = $data['common_name'];
        $userSan = $data['san'] ?? '';
        
        // Parse user input: split by comma, trim, filter empty
        $entries = array_filter(array_map('trim', explode(',', $userSan)));
        
        // Always include CN as the first DNS entry
        array_unshift($entries, $cn);
        
        $sanArray = array_unique(array_map(function($entry) {
            if (str_starts_with($entry, 'IP:') || str_starts_with($entry, 'DNS:')) {
                return $entry;
            }
            return filter_var($entry, FILTER_VALIDATE_IP) ? "IP:$entry" : "DNS:$entry";
        }, $entries));
        
        $sanString = implode(', ', $sanArray);

        $configFile = null;
        try {
            $configContent = "[req]\ndistinguished_name = req\nreq_extensions = v3_req\nprompt = no\n[req_distinguished_name]\nCN = $cn\n[v3_req]\nsubjectAltName = $sanString";
            $configFile = tempnam(sys_get_temp_dir(), 'openssl_');
            file_put_contents($configFile, $configContent);

            $keyBits = $data['key_bits'] ?? 2048;
            $privKey = openssl_pkey_new([
                'private_key_bits' => (int)$keyBits,
                'private_key_type' => OPENSSL_KEYTYPE_RSA,
                'config' => $configFile
            ]);
            if (!$privKey) throw new \Exception('Failed to generate Private Key: ' . openssl_error_string());

            \Log::debug("Generating Leaf with SAN: " . $sanString);

            $csr = openssl_csr_new($dn, $privKey, [
                'digest_alg' => 'sha256',
                'req_extensions' => 'v3_req',
                'config' => $configFile
            ]);
            if (!$csr) {
                $err = openssl_error_string();
                \Log::error("CSR Creation Failed: " . $err);
                throw new \Exception('Failed to generate CSR: ' . $err);
            }

            $serial = $this->generateSerialNumber();
            \Log::debug("Signing CSR with serial: " . $serial);
            
            $cert = openssl_csr_sign($csr, $intermediate->cert_content, $intermediate->key_content, 365, [
                'digest_alg' => 'sha256',
                'x509_extensions' => 'v3_req',
                'config' => $configFile,
            ], $serial);
            
            if (!$cert) {
                $err = openssl_error_string();
                \Log::error("Certificate Signing Failed: " . $err);
                throw new \Exception('Failed to sign Certificate: ' . $err);
            }

            // Verification: check if serial was actually applied
            $certInfo = openssl_x509_parse($cert);
            $actualSerialHex = isset($certInfo['serialNumberHex']) 
                ? $this->formatHex($certInfo['serialNumberHex'])
                : $this->formatSerialToHex($certInfo['serialNumber']);

            \Log::debug("Certificate signed. Embedded Serial: " . $actualSerialHex);

            if (!openssl_x509_export($cert, $certPem)) throw new \Exception('Failed to export Certificate');
            if (!openssl_pkey_export($privKey, $keyPem, null, ['config' => $configFile])) throw new \Exception('Failed to export Private Key');
            if (!openssl_csr_export($csr, $csrPem)) throw new \Exception('Failed to export CSR');

            return [
                'cert' => $certPem,
                'key' => $keyPem,
                'csr' => $csrPem,
                'serial' => $actualSerialHex,
                'valid_from' => isset($certInfo['validFrom_time_t']) ? date('Y-m-d H:i:s', $certInfo['validFrom_time_t']) : null,
                'valid_to' => isset($certInfo['validTo_time_t']) ? date('Y-m-d H:i:s', $certInfo['validTo_time_t']) : null,
            ];
        } finally {
            if ($configFile && file_exists($configFile)) {
                unlink($configFile);
            }
        }
    }

    /**
     * Generate a unique serial number.
     * Generates a large random integer for the serial number.
     */
    /**
     * Generate a unique serial number.
     * Generates a 20-byte (160-bit) positive integer string for X.509 compliance.
     */
    /**
     * Generate a unique serial number.
     * Note: strictly returns an int because openssl_csr_sign() in PHP requires type int.
     * This limits us to 64-bit entropy (PHP_INT_MAX), enabling approx 9e18 combinations.
     * While X.509 uses 160-bit, PHP's native extension wrapper limits this.
     */
    protected function generateSerialNumber(): int
    {
        try {
            return random_int(1, PHP_INT_MAX);
        } catch (\Exception $e) {
            return time();
        }
    }
    
    /**
     * Format a hex string (from serialNumberHex).
     */
    public function formatHex($hex)
    {
        // Capitalize and add colons
        $hex = strtoupper($hex);
        return implode(':', str_split($hex, 2));
    }

    /**
     * Renew (Re-sign) a CA Certificate.
     */
    public function renewCaCertificate(CaCertificate $cert, int $days)
    {
        $configFile = null;
        try {
            // 1. Prepare Config
            $configContent = "[req]\ndistinguished_name = req\n[v3_ca]\nsubjectKeyIdentifier = hash\nauthorityKeyIdentifier = keyid:always,issuer\nbasicConstraints = critical, CA:true\nkeyUsage = critical, digitalSignature, cRLSign, keyCertSign";
            $configFile = tempnam(sys_get_temp_dir(), 'renew_ca_');
            file_put_contents($configFile, $configContent);

            // 2. Get Private Key
            $privKey = openssl_pkey_get_private($cert->key_content);
            if (!$privKey) throw new \Exception('Failed to load Private Key');

            // 3. Get Subject DN from existing Cert
            $certInfo = openssl_x509_parse($cert->cert_content);
            $dn = $certInfo['subject'];
            
            // Reconstruct DN array for openssl_csr_new if needed, 
            // but we can just use the existing subject.
            // CAUTION: openssl_csr_new expects proper array keys (e.g. commonName, organizationName)
            // openssl_x509_parse returns abbreviated keys (CN, O, C, etc.) often.
            // We need to map them back or pass the DN info carefully.
            
            $dnMap = [
                'CN' => 'commonName',
                'O' => 'organizationName',
                'OU' => 'organizationalUnitName',
                'C' => 'countryName',
                'ST' => 'stateOrProvinceName',
                'L' => 'localityName',
                'emailAddress' => 'emailAddress'
            ];
            
            $newDn = [];
            foreach ($dn as $key => $value) {
                if (isset($dnMap[$key])) {
                    $newDn[$dnMap[$key]] = $value;
                }
            }

            // 4. Generate New CSR
            // Note: We use the SAME private key
            $csr = openssl_csr_new($newDn, $privKey, ['digest_alg' => 'sha256', 'config' => $configFile]);
            if (!$csr) throw new \Exception('Failed to generate Renewal CSR: ' . openssl_error_string());

            // 5. Determine Signer (Issuer)
            $issuerCert = null;
            $issuerKey = null;

            if ($cert->ca_type === 'root') {
                // Root signs itself
                $issuerCert = null; 
                $issuerKey = $privKey;
            } else {
                // Intermediate is signed by Root
                $root = CaCertificate::where('ca_type', 'root')->first();
                if (!$root) throw new \Exception('Root CA not found for signing intermediate renewal.');
                $issuerCert = $root->cert_content;
                $issuerKey = openssl_pkey_get_private($root->key_content);
            }

            // 6. Sign CSR
            $serial = $this->generateSerialNumber();
            $newCert = openssl_csr_sign($csr, $issuerCert, $issuerKey, $days, [
                'digest_alg' => 'sha256',
                'x509_extensions' => 'v3_ca',
                'config' => $configFile,
            ], $serial);

            if (!$newCert) throw new \Exception('Failed to sign Renewal Cert: ' . openssl_error_string());

            // 7. Export
            if (!openssl_x509_export($newCert, $newCertPem)) throw new \Exception('Failed to export Renewal Cert');
            
            // 8. Parse new details
            $newInfo = openssl_x509_parse($newCertPem);
            $newSerialHex = isset($newInfo['serialNumberHex']) 
                ? $this->formatHex($newInfo['serialNumberHex'])
                : $this->formatSerialToHex($newInfo['serialNumber']);

            return [
                'cert_content' => $newCertPem,
                'serial_number' => $newSerialHex,
                'valid_from' => date('Y-m-d H:i:s', $newInfo['validFrom_time_t']),
                'valid_to' => date('Y-m-d H:i:s', $newInfo['validTo_time_t']),
            ];

        } finally {
            if ($configFile && file_exists($configFile)) unlink($configFile);
        }
    }

    /**
     * Fallback format a decimal serial number to hex string.
     */
    public function formatSerialToHex($decimal)
    {
        // Sanitize input: remove anything that isn't a digit or minus sign
        // Note: serial numbers shouldn't be negative here normally, but just cleaning string
        $cleaned = preg_replace('/[^0-9]/', '', (string)$decimal);
        
        if ($cleaned === '') {
            $cleaned = '0';
        }

        // Support large integers that might be strings (bcmath based conversion)
        if (function_exists('bcdiv')) {
            $hex = '';
            $value = $cleaned;
            
            // Basic check if it's a valid BCMath number
            if (!preg_match('/^\d+$/', $value)) {
                 $value = '0';
            }

            // Loop while value > 0
            while (bccomp($value, '0') > 0) {
                $mod = bcmod($value, '16');
                $hex = dechex((int)$mod) . $hex;
                $value = bcdiv($value, '16', 0);
            }
            $hex = $hex ?: '0';
        } else {
            // Fallback for smaller integers if bcmath missing
            $hex = dechex((int)$cleaned);
        }

        // Ensure even length
        if (strlen($hex) % 2 !== 0) {
            $hex = '0' . $hex;
        }
        return strtoupper(implode(':', str_split($hex, 2)));
    }
}
