<?php
/**
 * Parses the exact AEAT RespuestaLinea that belongs to the submitted record.
 */

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

class VerifactuResponseParser
{
    public static function parse($xml, array $expected)
    {
        if (!class_exists('DOMDocument')) {
            throw new Exception('La extension DOM no esta disponible para interpretar la respuesta de la AEAT.');
        }

        $previous = libxml_use_internal_errors(true);
        libxml_clear_errors();
        $doc = new DOMDocument();
        $loaded = $doc->loadXML((string) $xml, LIBXML_NONET | LIBXML_NOBLANKS);
        $errors = libxml_get_errors();
        libxml_clear_errors();
        libxml_use_internal_errors($previous);
        if (!$loaded) {
            $message = !empty($errors) ? trim($errors[0]->message) : 'respuesta no XML';
            throw new Exception('La respuesta de la AEAT no es XML valido: ' . $message);
        }

        $xpath = new DOMXPath($doc);
        $submissionStatus = self::firstValue($xpath, '//*[local-name()="EstadoEnvio"]');
        $waitSeconds = (int) self::firstValue($xpath, '//*[local-name()="TiempoEsperaEnvio"]');
        $csv = self::firstValue($xpath, '//*[local-name()="CSV"]');
        $requestId = self::firstValue($xpath, '//*[local-name()="IdPeticion"]');
        $presentationTimestamp = self::firstValue($xpath, '//*[local-name()="TimestampPresentacion"]');

        $expectedOperation = self::normaliseOperation(isset($expected['operation']) ? $expected['operation'] : '');
        $matched = null;
        $lines = $xpath->query('//*[local-name()="RespuestaLinea"]');
        foreach ($lines as $line) {
            $number = self::relativeValue($xpath, $line, './*[local-name()="IDFactura"]/*[local-name()="NumSerieFactura"]');
            $date = self::relativeValue($xpath, $line, './*[local-name()="IDFactura"]/*[local-name()="FechaExpedicionFactura"]');
            $issuer = self::relativeValue($xpath, $line, './*[local-name()="IDFactura"]/*[local-name()="IDEmisorFactura"]');
            $operation = self::relativeValue($xpath, $line, './*[local-name()="Operacion"]/*[local-name()="TipoOperacion"]');

            if ($number !== (string) $expected['number'] || $date !== (string) $expected['issue_date']) {
                continue;
            }
            if (!empty($expected['issuer_nif']) && $issuer !== (string) $expected['issuer_nif']) {
                continue;
            }
            if ($expectedOperation !== '' && self::normaliseOperation($operation) !== $expectedOperation) {
                continue;
            }
            $matched = $line;
            break;
        }

        if (!$matched) {
            throw new Exception(
                'La respuesta de la AEAT no contiene una RespuestaLinea coincidente para '
                . (string) $expected['number'] . ' de fecha ' . (string) $expected['issue_date']
                . ($expectedOperation !== '' ? ' y operacion ' . $expectedOperation : '') . '.'
            );
        }

        $recordStatus = self::relativeValue($xpath, $matched, './*[local-name()="EstadoRegistro"]');
        $errorCode = self::relativeValue($xpath, $matched, './*[local-name()="CodigoErrorRegistro"]');
        $errorDescription = self::relativeValue($xpath, $matched, './*[local-name()="DescripcionErrorRegistro"]');
        $duplicateStatus = self::relativeValue(
            $xpath,
            $matched,
            './*[local-name()="RegistroDuplicado"]/*[local-name()="EstadoRegistroDuplicado"]'
        );
        $duplicateRequestId = self::relativeValue(
            $xpath,
            $matched,
            './*[local-name()="RegistroDuplicado"]/*[local-name()="IdPeticionRegistroDuplicado"]'
        );
        $duplicateErrorCode = self::relativeValue(
            $xpath,
            $matched,
            './*[local-name()="RegistroDuplicado"]/*[local-name()="CodigoErrorRegistro"]'
        );
        $duplicateErrorDescription = self::relativeValue(
            $xpath,
            $matched,
            './*[local-name()="RegistroDuplicado"]/*[local-name()="DescripcionErrorRegistro"]'
        );
        $operation = self::relativeValue(
            $xpath,
            $matched,
            './*[local-name()="Operacion"]/*[local-name()="TipoOperacion"]'
        );

        $accepted = in_array($recordStatus, array('Correcto', 'AceptadoConErrores'), true);
        $acceptedWithErrors = $recordStatus === 'AceptadoConErrores';
        $duplicateAccepted = in_array($duplicateStatus, array('Correcta', 'AceptadaConErrores'), true)
            || ($expectedOperation === 'ANULACION' && $duplicateStatus === 'Anulada');
        if (!$accepted && $duplicateAccepted) {
            $accepted = true;
            $acceptedWithErrors = $duplicateStatus === 'AceptadaConErrores';
        }

        return array(
            'accepted' => $accepted,
            'accepted_with_errors' => $acceptedWithErrors,
            'duplicate' => $duplicateStatus !== '',
            'submission_status' => $submissionStatus,
            'record_status' => $recordStatus,
            'duplicate_status' => $duplicateStatus,
            'error_code' => $errorDescription !== '' || $errorCode !== '' ? $errorCode : $duplicateErrorCode,
            'error_description' => $errorDescription !== '' ? $errorDescription : $duplicateErrorDescription,
            'wait_seconds' => max(0, $waitSeconds),
            'csv' => $csv,
            'request_id' => $requestId,
            'presentation_timestamp' => $presentationTimestamp,
            'duplicate_request_id' => $duplicateRequestId,
            'number' => self::relativeValue($xpath, $matched, './*[local-name()="IDFactura"]/*[local-name()="NumSerieFactura"]'),
            'issue_date' => self::relativeValue($xpath, $matched, './*[local-name()="IDFactura"]/*[local-name()="FechaExpedicionFactura"]'),
            'operation' => $operation,
        );
    }

    private static function firstValue(DOMXPath $xpath, $expression)
    {
        $nodes = $xpath->query($expression);

        return $nodes && $nodes->length > 0 ? trim((string) $nodes->item(0)->textContent) : '';
    }

    private static function relativeValue(DOMXPath $xpath, DOMNode $context, $expression)
    {
        $nodes = $xpath->query($expression, $context);

        return $nodes && $nodes->length > 0 ? trim((string) $nodes->item(0)->textContent) : '';
    }

    private static function normaliseOperation($value)
    {
        $value = strtoupper(trim((string) $value));
        if (in_array($value, array('ALTA', 'A'), true)) {
            return 'ALTA';
        }
        if (in_array($value, array('ANULACION', 'ANULACIÓN', 'BAJA', 'B'), true)) {
            return 'ANULACION';
        }

        return $value;
    }
}
