<?php

declare(strict_types=1);

namespace App\Services\QzTray;

use App\Repositories\QzCertificate\QzCertificateRepositoryInterface;
use App\Models\QzCertificate;
use Carbon\Carbon;

final class CertificateService
{
    public function __construct(
        private QzCertificateRepositoryInterface $repository,
        private KeyStorageService $keyStorage
    ) {}

    /**
     * Check if the OpenSSL extension is loaded.
     */
    public function isOpenSslAvailable(): bool
    {
        return extension_loaded('openssl');
    }

    /**
     * Get the currently active QZ certificate.
     */
    public function getActiveCertificate(): ?QzCertificate
    {
        return $this->repository->getActive();
    }

    /**
     * Calculate and return status of the certificate.
     */
    public function getCertificateStatus(?QzCertificate $cert): string
    {
        if (!$cert) {
            return 'not_generated';
        }

        if ($cert->status === 'invalid') {
            return 'invalid';
        }

        if ($cert->expires_at && Carbon::now()->greaterThan($cert->expires_at)) {
            return 'expired';
        }

        return $cert->status; // active
    }

    /**
     * Generate a new self-signed certificate and RSA keypair.
     *
     * @param string $name
     * @param array $dnOverrides
     * @param int|null $userId
     * @return QzCertificate
     * @throws \RuntimeException
     */
    public function generate(string $name, array $dnOverrides = [], ?int $userId = null): QzCertificate
    {
        if (!$this->isOpenSslAvailable()) {
            throw new \RuntimeException('OpenSSL PHP extension is not available.');
        }

        $keySize = config('qztray.key_size', 2048);
        $validityDays = config('qztray.validity_days', 3650);
        $opensslOpts = $this->getOpenSslOptions();

        // 1. Generate RSA Private Key
        $privateKeyResource = openssl_pkey_new(array_merge([
            'private_key_bits' => $keySize,
            'private_key_type' => OPENSSL_KEYTYPE_RSA,
        ], $opensslOpts));

        if ($privateKeyResource === false) {
            throw new \RuntimeException('Failed to generate private key: ' . openssl_error_string());
        }

        // 2. Configure Distinguished Name (DN)
        $configDn = config('qztray.dn', []);
        $dn = [
            'countryName' => $dnOverrides['country'] ?? $configDn['country'] ?? 'ID',
            'stateOrProvinceName' => $dnOverrides['state'] ?? $configDn['state'] ?? 'Jakarta',
            'localityName' => $dnOverrides['locality'] ?? $configDn['locality'] ?? 'South Jakarta',
            'organizationName' => $dnOverrides['organization'] ?? $configDn['organization'] ?? 'CafePOS Enterprise',
            'organizationalUnitName' => $dnOverrides['organizational_unit'] ?? $configDn['organizational_unit'] ?? 'IT Operations',
            'commonName' => $dnOverrides['common_name'] ?? $configDn['common_name'] ?? 'CafePOS QZ Self-Signed Certificate',
            'emailAddress' => $dnOverrides['email'] ?? $configDn['email'] ?? 'admin@cafepos.com',
        ];

        // 3. Generate CSR
        $csr = openssl_csr_new($dn, $privateKeyResource, array_merge(['digest_alg' => 'sha256'], $opensslOpts));
        if ($csr === false) {
            throw new \RuntimeException('Failed to generate CSR: ' . openssl_error_string());
        }

        // 4. Self-sign CSR to generate X509 public certificate
        $certResource = openssl_csr_sign($csr, null, $privateKeyResource, $validityDays, array_merge(['digest_alg' => 'sha256'], $opensslOpts));
        if ($certResource === false) {
            throw new \RuntimeException('Failed to sign certificate: ' . openssl_error_string());
        }

        // 5. Export Private Key PEM
        $privateKeyPem = '';
        if (!openssl_pkey_export($privateKeyResource, $privateKeyPem, null, $opensslOpts)) {
            throw new \RuntimeException('Failed to export private key: ' . openssl_error_string());
        }

        // 6. Export Public Certificate PEM
        $publicCertPem = '';
        if (!openssl_x509_export($certResource, $publicCertPem)) {
            throw new \RuntimeException('Failed to export public certificate: ' . openssl_error_string());
        }

        // 7. Parse certificate metadata (for expiry date)
        $parsed = openssl_x509_parse($certResource);
        $expiresAt = null;
        if (isset($parsed['validTo_time_t'])) {
            $expiresAt = Carbon::createFromTimestamp($parsed['validTo_time_t']);
        }

        // 8. Calculate SHA256 fingerprint
        $fingerprint = openssl_x509_fingerprint($certResource, 'sha256');
        if ($fingerprint === false) {
            $fingerprint = hash('sha256', $publicCertPem);
        }
        $formattedFingerprint = strtoupper(implode(':', str_split($fingerprint, 2)));

        // 9. Store private key encrypted on local storage
        $keyIdentifier = str_replace(' ', '_', strtolower($name)) . '_' . time();
        $privateKeyPath = $this->keyStorage->storeKey($keyIdentifier, $privateKeyPem);

        // 10. Save certificate in the database
        $cert = $this->repository->create([
            'name' => $name,
            'public_certificate' => $publicCertPem,
            'private_key_path' => $privateKeyPath,
            'fingerprint' => $formattedFingerprint,
            'algorithm' => 'RSA ' . $keySize,
            'status' => 'active',
            'expires_at' => $expiresAt,
            'created_by' => $userId,
        ]);

        // 11. Deactivate other active certificates
        $this->repository->deactivateOthers($cert->id);

        return $cert;
    }

    /**
     * Delete the certificate record and private key file.
     *
     * @param QzCertificate $certificate
     * @return bool
     */
    public function delete(QzCertificate $certificate): bool
    {
        if ($certificate->private_key_path) {
            $this->keyStorage->deleteKey($certificate->private_key_path);
        }

        return $this->repository->delete($certificate);
    }

    /**
     * Get OpenSSL configuration options.
     *
     * @return array<string, string>
     */
    private function getOpenSslOptions(): array
    {
        $options = [];

        // Check environment variable
        $envConf = getenv('OPENSSL_CONF');
        if ($envConf && file_exists($envConf)) {
            $options['config'] = $envConf;
            return $options;
        }

        // Scan common PHP/Windows configuration paths
        $phpDir = dirname(PHP_BINARY);
        $searchPaths = [
            $phpDir . '/extras/ssl/openssl.cnf',
            $phpDir . '/openssl.cnf',
            'C:/xampp/php/extras/ssl/openssl.cnf',
            'C:/laragon/bin/php/openssl.cnf',
        ];

        foreach ($searchPaths as $path) {
            if (file_exists($path)) {
                $options['config'] = $path;
                break;
            }
        }

        return $options;
    }
}
