<?php
/**
 * XmlSigner.php
 * 
 * Clase encargada de firmar XMLs usando el estándar XAdES-BES
 * Requerido por el SRI de Ecuador.
 * 
 * Autor: Gemini Antigravity
 */

class XmlSigner
{
    private $certs = [];

    /**
     * Firma un documento XML con un archivo P12
     * 
     * @param string $xmlContent El contenido XML a firmar
     * @param string $p12Path Ruta al archivo .p12
     * @param string $password Contraseña del archivo .p12
     * @return string|false El XML firmado o false si falla
     */
    public function firmarXml($xmlContent, $p12Path, $password)
    {
        if (!file_exists($p12Path)) {
            throw new Exception("El archivo de firma electrónica no existe en la ruta especificada.");
        }

        $pkcs12 = file_get_contents($p12Path);
        $cert_store = [];

        if (!openssl_pkcs12_read($pkcs12, $cert_store, $password)) {
            $msg = "";
            while ($err = openssl_error_string()) {
                $msg .= $err . " | ";
            }
            throw new Exception("No se pudo leer el archivo P12. Password: '$password'. Error OpenSSL: " . $msg);
        }

        // Extraer certificado y clave privada
        if (!isset($cert_store['cert']) || !isset($cert_store['pkey'])) {
            throw new Exception("El archivo P12 no contiene el certificado o la clave privada necesarios.");
        }

        $localCert = $cert_store['cert'];
        $privateKey = $cert_store['pkey'];

        // Limpiar XML y cargar en DOMDocument
        $doc = new DOMDocument('1.0', 'UTF-8');
        $doc->preserveWhiteSpace = true; // IMPORTANT: Maintain original structure for signature validity
        $doc->formatOutput = false;

        // Manejo de errores de carga XML
        libxml_use_internal_errors(true);
        if (!$doc->loadXML($xmlContent)) {
            $errors = libxml_get_errors();
            libxml_clear_errors();
            throw new Exception("XML inválido: " . $errors[0]->message);
        }

        // --- CALCULAR HASH DEL DOCUMENTO ORIGINAL (ANTES DE TOCAR EL DOM) ---
        // Esto evita problemas de alteración de espacios al insertar/borrar nodos de Firma.
        $xpath = new DOMXPath($doc);
        $nodes = $xpath->query('//*[@id="comprobante"]');
        if ($nodes->length > 0) {
            $nodeToSign = $nodes->item(0);
        } else {
            $nodeToSign = $doc->documentElement;
        }

        // C14N INCLUSIVO (Legacy standard) - Pre-Calculado
        $c14nForHash = $nodeToSign->C14N(false, false, null, null);
        $documentDigestValue = base64_encode(sha1($c14nForHash, true));
        // -------------------------------------------------------------------

        // Generar números aleatorios para IDs únicos
        $signatureID = 'Signature-' . mt_rand(100000, 999999);
        $signedInfoID = 'SignedInfo-' . mt_rand(100000, 999999);
        $signedPropertiesID = 'SignedProperties-' . mt_rand(100000, 999999);
        $referenceID = 'Reference-' . mt_rand(100000, 999999);
        $certificateID = 'Certificate-' . mt_rand(100000, 999999);
        $objectID = 'Object-' . mt_rand(100000, 999999);

        $xmlns_etsi = 'http://uri.etsi.org/01903/v1.3.2#'; // Constante namespace ETSI

        // --- 1. Preparar el nodo Signature ---
        $signatureNode = $doc->createElementNS('http://www.w3.org/2000/09/xmldsig#', 'ds:Signature');
        $signatureNode->setAttribute('Id', $signatureID);
        $doc->documentElement->appendChild($signatureNode);

        // --- 2. Preparar SignedInfo ---
        $signedInfoNode = $doc->createElement('ds:SignedInfo');
        $signedInfoNode->setAttribute('Id', $signedInfoID);
        $signatureNode->appendChild($signedInfoNode);

        // CanonicalizationMethod
        $canonMethodRel = $doc->createElement('ds:CanonicalizationMethod');
        $canonMethodRel->setAttribute('Algorithm', 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315');
        $signedInfoNode->appendChild($canonMethodRel);

        // SignatureMethod
        $sigMethod = $doc->createElement('ds:SignatureMethod');
        $sigMethod->setAttribute('Algorithm', 'http://www.w3.org/2000/09/xmldsig#rsa-sha1');
        $signedInfoNode->appendChild($sigMethod);

        // --- 3. Reference (al documento completo) ---
        $referenceNode = $doc->createElement('ds:Reference');
        $referenceNode->setAttribute('Id', $referenceID);
        $referenceNode->setAttribute('URI', '#comprobante'); // Referencia explícita al ID del comprobante
        // $referenceNode->setAttribute('Type', 'http://www.w3.org/2000/09/xmldsig#Object'); // REMOVED: Opcional y conflictivo a veces
        $signedInfoNode->appendChild($referenceNode);

        // Transforms
        $transformsNode = $doc->createElement('ds:Transforms');
        $referenceNode->appendChild($transformsNode);

        $transformEnv = $doc->createElement('ds:Transform');
        $transformEnv->setAttribute('Algorithm', 'http://www.w3.org/2000/09/xmldsig#enveloped-signature');
        $transformsNode->appendChild($transformEnv);

        // DigestMethod para Reference
        $digestMethodRef = $doc->createElement('ds:DigestMethod');
        $digestMethodRef->setAttribute('Algorithm', 'http://www.w3.org/2000/09/xmldsig#sha1');
        $referenceNode->appendChild($digestMethodRef);

        // Usamos el hash calculado PREVIAMENTE al inicio
        $digestValueNode = $doc->createElement('ds:DigestValue', $documentDigestValue);
        $referenceNode->appendChild($digestValueNode);


        // --- 4. KeyInfo y X509Data ---
        // Procesar certificado para obtener datos
        $certData = openssl_x509_parse($localCert);
        $certIssuer = $this->parseIssuer($certData['issuer']);
        $certSerial = $certData['serialNumber'];

        // Limpiar cabeceras del certificado PEM para obtener solo el base64
        $certClean = str_replace(array("-----BEGIN CERTIFICATE-----", "-----END CERTIFICATE-----", "\n", "\r"), '', $localCert);

        $keyInfoNode = $doc->createElement('ds:KeyInfo');
        $keyInfoNode->setAttribute('Id', $certificateID);
        $signatureNode->appendChild($keyInfoNode);

        $x509DataNode = $doc->createElement('ds:X509Data');
        $keyInfoNode->appendChild($x509DataNode);

        $x509CertNode = $doc->createElement('ds:X509Certificate', $certClean);
        $x509DataNode->appendChild($x509CertNode);

        // Agregar KeyValue (Módulo y Exponente RSA) - Restaurado
        $resPublicKey = openssl_pkey_get_public($localCert);
        $detailPublicKey = openssl_pkey_get_details($resPublicKey);
        $modulus = base64_encode($detailPublicKey['rsa']['n']);
        $exponent = base64_encode($detailPublicKey['rsa']['e']);

        $keyValueNode = $doc->createElement('ds:KeyValue');
        $rsaKeyValueNode = $doc->createElement('ds:RSAKeyValue');
        $modulusNode = $doc->createElement('ds:Modulus', $modulus);
        $exponentNode = $doc->createElement('ds:Exponent', $exponent);

        $rsaKeyValueNode->appendChild($modulusNode);
        $rsaKeyValueNode->appendChild($exponentNode);
        $keyValueNode->appendChild($rsaKeyValueNode);
        $keyInfoNode->appendChild($keyValueNode);


        // --- 5. Object y QualifyingProperties (XAdES-BES) ---
        $objectNode = $doc->createElement('ds:Object');
        $objectNode->setAttribute('Id', $objectID);
        $signatureNode->appendChild($objectNode);

        $qualifyingPropsNode = $doc->createElement('etsi:QualifyingProperties');
        $qualifyingPropsNode->setAttribute('Target', '#' . $signatureID);
        $qualifyingPropsNode->setAttribute('xmlns:etsi', 'http://uri.etsi.org/01903/v1.3.2#');
        $objectNode->appendChild($qualifyingPropsNode);

        $signedPropsNode = $doc->createElementNS($xmlns_etsi, 'etsi:SignedProperties');
        $signedPropsNode->setAttribute('Id', $signedPropertiesID);
        // Explicit namespaces valid for both but cleaner for Inclusive to let inheritance work
        // $signedPropsNode->setAttribute('xmlns:etsi', $xmlns_etsi); 
        $qualifyingPropsNode->appendChild($signedPropsNode);

        $signedSignaturePropsNode = $doc->createElementNS($xmlns_etsi, 'etsi:SignedSignatureProperties');
        $signedPropsNode->appendChild($signedSignaturePropsNode);

        // SigningTime
        $signingTimeNode = $doc->createElementNS($xmlns_etsi, 'etsi:SigningTime', date('Y-m-d\TH:i:sP')); // ISO 8601
        $signedSignaturePropsNode->appendChild($signingTimeNode);

        // SigningCertificate
        $signingCertNode = $doc->createElementNS($xmlns_etsi, 'etsi:SigningCertificate');
        $signedSignaturePropsNode->appendChild($signingCertNode);

        $certNode = $doc->createElementNS($xmlns_etsi, 'etsi:Cert');
        $signingCertNode->appendChild($certNode);

        $certDigestNode = $doc->createElementNS($xmlns_etsi, 'etsi:CertDigest');
        $certNode->appendChild($certDigestNode);

        $digestMethodCertNode = $doc->createElement('ds:DigestMethod');
        $digestMethodCertNode->setAttribute('Algorithm', 'http://www.w3.org/2000/09/xmldsig#sha1');
        $certDigestNode->appendChild($digestMethodCertNode);

        // Hash del certificado (DER, no PEM)
        $certDer = base64_decode($certClean);
        $certDigestValue = base64_encode(sha1($certDer, true));
        $digestValueCertNode = $doc->createElement('ds:DigestValue', $certDigestValue);
        $certDigestNode->appendChild($digestValueCertNode);

        $issuerSerialNode = $doc->createElementNS($xmlns_etsi, 'etsi:IssuerSerial');
        $certNode->appendChild($issuerSerialNode);

        $x509IssuerNameNode = $doc->createElement('ds:X509IssuerName', $certIssuer);
        $issuerSerialNode->appendChild($x509IssuerNameNode);

        $x509SerialNumberNode = $doc->createElement('ds:X509SerialNumber', $certSerial);
        $issuerSerialNode->appendChild($x509SerialNumberNode);

        // --- 6. Referencia a SignedProperties (Obligatorio en XAdES) ---
        $referencePropsNode = $doc->createElement('ds:Reference');
        $referencePropsNode->setAttribute('URI', '#' . $signedPropertiesID);
        $referencePropsNode->setAttribute('Type', 'http://uri.etsi.org/01903#SignedProperties');
        $signedInfoNode->appendChild($referencePropsNode);

        // Digest para SignedProperties
        // Para calcular el digest de SignedProperties, necesitamos su C14N
        // Hay que aislar el nodo SignedProperties para canonicalizarlo correctamente con sus namespaces

        // Truco: Renderizar el nodo y canonicalizarlo
        // NOTA: Para C14N inclusivo, los namespaces heredados son importantes.
        // En este contexto, 'etsi' está declarado en el padre QualifyingProperties.
        // Al canonicalizar SOLO SignedProperties, debemos asegurar que el namespace etsi aparezca.

        $c14nSignedProps = $signedPropsNode->C14N(false, false, null, null);

        // A veces C14N no incluye namespaces heredados si no se pasan explícitamente.
        // Implementación robusta: asegurar que xmlns:etsi esté en el nodo para el cálculo del hash si C14N no lo hace.
        // Sin embargo, C14N(true) debería manejarlo. Verificaremos.

        $digestMethodProps = $doc->createElement('ds:DigestMethod');
        $digestMethodProps->setAttribute('Algorithm', 'http://www.w3.org/2000/09/xmldsig#sha1');
        $referencePropsNode->appendChild($digestMethodProps);

        $digestValueProps = base64_encode(sha1($c14nSignedProps, true));
        $digestValuePropsNode = $doc->createElement('ds:DigestValue', $digestValueProps);
        $referencePropsNode->appendChild($digestValuePropsNode);


        // --- 7. CALCULAR LA FIRMA FINAL (SignatureValue) ---
        // Se firma el nodo SignedInfo canonicalizado
        $c14nSignedInfo = $signedInfoNode->C14N(false, false, null, null);

        $signatureValue = '';
        if (!openssl_sign($c14nSignedInfo, $signatureValue, $privateKey, OPENSSL_ALGO_SHA1)) {
            throw new Exception("Error al generar la firma criptográfica.");
        }

        $signatureValueEncoded = base64_encode($signatureValue);

        // Insertar SignatureValue en su lugar (entre SignedInfo y KeyInfo)
        $signatureValueNode = $doc->createElement('ds:SignatureValue', $signatureValueEncoded);
        $signatureValueNode->setAttribute('Id', 'SignatureValue-' . mt_rand(100000, 999999));

        $signatureNode->insertBefore($signatureValueNode, $keyInfoNode);

        return $doc->saveXML();
    }

    /**
     * Auxiliar para formatear el Issuer correctamente (RFC 2253 invertido para compatibilidad)
     * SRI suele requerir el formato LDAP string
     */
    private function parseIssuer($issuerData)
    {
        // $issuerData viene de openssl_x509_parse y es un array
        // Ejemplo: ['C' => 'EC', 'O' => 'BCE', 'CN' => '...']
        // Necesitamos string: "CN=...,OU=...,O=...,C=..."

        $parts = [];
        if (isset($issuerData['CN']))
            $parts[] = "CN=" . $issuerData['CN'];
        if (isset($issuerData['OU'])) {
            if (is_array($issuerData['OU'])) {
                foreach ($issuerData['OU'] as $ou)
                    $parts[] = "OU=" . $ou;
            } else {
                $parts[] = "OU=" . $issuerData['OU'];
            }
        }
        if (isset($issuerData['O']))
            $parts[] = "O=" . $issuerData['O'];
        if (isset($issuerData['L']))
            $parts[] = "L=" . $issuerData['L'];
        if (isset($issuerData['ST']))
            $parts[] = "ST=" . $issuerData['ST'];
        if (isset($issuerData['C']))
            $parts[] = "C=" . $issuerData['C'];

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