<?php
/**
 * Central configuration and fail-closed validation for the VeriFactu engine.
 */

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

class VerifactuConfig
{
    const ENV_PRODUCTION = 'produccion';
    const ENV_PREPRODUCTION = 'preproduccion';

    const CERT_PERSON = 'persona';
    const CERT_SEAL = 'sello';

    const SOAP_PRODUCTION_PERSON = 'https://www1.agenciatributaria.gob.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP';
    const SOAP_PREPRODUCTION_PERSON = 'https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP';
    const SOAP_PRODUCTION_SEAL = 'https://www10.agenciatributaria.gob.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP';
    const SOAP_PREPRODUCTION_SEAL = 'https://prewww10.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP';

    const QR_PRODUCTION = 'https://www2.agenciatributaria.gob.es/wlpl/TIKE-CONT/ValidarQR';
    const QR_PREPRODUCTION = 'https://prewww2.aeat.es/wlpl/TIKE-CONT/ValidarQR';

    public static function installDefaults($moduleVersion)
    {
        $defaults = array(
            'DD_VF_ENVIRONMENT' => self::ENV_PREPRODUCTION,
            'DD_VF_CERTIFICATE_TYPE' => self::CERT_PERSON,
            'DD_VF_ISSUER_NAME' => '',
            'DD_VF_CERT_ISSUER_NIF' => '',
            'DD_VF_CERT_ISSUER_NAME' => '',
            'DD_VF_CERT_HOLDER_NIF' => '',
            'DD_VF_CERT_HOLDER_NAME' => '',
            'DD_VF_CERT_REPRESENTATIVE' => 'N',
            'DD_VF_CERT_IDENTITY_SOURCE' => '',
            'DD_VF_PRODUCER_NAME' => '',
            'DD_VF_PRODUCER_NIF' => '',
            'DD_VF_SYSTEM_NAME' => 'ddverifactu',
            'DD_VF_SYSTEM_ID' => '',
            'DD_VF_ONLY_VERIFACTU' => 'S',
            'DD_VF_POSSIBLE_MULTI_OT' => 'N',
            'DD_VF_MULTIPLE_OT' => 'N',
            'DD_VF_ALLOW_F2' => 0,
            'DD_VF_F2_LIMIT' => '400.00',
            'DD_VF_DEFAULT_RECTIFICATIVE_TYPE' => 'R1',
            'DD_VF_FOREIGN_ID_TYPE' => '02',
            'DD_VF_QR_SIZE_MM' => '35.00',
            'DD_VF_TAX_MAPPING_JSON' => '{}',
            'DD_VF_MODULE_VERSION' => (string) $moduleVersion,
            'DD_VF_QUEUE_BATCH_SIZE' => 20,
            'DD_VF_DECLARATION_CONFIRMED' => 0,
            'DD_VF_DECLARATION_REFERENCE' => '',
            'MODO_ENVIO_VERIFACTU' => 'manual',
            'DD_VF_LEGACY_CHAIN_DECISION' => 'review',
            'DD_VF_WC_INVOICE_PREFIX' => 'F-',
            'DD_VF_WC_CREDIT_PREFIX' => 'R-',
            'DD_VF_WC_INVOICE_START' => 1,
            'DD_VF_WC_CREDIT_START' => 1,
            'DD_VF_WC_ORDER_STATUSES' => 'completed',
            'DD_VF_COMPOUND_TAX_REVIEWED' => 0,
            'DD_VF_EMAIL_QR' => 1,
        );

        foreach ($defaults as $key => $value) {
            if (DdvWpConfigStore::get($key) === false && !DdvWpConfigStore::updateValue($key, $value)) {
                return false;
            }
        }

        try {
            self::getInstallationId();
            self::getCronToken();
        } catch (Exception $e) {
            return false;
        }

        return true;
    }

    public static function getEnvironment()
    {
        $value = strtolower(trim((string) DdvWpConfigStore::get('DD_VF_ENVIRONMENT')));
        if ($value === '') {
            return self::ENV_PREPRODUCTION;
        }
        if (!in_array($value, array(self::ENV_PRODUCTION, self::ENV_PREPRODUCTION), true)) {
            throw new Exception('El entorno VeriFactu configurado no es valido.');
        }

        return $value;
    }

    public static function isProduction()
    {
        return self::getEnvironment() === self::ENV_PRODUCTION;
    }

    public static function getCertificateType()
    {
        $value = strtolower(trim((string) DdvWpConfigStore::get('DD_VF_CERTIFICATE_TYPE')));
        if ($value === '') {
            return self::CERT_PERSON;
        }
        if (!in_array($value, array(self::CERT_PERSON, self::CERT_SEAL), true)) {
            throw new Exception('El tipo de certificado configurado no es valido.');
        }

        return $value;
    }

    public static function getSoapEndpoint()
    {
        return self::getSoapEndpointForEnvironment(self::getEnvironment(), self::getCertificateType());
    }

    /**
     * Resolves an AEAT endpoint without changing the module transmission environment.
     * This is used by the read-only consultation screen so production records can be
     * inspected while invoice transmission remains safely configured in preproduction.
     */
    public static function getSoapEndpointForEnvironment($environment, $certificateType = null)
    {
        $environment = strtolower(trim((string) $environment));
        if (!in_array($environment, array(self::ENV_PRODUCTION, self::ENV_PREPRODUCTION), true)) {
            throw new Exception('El entorno AEAT solicitado no es valido.');
        }

        $certificateType = $certificateType === null
            ? self::getCertificateType()
            : strtolower(trim((string) $certificateType));
        if (!in_array($certificateType, array(self::CERT_PERSON, self::CERT_SEAL), true)) {
            throw new Exception('El tipo de certificado solicitado no es valido.');
        }

        if ($certificateType === self::CERT_SEAL) {
            return $environment === self::ENV_PRODUCTION
                ? self::SOAP_PRODUCTION_SEAL
                : self::SOAP_PREPRODUCTION_SEAL;
        }

        return $environment === self::ENV_PRODUCTION
            ? self::SOAP_PRODUCTION_PERSON
            : self::SOAP_PREPRODUCTION_PERSON;
    }

    public static function getQrEndpoint()
    {
        return self::isProduction() ? self::QR_PRODUCTION : self::QR_PREPRODUCTION;
    }

    public static function getIssuerNif()
    {
        $certificateNif = self::normalizeNif(DdvWpConfigStore::get('DD_VF_CERT_ISSUER_NIF'));
        if (!self::isValidNifLength($certificateNif)) {
            throw new Exception(
                'No existe un NIF de emisor verificado a partir del certificado. Suba o verifique el certificado antes de generar, consultar o remitir registros VeriFactu.'
            );
        }
        return $certificateNif;
    }

    public static function getIssuerName()
    {
        $certificateName = trim((string) DdvWpConfigStore::get('DD_VF_CERT_ISSUER_NAME'));
        if ($certificateName === '') {
            throw new Exception(
                'No existe una razon social de emisor verificada a partir del certificado. Suba o verifique el certificado antes de generar, consultar o remitir registros VeriFactu.'
            );
        }
        return self::limitText($certificateName, 120, 'razon social del emisor obtenida del certificado');
    }

    public static function persistCertificateIdentity(array $identity)
    {
        $nif = self::normaliseSpanishNif(isset($identity['issuer_nif']) ? $identity['issuer_nif'] : '', 'NIF obtenido del certificado');
        $name = self::limitText(isset($identity['issuer_name']) ? $identity['issuer_name'] : '', 120, 'razon social obtenida del certificado');
        if ($name === '') {
            throw new Exception('El certificado no contiene una razon social utilizable para el emisor.');
        }

        DdvWpConfigStore::updateValue('DD_VF_CERT_ISSUER_NIF', $nif, true);
        DdvWpConfigStore::updateValue('DD_VF_CERT_ISSUER_NAME', $name, true);
        DdvWpConfigStore::updateValue('DD_VF_CERT_HOLDER_NIF', self::normalizeNif(isset($identity['holder_nif']) ? $identity['holder_nif'] : ''), true);
        DdvWpConfigStore::updateValue('DD_VF_CERT_HOLDER_NAME', trim((string) (isset($identity['holder_name']) ? $identity['holder_name'] : '')), true);
        DdvWpConfigStore::updateValue('DD_VF_CERT_REPRESENTATIVE', !empty($identity['representative']) ? 'S' : 'N');
        DdvWpConfigStore::updateValue('DD_VF_CERT_IDENTITY_SOURCE', trim((string) (isset($identity['source']) ? $identity['source'] : 'certificate')), true);
        $certificateType = isset($identity['certificate_type']) ? strtolower(trim((string) $identity['certificate_type'])) : '';
        if (!in_array($certificateType, array(self::CERT_PERSON, self::CERT_SEAL), true)) {
            throw new Exception('No se ha podido determinar de forma segura el tipo de certificado (persona/representante o sello).');
        }
        DdvWpConfigStore::updateValue('DD_VF_CERTIFICATE_TYPE', $certificateType);

        // Keep legacy keys synchronized so older auxiliary code cannot disagree with the certificate.
        DdvWpConfigStore::updateValue('ddverifactu_EMISOR_NIF', $nif, true);
        DdvWpConfigStore::updateValue('DD_VF_ISSUER_NAME', $name, true);
        return true;
    }

    public static function getCertificateIdentity()
    {
        return array(
            'issuer_nif' => self::normalizeNif(DdvWpConfigStore::get('DD_VF_CERT_ISSUER_NIF')),
            'issuer_name' => trim((string) DdvWpConfigStore::get('DD_VF_CERT_ISSUER_NAME')),
            'holder_nif' => self::normalizeNif(DdvWpConfigStore::get('DD_VF_CERT_HOLDER_NIF')),
            'holder_name' => trim((string) DdvWpConfigStore::get('DD_VF_CERT_HOLDER_NAME')),
            'representative' => strtoupper(trim((string) DdvWpConfigStore::get('DD_VF_CERT_REPRESENTATIVE'))) === 'S',
            'source' => trim((string) DdvWpConfigStore::get('DD_VF_CERT_IDENTITY_SOURCE')),
            'certificate_type' => self::getCertificateType(),
        );
    }

    public static function isCertificateRepresentative()
    {
        return strtoupper(trim((string) DdvWpConfigStore::get('DD_VF_CERT_REPRESENTATIVE'))) === 'S';
    }

    public static function getProducerName()
    {
        $name = trim((string) DdvWpConfigStore::get('DD_VF_PRODUCER_NAME'));
        if ($name === '') {
            throw new Exception(
                'Debe configurar la razon social del productor exactamente como figure en la declaracion responsable de esta version.'
            );
        }

        return self::limitText($name, 120, 'razon social del productor');
    }

    public static function getProducerNif()
    {
        return self::normaliseSpanishNif(
            DdvWpConfigStore::get('DD_VF_PRODUCER_NIF'),
            'NIF del productor del sistema informatico'
        );
    }

    public static function getSystemName()
    {
        $value = trim((string) DdvWpConfigStore::get('DD_VF_SYSTEM_NAME'));
        if ($value === '') {
            throw new Exception('Debe configurar el nombre declarado del sistema informatico.');
        }

        return self::limitText($value, 30, 'nombre del sistema informatico');
    }

    public static function getSystemId()
    {
        $value = strtoupper(trim((string) DdvWpConfigStore::get('DD_VF_SYSTEM_ID')));
        if ($value === '' || !preg_match('/^[A-Z0-9]{1,2}$/', $value)) {
            throw new Exception(
                'Debe configurar el identificador del sistema (1 o 2 caracteres alfanumericos) exactamente como figure en la declaracion responsable.'
            );
        }

        return $value;
    }

    public static function getInstallationId()
    {
        $value = trim((string) DdvWpConfigStore::get('DD_VF_INSTALLATION_ID'));
        if ($value === '') {
            $value = 'WC-' . self::randomToken(16);
            if (!DdvWpConfigStore::updateValue('DD_VF_INSTALLATION_ID', $value)) {
                throw new Exception('No se ha podido crear el identificador estable de esta instalacion.');
            }
        }

        return self::limitText($value, 100, 'numero de instalacion');
    }

    public static function getCronToken()
    {
        $value = trim((string) DdvWpConfigStore::get('DD_VF_CRON_TOKEN'));
        if ($value === '') {
            $value = self::randomToken(32);
            if (!DdvWpConfigStore::updateValue('DD_VF_CRON_TOKEN', $value)) {
                throw new Exception('No se ha podido crear el token del procesador VeriFactu.');
            }
        }

        if (!preg_match('/^[a-f0-9]{64}$/i', $value)) {
            throw new Exception('El token del procesador VeriFactu tiene un formato no valido.');
        }

        return strtolower($value);
    }

    public static function getLegacyChainDecision()
    {
        $value = strtolower(trim((string) DdvWpConfigStore::get('DD_VF_LEGACY_CHAIN_DECISION')));
        if ($value === '') {
            $value = 'review';
        }
        if (!in_array($value, array('review', 'use_latest', 'ignore_preproduction'), true)) {
            throw new Exception('La decision de migracion de la cadena historica no es valida.');
        }
        if (self::isProduction() && $value === 'ignore_preproduction') {
            throw new Exception('No se puede ignorar una cadena historica en produccion.');
        }
        return $value;
    }

    public static function assertLegacyChainDecision()
    {
        $table = DdvWpDb::prefix() . 'ddverifactu_huella';
        $exists = (bool) DdvWpDb::getInstance()->getValue(
            "SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE()"
            . " AND TABLE_NAME = '" . DdvWpDb::escape($table) . "'"
        );
        if (!$exists) {
            return true;
        }
        $rows = (int) DdvWpDb::getInstance()->getValue(
            'SELECT COUNT(*) FROM `' . DdvWpDb::quoteIdentifier($table) . '`'
            . " WHERE `huella` IS NOT NULL AND `huella` <> ''"
        );
        if ($rows > 0 && self::getLegacyChainDecision() === 'review') {
            throw new Exception(
                'Existen huellas de versiones anteriores. Debe verificarse su identidad y decidir si se encadena con la ultima huella antes de generar nuevos registros.'
            );
        }
        return true;
    }

    public static function getMode()
    {
        $value = strtolower(trim((string) DdvWpConfigStore::get('MODO_ENVIO_VERIFACTU')));
        if (!in_array($value, array('manual', 'automatico'), true)) {
            throw new Exception('El modo de envio debe ser manual o automatico.');
        }
        return $value;
    }

    public static function getChainKey()
    {
        return VerifactuRepository::buildChainKey(
            self::getEnvironment(),
            self::getIssuerNif(),
            self::getInstallationId()
        );
    }

    public static function assertResponsibleDeclaration()
    {
        if (!(bool) DdvWpConfigStore::get('DD_VF_DECLARATION_CONFIRMED')) {
            throw new Exception(
                'Debe existir y haberse confirmado la declaracion responsable del productor para esta version antes de usar produccion.'
            );
        }
        $reference = trim((string) DdvWpConfigStore::get('DD_VF_DECLARATION_REFERENCE'));
        if ($reference === '') {
            throw new Exception('Indique la referencia interna o ubicacion de la declaracion responsable de esta version.');
        }
        return self::limitText($reference, 255, 'referencia de la declaracion responsable');
    }

    public static function getSystemInfo($moduleVersion)
    {
        return array(
            'producer_name' => self::getProducerName(),
            'producer_nif' => self::getProducerNif(),
            'system_name' => self::getSystemName(),
            'system_id' => self::getSystemId(),
            'version' => self::limitText((string) $moduleVersion, 50, 'version del sistema'),
            'installation_id' => self::getInstallationId(),
            'only_verifactu' => self::getSiNo('DD_VF_ONLY_VERIFACTU', 'TipoUsoPosibleSoloVerifactu'),
            'possible_multi_ot' => self::getSiNo('DD_VF_POSSIBLE_MULTI_OT', 'TipoUsoPosibleMultiOT'),
            'multiple_ot' => self::getSiNo('DD_VF_MULTIPLE_OT', 'IndicadorMultiplesOT'),
        );
    }

    public static function getQrSizeMm()
    {
        $size = (float) DdvWpConfigStore::get('DD_VF_QR_SIZE_MM');
        if ($size < 30.0 || $size > 40.0) {
            throw new Exception('El QR tributario debe configurarse entre 30 y 40 mm.');
        }

        return round($size, 2);
    }

    public static function allowSimplifiedInvoices()
    {
        return (bool) DdvWpConfigStore::get('DD_VF_ALLOW_F2');
    }

    public static function getSimplifiedInvoiceLimit()
    {
        $limit = (float) DdvWpConfigStore::get('DD_VF_F2_LIMIT');
        if ($limit <= 0 || $limit > 3000) {
            throw new Exception('El limite configurado para facturas simplificadas no es valido.');
        }

        return round($limit, 2);
    }

    public static function getSimplifiedMaxTotal()
    {
        return self::getSimplifiedInvoiceLimit();
    }

    public static function getDefaultRectificativeType()
    {
        $value = strtoupper(trim((string) DdvWpConfigStore::get('DD_VF_DEFAULT_RECTIFICATIVE_TYPE')));
        if (!in_array($value, array('R1', 'R2', 'R3', 'R4'), true)) {
            throw new Exception(
                'Debe configurar la causa rectificativa automatica como R1, R2, R3 o R4. R5 se asigna automaticamente al rectificar una F2.'
            );
        }

        return $value;
    }

    public static function getForeignIdType()
    {
        $value = trim((string) DdvWpConfigStore::get('DD_VF_FOREIGN_ID_TYPE'));
        if (!in_array($value, array('02', '03', '04', '05', '06', '07'), true)) {
            throw new Exception('El tipo de identificador extranjero debe ser 02, 03, 04, 05, 06 o 07.');
        }

        return $value;
    }

    public static function getTaxMappings()
    {
        $raw = trim((string) DdvWpConfigStore::get('DD_VF_TAX_MAPPING_JSON'));
        if ($raw === '') {
            return array();
        }

        $decoded = json_decode($raw, true);
        if (!is_array($decoded) || json_last_error() !== JSON_ERROR_NONE) {
            throw new Exception('El mapa fiscal DD_VF_TAX_MAPPING_JSON no contiene JSON valido.');
        }

        foreach ($decoded as $key => $mapping) {
            if (!is_string($key) || !is_array($mapping)) {
                throw new Exception('Cada entrada del mapa fiscal debe ser un objeto identificado por una clave de texto.');
            }
        }

        return $decoded;
    }

    public static function getQueueBatchSize()
    {
        $value = (int) DdvWpConfigStore::get('DD_VF_QUEUE_BATCH_SIZE');

        return max(1, min(100, $value > 0 ? $value : 20));
    }

    public static function getRetryDelaySeconds($attempt)
    {
        $attempt = max(1, (int) $attempt);

        return max(60, min(21600, 60 * (int) pow(2, min(8, $attempt - 1))));
    }

    public static function getTimezone()
    {
        $timezone = function_exists('wp_timezone_string') ? trim((string) wp_timezone_string()) : '';
        if ($timezone === '') {
            $timezone = 'Europe/Madrid';
        }

        try {
            return new DateTimeZone($timezone);
        } catch (Exception $e) {
            return new DateTimeZone('Europe/Madrid');
        }
    }

    public static function now()
    {
        return (new DateTimeImmutable('now', self::getTimezone()))->format('Y-m-d\TH:i:sP');
    }

    public static function validateRecordConfiguration($moduleVersion)
    {
        if (PHP_VERSION_ID < 80100) {
            throw new Exception('El modulo requiere PHP 8.1 o superior por la libreria local de codigos QR incluida.');
        }
        if (!extension_loaded('dom')) {
            throw new Exception('La extension DOM de PHP es obligatoria para construir y validar los registros VeriFactu.');
        }
        if (!extension_loaded('openssl')) {
            throw new Exception('La extension OpenSSL de PHP es obligatoria para calcular huellas y custodiar el certificado.');
        }
        if (!class_exists('Endroid\\QrCode\\QrCode')) {
            throw new Exception('No se encuentra la libreria local de codigos QR incluida en el modulo.');
        }

        self::getIssuerName();
        self::getIssuerNif();
        self::getSystemInfo($moduleVersion);
        self::getQrSizeMm();
        self::getTaxMappings();
        self::getDefaultRectificativeType();
        self::getMode();
        self::getLegacyChainDecision();
        self::assertLegacyChainDecision();

        return true;
    }

    public static function validateTransmissionConfiguration($moduleVersion)
    {
        self::validateRecordConfiguration($moduleVersion);
        if (!extension_loaded('curl')) {
            throw new Exception('La extension cURL de PHP es obligatoria para remitir registros a la AEAT mediante SOAP 1.1.');
        }
        if (self::isProduction()) {
            if (self::getMode() !== 'automatico') {
                throw new Exception(
                    'El entorno de produccion exige el modo automatico. Cambie a preproduccion para realizar pruebas manuales.'
                );
            }
            self::assertResponsibleDeclaration();
        }

        return true;
    }

    public static function normaliseSpanishNif($value, $label)
    {
        $nif = self::normalizeNif($value);
        if (!self::isValidNifLength($nif)) {
            throw new Exception($label . ' debe contener exactamente 9 caracteres alfanumericos.');
        }

        return $nif;
    }

    public static function normalizeNif($value)
    {
        $nif = preg_replace('/[^A-Z0-9]/', '', strtoupper((string) $value));
        if (strpos($nif, 'ES') === 0 && strlen($nif) === 11) {
            $nif = substr($nif, 2);
        }

        return $nif;
    }

    public static function isValidNifLength($value)
    {
        return (bool) preg_match('/^[A-Z0-9]{9}$/', (string) $value);
    }

    public static function limitText($value, $maxLength, $label)
    {
        $value = trim((string) $value);
        $length = function_exists('mb_strlen') ? mb_strlen($value, 'UTF-8') : strlen($value);
        if ($length > (int) $maxLength) {
            throw new Exception('El campo ' . $label . ' supera los ' . (int) $maxLength . ' caracteres.');
        }

        return $value;
    }

    private static function getSiNo($key, $label)
    {
        $value = strtoupper(trim((string) DdvWpConfigStore::get($key)));
        if (!in_array($value, array('S', 'N'), true)) {
            throw new Exception($label . ' debe configurarse como S o N.');
        }

        return $value;
    }

    private static function randomToken($bytes)
    {
        try {
            return bin2hex(random_bytes((int) $bytes));
        } catch (Throwable $e) {
            return hash('sha256', uniqid((string) mt_rand(), true));
        }
    }
}
