<?php

namespace Daniv04\HaciendaPackage\Services\Webhook;

use DOMDocument;
use DOMXPath;
use Daniv04\HaciendaPackage\Models\Receipt;
use Daniv04\HaciendaPackage\Traits\XmlBase64DecoderTrait;
use Illuminate\Support\Facades\Log;

class WebhookVerifierService
{
    use XmlBase64DecoderTrait;

    private const NS_DS = 'http://www.w3.org/2000/09/xmldsig#';

    /**
     * Known Hacienda certificate subject identifiers.
     * The sello electrónico of Ministerio de Hacienda uses this cédula jurídica.
     */
    private const HACIENDA_CERT_IDENTIFIERS = [
        'CPJ-2-100-042005',
        'MINISTERIO DE HACIENDA',
    ];

    /**
     * Verify the webhook payload authenticity.
     *
     * Performs two layers of verification:
     *  1. Clave structure — emisor ID embedded in the 50-digit clave must match the receipt.
     *  2. XML digital signature — the respuesta-xml must be signed by Hacienda's certificate.
     *
     * @return array{valid: bool, reason: ?string}
     */
    public function verify(array $webhookData, Receipt $receipt): array
    {
        // Layer 1: Validate clave contains the correct emisor
        $claveCheck = $this->verifyClave($webhookData['clave'] ?? '', $receipt);
        if (!$claveCheck['valid']) {
            return $claveCheck;
        }

        // Layer 2: Verify XML digital signature if respuesta-xml is present
        if (!empty($webhookData['respuesta-xml'])) {
            $signatureCheck = $this->verifyXmlSignature($webhookData['respuesta-xml']);
            if (!$signatureCheck['valid']) {
                return $signatureCheck;
            }
        }

        return ['valid' => true, 'reason' => null];
    }

    /**
     * Verify that the emisor ID embedded in the clave matches the receipt's emisor.
     *
     * Clave structure (50 digits):
     *   Pos 0-2:   Country code (506)
     *   Pos 3-4:   Day
     *   Pos 5-6:   Month
     *   Pos 7-8:   Year
     *   Pos 9-20:  Emisor ID (12 digits, zero-padded)
     *   Pos 21-40: Consecutive key (20 digits)
     *   Pos 41:    Situation
     *   Pos 42-49: Security code
     */
    private function verifyClave(string $clave, Receipt $receipt): array
    {
        if (strlen($clave) !== 50) {
            return ['valid' => false, 'reason' => 'Invalid clave length: expected 50 digits'];
        }

        // The clave in the webhook must match the receipt's stored ui_key
        if ($receipt->ui_key !== $clave) {
            return ['valid' => false, 'reason' => 'Clave does not match receipt ui_key'];
        }

        // Extract emisor ID from clave (positions 9-20, 12 digits)
        $claveEmisorId = substr($clave, 9, 12);

        // Pad the stored emisor number to 12 digits for comparison
        $storedEmisorId = str_pad(
            preg_replace('/[^0-9]/', '', $receipt->emissor_number),
            12,
            '0',
            STR_PAD_LEFT
        );

        if ($claveEmisorId !== $storedEmisorId) {
            return [
                'valid' => false,
                'reason' => "Emisor ID mismatch: clave contains {$claveEmisorId}, receipt has {$storedEmisorId}",
            ];
        }

        return ['valid' => true, 'reason' => null];
    }

    /**
     * Verify the XAdES digital signature of the Hacienda response XML.
     *
     * Steps:
     *  1. Decode base64 → load XML into DOMDocument
     *  2. Extract ds:SignedInfo and canonicalize with xml-exc-c14n
     *  3. Extract ds:SignatureValue (raw bytes)
     *  4. Extract X509Certificate from ds:KeyInfo
     *  5. openssl_verify(canonicalSignedInfo, signatureRaw, cert, SHA256)
     *  6. Validate the certificate belongs to Hacienda
     */
    private function verifyXmlSignature(string $base64Xml): array
    {
        try {
            $xmlString = $this->decodeBase64Xml($base64Xml);
            if ($xmlString === false) {
                return ['valid' => false, 'reason' => 'Failed to decode base64 XML response'];
            }

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

            if (!$dom->loadXML($xmlString)) {
                return ['valid' => false, 'reason' => 'Failed to parse XML response'];
            }

            $xpath = new DOMXPath($dom);
            $xpath->registerNamespace('ds', self::NS_DS);

            // Extract SignedInfo
            $signedInfoNode = $xpath->query('//ds:Signature/ds:SignedInfo')->item(0);
            if (!$signedInfoNode) {
                return ['valid' => false, 'reason' => 'No ds:SignedInfo found in XML signature'];
            }

            // Canonicalize SignedInfo with exclusive C14N
            $canonicalSignedInfo = $signedInfoNode->C14N(true, false);

            // Extract SignatureValue
            $signatureValueNode = $xpath->query('//ds:Signature/ds:SignatureValue')->item(0);
            if (!$signatureValueNode) {
                return ['valid' => false, 'reason' => 'No ds:SignatureValue found in XML signature'];
            }

            $signatureRaw = base64_decode(trim($signatureValueNode->textContent));
            if ($signatureRaw === false) {
                return ['valid' => false, 'reason' => 'Failed to decode SignatureValue'];
            }

            // Extract X509Certificate
            $certNode = $xpath->query('//ds:Signature/ds:KeyInfo/ds:X509Data/ds:X509Certificate')->item(0);
            if (!$certNode) {
                return ['valid' => false, 'reason' => 'No X509Certificate found in XML signature'];
            }

            $certBase64 = trim($certNode->textContent);
            $pemCert = "-----BEGIN CERTIFICATE-----\n"
                . chunk_split($certBase64, 64)
                . "-----END CERTIFICATE-----";

            // Verify the signature
            $publicKey = openssl_pkey_get_public($pemCert);
            if (!$publicKey) {
                return ['valid' => false, 'reason' => 'Failed to load public key from certificate'];
            }

            $verifyResult = openssl_verify($canonicalSignedInfo, $signatureRaw, $publicKey, OPENSSL_ALGO_SHA256);

            if ($verifyResult !== 1) {
                $opensslError = openssl_error_string() ?: 'unknown error';
                return [
                    'valid' => false,
                    'reason' => "XML signature verification failed: {$opensslError}",
                ];
            }

            // Verify the certificate belongs to Hacienda
            $certCheck = $this->verifyHaciendaCertificate($pemCert);
            if (!$certCheck['valid']) {
                return $certCheck;
            }

            return ['valid' => true, 'reason' => null];

        } catch (\Exception $e) {
            Log::error('Error verifying webhook XML signature', [
                'error' => $e->getMessage(),
            ]);
            return ['valid' => false, 'reason' => 'Signature verification error: ' . $e->getMessage()];
        }
    }

    /**
     * Verify that the X509 certificate in the signature belongs to Hacienda.
     * Checks the certificate subject for known Hacienda identifiers.
     */
    private function verifyHaciendaCertificate(string $pemCert): array
    {
        $certData = openssl_x509_parse($pemCert);

        if ($certData === false) {
            return ['valid' => false, 'reason' => 'Failed to parse X509 certificate from response'];
        }

        // Build a searchable string from the certificate subject
        /** @var array<string, string|array<string>> $subject */
        $subject = $certData['subject'] ?? [];
        $subjectParts = [];
        foreach ($subject as $value) {
            $subjectParts[] = is_array($value) ? implode(',', $value) : (string) $value;
        }
        $subjectString = strtoupper(implode(' ', $subjectParts));

        // Check if any known Hacienda identifier is present in the subject
        foreach (self::HACIENDA_CERT_IDENTIFIERS as $identifier) {
            if (str_contains($subjectString, strtoupper($identifier))) {
                return ['valid' => true, 'reason' => null];
            }
        }

        Log::warning('Webhook XML signed with unknown certificate', [
            'subject' => $certData['subject'],
            'issuer' => $certData['issuer'],
        ]);

        return [
            'valid' => false,
            'reason' => 'Certificate does not belong to Hacienda: subject does not contain known identifiers',
        ];
    }
}
