<?php
/**
 * Plugin Name: digitalDot VeriFactu for WooCommerce
 * Plugin URI: https://digitaldot.es/
 * Description: Integracion VeriFactu para WooCommerce con facturas fiscales inmutables, rectificativas sobre reembolsos, encadenamiento SHA-256, cola persistente, QR local, consulta AEAT y certificado mTLS.
 * Version: 1.2.16.4
 * Author: digitalDot
 * Requires at least: 6.5
 * Requires PHP: 8.1
 * Requires Plugins: woocommerce
 * WC requires at least: 8.2
 * Text Domain: ddverifactu
 */

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

final class DdvVerifactuPlugin
{
    const VERSION = '1.2.16.4';
    const CERT_FILE = 'certificate.v2.enc';
    private static $instance;
    public $version = self::VERSION;
    private $service;
    private $documents;
    private $orderPanelRendered = false;

    public static function instance()
    {
        if (!self::$instance) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    private function __construct()
    {
        $this->loadCore();
        add_action('before_woocommerce_init', array($this, 'declareWooCompatibility'));
        add_action('plugins_loaded', array($this, 'pluginsLoaded'));
        add_filter('cron_schedules', array($this, 'cronSchedules'));
        add_action('ddverifactu_process_queue', array($this, 'cronProcessQueue'));
        add_action('rest_api_init', array($this, 'registerRestRoutes'));

        add_action('admin_menu', array($this, 'adminMenu'));
        add_action('admin_notices', array($this, 'adminNotices'));
        add_action('admin_footer', array($this, 'renderOrderPanelScript'));
        add_action('woocommerce_admin_order_data_after_order_details', array($this, 'renderOrderPanel'));
        add_action('woocommerce_order_status_changed', array($this, 'automaticOrderStatus'), 20, 4);
        add_action('woocommerce_refund_created', array($this, 'automaticRefund'), 20, 2);
        add_filter('woocommerce_checkout_fields', array($this, 'checkoutFields'));
        add_action('woocommerce_checkout_create_order', array($this, 'saveCheckoutTaxId'), 20, 2);
        add_filter('woocommerce_my_account_my_orders_actions', array($this, 'myAccountInvoiceAction'), 20, 2);
        add_action('woocommerce_email_after_order_table', array($this, 'emailFiscalLink'), 20, 4);

        foreach (array(
            'ddv_save_settings' => 'handleSaveSettings',
            'ddv_upload_certificate' => 'handleUploadCertificate',
            'ddv_test_certificate' => 'handleTestCertificate',
            'ddv_process_queue' => 'handleProcessQueue',
            'ddv_issue_invoice' => 'handleIssueInvoice',
            'ddv_send_invoice' => 'handleSendInvoice',
            'ddv_subsanate_invoice' => 'handleSubsanateInvoice',
            'ddv_cancel_invoice' => 'handleCancelInvoice',
            'ddv_issue_refund' => 'handleIssueRefund',
            'ddv_send_refund' => 'handleSendRefund',
            'ddv_subsanate_refund' => 'handleSubsanateRefund',
            'ddv_cancel_refund' => 'handleCancelRefund',
            'ddv_retry_record' => 'handleRetryRecord',
            'ddv_external_cancel' => 'handleExternalCancel',
            'ddv_print_invoice' => 'handlePrintInvoice',
            'ddv_print_refund' => 'handlePrintRefund',
            'ddv_pdf_invoice' => 'handlePdfInvoice',
            'ddv_pdf_refund' => 'handlePdfRefund',
            'ddv_customer_invoice' => 'handleCustomerInvoice',
            'ddv_customer_refund' => 'handleCustomerRefund',
            'ddv_customer_invoice_pdf' => 'handleCustomerInvoicePdf',
            'ddv_customer_refund_pdf' => 'handleCustomerRefundPdf',
        ) as $action => $method) {
            add_action('admin_post_' . $action, array($this, $method));
        }
        add_action('admin_post_nopriv_ddv_customer_invoice', array($this, 'handleCustomerInvoice'));
        add_action('admin_post_nopriv_ddv_customer_refund', array($this, 'handleCustomerRefund'));
        add_action('admin_post_nopriv_ddv_customer_invoice_pdf', array($this, 'handleCustomerInvoicePdf'));
        add_action('admin_post_nopriv_ddv_customer_refund_pdf', array($this, 'handleCustomerRefundPdf'));
    }

    public static function activate()
    {
        self::instance();
        if (!VerifactuConfig::installDefaults(self::VERSION)) {
            throw new RuntimeException('No se ha podido inicializar la configuracion de VeriFactu.');
        }
        if (!VerifactuRepository::installSchema() || !DdvWooDocumentRepository::installSchema()) {
            throw new RuntimeException('No se ha podido crear o actualizar el esquema de datos de VeriFactu.');
        }
        DdvWpStorage::ensure();
        update_option('ddverifactu_schema_version', self::VERSION, false);
        if (!wp_next_scheduled('ddverifactu_process_queue')) {
            wp_schedule_event(time() + 60, 'ddv_every_minute', 'ddverifactu_process_queue');
        }
    }

    public static function deactivate()
    {
        $timestamp = wp_next_scheduled('ddverifactu_process_queue');
        while ($timestamp) {
            wp_unschedule_event($timestamp, 'ddverifactu_process_queue');
            $timestamp = wp_next_scheduled('ddverifactu_process_queue');
        }
        // Fiscal evidence, chains, XML and certificate are intentionally preserved.
    }

    private function loadCore()
    {
        $base = __DIR__ . '/classes/';
        require_once __DIR__ . '/vendor/autoload.php';
        foreach (array(
            'DdvWpConfigStore.php',
            'DdvWpDb.php',
            'DdvWpStorage.php',
            'VerifactuRepository.php',
            'VerifactuConfig.php',
            'VerifactuCertificateManager.php',
            'VerifactuResponseParser.php',
            'VerifactuTransportException.php',
            'VerifactuSoapHttpClient.php',
            'VerifactuTransport.php',
            'VerifactuXmlBuilder.php',
            'DdvWooDocumentRepository.php',
            'VerifactuDocumentIdentity.php',
            'VerifactuInvoiceData.php',
            'VerifactuQrService.php',
            'DdvWooPdfService.php',
            'VerifactuConsultaService.php',
            'VerifactuService.php',
        ) as $file) {
            require_once $base . $file;
        }
    }

    public function declareWooCompatibility()
    {
        if (class_exists('Automattic\\WooCommerce\\Utilities\\FeaturesUtil')) {
            Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility('custom_order_tables', __FILE__, true);
        }
    }

    public function pluginsLoaded()
    {
        VerifactuConfig::installDefaults(self::VERSION);
        if (class_exists('WooCommerce') && get_option('ddverifactu_schema_version') !== self::VERSION) {
            if (!DdvWooDocumentRepository::installSchema() || !VerifactuRepository::installSchema()) {
                $this->setNotice('error', 'No se ha podido actualizar el esquema de datos de VeriFactu.');
            } else {
                DdvWpStorage::ensure();
                update_option('ddverifactu_schema_version', self::VERSION, false);
            }
        }
        if (class_exists('WooCommerce') && !wp_next_scheduled('ddverifactu_process_queue')) {
            wp_schedule_event(time() + 60, 'ddv_every_minute', 'ddverifactu_process_queue');
        }
    }

    public function cronSchedules($schedules)
    {
        $schedules['ddv_every_minute'] = array('interval' => 60, 'display' => 'Cada minuto (ddverifactu)');
        return $schedules;
    }

    public function cronProcessQueue()
    {
        if (!class_exists('WooCommerce')) {
            return;
        }
        try {
            $this->getService()->processQueue(VerifactuConfig::getQueueBatchSize());
        } catch (Throwable $e) {
            $this->logAutomaticError('CRON_ERROR', $e, 0, 0, 0);
        }
    }

    public function registerRestRoutes()
    {
        register_rest_route('ddverifactu/v1', '/cron', array(
            'methods' => WP_REST_Server::READABLE,
            'permission_callback' => '__return_true',
            'callback' => function (WP_REST_Request $request) {
                $token = (string) $request->get_param('token');
                try {
                    if ($token === '' || !hash_equals(VerifactuConfig::getCronToken(), strtolower($token))) {
                        return new WP_Error('ddv_forbidden', 'Token no valido.', array('status' => 403));
                    }
                    return rest_ensure_response($this->getService()->processQueue(VerifactuConfig::getQueueBatchSize()));
                } catch (Throwable $e) {
                    return new WP_Error('ddv_cron_error', $e->getMessage(), array('status' => 500));
                }
            },
        ));
    }

    public function adminMenu()
    {
        add_submenu_page(
            'woocommerce',
            'VeriFactu',
            'VeriFactu',
            'manage_woocommerce',
            'ddverifactu',
            array($this, 'renderAdminPage')
        );
    }

    public function renderAdminPage()
    {
        if (!current_user_can('manage_woocommerce')) {
            wp_die('No autorizado.');
        }
        if (!class_exists('WooCommerce')) {
            echo '<div class="wrap"><h1>VeriFactu</h1><div class="notice notice-error"><p>WooCommerce debe estar activo.</p></div></div>';
            return;
        }
        $tab = isset($_GET['tab']) ? sanitize_key(wp_unslash($_GET['tab'])) : 'dashboard';
        echo '<div class="wrap"><h1>digitalDot VeriFactu para WooCommerce <small style="font-size:13px">' . esc_html(self::VERSION) . '</small></h1>';
        echo '<nav class="nav-tab-wrapper">';
        foreach (array('dashboard'=>'Estado y registros','settings'=>'Configuracion','query'=>'Consulta AEAT','logs'=>'Eventos') as $key=>$label) {
            $url = admin_url('admin.php?page=ddverifactu&tab=' . $key);
            echo '<a class="nav-tab ' . ($tab===$key?'nav-tab-active':'') . '" href="' . esc_url($url) . '">' . esc_html($label) . '</a>';
        }
        echo '</nav>';
        try {
            if ($tab === 'settings') { $this->renderSettings(); }
            elseif ($tab === 'query') { $this->renderQuery(); }
            elseif ($tab === 'logs') { $this->renderLogs(); }
            else { $this->renderDashboard(); }
        } catch (Throwable $e) {
            echo '<div class="notice notice-error"><p>' . esc_html($e->getMessage()) . '</p></div>';
        }
        echo '</div>';
    }

    private function renderDashboard()
    {
        $repo = new VerifactuRepository();
        $docs = $this->getDocuments()->listDocuments(50);
        $records = $repo->listRecords(50);
        $queue = $repo->listQueue(50);
        $identity = VerifactuConfig::getCertificateIdentity();
        echo '<h2>Estado</h2><table class="widefat striped" style="max-width:1050px"><tbody>';
        $state = array(
            'Entorno' => VerifactuConfig::getEnvironment(),
            'Modo' => VerifactuConfig::getMode(),
            'NIF emisor certificado' => $identity['issuer_nif'] ?: 'No verificado',
            'Emisor certificado' => $identity['issuer_name'] ?: 'No verificado',
            'Instalacion' => VerifactuConfig::getInstallationId(),
            'Documentos fiscales' => count($docs),
            'Registros visibles' => count($records),
            'Tareas visibles' => count($queue),
        );
        foreach ($state as $k=>$v) { echo '<tr><th style="width:260px">'.esc_html($k).'</th><td>'.esc_html((string)$v).'</td></tr>'; }
        echo '</tbody></table>';
        echo '<p><form method="post" action="'.esc_url(admin_url('admin-post.php')).'" style="display:inline">';
        wp_nonce_field('ddv_process_queue');
        echo '<input type="hidden" name="action" value="ddv_process_queue"><button class="button button-primary">Procesar cola ahora</button></form></p>';

        echo '<h2>Documentos fiscales WooCommerce</h2>';
        echo '<table class="widefat striped"><thead><tr><th>ID</th><th>Tipo</th><th>Numero</th><th>Pedido</th><th>Origen</th><th>Fecha</th><th>Total</th></tr></thead><tbody>';
        foreach ($docs as $doc) {
            echo '<tr><td>'.(int)$doc->id.'</td><td>'.esc_html($doc->document_type).'</td><td><strong>'.esc_html($doc->number).'</strong></td><td>#'.(int)$doc->id_order.'</td><td>#'.(int)$doc->source_id.'</td><td>'.esc_html($doc->issue_date).'</td><td>'.esc_html(number_format((float)$doc->total_paid_tax_incl,2,',','.')).' EUR</td></tr>';
        }
        if (!$docs) { echo '<tr><td colspan="7">Todavia no hay documentos fiscales emitidos.</td></tr>'; }
        echo '</tbody></table>';

        echo '<h2>Diario VeriFactu</h2>';
        $this->renderRecordsTable($records);
        echo '<h2>Cola</h2>';
        echo '<table class="widefat striped"><thead><tr><th>ID</th><th>Accion</th><th>Documento</th><th>Estado</th><th>Intentos</th><th>Error</th></tr></thead><tbody>';
        foreach ($queue as $row) {
            echo '<tr><td>'.(int)$row['id_queue'].'</td><td>'.esc_html($row['action']).'</td><td>'.esc_html(($row['id_order_invoice']?'I'.$row['id_order_invoice']:($row['id_order_slip']?'R'.$row['id_order_slip']:'Pedido '.$row['id_order']))).'</td><td>'.esc_html($row['status']).'</td><td>'.(int)$row['attempts'].'</td><td>'.esc_html((string)$row['last_error']).'</td></tr>';
        }
        if (!$queue) { echo '<tr><td colspan="6">Cola vacia.</td></tr>'; }
        echo '</tbody></table>';
    }

    private function renderSettings()
    {
        $id = VerifactuConfig::getCertificateIdentity();
        echo '<form method="post" action="'.esc_url(admin_url('admin-post.php')).'">';
        wp_nonce_field('ddv_save_settings');
        echo '<input type="hidden" name="action" value="ddv_save_settings">';
        echo '<h2>Identidad y entorno</h2><table class="form-table">';
        $this->selectField('DD_VF_ENVIRONMENT','Entorno',array('preproduccion'=>'Preproduccion','produccion'=>'Produccion'));
        $this->selectField('MODO_ENVIO_VERIFACTU','Modo de envio',array('manual'=>'Manual','automatico'=>'Automatico'));
        $this->textField('DD_VF_PRODUCER_NAME','Razon social del productor');
        $this->textField('DD_VF_PRODUCER_NIF','NIF del productor');
        $this->textField('DD_VF_SYSTEM_NAME','Nombre declarado del sistema');
        $this->textField('DD_VF_SYSTEM_ID','Identificador del sistema (1-2 caracteres)');
        $this->selectField('DD_VF_ONLY_VERIFACTU','Solo VeriFactu',array('S'=>'Si','N'=>'No'));
        $this->selectField('DD_VF_POSSIBLE_MULTI_OT','Posible multi-OT',array('S'=>'Si','N'=>'No'));
        $this->selectField('DD_VF_MULTIPLE_OT','Multiples OT',array('S'=>'Si','N'=>'No'));
        echo '</table>';
        echo '<h2>Documento fiscal WooCommerce</h2><table class="form-table">';
        $this->textField('DD_VF_WC_INVOICE_PREFIX','Serie/prefijo de facturas');
        $this->numberField('DD_VF_WC_INVOICE_START','Primer numero de factura',1);
        $this->textField('DD_VF_WC_CREDIT_PREFIX','Serie/prefijo de rectificativas');
        $this->numberField('DD_VF_WC_CREDIT_START','Primer numero de rectificativa',1);
        $this->textField('DD_VF_WC_ORDER_STATUSES','Estados automaticos (slugs separados por coma)','Ej.: processing,completed');
        echo '</table>';
        echo '<h2>Reglas fiscales</h2><table class="form-table">';
        $this->checkboxField('DD_VF_ALLOW_F2','Permitir F2 cuando no hay NIF y se cumplen los limites');
        $this->textField('DD_VF_F2_LIMIT','Limite F2 (EUR)');
        $this->selectField('DD_VF_DEFAULT_RECTIFICATIVE_TYPE','Tipo rectificativa por defecto',array('R1'=>'R1','R2'=>'R2','R3'=>'R3','R4'=>'R4'));
        $this->selectField('DD_VF_FOREIGN_ID_TYPE','Tipo identificador extranjero',array('02'=>'02 NIF-IVA','03'=>'03 Pasaporte','04'=>'04 Documento oficial','05'=>'05 Certificado residencia','06'=>'06 Otro','07'=>'07 No censado'));
        $this->textField('DD_VF_QR_SIZE_MM','Tamano QR (mm)');
        $this->numberField('DD_VF_QUEUE_BATCH_SIZE','Tamano de lote de cola',1,100);
        $this->checkboxField('DD_VF_COMPOUND_TAX_REVIEWED','He revisado expresamente impuestos compuestos/recargo');
        $this->checkboxField('DD_VF_EMAIL_QR','Incluir enlace al documento fiscal en emails de WooCommerce');
        echo '<tr><th><label for="DD_VF_TAX_MAPPING_JSON">Mapa fiscal JSON</label></th><td><textarea class="large-text code" rows="9" id="DD_VF_TAX_MAPPING_JSON" name="DD_VF_TAX_MAPPING_JSON">'.esc_textarea((string)DdvWpConfigStore::get('DD_VF_TAX_MAPPING_JSON')).'</textarea><p class="description">Admite rate_id:ID, id_tax:ID, rate:21.00, scope:domestic, scope:foreign y default.</p></td></tr>';
        echo '</table>';
        echo '<h2>Produccion</h2><table class="form-table">';
        $this->checkboxField('DD_VF_DECLARATION_CONFIRMED','Declaracion responsable confirmada para esta version');
        $this->textField('DD_VF_DECLARATION_REFERENCE','Referencia/ubicacion de la declaracion responsable');
        echo '</table><p><button class="button button-primary">Guardar configuracion</button></p></form>';

        echo '<hr><h2>Certificado electronico</h2>';
        echo '<p><strong>Emisor:</strong> '.esc_html($id['issuer_name'] ?: 'No verificado').' &nbsp; <strong>NIF:</strong> '.esc_html($id['issuer_nif'] ?: 'No verificado').' &nbsp; <strong>Tipo:</strong> '.esc_html($id['certificate_type']).'</p>';
        echo '<form method="post" enctype="multipart/form-data" action="'.esc_url(admin_url('admin-post.php')).'">';
        wp_nonce_field('ddv_upload_certificate');
        echo '<input type="hidden" name="action" value="ddv_upload_certificate"><input type="file" name="ddv_certificate" accept=".p12,.pfx,.pem" required> <input type="password" name="ddv_certificate_password" autocomplete="new-password" placeholder="Contrasena del certificado"> <button class="button">Subir y verificar</button></form>';
        echo '<p><form method="post" action="'.esc_url(admin_url('admin-post.php')).'" style="display:inline">';
        wp_nonce_field('ddv_test_certificate');
        echo '<input type="hidden" name="action" value="ddv_test_certificate"><button class="button">Probar certificado configurado</button></form></p>';
        echo '<p><strong>Endpoint cron externo:</strong> <code>'.esc_html(rest_url('ddverifactu/v1/cron?token='.VerifactuConfig::getCronToken())).'</code></p>';
        echo '<p class="description">El certificado se cifra con AES-256-GCM. Los datos fiscales, XML, QR y certificado se guardan fuera del directorio del plugin para no perderse en actualizaciones.</p>';
    }

    private function renderQuery()
    {
        $result = null; $error = '';
        if ($_SERVER['REQUEST_METHOD']==='POST' && isset($_POST['ddv_query_aeat'])) {
            check_admin_referer('ddv_query_aeat');
            try {
                $filters = array(
                    'year' => isset($_POST['year']) ? (int)$_POST['year'] : (int)gmdate('Y'),
                    'period' => isset($_POST['period']) ? (int)$_POST['period'] : (int)gmdate('m'),
                    'query_environment' => isset($_POST['query_environment']) ? sanitize_key(wp_unslash($_POST['query_environment'])) : VerifactuConfig::getEnvironment(),
                );
                if (!empty($_POST['document_number'])) { $filters['document_number']=sanitize_text_field(wp_unslash($_POST['document_number'])); }
                if (!empty($_POST['issue_date'])) { $filters['issue_date']=sanitize_text_field(wp_unslash($_POST['issue_date'])); }
                $result = (new VerifactuConsultaService($this))->query($filters);
            } catch (Throwable $e) { $error=$e->getMessage(); }
        }
        echo '<h2>Consulta directa AEAT</h2>';
        if ($error!=='') { echo '<div class="notice notice-error"><p>'.esc_html($error).'</p></div>'; }
        echo '<form method="post">'; wp_nonce_field('ddv_query_aeat');
        echo '<input type="hidden" name="ddv_query_aeat" value="1"><table class="form-table"><tr><th>Ejercicio</th><td><input type="number" name="year" min="2000" max="2100" value="'.esc_attr(gmdate('Y')).'"></td></tr><tr><th>Mes</th><td><input type="number" name="period" min="1" max="12" value="'.esc_attr(gmdate('n')).'"></td></tr><tr><th>Entorno de consulta</th><td><select name="query_environment"><option value="preproduccion">Preproduccion</option><option value="produccion">Produccion</option></select></td></tr><tr><th>Numero exacto (opcional)</th><td><input type="text" name="document_number"></td></tr><tr><th>Fecha exacta d-m-AAAA (opcional)</th><td><input type="text" name="issue_date" placeholder="01-09-2026"></td></tr></table><p><button class="button button-primary">Consultar AEAT</button></p></form>';
        if (is_array($result)) {
            echo '<h3>Resultado: '.esc_html($result['result']).'</h3><p>Entorno: '.esc_html($result['environment']).' | Registros: '.count($result['records']).'</p>';
            echo '<table class="widefat striped"><thead><tr><th>Numero</th><th>Fecha</th><th>Tipo</th><th>Total</th><th>Estado</th><th>Error</th></tr></thead><tbody>';
            foreach ($result['records'] as $r) { echo '<tr><td>'.esc_html($r['document_number']).'</td><td>'.esc_html($r['issue_date']).'</td><td>'.esc_html($r['invoice_type']).'</td><td>'.esc_html($r['total']).'</td><td>'.esc_html($r['status']).'</td><td>'.esc_html(trim($r['error_code'].' '.$r['error_description'])).'</td></tr>'; }
            echo '</tbody></table>';
        }
        echo '<hr><h2>Anulacion historica excepcional</h2><p>Use esta operacion solo cuando conozca la identidad exacta que figura en la AEAT y deba corregirse un alta historica. No cambia la factura WooCommerce ni su instantanea fiscal.</p>';
        echo '<form method="post" action="'.esc_url(admin_url('admin-post.php')).'">';
        wp_nonce_field('ddv_external_cancel');
        echo '<input type="hidden" name="action" value="ddv_external_cancel"><table class="form-table"><tr><th>Numero exacto</th><td><input type="text" name="number" required></td></tr><tr><th>Fecha exacta d-m-AAAA</th><td><input type="text" name="issue_date" placeholder="01-09-2026" required></td></tr><tr><th>Confirmacion</th><td><label><input type="checkbox" name="confirm" value="1" required> Confirmo que numero y fecha coinciden exactamente con el alta remota que debe anularse.</label></td></tr></table><p><button class="button" onclick="return confirm(&quot;Esta operacion genera una anulacion fiscal. ¿Continuar?&quot;);">Anular identidad historica</button></p></form>';
    }

    private function renderLogs()
    {
        $events = (new VerifactuRepository())->listEvents(200);
        echo '<h2>Eventos</h2><table class="widefat striped"><thead><tr><th>Fecha</th><th>Nivel</th><th>Evento</th><th>Pedido</th><th>Registro</th><th>Mensaje</th></tr></thead><tbody>';
        foreach ($events as $e) { echo '<tr><td>'.esc_html($e['created_at']).'</td><td>'.esc_html($e['severity']).'</td><td>'.esc_html($e['event_type']).'</td><td>'.(int)$e['id_order'].'</td><td>'.(int)$e['id_record'].'</td><td>'.esc_html($e['message']).'</td></tr>'; }
        if (!$events) { echo '<tr><td colspan="6">Sin eventos.</td></tr>'; }
        echo '</tbody></table>';
    }

    private function renderRecordsTable(array $records)
    {
        echo '<table class="widefat striped"><thead><tr><th>ID</th><th>Documento</th><th>Operacion</th><th>Tipo</th><th>Estado local</th><th>AEAT</th><th>Error</th><th>Accion</th></tr></thead><tbody>';
        foreach ($records as $r) {
            echo '<tr><td>'.(int)$r['id_record'].'</td><td><strong>'.esc_html($r['document_number']).'</strong><br>'.esc_html($r['issue_date']).'</td><td>'.esc_html($r['operation'].' / '.$r['record_mode']).'</td><td>'.esc_html((string)$r['invoice_type']).'</td><td>'.esc_html($r['local_status']).'</td><td>'.esc_html((string)$r['aeat_record_status']).'</td><td>'.esc_html(trim((string)$r['error_code'].' '.(string)$r['error_description'])).'</td><td>';
            if (!in_array($r['local_status'], array(VerifactuRepository::STATUS_ACCEPTED,VerifactuRepository::STATUS_ACCEPTED_WITH_ERRORS,VerifactuRepository::STATUS_REJECTED), true)) {
                echo '<form method="post" action="'.esc_url(admin_url('admin-post.php')).'">'; wp_nonce_field('ddv_retry_record_'.(int)$r['id_record']); echo '<input type="hidden" name="action" value="ddv_retry_record"><input type="hidden" name="id_record" value="'.(int)$r['id_record'].'"><button class="button button-small">Reintentar</button></form>';
            }
            echo '</td></tr>';
        }
        if (!$records) { echo '<tr><td colspan="8">Todavia no hay registros.</td></tr>'; }
        echo '</tbody></table>';
    }

    public function renderOrderPanel($order)
    {
        if (!current_user_can('manage_woocommerce') || !($order instanceof WC_Order) || $order instanceof WC_Order_Refund) {
            return;
        }
        $this->orderPanelRendered = true;
        $repo = new VerifactuRepository();
        $acceptedStates = array(VerifactuRepository::STATUS_ACCEPTED, VerifactuRepository::STATUS_ACCEPTED_WITH_ERRORS);
        $terminalStates = array(VerifactuRepository::STATUS_ACCEPTED, VerifactuRepository::STATUS_ACCEPTED_WITH_ERRORS, VerifactuRepository::STATUS_REJECTED);
        $orderId = (int) $order->get_id();

        echo '<div class="order_data_column" style="width:100%;clear:both;padding-top:18px"><h3>VeriFactu</h3>';
        $doc = $this->getDocuments()->getInvoiceByOrder($orderId);
        if (!$doc) {
            $taxIdField = 'ddv_tax_id_' . $orderId;
            $noteField = 'ddv_fiscal_note_' . $orderId;
            $dynamicFields = array(
                'tax_id' => '#' . $taxIdField,
                'fiscal_note' => '#' . $noteField,
            );

            echo '<p>El pedido aun no es una factura fiscal. Emitir el documento crea una numeracion y una instantanea economica inmutables antes de generar VeriFactu.</p>';
            echo '<p><label for="'.esc_attr($taxIdField).'">NIF/DNI fiscal (si procede)<br><input type="text" id="'.esc_attr($taxIdField).'" value="'.esc_attr($this->detectTaxId($order)).'"></label></p>';
            echo '<p><label for="'.esc_attr($noteField).'">Nota fiscal / REBU (si procede)<br><input type="text" id="'.esc_attr($noteField).'" class="widefat" value="'.esc_attr((string) $order->get_meta('_ddv_fiscal_note', true)).'"></label></p>';
            $this->orderPostButton('ddv_issue_invoice', 'Emitir y poner en cola', array('order_id'=>$orderId, 'send_now'=>0), 'ddv_issue_invoice_'.$orderId, true, '', $dynamicFields);
            echo ' ';
            $this->orderPostButton('ddv_issue_invoice', 'Emitir y enviar', array('order_id'=>$orderId, 'send_now'=>1), 'ddv_issue_invoice_'.$orderId, false, '', $dynamicFields);
        } else {
            $record = $repo->getLatestAltaByInvoice((int) $doc->id);
            echo '<p><strong>Factura fiscal:</strong> '.esc_html($doc->number).' · '.esc_html($doc->issue_date).' · '.esc_html(number_format((float) $doc->total_paid_tax_incl, 2, ',', '.')).' EUR</p>';
            echo '<p><strong>VeriFactu:</strong> '.esc_html($record ? $record['local_status'] : 'SIN REGISTRO').($record && $record['aeat_record_status'] ? ' / '.esc_html($record['aeat_record_status']) : '').'</p>';

            if (!$record || !in_array($record['local_status'], $terminalStates, true)) {
                $this->orderPostButton('ddv_send_invoice', 'Enviar/reintentar factura', array('invoice_id'=>$doc->id), 'ddv_send_invoice_'.$doc->id);
                echo ' ';
            }
            if ($record) {
                $this->orderGetButton('ddv_print_invoice', 'Ver factura imprimible', array('invoice_id'=>$doc->id), 'ddv_print_invoice_'.$doc->id);
                echo ' ';
                $this->orderGetButton('ddv_pdf_invoice', 'Descargar factura PDF', array('invoice_id'=>$doc->id), 'ddv_pdf_invoice_'.$doc->id);
            }
            if ($record && in_array($record['local_status'], $terminalStates, true)) {
                echo ' ';
                $this->orderPostButton('ddv_subsanate_invoice', 'Crear subsanacion', array('invoice_id'=>$doc->id), 'ddv_subsanate_invoice_'.$doc->id);
            }
            if ($record && in_array($record['local_status'], $acceptedStates, true)) {
                echo ' ';
                $this->orderPostButton('ddv_cancel_invoice', 'Anular fiscalmente', array('invoice_id'=>$doc->id), 'ddv_cancel_invoice_'.$doc->id, false, 'Se generara y enviara un registro de anulacion a la AEAT. ¿Continuar?');
            }
        }

        $refunds = $order->get_refunds();
        if ($refunds) {
            echo '<h4>Reembolsos / facturas rectificativas</h4><ul>';
            foreach ($refunds as $refund) {
                $refundId = (int) $refund->get_id();
                $credit = $this->getDocuments()->getCreditNoteByRefund($refundId);
                echo '<li style="margin-bottom:10px">Reembolso #'.$refundId.' ('.esc_html(number_format(abs((float) $refund->get_total()), 2, ',', '.')).' EUR): ';
                if (!$credit) {
                    $this->orderPostButton('ddv_issue_refund', 'Emitir y poner en cola rectificativa', array('refund_id'=>$refundId), 'ddv_issue_refund_'.$refundId);
                } else {
                    $rr = $repo->getLatestAltaBySlip($refundId);
                    echo '<strong>'.esc_html($credit->number).'</strong> · '.esc_html($rr ? $rr['local_status'] : 'SIN REGISTRO').' ';
                    if (!$rr || !in_array($rr['local_status'], $terminalStates, true)) {
                        $this->orderPostButton('ddv_send_refund', 'Enviar/reintentar', array('refund_id'=>$refundId), 'ddv_send_refund_'.$refundId);
                        echo ' ';
                    }
                    if ($rr) {
                        $this->orderGetButton('ddv_print_refund', 'Ver rectificativa', array('refund_id'=>$refundId), 'ddv_print_refund_'.$refundId);
                        echo ' ';
                        $this->orderGetButton('ddv_pdf_refund', 'Descargar PDF', array('refund_id'=>$refundId), 'ddv_pdf_refund_'.$refundId);
                    }
                    if ($rr && in_array($rr['local_status'], $terminalStates, true)) {
                        echo ' ';
                        $this->orderPostButton('ddv_subsanate_refund', 'Subsanar', array('refund_id'=>$refundId), 'ddv_subsanate_refund_'.$refundId);
                    }
                    if ($rr && in_array($rr['local_status'], $acceptedStates, true)) {
                        echo ' ';
                        $this->orderPostButton('ddv_cancel_refund', 'Anular fiscalmente', array('refund_id'=>$refundId), 'ddv_cancel_refund_'.$refundId, false, 'Se generara y enviara la anulacion de esta rectificativa a la AEAT. ¿Continuar?');
                    }
                }
                echo '</li>';
            }
            echo '</ul>';
        }
        echo '</div>';
    }

    public function renderOrderPanelScript()
    {
        if (!$this->orderPanelRendered) {
            return;
        }
        $postUrl = admin_url('admin-post.php');
        ?>
        <script>
        (function () {
            document.addEventListener('click', function (event) {
                var button = event.target.closest ? event.target.closest('.ddv-order-post-action') : null;
                if (!button) {
                    return;
                }

                event.preventDefault();
                var confirmText = button.getAttribute('data-ddv-confirm') || '';
                if (confirmText && !window.confirm(confirmText)) {
                    return;
                }

                var payload = {};
                var fields = {};
                try {
                    payload = JSON.parse(button.getAttribute('data-ddv-payload') || '{}');
                    fields = JSON.parse(button.getAttribute('data-ddv-fields') || '{}');
                } catch (e) {
                    window.alert('No se ha podido preparar la accion de VeriFactu. Recargue la pagina e intentelo de nuevo.');
                    return;
                }

                var form = document.createElement('form');
                form.method = 'post';
                form.action = <?php echo wp_json_encode($postUrl); ?>;
                form.style.display = 'none';

                function appendField(name, value) {
                    var input = document.createElement('input');
                    input.type = 'hidden';
                    input.name = name;
                    input.value = value === null || typeof value === 'undefined' ? '' : String(value);
                    form.appendChild(input);
                }

                appendField('action', button.getAttribute('data-ddv-action') || '');
                appendField('_wpnonce', button.getAttribute('data-ddv-nonce') || '');

                Object.keys(payload).forEach(function (name) {
                    appendField(name, payload[name]);
                });

                Object.keys(fields).forEach(function (name) {
                    var source = document.querySelector(fields[name]);
                    appendField(name, source ? source.value : '');
                });

                button.disabled = true;
                document.body.appendChild(form);
                form.submit();
            });
        }());
        </script>
        <?php
    }

    public function automaticOrderStatus($orderId, $from, $to, $order)
    {
        unset($from);
        if (!$this->automaticModeEnabled() || !($order instanceof WC_Order) || $order instanceof WC_Order_Refund) { return; }
        $statuses = array_filter(array_map('trim', explode(',', (string)DdvWpConfigStore::get('DD_VF_WC_ORDER_STATUSES'))));
        $to = preg_replace('/^wc-/', '', (string)$to);
        if (!$statuses || !in_array($to, array_map(static function($v){return preg_replace('/^wc-/','',$v);},$statuses), true)) { return; }
        try {
            $doc=$this->getDocuments()->issueInvoiceForOrder($order);
            $this->getService()->prepareInvoice((int)$doc->id);
            $this->getService()->processQueue(VerifactuConfig::getQueueBatchSize());
        } catch (Throwable $e) { $this->logAutomaticError('AUTOMATIC_INVOICE_ERROR',$e,$orderId,0,0); }
    }

    public function automaticRefund($refundId, $args)
    {
        unset($args);
        if (!$this->automaticModeEnabled()) { return; }
        try {
            $refund=wc_get_order((int)$refundId);
            if (!($refund instanceof WC_Order_Refund)) { return; }
            $this->getDocuments()->ensureCreditNoteForRefund($refund);
            $this->getService()->prepareCreditSlip((int)$refundId);
            $this->getService()->processQueue(VerifactuConfig::getQueueBatchSize());
        } catch (Throwable $e) {
            $refund=wc_get_order((int)$refundId);
            $this->logAutomaticError('AUTOMATIC_REFUND_ERROR',$e,$refund instanceof WC_Order_Refund?(int)$refund->get_parent_id():0,0,$refundId);
        }
    }

    public function myAccountInvoiceAction($actions, $order)
    {
        if (!($order instanceof WC_Order)) { return $actions; }
        $doc=$this->getDocuments()->getInvoiceByOrder((int)$order->get_id());
        if (!$doc) { return $actions; }
        $actions['ddverifactu_invoice']=array('url'=>$this->customerInvoiceUrl($order),'name'=>'Factura VeriFactu');
        $record=(new VerifactuRepository())->getLatestAltaByInvoice((int)$doc->id);
        if ($record) {
            $actions['ddverifactu_invoice_pdf']=array('url'=>$this->customerInvoicePdfUrl($order),'name'=>'PDF factura');
        }
        return $actions;
    }

    public function emailFiscalLink($order, $sentToAdmin, $plainText, $email)
    {
        unset($sentToAdmin,$email);
        if (!(bool)DdvWpConfigStore::get('DD_VF_EMAIL_QR') || !($order instanceof WC_Order)) { return; }
        $doc=$this->getDocuments()->getInvoiceByOrder((int)$order->get_id());
        if (!$doc) { return; }
        $url=$this->customerInvoiceUrl($order);
        $record=(new VerifactuRepository())->getLatestAltaByInvoice((int)$doc->id);
        $pdfUrl=$record ? $this->customerInvoicePdfUrl($order) : '';
        if ($plainText) {
            echo "\nFactura VeriFactu: ".$url."\n";
            if ($pdfUrl !== '') { echo "PDF factura: ".$pdfUrl."\n"; }
        } else {
            echo '<p><strong>Factura VeriFactu:</strong> <a href="'.esc_url($url).'">ver documento fiscal con QR</a>';
            if ($pdfUrl !== '') { echo ' · <a href="'.esc_url($pdfUrl).'">descargar PDF</a>'; }
            echo '</p>';
        }
    }

    public function handleSaveSettings()
    {
        $this->requireAdmin('ddv_save_settings');
        $textKeys=array('DD_VF_ENVIRONMENT','MODO_ENVIO_VERIFACTU','DD_VF_PRODUCER_NAME','DD_VF_PRODUCER_NIF','DD_VF_SYSTEM_NAME','DD_VF_SYSTEM_ID','DD_VF_ONLY_VERIFACTU','DD_VF_POSSIBLE_MULTI_OT','DD_VF_MULTIPLE_OT','DD_VF_WC_INVOICE_PREFIX','DD_VF_WC_CREDIT_PREFIX','DD_VF_WC_ORDER_STATUSES','DD_VF_F2_LIMIT','DD_VF_DEFAULT_RECTIFICATIVE_TYPE','DD_VF_FOREIGN_ID_TYPE','DD_VF_QR_SIZE_MM','DD_VF_DECLARATION_REFERENCE');
        $invoicePrefix=isset($_POST['DD_VF_WC_INVOICE_PREFIX'])?trim(sanitize_text_field(wp_unslash($_POST['DD_VF_WC_INVOICE_PREFIX']))):'';
        $creditPrefix=isset($_POST['DD_VF_WC_CREDIT_PREFIX'])?trim(sanitize_text_field(wp_unslash($_POST['DD_VF_WC_CREDIT_PREFIX']))):'';
        if($invoicePrefix==='' || $creditPrefix==='' || $invoicePrefix===$creditPrefix){
            $this->redirectNotice('error','Las series de facturas y rectificativas deben existir y ser distintas.','settings');
        }
        foreach($textKeys as $key){ if(isset($_POST[$key])) DdvWpConfigStore::updateValue($key,sanitize_text_field(wp_unslash($_POST[$key]))); }
        foreach(array('DD_VF_WC_INVOICE_START','DD_VF_WC_CREDIT_START','DD_VF_QUEUE_BATCH_SIZE') as $key){ if(isset($_POST[$key])) DdvWpConfigStore::updateValue($key,max(1,(int)$_POST[$key])); }
        foreach(array('DD_VF_ALLOW_F2','DD_VF_COMPOUND_TAX_REVIEWED','DD_VF_DECLARATION_CONFIRMED','DD_VF_EMAIL_QR') as $key){ DdvWpConfigStore::updateValue($key,isset($_POST[$key])?1:0); }
        if(isset($_POST['DD_VF_TAX_MAPPING_JSON'])) {
            $raw=trim((string)wp_unslash($_POST['DD_VF_TAX_MAPPING_JSON'])); $decoded=json_decode($raw,true);
            if(!is_array($decoded)) { $this->redirectNotice('error','El mapa fiscal JSON no es valido.','settings'); }
            DdvWpConfigStore::updateValue('DD_VF_TAX_MAPPING_JSON',wp_json_encode($decoded,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES));
        }
        DdvWpConfigStore::updateValue('DD_VF_MODULE_VERSION',self::VERSION);
        $this->redirectNotice('success','Configuracion guardada.','settings');
    }

    public function handleUploadCertificate()
    {
        $this->requireAdmin('ddv_upload_certificate');
        try {
            if (empty($_FILES['ddv_certificate']) || !is_array($_FILES['ddv_certificate'])) { throw new Exception('Seleccione un certificado P12/PFX/PEM.'); }
            $file=$_FILES['ddv_certificate'];
            if ((int)$file['error']!==UPLOAD_ERR_OK || empty($file['tmp_name']) || !is_uploaded_file($file['tmp_name'])) { throw new Exception('La subida del certificado no es valida.'); }
            $ext=strtolower(pathinfo((string)$file['name'],PATHINFO_EXTENSION));
            if(!in_array($ext,array('p12','pfx','pem'),true)) { throw new Exception('El certificado debe ser P12, PFX o PEM.'); }
            $tmp=wp_tempnam('ddverifactu-cert-'.$ext);
            if(!$tmp || !move_uploaded_file($file['tmp_name'],$tmp)) { throw new Exception('No se ha podido mover la subida a un fichero local controlado.'); }
            try { $raw=file_get_contents($tmp); } finally { @unlink($tmp); }
            if(!is_string($raw) || $raw==='') { throw new Exception('El certificado subido esta vacio.'); }
            $pass=isset($_POST['ddv_certificate_password'])?(string)wp_unslash($_POST['ddv_certificate_password']):'';
            $identity=$this->storeCertificate($raw,$pass);
            $this->redirectNotice('success','Certificado verificado para '.$identity['issuer_name'].' ('.$identity['issuer_nif'].').','settings');
        } catch(Throwable $e){ $this->redirectNotice('error',$e->getMessage(),'settings'); }
    }

    public function handleTestCertificate()
    {
        $this->requireAdmin('ddv_test_certificate');
        try { $identity=$this->refreshCertificateIdentity(); $this->redirectNotice('success','Certificado correcto: '.$identity['issuer_name'].' ('.$identity['issuer_nif'].').','settings'); }
        catch(Throwable $e){ $this->redirectNotice('error',$e->getMessage(),'settings'); }
    }

    public function handleProcessQueue(){ $this->requireAdmin('ddv_process_queue'); try{$r=$this->getService()->processQueue(VerifactuConfig::getQueueBatchSize());$this->redirectNotice('success','Cola procesada: '.$r['processed'].' procesados, '.$r['accepted'].' aceptados, '.$r['retried'].' reintentos.');}catch(Throwable $e){$this->redirectNotice('error',$e->getMessage());} }

    public function handleIssueInvoice()
    {
        $orderId=isset($_POST['order_id'])?(int)$_POST['order_id']:0; $this->requireAdmin('ddv_issue_invoice_'.$orderId);
        try{
            $order=wc_get_order($orderId); if(!($order instanceof WC_Order)) throw new Exception('Pedido no valido.');
            $taxId=isset($_POST['tax_id'])?sanitize_text_field(wp_unslash($_POST['tax_id'])):''; $note=isset($_POST['fiscal_note'])?sanitize_text_field(wp_unslash($_POST['fiscal_note'])):'';
            if($taxId!==''){$order->update_meta_data('_ddv_tax_id',$taxId);} if($note!==''){$order->update_meta_data('_ddv_fiscal_note',$note);} $order->save();
            $doc=$this->getDocuments()->issueInvoiceForOrder($order,array('tax_id'=>$taxId,'fiscal_note'=>$note));
            if(!empty($_POST['send_now'])) $this->getService()->sendInvoice((int)$doc->id); else $this->getService()->prepareInvoice((int)$doc->id);
            $this->redirectOrderNotice($orderId,'success','Factura fiscal '.$doc->number.' emitida y '.(!empty($_POST['send_now'])?'procesada':'puesta en cola').'.');
        }catch(Throwable $e){$this->redirectOrderNotice($orderId,'error',$e->getMessage());}
    }
    public function handleSendInvoice(){ $id=isset($_POST['invoice_id'])?(int)$_POST['invoice_id']:0;$this->requireAdmin('ddv_send_invoice_'.$id);try{$r=$this->getService()->sendInvoice($id);$doc=VerifactuDocumentIdentity::getInvoiceById($id);$this->redirectOrderNotice($doc->id_order,'success','Resultado: '.$r['local_status'].'.');}catch(Throwable $e){$doc=$this->safeInvoice($id);$this->redirectOrderNotice($doc?$doc->id_order:0,'error',$e->getMessage());} }
    public function handleSubsanateInvoice(){ $id=isset($_POST['invoice_id'])?(int)$_POST['invoice_id']:0;$this->requireAdmin('ddv_subsanate_invoice_'.$id);try{$r=$this->getService()->subsanateInvoice($id,array('send_now'=>true));$doc=VerifactuDocumentIdentity::getInvoiceById($id);$this->redirectOrderNotice($doc->id_order,'success','Subsanacion creada: '.$r['local_status'].'.');}catch(Throwable $e){$doc=$this->safeInvoice($id);$this->redirectOrderNotice($doc?$doc->id_order:0,'error',$e->getMessage());} }
    public function handleCancelInvoice(){ $id=isset($_POST['invoice_id'])?(int)$_POST['invoice_id']:0;$this->requireAdmin('ddv_cancel_invoice_'.$id);try{$r=$this->getService()->cancelInvoice($id);$doc=VerifactuDocumentIdentity::getInvoiceById($id);$this->redirectOrderNotice($doc->id_order,'success','Anulacion procesada: '.$r['local_status'].'.');}catch(Throwable $e){$doc=$this->safeInvoice($id);$this->redirectOrderNotice($doc?$doc->id_order:0,'error',$e->getMessage());} }
    public function handleIssueRefund(){ $id=isset($_POST['refund_id'])?(int)$_POST['refund_id']:0;$this->requireAdmin('ddv_issue_refund_'.$id);try{$refund=wc_get_order($id);$doc=$this->getDocuments()->ensureCreditNoteForRefund($refund);$record=$this->getService()->prepareCreditSlip($id);$this->redirectOrderNotice($doc->id_order,'success','Rectificativa '.$doc->number.' emitida y puesta en cola. Estado local: '.$record['local_status'].'.');}catch(Throwable $e){$refund=wc_get_order($id);$this->redirectOrderNotice($refund instanceof WC_Order_Refund?$refund->get_parent_id():0,'error',$e->getMessage());} }
    public function handleSendRefund(){ $id=isset($_POST['refund_id'])?(int)$_POST['refund_id']:0;$this->requireAdmin('ddv_send_refund_'.$id);try{$this->getDocuments()->ensureCreditNoteForRefund($id);$r=$this->getService()->sendCreditSlip($id);$refund=wc_get_order($id);$this->redirectOrderNotice($refund->get_parent_id(),'success','Rectificativa: '.$r['local_status'].'.');}catch(Throwable $e){$refund=wc_get_order($id);$this->redirectOrderNotice($refund instanceof WC_Order_Refund?$refund->get_parent_id():0,'error',$e->getMessage());} }
    public function handleSubsanateRefund(){ $id=isset($_POST['refund_id'])?(int)$_POST['refund_id']:0;$this->requireAdmin('ddv_subsanate_refund_'.$id);try{$r=$this->getService()->subsanateCreditSlip($id,array('send_now'=>true));$refund=wc_get_order($id);$this->redirectOrderNotice($refund->get_parent_id(),'success','Subsanacion rectificativa: '.$r['local_status'].'.');}catch(Throwable $e){$refund=wc_get_order($id);$this->redirectOrderNotice($refund instanceof WC_Order_Refund?$refund->get_parent_id():0,'error',$e->getMessage());} }
    public function handleCancelRefund(){ $id=isset($_POST['refund_id'])?(int)$_POST['refund_id']:0;$this->requireAdmin('ddv_cancel_refund_'.$id);try{$r=$this->getService()->cancelCreditSlip($id);$refund=wc_get_order($id);$this->redirectOrderNotice($refund->get_parent_id(),'success','Anulacion rectificativa: '.$r['local_status'].'.');}catch(Throwable $e){$refund=wc_get_order($id);$this->redirectOrderNotice($refund instanceof WC_Order_Refund?$refund->get_parent_id():0,'error',$e->getMessage());} }
    public function handleRetryRecord(){ $id=isset($_POST['id_record'])?(int)$_POST['id_record']:0;$this->requireAdmin('ddv_retry_record_'.$id);try{$r=$this->getService()->retryRecord($id);$this->redirectNotice('success','Reintento: '.$r['local_status'].'.');}catch(Throwable $e){$this->redirectNotice('error',$e->getMessage());} }
    public function handleExternalCancel(){ $this->requireAdmin('ddv_external_cancel');try{$number=sanitize_text_field(wp_unslash($_POST['number']??''));$date=sanitize_text_field(wp_unslash($_POST['issue_date']??''));$r=$this->getService()->cancelExternalIdentity($number,$date,array('confirm_exact_remote_identity'=>!empty($_POST['confirm'])));$this->redirectNotice('success','Anulacion historica: '.$r['local_status'].'.');}catch(Throwable $e){$this->redirectNotice('error',$e->getMessage());} }

    public function handlePrintInvoice()
    {
        $id = isset($_REQUEST['invoice_id']) ? (int) $_REQUEST['invoice_id'] : 0;
        if (!current_user_can('manage_woocommerce')) {
            wp_die('No autorizado.', 403);
        }
        check_admin_referer('ddv_print_invoice_'.$id);
        try {
            $doc = VerifactuDocumentIdentity::getInvoiceById($id);
        } catch (Throwable $e) {
            wp_die(esc_html($e->getMessage()), 404);
        }
        $order = wc_get_order((int) $doc->id_order);
        if (!($order instanceof WC_Order)) {
            wp_die('Pedido no encontrado.', 404);
        }
        $this->renderFiscalDocument($order, $doc, false);
    }

    public function handlePrintRefund()
    {
        $refundId = isset($_REQUEST['refund_id']) ? (int) $_REQUEST['refund_id'] : 0;
        if (!current_user_can('manage_woocommerce')) {
            wp_die('No autorizado.', 403);
        }
        check_admin_referer('ddv_print_refund_'.$refundId);
        $refund = wc_get_order($refundId);
        if (!($refund instanceof WC_Order_Refund)) {
            wp_die('Reembolso no encontrado.', 404);
        }
        $order = wc_get_order((int) $refund->get_parent_id());
        $doc = $this->getDocuments()->getCreditNoteByRefund($refundId);
        if (!($order instanceof WC_Order) || !$doc) {
            wp_die('La factura rectificativa aun no ha sido emitida.', 404);
        }
        $this->renderFiscalDocument($order, $doc, false, $refund);
    }


    public function handlePdfInvoice()
    {
        $id = isset($_REQUEST['invoice_id']) ? (int) $_REQUEST['invoice_id'] : 0;
        if (!current_user_can('manage_woocommerce')) {
            wp_die('No autorizado.', 403);
        }
        check_admin_referer('ddv_pdf_invoice_'.$id);
        try {
            $doc = VerifactuDocumentIdentity::getInvoiceById($id);
            $order = wc_get_order((int) $doc->id_order);
            if (!($order instanceof WC_Order)) {
                throw new Exception('Pedido no encontrado.');
            }
            (new DdvWooPdfService())->streamInvoice($order, $doc);
        } catch (Throwable $e) {
            wp_die(esc_html($e->getMessage()), 'No se ha podido generar el PDF', array('response' => 500));
        }
    }

    public function handlePdfRefund()
    {
        $refundId = isset($_REQUEST['refund_id']) ? (int) $_REQUEST['refund_id'] : 0;
        if (!current_user_can('manage_woocommerce')) {
            wp_die('No autorizado.', 403);
        }
        check_admin_referer('ddv_pdf_refund_'.$refundId);
        try {
            $refund = wc_get_order($refundId);
            if (!($refund instanceof WC_Order_Refund)) {
                throw new Exception('Reembolso no encontrado.');
            }
            $order = wc_get_order((int) $refund->get_parent_id());
            $doc = $this->getDocuments()->getCreditNoteByRefund($refundId);
            if (!($order instanceof WC_Order) || !$doc) {
                throw new Exception('La factura rectificativa aun no ha sido emitida.');
            }
            (new DdvWooPdfService())->streamCredit($order, $refund, $doc);
        } catch (Throwable $e) {
            wp_die(esc_html($e->getMessage()), 'No se ha podido generar el PDF', array('response' => 500));
        }
    }

    public function handleCustomerInvoice()
    {
        $orderId = isset($_GET['order_id']) ? (int) $_GET['order_id'] : 0;
        $key = isset($_GET['key']) ? wc_clean(wp_unslash($_GET['key'])) : '';
        $order = wc_get_order($orderId);
        if (!($order instanceof WC_Order)) {
            wp_die('Documento no encontrado.', 404);
        }
        if (!$this->customerCanViewOrder($order, $key)) {
            wp_die('No autorizado.', 403);
        }
        $doc = $this->getDocuments()->getInvoiceByOrder($orderId);
        if (!$doc) {
            wp_die('La factura fiscal aun no ha sido emitida.', 404);
        }
        $this->renderFiscalDocument($order, $doc, true);
    }

    public function handleCustomerRefund()
    {
        $orderId = isset($_GET['order_id']) ? (int) $_GET['order_id'] : 0;
        $refundId = isset($_GET['refund_id']) ? (int) $_GET['refund_id'] : 0;
        $key = isset($_GET['key']) ? wc_clean(wp_unslash($_GET['key'])) : '';
        $order = wc_get_order($orderId);
        $refund = wc_get_order($refundId);
        if (!($order instanceof WC_Order) || !($refund instanceof WC_Order_Refund) || (int) $refund->get_parent_id() !== $orderId) {
            wp_die('Documento no encontrado.', 404);
        }
        if (!$this->customerCanViewOrder($order, $key)) {
            wp_die('No autorizado.', 403);
        }
        $doc = $this->getDocuments()->getCreditNoteByRefund($refundId);
        if (!$doc) {
            wp_die('La factura rectificativa aun no ha sido emitida.', 404);
        }
        $this->renderFiscalDocument($order, $doc, true, $refund);
    }


    public function handleCustomerInvoicePdf()
    {
        $orderId = isset($_GET['order_id']) ? (int) $_GET['order_id'] : 0;
        $key = isset($_GET['key']) ? wc_clean(wp_unslash($_GET['key'])) : '';
        $order = wc_get_order($orderId);
        if (!($order instanceof WC_Order) || !$this->customerCanViewOrder($order, $key)) {
            wp_die('No autorizado.', 403);
        }
        $doc = $this->getDocuments()->getInvoiceByOrder($orderId);
        if (!$doc) {
            wp_die('La factura fiscal aun no ha sido emitida.', 404);
        }
        try {
            (new DdvWooPdfService())->streamInvoice($order, $doc);
        } catch (Throwable $e) {
            wp_die(esc_html($e->getMessage()), 'No se ha podido generar el PDF', array('response' => 500));
        }
    }

    public function handleCustomerRefundPdf()
    {
        $orderId = isset($_GET['order_id']) ? (int) $_GET['order_id'] : 0;
        $refundId = isset($_GET['refund_id']) ? (int) $_GET['refund_id'] : 0;
        $key = isset($_GET['key']) ? wc_clean(wp_unslash($_GET['key'])) : '';
        $order = wc_get_order($orderId);
        $refund = wc_get_order($refundId);
        if (!($order instanceof WC_Order) || !($refund instanceof WC_Order_Refund) || (int) $refund->get_parent_id() !== $orderId || !$this->customerCanViewOrder($order, $key)) {
            wp_die('No autorizado.', 403);
        }
        $doc = $this->getDocuments()->getCreditNoteByRefund($refundId);
        if (!$doc) {
            wp_die('La factura rectificativa aun no ha sido emitida.', 404);
        }
        try {
            (new DdvWooPdfService())->streamCredit($order, $refund, $doc);
        } catch (Throwable $e) {
            wp_die(esc_html($e->getMessage()), 'No se ha podido generar el PDF', array('response' => 500));
        }
    }

    private function customerCanViewOrder(WC_Order $order, $key)
    {
        if (current_user_can('manage_woocommerce')) {
            return true;
        }
        if (is_user_logged_in() && (int) $order->get_user_id() > 0 && (int) $order->get_user_id() === (int) get_current_user_id()) {
            return true;
        }
        return $key !== '' && hash_equals((string) $order->get_order_key(), (string) $key);
    }

    private function renderFiscalDocument(WC_Order $order, DdvWooFiscalDocument $doc, $customerView, $refund = null)
    {
        $isCredit = $refund instanceof WC_Order_Refund || $doc->document_type === DdvWooDocumentRepository::TYPE_CREDIT;
        if ($isCredit && !($refund instanceof WC_Order_Refund)) {
            $refund = wc_get_order((int) $doc->source_id);
        }
        if ($isCredit && !($refund instanceof WC_Order_Refund)) {
            wp_die('No se ha podido cargar el reembolso asociado.', 404);
        }

        $repo = new VerifactuRepository();
        $record = $isCredit ? $repo->getLatestAltaBySlip((int) $refund->get_id()) : $repo->getLatestAltaByInvoice((int) $doc->id);
        $qr = null;
        $qrError = '';
        if ($record) {
            try {
                $qr = $isCredit
                    ? VerifactuQrService::generateCreditSlipForRecord($order, $refund, $record)
                    : VerifactuQrService::generateInvoiceForRecord($order, $doc, $record);
            } catch (Throwable $e) {
                $qrError = $e->getMessage();
            }
        }

        $issuerName = get_bloginfo('name');
        $issuerNif = $record && !empty($record['issuer_nif']) ? (string) $record['issuer_nif'] : '';
        try { $issuerName = VerifactuConfig::getIssuerName(); } catch (Throwable $ignored) {}
        if ($issuerNif === '') { try { $issuerNif = VerifactuConfig::getIssuerNif(); } catch (Throwable $ignored) {} }

        $s = $doc->snapshot;
        $billing = (array) ($s['billing_address'] ?? array());
        $currency = !empty($doc->currency) ? (string) $doc->currency : 'EUR';
        $sign = $isCredit ? -1 : 1;
        $title = $isCredit ? 'Factura rectificativa' : 'Factura';
        $sourceText = '';
        $pdfUrl = '';
        if ($record) {
            if ($customerView) {
                $pdfUrl = $isCredit
                    ? $this->customerRefundPdfUrl($order, (int) $refund->get_id())
                    : $this->customerInvoicePdfUrl($order);
            } else {
                $pdfUrl = $isCredit
                    ? wp_nonce_url(add_query_arg(array('action'=>'ddv_pdf_refund','refund_id'=>(int)$refund->get_id()), admin_url('admin-post.php')), 'ddv_pdf_refund_'.(int)$refund->get_id())
                    : wp_nonce_url(add_query_arg(array('action'=>'ddv_pdf_invoice','invoice_id'=>(int)$doc->id), admin_url('admin-post.php')), 'ddv_pdf_invoice_'.(int)$doc->id);
            }
        }
        if ($isCredit && $doc->source_invoice_id) {
            try {
                $source = VerifactuDocumentIdentity::getInvoiceById((int) $doc->source_invoice_id);
                $sourceText = '<p>Rectifica la factura '.esc_html($source->number).'</p>';
            } catch (Throwable $ignored) {}
        }

        nocache_headers();
        header('Content-Type: text/html; charset='.get_bloginfo('charset'));
        echo '<!doctype html><html><head><meta charset="'.esc_attr(get_bloginfo('charset')).'"><title>'.esc_html($doc->number).'</title><style>body{font-family:Arial,sans-serif;color:#222;max-width:900px;margin:30px auto;padding:20px}header{display:flex;justify-content:space-between;gap:30px;border-bottom:2px solid #222;padding-bottom:20px}.qr{text-align:center;max-width:220px}.qr img{width:170px;height:170px}.muted{color:#666}.grid{display:grid;grid-template-columns:1fr 1fr;gap:30px;margin:25px 0}table{width:100%;border-collapse:collapse}th,td{padding:8px;border-bottom:1px solid #ddd;text-align:left}.num{text-align:right}.total{font-size:20px;font-weight:bold}.print-actions{position:fixed;right:20px;top:20px;display:flex;gap:8px}.print-actions button,.print-actions a{font:inherit;padding:6px 10px;border:1px solid #999;background:#fff;color:#222;text-decoration:none;border-radius:3px;cursor:pointer}@media print{.print-actions{display:none}body{margin:0;max-width:none}}</style></head><body><div class="print-actions"><button onclick="window.print()">Imprimir</button>' . ($pdfUrl !== '' ? '<a href="'.esc_url($pdfUrl).'">Descargar PDF</a>' : '') . '</div><header><div><h1>'.esc_html($title).' '.esc_html($doc->number).'</h1><p>Fecha de expedicion: '.esc_html($doc->issue_date).'</p><p>Pedido WooCommerce #'.esc_html($order->get_order_number()).'</p>'.$sourceText.'</div><div class="qr">';
        if ($qr && !empty($qr['path']) && is_file($qr['path'])) {
            $qrRaw = file_get_contents($qr['path']);
            if (is_string($qrRaw) && $qrRaw !== '') {
                echo '<div>QR tributario:</div><img alt="QR VeriFactu" src="data:image/png;base64,'.esc_attr(base64_encode($qrRaw)).'"><div><strong>VERI*FACTU</strong></div><small>Factura verificable en la sede electronica de la AEAT</small>';
            }
        } else {
            echo '<div class="muted">QR no disponible'.($qrError !== '' ? ': '.esc_html($qrError) : ' hasta generar el registro VeriFactu').'</div>';
        }
        echo '</div></header><div class="grid"><section><h3>Emisor</h3><p><strong>'.esc_html($issuerName).'</strong><br>NIF: '.esc_html($issuerNif).'</p></section><section><h3>Destinatario</h3><p><strong>'.esc_html((string) ($s['recipient']['name'] ?? '')).'</strong><br>'.esc_html(trim(implode(' ', array_filter(array($billing['address_1'] ?? '', $billing['address_2'] ?? ''))))).'<br>'.esc_html(trim(($billing['postcode'] ?? '').' '.($billing['city'] ?? ''))).'<br>'.esc_html((string) ($billing['country'] ?? '')).'<br>'.(!empty($s['recipient']['tax_id']) ? 'NIF/VAT: '.esc_html($s['recipient']['tax_id']) : '').'</p></section></div>';
        echo '<table><thead><tr><th>Concepto</th><th>Cant.</th><th class="num">Base</th><th class="num">Impuestos</th><th class="num">Total</th></tr></thead><tbody>';
        foreach ((array) ($s['items'] ?? array()) as $item) {
            echo '<tr><td>'.esc_html($item['name']).'</td><td>'.esc_html((string) $item['quantity']).'</td><td class="num">'.esc_html($this->formatPrintAmount($sign * (float) $item['base'])).' '.esc_html($currency).'</td><td class="num">'.esc_html($this->formatPrintAmount($sign * (float) $item['tax'])).' '.esc_html($currency).'</td><td class="num">'.esc_html($this->formatPrintAmount($sign * (float) $item['total'])).' '.esc_html($currency).'</td></tr>';
        }
        echo '</tbody><tfoot><tr><th colspan="4" class="num">Base</th><td class="num">'.esc_html($this->formatPrintAmount($sign * (float) ($s['total_excl'] ?? 0))).' '.esc_html($currency).'</td></tr><tr><th colspan="4" class="num">Impuestos</th><td class="num">'.esc_html($this->formatPrintAmount($sign * (float) ($s['total_tax'] ?? 0))).' '.esc_html($currency).'</td></tr><tr><th colspan="4" class="num total">TOTAL</th><td class="num total">'.esc_html($this->formatPrintAmount($sign * (float) ($s['total_incl'] ?? 0))).' '.esc_html($currency).'</td></tr></tfoot></table>';
        if (!empty($s['refund_reason'])) { echo '<p><strong>Motivo de rectificacion:</strong> '.esc_html($s['refund_reason']).'</p>'; }
        if (!empty($s['fiscal_note'])) { echo '<p><strong>Nota fiscal:</strong> '.esc_html($s['fiscal_note']).'</p>'; }

        if (!$isCredit) {
            $links = array();
            foreach ($order->get_refunds() as $relatedRefund) {
                $credit = $this->getDocuments()->getCreditNoteByRefund((int) $relatedRefund->get_id());
                if ($credit) {
                    $links[] = '<a href="'.esc_url($this->customerRefundUrl($order, (int) $relatedRefund->get_id())).'">'.esc_html($credit->number).'</a>';
                }
            }
            if ($links) { echo '<p><strong>Rectificativas asociadas:</strong> '.implode(' · ', $links).'</p>'; }
        }
        echo '<p class="muted">Documento fiscal generado por digitalDot VeriFactu para WooCommerce. La instantanea economica se conserva de forma inmutable junto con su huella de integridad.</p></body></html>';
        exit;
    }

    private function formatPrintAmount($value)
    {
        return number_format((float) $value, 2, ',', '.');
    }

    public function assertPdfCompliance()
    {
        // Preserve the existing fiscal transport contract: AEAT sending only
        // depends on the local QR capability. Dompdf is validated lazily when
        // a PDF is explicitly requested.
        if(!extension_loaded('gd')) throw new Exception('La extension GD es obligatoria para generar el QR del documento fiscal.');
        DdvWpStorage::ensure();
        return true;
    }

    public function getDecryptedCertificate()
    {
        $file=(string)DdvWpConfigStore::get('ddverifactu_CERT'); if($file==='') throw new Exception('No hay certificado configurado.');
        $path=DdvWpStorage::path('certs',basename($file)); if(!is_file($path)) throw new Exception('No se encuentra el certificado cifrado.');
        $blob=file_get_contents($path); if(!is_string($blob)||substr($blob,0,5)!=='DDVF2') throw new Exception('El contenedor cifrado del certificado no tiene un formato soportado.');
        return $this->decryptBlobV2($blob);
    }
    public function getCertificatePassword(){ $enc=(string)DdvWpConfigStore::get('ddverifactu_CERT_PASS'); if($enc==='')return ''; if(strpos($enc,'DDVF2:')!==0) throw new Exception('La contrasena cifrada del certificado tiene un formato no soportado.'); return $this->decryptSecretV2(substr($enc,6)); }
    public function refreshCertificateIdentity(){ $info=(new VerifactuTransport())->inspectCertificate($this);$identity=VerifactuCertificateManager::extractFiscalIdentity($info);VerifactuConfig::persistCertificateIdentity($identity);return $identity; }

    private function storeCertificate($raw,$password)
    {
        if(!extension_loaded('openssl')) throw new Exception('OpenSSL es obligatorio para custodiar el certificado.');
        $identity=$this->validateRawCertificate($raw,$password); DdvWpStorage::ensure();
        $blob=$this->encryptBlobV2($raw); $dest=DdvWpStorage::path('certs',self::CERT_FILE); $tmp=$dest.'.tmp-'.bin2hex(random_bytes(4));
        if(file_put_contents($tmp,$blob,LOCK_EX)===false || !@rename($tmp,$dest)){@unlink($tmp);throw new Exception('No se ha podido guardar atomicamente el certificado cifrado.');}
        @chmod($dest,0600); DdvWpConfigStore::updateValue('ddverifactu_CERT',self::CERT_FILE); DdvWpConfigStore::updateValue('ddverifactu_CERT_PASS','DDVF2:'.$this->encryptSecretV2($password)); VerifactuConfig::persistCertificateIdentity($identity); return $identity;
    }
    private function validateRawCertificate($raw,$password)
    {
        $certPem='';$keyPem='';
        if(strpos($raw,'-----BEGIN CERTIFICATE-----')!==false){if(!preg_match('/-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----/s',$raw,$cm)||!preg_match('/-----BEGIN (?:ENCRYPTED |RSA |EC )?PRIVATE KEY-----.*?-----END (?:ENCRYPTED |RSA |EC )?PRIVATE KEY-----/s',$raw,$km))throw new Exception('El PEM debe incluir certificado y clave privada.');$certPem=$cm[0];$keyPem=$km[0];}
        else{$certs=array();if(!@openssl_pkcs12_read($raw,$certs,(string)$password)||empty($certs['cert'])||empty($certs['pkey']))throw new Exception('No se ha podido abrir el P12/PFX con la contrasena indicada.');$certPem=(string)$certs['cert'];$keyPem=(string)$certs['pkey'];}
        $cert=@openssl_x509_read($certPem);$key=@openssl_pkey_get_private($keyPem,(string)$password);if(!$cert||!$key||!@openssl_x509_check_private_key($cert,$key))throw new Exception('La clave privada no corresponde al certificado.');$info=@openssl_x509_parse($cert);$now=time();if(!is_array($info)||(!empty($info['validFrom_time_t'])&&$now<(int)$info['validFrom_time_t'])||empty($info['validTo_time_t'])||$now>=(int)$info['validTo_time_t'])throw new Exception('El certificado no se encuentra dentro de su periodo de validez.');return VerifactuCertificateManager::extractFiscalIdentity($info);
    }
    private function encryptBlobV2($raw){$nonce=random_bytes(12);$tag='';$cipher=openssl_encrypt($raw,'aes-256-gcm',$this->storageKey(),OPENSSL_RAW_DATA,$nonce,$tag,'ddverifactu-certificate-v2');if($cipher===false||strlen($tag)!==16)throw new Exception('No se ha podido cifrar el certificado con AES-256-GCM.');return 'DDVF2'.$nonce.$tag.$cipher;}
    private function decryptBlobV2($blob){if(strlen($blob)<34)throw new Exception('El contenedor cifrado esta incompleto.');$raw=openssl_decrypt(substr($blob,33),'aes-256-gcm',$this->storageKey(),OPENSSL_RAW_DATA,substr($blob,5,12),substr($blob,17,16),'ddverifactu-certificate-v2');if($raw===false)throw new Exception('La autenticidad del certificado cifrado no se ha podido verificar.');return $raw;}
    private function encryptSecretV2($value){$nonce=random_bytes(12);$tag='';$cipher=openssl_encrypt((string)$value,'aes-256-gcm',$this->storageKey(),OPENSSL_RAW_DATA,$nonce,$tag,'ddverifactu-secret-v2');if($cipher===false)throw new Exception('No se ha podido cifrar la contrasena.');return base64_encode($nonce.$tag.$cipher);}
    private function decryptSecretV2($encoded){$blob=base64_decode((string)$encoded,true);if($blob===false||strlen($blob)<29)throw new Exception('La contrasena cifrada no tiene un formato valido.');$value=openssl_decrypt(substr($blob,28),'aes-256-gcm',$this->storageKey(),OPENSSL_RAW_DATA,substr($blob,0,12),substr($blob,12,16),'ddverifactu-secret-v2');if($value===false)throw new Exception('No se ha podido autenticar la contrasena cifrada.');return $value;}
    private function storageKey(){ $salt=defined('AUTH_KEY')?AUTH_KEY:(function_exists('wp_salt')?wp_salt('auth'):'ddv');$db=defined('DB_NAME')?DB_NAME:'wordpress';$site=function_exists('get_current_blog_id')?(int)get_current_blog_id():1;return hash('sha256',$salt.'|ddverifactu|'.$db.'|site:'.$site,true); }

    private function getService(){ if(!$this->service)$this->service=new VerifactuService($this); return $this->service; }
    private function getDocuments(){ if(!$this->documents)$this->documents=new DdvWooDocumentRepository(); return $this->documents; }
    private function automaticModeEnabled(){ try{return VerifactuConfig::getMode()==='automatico';}catch(Throwable $e){return false;} }
    private function logAutomaticError($type,Throwable $e,$orderId,$invoiceId,$refundId){try{(new VerifactuRepository())->logEvent('ERROR',$type,array('id_shop'=>function_exists('get_current_blog_id')?get_current_blog_id():1,'id_order'=>(int)$orderId,'id_order_invoice'=>$invoiceId?:null,'id_order_slip'=>$refundId?:null,'message'=>$e->getMessage()));}catch(Throwable $ignored){} }
    private function detectTaxId(WC_Order $order){foreach(array('_ddv_tax_id','_billing_nif','billing_nif','_billing_vat','billing_vat','_billing_vat_number','vat_number','_billing_dni','dni') as $key){$v=trim((string)$order->get_meta($key,true));if($v!=='')return $v;}return '';}
    private function safeInvoice($id){try{return VerifactuDocumentIdentity::getInvoiceById((int)$id);}catch(Throwable $e){return null;}}
    private function customerInvoiceUrl(WC_Order $order){return add_query_arg(array('action'=>'ddv_customer_invoice','order_id'=>$order->get_id(),'key'=>$order->get_order_key()),admin_url('admin-post.php'));}
    private function customerRefundUrl(WC_Order $order,$refundId){return add_query_arg(array('action'=>'ddv_customer_refund','order_id'=>$order->get_id(),'refund_id'=>(int)$refundId,'key'=>$order->get_order_key()),admin_url('admin-post.php'));}
    private function customerInvoicePdfUrl(WC_Order $order){return add_query_arg(array('action'=>'ddv_customer_invoice_pdf','order_id'=>$order->get_id(),'key'=>$order->get_order_key()),admin_url('admin-post.php'));}
    private function customerRefundPdfUrl(WC_Order $order,$refundId){return add_query_arg(array('action'=>'ddv_customer_refund_pdf','order_id'=>$order->get_id(),'refund_id'=>(int)$refundId,'key'=>$order->get_order_key()),admin_url('admin-post.php'));}

    private function requireAdmin($nonceAction){ if(!current_user_can('manage_woocommerce'))wp_die('No autorizado.',403);check_admin_referer($nonceAction); }
    private function actionButton($action,$label,array $hidden,$nonce,$primary=false,$get=false,$confirm=''){$method=$get?'get':'post';$onsubmit=$confirm!==''?' onsubmit="return confirm('.esc_attr(wp_json_encode((string)$confirm)).');"':'';echo '<form method="'.$method.'" action="'.esc_url(admin_url('admin-post.php')).'" style="display:inline"'.$onsubmit.'>';wp_nonce_field($nonce);echo '<input type="hidden" name="action" value="'.esc_attr($action).'">';foreach($hidden as $k=>$v)echo '<input type="hidden" name="'.esc_attr($k).'" value="'.esc_attr($v).'">';echo '<button class="button '.($primary?'button-primary':'').'">'.esc_html($label).'</button></form>';}
    private function orderPostButton($action,$label,array $hidden,$nonce,$primary=false,$confirm='',array $fieldSelectors=array())
    {
        $classes = 'button' . ($primary ? ' button-primary' : '') . ' ddv-order-post-action';
        echo '<button type="button" class="'.esc_attr($classes).'" data-ddv-action="'.esc_attr($action).'" data-ddv-nonce="'.esc_attr(wp_create_nonce($nonce)).'" data-ddv-payload="'.esc_attr(wp_json_encode($hidden)).'" data-ddv-fields="'.esc_attr(wp_json_encode($fieldSelectors)).'"'.($confirm !== '' ? ' data-ddv-confirm="'.esc_attr($confirm).'"' : '').'>'.esc_html($label).'</button>';
    }
    private function orderGetButton($action,$label,array $query,$nonce)
    {
        $url = add_query_arg(array_merge(array('action'=>$action), $query), admin_url('admin-post.php'));
        $url = wp_nonce_url($url, $nonce);
        echo '<a class="button" href="'.esc_url($url).'">'.esc_html($label).'</a>';
    }
    private function redirectOrderNotice($orderId,$type,$message){$this->setNotice($type,$message);$url=admin_url('admin.php?page=ddverifactu');if($orderId>0){$order=wc_get_order((int)$orderId);if($order instanceof WC_Order && method_exists($order,'get_edit_order_url')){$url=$order->get_edit_order_url();}else{$url=admin_url('admin.php?page=wc-orders&action=edit&id='.(int)$orderId);}}wp_safe_redirect($url);exit;}
    private function redirectNotice($type,$message,$tab='dashboard'){$this->setNotice($type,$message);wp_safe_redirect(admin_url('admin.php?page=ddverifactu&tab='.$tab));exit;}
    private function setNotice($type,$message){set_transient('ddv_notice_'.get_current_user_id(),array('type'=>$type,'message'=>(string)$message),60);}
    public function adminNotices(){if(!current_user_can('manage_woocommerce'))return;if(!class_exists('WooCommerce'))echo '<div class="notice notice-error"><p>digitalDot VeriFactu requiere WooCommerce activo.</p></div>';$key='ddv_notice_'.get_current_user_id();$n=get_transient($key);if(is_array($n)){delete_transient($key);echo '<div class="notice notice-'.esc_attr($n['type']).' is-dismissible"><p>'.esc_html($n['message']).'</p></div>';}}

    private function textField($key,$label,$description=''){echo '<tr><th><label for="'.esc_attr($key).'">'.esc_html($label).'</label></th><td><input class="regular-text" type="text" id="'.esc_attr($key).'" name="'.esc_attr($key).'" value="'.esc_attr((string)DdvWpConfigStore::get($key)).'">'.($description?'<p class="description">'.esc_html($description).'</p>':'').'</td></tr>';}
    private function numberField($key,$label,$min,$max=null){$value=(int)DdvWpConfigStore::get($key);echo '<tr><th><label for="'.esc_attr($key).'">'.esc_html($label).'</label></th><td><input type="number" id="'.esc_attr($key).'" name="'.esc_attr($key).'" min="'.(int)$min.'" '.($max!==null?'max="'.(int)$max.'"':'').' value="'.esc_attr($value).'" class="small-text"></td></tr>';}
    private function selectField($key,$label,array $options){$value=(string)DdvWpConfigStore::get($key);echo '<tr><th><label for="'.esc_attr($key).'">'.esc_html($label).'</label></th><td><select id="'.esc_attr($key).'" name="'.esc_attr($key).'">';foreach($options as $v=>$t)echo '<option value="'.esc_attr($v).'" '.selected($value,(string)$v,false).'>'.esc_html($t).'</option>';echo '</select></td></tr>';}
    private function checkboxField($key,$label){$checked=(bool)DdvWpConfigStore::get($key);echo '<tr><th>'.esc_html($label).'</th><td><label><input type="checkbox" name="'.esc_attr($key).'" value="1" '.checked($checked,true,false).'> Si</label></td></tr>';}
}

register_activation_hook(__FILE__, array('DdvVerifactuPlugin','activate'));
register_deactivation_hook(__FILE__, array('DdvVerifactuPlugin','deactivate'));
DdvVerifactuPlugin::instance();
