<?php
/**
 * Direct AEAT VERI*FACTU query service.
 * Uses ConsultaFactuSistemaFacturacion and parses the official response.
 */

if (!defined('ABSPATH')) {
    exit;
}

class VerifactuConsultaService
{
    private $module;

    public function __construct($module)
    {
        $this->module = $module;
    }

    public function query(array $filters)
    {
        if (!extension_loaded('curl')) {
            throw new Exception('La extension cURL de PHP no esta activa.');
        }
        if (!extension_loaded('openssl')) {
            throw new Exception('La extension OpenSSL de PHP no esta activa.');
        }
        if (!extension_loaded('dom')) {
            throw new Exception('La extension DOM de PHP no esta activa.');
        }

        $year = isset($filters['year']) ? (int) $filters['year'] : 0;
        $period = isset($filters['period']) ? str_pad((string) (int) $filters['period'], 2, '0', STR_PAD_LEFT) : '';
        if ($year < 2000 || $year > 2100) {
            throw new Exception('El ejercicio de consulta no es valido.');
        }
        if (!preg_match('/^(0[1-9]|1[0-2])$/', $period)) {
            throw new Exception('El periodo de consulta debe estar entre 01 y 12.');
        }
        $queryEnvironment = isset($filters['query_environment'])
            ? strtolower(trim((string) $filters['query_environment']))
            : VerifactuConfig::getEnvironment();
        if (!in_array($queryEnvironment, array(VerifactuConfig::ENV_PRODUCTION, VerifactuConfig::ENV_PREPRODUCTION), true)) {
            throw new Exception('El entorno de consulta AEAT no es valido.');
        }

        // A read-only query must authenticate first and use the fiscal identity
        // carried by the configured certificate. It must not depend on manually
        // typed issuer fields that can disagree with that certificate.
        VerifactuConfig::getEnvironment();
        $certificate = array();
        try {
            $certificate = VerifactuCertificateManager::createTemporaryPem($this->module);
            $identity = isset($certificate['identity']) && is_array($certificate['identity'])
                ? $certificate['identity']
                : VerifactuCertificateManager::extractFiscalIdentity($certificate['info']);
            $xml = $this->buildQueryXml($year, $period, $filters, $identity);
            $this->validateQueryXml($xml);
            $certificateType = isset($identity['certificate_type']) ? $identity['certificate_type'] : null;
            $endpoint = VerifactuConfig::getSoapEndpointForEnvironment($queryEnvironment, $certificateType);
            $http = VerifactuSoapHttpClient::post(
                $xml,
                $endpoint,
                $certificate,
                'ddverifactu/' . $this->module->version
            );
            $response = (string) $http['response'];
            if ($response === '') {
                throw new Exception('La AEAT no ha devuelto respuesta a la consulta.');
            }
            $result = $this->parseResponse($response);
            $result['query_identity'] = $identity;
            $result['environment'] = $queryEnvironment;
            $result['transmission_environment'] = VerifactuConfig::getEnvironment();
            $result['endpoint'] = $endpoint;
            if ($result['result'] === 'ConDatos' && empty($result['records'])) {
                throw new Exception(
                    'La AEAT indica ConDatos, pero no se han podido interpretar los registros de la respuesta. '
                    . 'Se bloquea el resultado para no mostrar falsamente 0 registros.'
                );
            }
            return $result;
        } catch (VerifactuTransportException $e) {
            throw new Exception('Error SOAP al consultar VeriFactu: ' . $e->getMessage());
        } finally {
            if (!empty($certificate)) {
                VerifactuCertificateManager::cleanup($certificate);
            }
        }
    }

    public function buildQueryXml($year, $period, array $filters = array(), array $identity = array())
    {
        $nsConsulta = 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/ConsultaLR.xsd';
        $nsInfo = 'https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd';
        $dom = new DOMDocument('1.0', 'UTF-8');
        $dom->formatOutput = false;
        $root = $dom->createElementNS($nsConsulta, 'sfLRC:ConsultaFactuSistemaFacturacion');
        $root->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:sf', $nsInfo);
        $dom->appendChild($root);

        $header = $dom->createElementNS($nsConsulta, 'sfLRC:Cabecera');
        $root->appendChild($header);
        $this->addText($dom, $header, $nsInfo, 'sf:IDVersion', '1.0');
        $issuerName = isset($identity['issuer_name']) && trim((string) $identity['issuer_name']) !== ''
            ? trim((string) $identity['issuer_name']) : VerifactuConfig::getIssuerName();
        $issuerNif = isset($identity['issuer_nif']) && trim((string) $identity['issuer_nif']) !== ''
            ? VerifactuConfig::normaliseSpanishNif($identity['issuer_nif'], 'NIF del certificado')
            : VerifactuConfig::getIssuerNif();
        $obligado = $dom->createElementNS($nsInfo, 'sf:ObligadoEmision');
        $header->appendChild($obligado);
        $this->addText($dom, $obligado, $nsInfo, 'sf:NombreRazon', $issuerName);
        $this->addText($dom, $obligado, $nsInfo, 'sf:NIF', $issuerNif);
        // IndicadorRepresentante is not a certificate-type flag. According to the AEAT
        // query schema it narrows the search to records in which the caller appears as
        // representative/advisor. A corporate representative certificate can query the
        // represented company's own records without that filter. Enabling it implicitly
        // would hide ordinary records whose NIFRepresentante is empty.
        if (!empty($filters['query_records_as_representative'])) {
            $this->addText($dom, $header, $nsInfo, 'sf:IndicadorRepresentante', 'S');
        }

        $filter = $dom->createElementNS($nsConsulta, 'sfLRC:FiltroConsulta');
        $root->appendChild($filter);
        $periodNode = $dom->createElementNS($nsConsulta, 'sfLRC:PeriodoImputacion');
        $filter->appendChild($periodNode);
        $this->addText($dom, $periodNode, $nsInfo, 'sf:Ejercicio', (string) $year);
        $this->addText($dom, $periodNode, $nsInfo, 'sf:Periodo', (string) $period);

        $number = isset($filters['document_number']) ? trim((string) $filters['document_number']) : '';
        if ($number !== '') {
            $this->addText($dom, $filter, $nsConsulta, 'sfLRC:NumSerieFactura', $number);
        }
        $date = isset($filters['issue_date']) ? trim((string) $filters['issue_date']) : '';
        if ($date !== '') {
            if (!$this->isValidDate($date)) {
                throw new Exception('La fecha de expedicion debe tener formato DD-MM-AAAA.');
            }
            $dateNode = $dom->createElementNS($nsConsulta, 'sfLRC:FechaExpedicionFactura');
            $filter->appendChild($dateNode);
            $this->addText($dom, $dateNode, $nsInfo, 'sf:FechaExpedicionFactura', $date);
        }
        if (!empty($filters['page_key']) && is_array($filters['page_key'])) {
            $key = $filters['page_key'];
            if (!empty($key['issuer']) && !empty($key['number']) && !empty($key['date'])) {
                $keyNode = $dom->createElementNS($nsConsulta, 'sfLRC:ClavePaginacion');
                $filter->appendChild($keyNode);
                $this->addText($dom, $keyNode, $nsInfo, 'sf:IDEmisorFactura', $key['issuer']);
                $this->addText($dom, $keyNode, $nsInfo, 'sf:NumSerieFactura', $key['number']);
                $this->addText($dom, $keyNode, $nsInfo, 'sf:FechaExpedicionFactura', $key['date']);
            }
        }
        $additional = $dom->createElementNS($nsConsulta, 'sfLRC:DatosAdicionalesRespuesta');
        $root->appendChild($additional);
        $this->addText($dom, $additional, $nsConsulta, 'sfLRC:MostrarNombreRazonEmisor', 'S');
        $this->addText($dom, $additional, $nsConsulta, 'sfLRC:MostrarSistemaInformatico', 'S');

        return $dom->saveXML($dom->documentElement);
    }

    public function parseResponse($soapXml)
    {
        $dom = new DOMDocument();
        libxml_use_internal_errors(true);
        if (!$dom->loadXML((string) $soapXml, LIBXML_NONET | LIBXML_NOBLANKS)) {
            libxml_clear_errors();
            throw new Exception('La respuesta de consulta AEAT no contiene XML valido.');
        }
        libxml_clear_errors();
        $xp = new DOMXPath($dom);
        $result = array(
            'result' => $this->firstText($xp, '//*[local-name()="ResultadoConsulta"]'),
            'pagination' => $this->firstText($xp, '//*[local-name()="IndicadorPaginacion"]'),
            'year' => $this->firstText($xp, '//*[local-name()="PeriodoImputacion"]/*[local-name()="Ejercicio"]'),
            'period' => $this->firstText($xp, '//*[local-name()="PeriodoImputacion"]/*[local-name()="Periodo"]'),
            'records' => array(),
            'page_key' => null,
            'raw_response' => (string) $soapXml,
        );
        if (!in_array($result['result'], array('ConDatos', 'SinDatos'), true)) {
            throw new Exception('La respuesta AEAT no contiene un ResultadoConsulta reconocido.');
        }
        foreach ($xp->query('//*[local-name()="RegistroRespuestaConsultaFactuSistemaFacturacion"]') as $node) {
            $local = new DOMXPath($dom);
            $record = array(
                'issuer_nif' => $this->firstText($local, './/*[local-name()="IDFactura"]/*[local-name()="IDEmisorFactura"]', $node),
                'document_number' => $this->firstText($local, './/*[local-name()="IDFactura"]/*[local-name()="NumSerieFactura"]', $node),
                'issue_date' => $this->firstText($local, './/*[local-name()="IDFactura"]/*[local-name()="FechaExpedicionFactura"]', $node),
                'issuer_name' => $this->firstText($local, './/*[local-name()="DatosRegistroFacturacion"]/*[local-name()="NombreRazonEmisor"]', $node),
                'invoice_type' => $this->firstText($local, './/*[local-name()="DatosRegistroFacturacion"]/*[local-name()="TipoFactura"]', $node),
                'rectification_type' => $this->firstText($local, './/*[local-name()="DatosRegistroFacturacion"]/*[local-name()="TipoRectificativa"]', $node),
                'description' => $this->firstText($local, './/*[local-name()="DatosRegistroFacturacion"]/*[local-name()="DescripcionOperacion"]', $node),
                'tax_total' => $this->firstText($local, './/*[local-name()="DatosRegistroFacturacion"]/*[local-name()="CuotaTotal"]', $node),
                'total' => $this->firstText($local, './/*[local-name()="DatosRegistroFacturacion"]/*[local-name()="ImporteTotal"]', $node),
                'fingerprint' => $this->firstText($local, './/*[local-name()="DatosRegistroFacturacion"]/*[local-name()="Huella"]', $node),
                'record_timestamp' => $this->firstText($local, './/*[local-name()="DatosRegistroFacturacion"]/*[local-name()="FechaHoraHusoGenRegistro"]', $node),
                'presenter_nif' => $this->firstText($local, './/*[local-name()="DatosPresentacion"]/*[local-name()="NIFPresentador"]', $node),
                'presentation_timestamp' => $this->firstText($local, './/*[local-name()="DatosPresentacion"]/*[local-name()="TimestampPresentacion"]', $node),
                'request_id' => $this->firstText($local, './/*[local-name()="DatosPresentacion"]/*[local-name()="IdPeticion"]', $node),
                'status' => $this->firstText($local, './/*[local-name()="EstadoRegistro"]/*[local-name()="EstadoRegistro"]', $node),
                'error_code' => $this->firstText($local, './/*[local-name()="EstadoRegistro"]/*[local-name()="CodigoErrorRegistro"]', $node),
                'error_description' => $this->firstText($local, './/*[local-name()="EstadoRegistro"]/*[local-name()="DescripcionErrorRegistro"]', $node),
                'last_modified' => $this->firstText($local, './/*[local-name()="EstadoRegistro"]/*[local-name()="TimestampUltimaModificacion"]', $node),
            );
            $result['records'][] = $record;
        }
        $keyNode = $xp->query('//*[local-name()="RespuestaConsultaFactuSistemaFacturacion"]/*[local-name()="ClavePaginacion"]')->item(0);
        if ($keyNode) {
            $result['page_key'] = array(
                'issuer' => $this->firstText($xp, './*[local-name()="IDEmisorFactura"]', $keyNode),
                'number' => $this->firstText($xp, './*[local-name()="NumSerieFactura"]', $keyNode),
                'date' => $this->firstText($xp, './*[local-name()="FechaExpedicionFactura"]', $keyNode),
            );
        }
        return $result;
    }

    private function validateQueryXml($xml)
    {
        $dom = new DOMDocument();
        libxml_use_internal_errors(true);
        if (!$dom->loadXML($xml, LIBXML_NONET)) {
            libxml_clear_errors();
            throw new Exception('No se ha podido construir el XML de consulta.');
        }
        $xsd = dirname(__DIR__) . '/wsdl/ConsultaLR.xsd';
        if (!$dom->schemaValidate($xsd)) {
            $errors = libxml_get_errors();
            $message = isset($errors[0]) ? trim($errors[0]->message) : 'error XSD desconocido';
            libxml_clear_errors();
            throw new Exception('La consulta no cumple el XSD oficial de la AEAT: ' . $message);
        }
        libxml_clear_errors();
    }

    private function addText(DOMDocument $dom, DOMElement $parent, $namespace, $qualifiedName, $value)
    {
        $node = $dom->createElementNS($namespace, $qualifiedName);
        $node->appendChild($dom->createTextNode((string) $value));
        $parent->appendChild($node);
        return $node;
    }

    private function firstText(DOMXPath $xp, $query, ?DOMNode $context = null)
    {
        $nodes = $context ? $xp->query($query, $context) : $xp->query($query);
        return ($nodes && $nodes->length) ? trim((string) $nodes->item(0)->textContent) : '';
    }

    private function isValidDate($date)
    {
        $dt = DateTime::createFromFormat('!d-m-Y', (string) $date);
        return $dt && $dt->format('d-m-Y') === (string) $date;
    }
}
