<?php

namespace Daniv04\HaciendaPackage\Services\Hacienda;

/**
 * CertificateLoader
 *
 * Loads a .p12 (PKCS#12) certificate from the SINPE Digital Signature
 * and exposes the private key and public X509 certificate.
 */
class CertificateLoaderService
{
    private string $privateKey;
    private string $certificate;
    private array  $chain = [];

    private string $certificateBase64;
    private string $certificateDigestSha1;
    private string $certificateDigestSha256;
    private array  $issuerSerial;

    /**
     * @param string $p12Path  Absolute path to the .p12 file
     * @param string $password Password for the .p12 file
     *
     * @throws \RuntimeException if the file does not exist or the password is incorrect
     */
    public function __construct(string $p12Path, string $password)
    {
        if (!file_exists($p12Path)) {
            throw new \RuntimeException("Certified not found: {$p12Path}");
        }

        $p12Contents = file_get_contents($p12Path);
        $certs       = [];

        // openssl_pkcs12_read extracts:
        //   $certs['pkey']       → private key in PEM format
        //   $certs['cert']       → public certificate in PEM format
        //   $certs['extracerts'] → intermediate certificate chain
        if (!openssl_pkcs12_read($p12Contents, $certs, $password)) {
            throw new \RuntimeException(
                'Could not read the .p12 file. Please verify the password or the file format.'
            );
        }

        $this->privateKey  = $certs['pkey'];
        $this->certificate = $certs['cert'];
        $this->chain       = $certs['extracerts'] ?? [];

        // Pre-compute immutable derived values
        $this->certificateBase64 = preg_replace(
            '/-----BEGIN CERTIFICATE-----|-----END CERTIFICATE-----|\s/',
            '',
            $this->certificate
        );

        $derCert = base64_decode($this->certificateBase64);
        $this->certificateDigestSha1   = base64_encode(hash('sha1', $derCert, true));
        $this->certificateDigestSha256 = base64_encode(hash('sha256', $derCert, true));

        $this->issuerSerial = $this->parseIssuerSerial();
    }

    /**
     * Returns the private key in PEM format.
     */
    public function getPrivateKey(): string
    {
        return $this->privateKey;
    }

    /**
     * Returns the public certificate in PEM format.
     */
    public function getCertificate(): string
    {
        return $this->certificate;
    }

    /**
     * Returns the X509 certificate as a clean base64 string (without PEM headers).
     * This is what goes inside <ds:X509Certificate> in the XML.
     */
    public function getCertificateBase64(): string
    {
        return $this->certificateBase64;
    }

    /**
     * Returns the SHA-1 digest of the certificate in base64.
     * This goes inside <xades:CertDigest> as required by XAdES.
     */
    public function getCertificateDigestSha1(): string
    {
        return $this->certificateDigestSha1;
    }

    /**
     * Returns the SHA-256 digest of the certificate in base64.
     * More modern alternative for <xades:CertDigest>.
     */
    public function getCertificateDigestSha256(): string
    {
        return $this->certificateDigestSha256;
    }

    /**
     * Extracts the Issuer Name and Serial Number from the certificate.
     * These go inside <xades:IssuerSerial>.
     *
     * @return array{issuer: string, serial: string}
     */
    public function getIssuerSerial(): array
    {
        return $this->issuerSerial;
    }

    /**
     * Parses issuer and serial number from the certificate (called once in constructor).
     */
    private function parseIssuerSerial(): array
    {
        $certResource = openssl_x509_read($this->certificate);
        $certDetails  = openssl_x509_parse($certResource);

        if ($certDetails === false) {
            throw new \RuntimeException('Could not parse the X509 certificate.');
        }

        $issuer = $certDetails['issuer'];

        if (!is_array($issuer)) {
            throw new \RuntimeException('The certificate does not contain a valid Issuer.');
        }

        return [
            'issuer' => $this->buildDn($issuer),
            'serial' => (string) $certDetails['serialNumber'],
        ];
    }

    /**
     * Converts the openssl issuer array to DN string format.
     */
    private function buildDn(array $issuer): string
    {
        $parts = [];

        // Standard DN field order for SINPE certificates
        $order = ['CN', 'OU', 'O', 'C', 'serialNumber'];

        foreach ($order as $key) {
            if (isset($issuer[$key])) {
                $parts[] = "{$key}={$issuer[$key]}";
            }
        }

        return implode(',', $parts);
    }
}