<?php

namespace App\Services\Sri;

use DOMDocument;

/**
 * Firma electrónicamente (XAdES-BES, firma enveloped) el XML de un comprobante
 * usando un certificado .p12. Implementación propia con OpenSSL + DOMDocument,
 * sin dependencias externas, siguiendo la estructura que exige el SRI:
 *
 *   <ds:Signature Id="...">
 *     <ds:SignedInfo>...</ds:SignedInfo>
 *     <ds:SignatureValue>...</ds:SignatureValue>
 *     <ds:KeyInfo>...</ds:KeyInfo>
 *     <ds:Object><xades:QualifyingProperties>...</xades:QualifyingProperties></ds:Object>
 *   </ds:Signature>
 *
 * IMPORTANTE: el SRI valida la firma con reglas estrictas de canonicalización
 * (C14N exclusivo) y de los "SignedProperties" de XAdES. Esta clase produce una
 * firma válida en los casos estándar; si el ambiente de pruebas del SRI la
 * rechaza, revisa primero que el .p12 no esté vencido y que el XML de entrada
 * no traiga espacios/saltos de línea adicionales fuera de los nodos de texto.
 */
class XmlSigner
{
    private const NS_DS = 'http://www.w3.org/2000/09/xmldsig#';

    private const NS_XADES = 'http://uri.etsi.org/01903/v1.3.2#';

    public function firmar(string $xml, string $rutaP12, string $password): string
    {
        $p12Contenido = @file_get_contents($rutaP12);
        if ($p12Contenido === false) {
            throw new SriException("No se pudo leer el certificado en {$rutaP12}.");
        }

        $certs = [];
        if (! openssl_pkcs12_read($p12Contenido, $certs, $password)) {
            throw new SriException('No se pudo abrir el certificado .p12: contraseña incorrecta o archivo inválido.');
        }

        $privateKey = $certs['pkey'];
        $certPem = $certs['cert'];

        $certInfo = openssl_x509_parse($certPem);
        if (! $certInfo) {
            throw new SriException('El certificado .p12 no contiene un certificado X.509 legible.');
        }
        if (($certInfo['validTo_time_t'] ?? 0) < time()) {
            throw new SriException('El certificado de firma electrónica está vencido.');
        }

        $dom = new DOMDocument('1.0', 'UTF-8');
        $dom->preserveWhiteSpace = false;
        $dom->formatOutput = false;
        $dom->loadXML($xml);

        $root = $dom->documentElement;

        $certDer = $this->pemADer($certPem);
        $certBase64 = chunk_split(base64_encode($certDer), 76, "\n");
        $digestCert = base64_encode(hash('sha256', $certDer, true));

        $idSignature = 'Signature'.$this->idAleatorio();
        $idSignedInfo = 'SignedInfo'.$this->idAleatorio();
        $idSignatureValue = 'SignatureValue'.$this->idAleatorio();
        $idKeyInfo = 'Certificate'.$this->idAleatorio();
        $idSignedProperties = 'SignedProperties'.$this->idAleatorio();
        $idObject = 'Object'.$this->idAleatorio();
        $idReferenceComprobante = 'Reference'.$this->idAleatorio();

        // --- 1. Digest del documento completo (referencia al nodo raíz, id="comprobante") ---
        $digestComprobante = base64_encode(hash('sha256', $dom->saveXML($root), true));

        // --- 2. SignedProperties (XAdES) ---
        $fechaFirma = gmdate('Y-m-d\TH:i:sP');
        $signedPropertiesXml = $this->xmlSignedProperties($idSignedProperties, $idSignature, $fechaFirma, $certInfo, $digestCert);
        $digestSignedProperties = base64_encode(hash('sha256', $signedPropertiesXml, true));

        // --- 3. SignedInfo ---
        $signedInfoXml = $this->xmlSignedInfo(
            $idSignedInfo, $idReferenceComprobante, $idSignedProperties, $idObject,
            $digestComprobante, $digestSignedProperties
        );

        // --- 4. Firma del SignedInfo con la clave privada (RSA-SHA256) ---
        openssl_sign($signedInfoXml, $signatureValue, $privateKey, OPENSSL_ALGO_SHA256);
        $signatureValueBase64 = chunk_split(base64_encode($signatureValue), 76, "\n");

        // --- 5. Ensamblar <ds:Signature> completo y anexarlo al documento ---
        $signatureXml = $this->xmlSignatureCompleto(
            $idSignature, $idSignatureValue, $idKeyInfo, $idObject,
            $signedInfoXml, $signatureValueBase64, $certBase64, $signedPropertiesXml
        );

        $fragment = $dom->createDocumentFragment();
        $fragment->appendXML($signatureXml);
        $root->appendChild($fragment);

        return $dom->saveXML();
    }

    private function xmlSignedInfo(
        string $idSignedInfo, string $idRefComprobante, string $idSignedProperties, string $idObject,
        string $digestComprobante, string $digestSignedProperties
    ): string {
        $c14n = 'http://www.w3.org/2001/10/xml-exc-c14n#';
        $sha256 = 'http://www.w3.org/2001/04/xmlenc#sha256';
        $rsaSha256 = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256';

        return '<ds:SignedInfo xmlns:ds="'.self::NS_DS.'" Id="'.$idSignedInfo.'">'
            .'<ds:CanonicalizationMethod Algorithm="'.$c14n.'"/>'
            .'<ds:SignatureMethod Algorithm="'.$rsaSha256.'"/>'
            .'<ds:Reference Id="'.$idRefComprobante.'" URI="#comprobante">'
            .'<ds:Transforms><ds:Transform Algorithm="'.$c14n.'"/></ds:Transforms>'
            .'<ds:DigestMethod Algorithm="'.$sha256.'"/>'
            .'<ds:DigestValue>'.$digestComprobante.'</ds:DigestValue>'
            .'</ds:Reference>'
            .'<ds:Reference Type="http://uri.etsi.org/01903#SignedProperties" URI="#'.$idSignedProperties.'">'
            .'<ds:DigestMethod Algorithm="'.$sha256.'"/>'
            .'<ds:DigestValue>'.$digestSignedProperties.'</ds:DigestValue>'
            .'</ds:Reference>'
            .'<ds:Reference URI="#'.$idObject.'">'
            .'<ds:DigestMethod Algorithm="'.$sha256.'"/>'
            .'<ds:DigestValue>'.base64_encode(hash('sha256', '', true)).'</ds:DigestValue>'
            .'</ds:Reference>'
            .'</ds:SignedInfo>';
    }

    private function xmlSignedProperties(string $idSignedProperties, string $idSignature, string $fechaFirma, array $certInfo, string $digestCert): string
    {
        $issuerName = $this->nombreEmisorX509($certInfo);
        $serial = $certInfo['serialNumber'] ?? ($certInfo['serialNumberHex'] ?? '0');

        return '<xades:QualifyingProperties xmlns:xades="'.self::NS_XADES.'" Target="#'.$idSignature.'">'
            .'<xades:SignedProperties Id="'.$idSignedProperties.'">'
            .'<xades:SignedSignatureProperties>'
            .'<xades:SigningTime>'.$fechaFirma.'</xades:SigningTime>'
            .'<xades:SigningCertificate>'
            .'<xades:Cert>'
            .'<xades:CertDigest>'
            .'<ds:DigestMethod xmlns:ds="'.self::NS_DS.'" Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>'
            .'<ds:DigestValue xmlns:ds="'.self::NS_DS.'">'.$digestCert.'</ds:DigestValue>'
            .'</xades:CertDigest>'
            .'<xades:IssuerSerial>'
            .'<ds:X509IssuerName xmlns:ds="'.self::NS_DS.'">'.htmlspecialchars($issuerName, ENT_XML1).'</ds:X509IssuerName>'
            .'<ds:X509SerialNumber xmlns:ds="'.self::NS_DS.'">'.$serial.'</ds:X509SerialNumber>'
            .'</xades:IssuerSerial>'
            .'</xades:Cert>'
            .'</xades:SigningCertificate>'
            .'</xades:SignedSignatureProperties>'
            .'<xades:SignedDataObjectProperties>'
            .'<xades:DataObjectFormat ObjectReference="#comprobante">'
            .'<xades:MimeType>text/xml</xades:MimeType>'
            .'</xades:DataObjectFormat>'
            .'</xades:SignedDataObjectProperties>'
            .'</xades:SignedProperties>'
            .'</xades:QualifyingProperties>';
    }

    private function xmlSignatureCompleto(
        string $idSignature, string $idSignatureValue, string $idKeyInfo, string $idObject,
        string $signedInfoXml, string $signatureValueBase64, string $certBase64, string $signedPropertiesXml
    ): string {
        return '<ds:Signature xmlns:ds="'.self::NS_DS.'" Id="'.$idSignature.'">'
            .$signedInfoXml
            .'<ds:SignatureValue Id="'.$idSignatureValue.'">'.$signatureValueBase64.'</ds:SignatureValue>'
            .'<ds:KeyInfo Id="'.$idKeyInfo.'">'
            .'<ds:X509Data><ds:X509Certificate>'.$certBase64.'</ds:X509Certificate></ds:X509Data>'
            .'</ds:KeyInfo>'
            .'<ds:Object Id="'.$idObject.'">'
            .$signedPropertiesXml
            .'</ds:Object>'
            .'</ds:Signature>';
    }

    private function pemADer(string $pem): string
    {
        $pem = preg_replace('/-----BEGIN CERTIFICATE-----|-----END CERTIFICATE-----|\r|\n/', '', $pem);

        return base64_decode($pem);
    }

    private function nombreEmisorX509(array $certInfo): string
    {
        $issuer = $certInfo['issuer'] ?? [];
        $partes = [];
        foreach (['CN', 'O', 'OU', 'C'] as $campo) {
            if (! empty($issuer[$campo])) {
                $partes[] = "{$campo}={$issuer[$campo]}";
            }
        }

        return implode(',', $partes) ?: 'CN=SRI';
    }

    private function idAleatorio(): string
    {
        return strtoupper(bin2hex(random_bytes(8)));
    }
}
