<?php

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

class Freefactu_Admin {

    /**
     * Default number of days before certificate expiration to show warning
     */
    const DEFAULT_CERT_EXPIRY_WARNING_DAYS = 60;

    /**
     * Encryption method for password storage
     */
    const ENCRYPTION_METHOD = 'AES-256-CBC';

	private $option_name = 'freefactu_settings';

    /**
     * Get the configured certificate expiry warning days
     */
    public static function get_cert_expiry_warning_days() {
        $options = get_option( 'freefactu_settings' );
        return isset( $options['cert_expiry_warning_days'] ) ? absint( $options['cert_expiry_warning_days'] ) : self::DEFAULT_CERT_EXPIRY_WARNING_DAYS;
    }

    /**
     * Encrypt a password for secure storage.
     *
     * @param string $password The password to encrypt.
     * @return string The encrypted password (base64 encoded).
     */
    public static function encrypt_password( $password ) {
        if ( empty( $password ) ) {
            return '';
        }

        $key = wp_salt( 'auth' );
        $iv = substr( wp_salt( 'secure_auth' ), 0, 16 );

        $encrypted = openssl_encrypt( $password, self::ENCRYPTION_METHOD, $key, 0, $iv );

        if ( $encrypted === false ) {
            return $password; // Fallback to plaintext if encryption fails
        }

        return 'enc:' . base64_encode( $encrypted );
    }

    /**
     * Decrypt a stored password.
     *
     * @param string $encrypted_password The encrypted password.
     * @return string The decrypted password.
     */
    public static function decrypt_password( $encrypted_password ) {
        if ( empty( $encrypted_password ) ) {
            return '';
        }

        // Check if password is encrypted (has 'enc:' prefix)
        if ( strpos( $encrypted_password, 'enc:' ) !== 0 ) {
            return $encrypted_password; // Return as-is if not encrypted (legacy)
        }

        $key = wp_salt( 'auth' );
        $iv = substr( wp_salt( 'secure_auth' ), 0, 16 );

        $encrypted_data = base64_decode( substr( $encrypted_password, 4 ) );
        $decrypted = openssl_decrypt( $encrypted_data, self::ENCRYPTION_METHOD, $key, 0, $iv );

        if ( $decrypted === false ) {
            return ''; // Return empty if decryption fails
        }

        return $decrypted;
    }

    /**
     * Handle secure certificate upload using wp_handle_upload().
     *
     * This method uses WordPress's built-in upload handling with custom overrides
     * to store certificate files (.p12/.pfx) in a protected directory outside
     * the media library.
     *
     * @param array  $file The $_FILES array element for the uploaded file.
     * @param string $target_filename The target filename to save as.
     * @return array|WP_Error Array with 'file' path on success, WP_Error on failure.
     */
    public static function handle_certificate_upload( $file, $target_filename = 'certificate.p12' ) {
        // Ensure the secure directory exists
        $upload_dir = wp_upload_dir();
        $cert_dir   = $upload_dir['basedir'] . '/freefactu-certs';

        if ( ! Freefactu_Filesystem::exists( $cert_dir ) ) {
            Freefactu_Filesystem::mkdir( $cert_dir );
            // Secure the directory with .htaccess and index.php
            Freefactu_Filesystem::put_contents( $cert_dir . '/.htaccess', 'Deny from all' );
            Freefactu_Filesystem::put_contents( $cert_dir . '/index.php', '<?php // Silence is golden' );
        }

        // Define custom upload directory filter
        $custom_upload_dir = function ( $dirs ) use ( $cert_dir ) {
            return array(
                'path'    => $cert_dir,
                'url'     => '', // No public URL - files are protected
                'subdir'  => '',
                'basedir' => $cert_dir,
                'baseurl' => '',
                'error'   => false,
            );
        };

        // Custom unique filename handler to always use consistent certificate name
        $unique_filename = function ( $dir, $name, $ext ) use ( $target_filename ) {
            return $target_filename;
        };

        // Add allowed MIME types for certificates
        $allow_certificate_mimes = function ( $mimes ) {
            $mimes['p12'] = 'application/x-pkcs12';
            $mimes['pfx'] = 'application/x-pkcs12';
            return $mimes;
        };

        // Register filters
        add_filter( 'upload_dir', $custom_upload_dir );
        add_filter( 'wp_unique_filename', $unique_filename, 10, 3 );
        add_filter( 'upload_mimes', $allow_certificate_mimes );

        // Use wp_handle_upload with custom overrides
        $overrides = array(
            'test_form' => false, // Don't check for form submission
            'test_type' => false, // Skip MIME type checking (handled by caller)
            'action'    => 'freefactu_certificate_upload', // Custom action prevents media lib registration
            'mimes'     => array(
                'p12' => 'application/x-pkcs12',
                'pfx' => 'application/x-pkcs12',
            ),
        );

        $result = wp_handle_upload( $file, $overrides );

        // Remove filters immediately after use
        remove_filter( 'upload_dir', $custom_upload_dir );
        remove_filter( 'wp_unique_filename', $unique_filename, 10 );
        remove_filter( 'upload_mimes', $allow_certificate_mimes );

        // Handle errors
        if ( isset( $result['error'] ) && $result['error'] ) {
            return new WP_Error( 'upload_failed', $result['error'] );
        }

        return $result;
    }

    public function __construct() {
        add_action( 'admin_menu', array( $this, 'add_admin_menu' ), 1000 ); // Priority 99 to appear last
        add_action( 'admin_init', array( $this, 'register_settings' ) );
        add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ) );
        // Certificate management AJAX handlers
        add_action( 'wp_ajax_freefactu_remove_certificate', array( $this, 'ajax_remove_certificate' ) );
        add_action( 'wp_ajax_freefactu_upload_certificate', array( $this, 'ajax_upload_certificate' ) );
        // QR cache management AJAX handler
        add_action( 'wp_ajax_freefactu_clear_qr_cache', array( $this, 'ajax_clear_qr_cache' ) );
        // Unified invoice action handler (replaces deprecated ajax_send_invoice and ajax_retry_invoice)
        add_action( 'wp_ajax_freefactu_unified_invoice_action', array( $this, 'handle_unified_invoice_action' ) );
        // Subsanation handler (retry with fresh snapshot after AEAT rejection)
        add_action( 'wp_ajax_freefactu_subsanar_invoice', array( $this, 'ajax_subsanar_invoice' ) );
        add_action( 'admin_post_freefactu_download_invoice', array( $this, 'handle_download_invoice' ) );
        // Rectificativa invoice handler
        add_action( 'wp_ajax_freefactu_create_rectificativa', array( $this, 'ajax_create_rectificativa' ) );
        add_action( 'wp_ajax_freefactu_search_invoices', array( $this, 'ajax_search_invoices' ) );
        add_action( 'wp_ajax_freefactu_get_invoice_preview', array( $this, 'ajax_get_invoice_preview' ) );
        // Declaration management AJAX handlers
        add_action( 'wp_ajax_freefactu_save_declaration', array( $this, 'ajax_save_declaration' ) );
        add_action( 'wp_ajax_freefactu_get_declaration', array( $this, 'ajax_get_declaration' ) );
        // Series management AJAX handlers
        add_action( 'wp_ajax_freefactu_create_series', array( $this, 'ajax_create_series' ) );
        add_action( 'wp_ajax_freefactu_set_active_series', array( $this, 'ajax_set_active_series' ) );
        add_action( 'wp_ajax_freefactu_delete_series', array( $this, 'ajax_delete_series' ) );
        // Save order and create invoice in one seamless operation
        add_action( 'wp_ajax_freefactu_save_and_create_invoice', array( $this, 'ajax_save_and_create_invoice' ) );
        // Save order and retry a failed invoice in one seamless operation
        add_action( 'wp_ajax_freefactu_save_and_retry_invoice', array( $this, 'ajax_save_and_retry_invoice' ) );
        // Refresh meta box content (for updating UI after errors without page refresh)
        add_action( 'wp_ajax_freefactu_refresh_metabox', array( $this, 'ajax_refresh_metabox' ) );
        // Declaration download handlers
        add_action( 'admin_post_freefactu_download_declaration_pdf', array( $this, 'handle_download_declaration_pdf' ) );
        add_action( 'admin_post_freefactu_download_declaration_zip', array( $this, 'handle_download_declaration_zip' ) );
        // Generate declaration content AJAX handler
        add_action( 'wp_ajax_freefactu_generate_declaration_content', array( $this, 'ajax_generate_declaration_content' ) );
        // System requirements acknowledgment handler
        add_action( 'wp_ajax_freefactu_ack_system_requirements', array( $this, 'ajax_ack_system_requirements' ) );
        // Note: admin_post_freefactu_standalone_pdf is handled by the standalone generator's register_output_hooks()
        // WooCommerce Actions column visibility warning (uses admin_footer to bypass WooCommerce's notice hiding)
        add_action( 'admin_footer', array( $this, 'check_wc_actions_column_visibility' ) );
        add_action( 'admin_post_freefactu_enable_wc_actions_column', array( $this, 'handle_enable_wc_actions_column' ) );
    }

    /**
     * Check if WooCommerce "Actions" column is hidden and display a warning.
     *
     * The plugin adds invoice creation buttons to the WooCommerce order list
     * via the wc_actions column. If this column is hidden, users won't see
     * the buttons and won't be able to create invoices from the order list.
     *
     * Uses JavaScript injection via admin_footer to bypass WooCommerce's
     * notice hiding container (.woocommerce-layout__notice-list-hide).
     *
     * @return void
     */
    public function check_wc_actions_column_visibility() {
        // Only check on WooCommerce order list pages
        $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
        if ( ! $screen ) {
            return;
        }

        // Check if we're on the orders list page
        // Supports both legacy (edit-shop_order) and HPOS (woocommerce_page_wc-orders) storage
        $is_orders_list = (
            $screen->id === 'edit-shop_order' ||
            $screen->id === 'woocommerce_page_wc-orders'
        );

        if ( ! $is_orders_list ) {
            return;
        }

        // Only show to users who can manage WooCommerce
        if ( ! current_user_can( 'manage_woocommerce' ) ) {
            return;
        }

        $user_id = get_current_user_id();

        // Handle dismiss action (needs to be processed early)
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        if ( isset( $_GET['freefactu_dismiss_wc_actions_notice'] ) ) {
            // phpcs:ignore WordPress.Security.NonceVerification.Recommended
            if ( wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ?? '' ) ), 'freefactu_dismiss_wc_actions_notice' ) ) {
                update_user_meta( $user_id, 'freefactu_wc_actions_notice_dismissed', true );
                wp_safe_redirect( remove_query_arg( array( 'freefactu_dismiss_wc_actions_notice', '_wpnonce' ) ) );
                exit;
            }
        }

        // Determine what to show
        $show_success = false;
        $show_warning = false;

        // Show success message if the column was just enabled
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        if ( isset( $_GET['freefactu_wc_actions_enabled'] ) && sanitize_text_field( wp_unslash( $_GET['freefactu_wc_actions_enabled'] ) ) === '1' ) {
            $show_success = true;
        } else {
            // Check if user has dismissed the warning notice
            $dismissed = get_user_meta( $user_id, 'freefactu_wc_actions_notice_dismissed', true );
            if ( ! $dismissed ) {
                // Determine which meta key to check based on storage mode
                $meta_key = $screen->id === 'woocommerce_page_wc-orders'
                    ? 'managewoocommerce_page_wc-orderscolumnshidden'
                    : 'manageedit-shop_ordercolumnshidden';

                $hidden_columns = get_user_meta( $user_id, $meta_key, true );

                // Check if 'wc_actions' is in the hidden columns array
                if ( is_array( $hidden_columns ) && in_array( 'wc_actions', $hidden_columns, true ) ) {
                    $show_warning = true;
                }
            }
        }

        // Nothing to show
        if ( ! $show_success && ! $show_warning ) {
            return;
        }

        // Build URLs for buttons
        $enable_url = wp_nonce_url(
            admin_url( 'admin-post.php?action=freefactu_enable_wc_actions_column' ),
            'freefactu_enable_wc_actions'
        );
        $dismiss_url = wp_nonce_url(
            add_query_arg( 'freefactu_dismiss_wc_actions_notice', '1' ),
            'freefactu_dismiss_wc_actions_notice'
        );

        // Build messages
        $success_message = esc_js( __( 'La columna "Acciones" ha sido activada. Ahora puedes ver los botones de facturación Verifactu en la lista de pedidos.', 'freefactu' ) );
        $warning_message = esc_js( __( 'La columna "Acciones" de WooCommerce está oculta. Para ver los botones de creación de facturas Verifactu, necesitas activar esta columna.', 'freefactu' ) );
        $enable_button_text = esc_js( __( 'Activar columna "Acciones"', 'freefactu' ) );
        $dismiss_button_text = esc_js( __( 'No mostrar de nuevo', 'freefactu' ) );
        $help_text = esc_js( __( 'También puedes activarla manualmente desde "Opciones de pantalla" en la parte superior de esta página.', 'freefactu' ) );

        // Build the inline script to inject the notice
        $notice_html = '';
        if ( $show_success ) {
            $notice_html = '<div id="freefactu-wc-actions-notice" style="background: #d4edda; border-left: 4px solid #28a745; padding: 12px 15px; margin: 10px 20px 10px 0; border-radius: 4px; position: relative; z-index: 100;"><p style="margin: 0; color: #155724; font-size: 14px;"><span class="dashicons dashicons-yes-alt" style="color: #28a745; vertical-align: text-bottom;"></span> <strong>Freefactu:</strong> ' . $success_message . '</p></div>';
        } elseif ( $show_warning ) {
            $notice_html = '<div id="freefactu-wc-actions-notice" style="background: #fff3cd; border-left: 4px solid #ffc107; padding: 12px 15px; margin: 10px 20px 10px 0; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); position: relative; z-index: 100;"><p style="margin: 0 0 10px 0; color: #856404; font-size: 14px;"><span class="dashicons dashicons-warning" style="color: #ffc107; vertical-align: text-bottom;"></span> <strong>Freefactu:</strong> ' . $warning_message . '</p><p style="margin: 0 0 10px 0;"><a href="' . esc_url( $enable_url ) . '" class="button button-primary">' . $enable_button_text . '</a><a href="' . esc_url( $dismiss_url ) . '" class="button" style="margin-left: 10px;">' . $dismiss_button_text . '</a></p><p style="margin: 0; color: #6c757d; font-size: 12px;">' . $help_text . '</p></div>';
        }

        $inline_script = '
(function() {
    "use strict";
    function injectFreefactuNotice() {
        if (document.getElementById("freefactu-wc-actions-notice")) {
            return;
        }
        var noticeHtml = ' . wp_json_encode( $notice_html ) . ';
        if (!noticeHtml) {
            return;
        }
        var insertionPoints = [
            ".woocommerce-layout__main",
            "#woocommerce-order-list",
            ".wrap h1",
            ".wrap .wp-header-end",
            ".wrap"
        ];
        var inserted = false;
        for (var i = 0; i < insertionPoints.length && !inserted; i++) {
            var target = document.querySelector(insertionPoints[i]);
            if (target) {
                var div = document.createElement("div");
                div.innerHTML = noticeHtml;
                var notice = div.firstChild;
                if (insertionPoints[i] === ".wrap h1" || insertionPoints[i] === ".wrap .wp-header-end") {
                    target.parentNode.insertBefore(notice, target.nextSibling);
                } else if (insertionPoints[i] === ".woocommerce-layout__main") {
                    target.insertBefore(notice, target.firstChild);
                } else {
                    target.insertBefore(notice, target.firstChild);
                }
                inserted = true;
            }
        }
    }
    if (document.readyState === "loading") {
        document.addEventListener("DOMContentLoaded", injectFreefactuNotice);
    } else {
        setTimeout(injectFreefactuNotice, 100);
    }
})();';

        wp_add_inline_script( 'jquery', $inline_script );
    }

    /**
     * Handle request to enable the WooCommerce Actions column.
     *
     * Removes 'wc_actions' from the current user's hidden columns preference
     * and redirects back to the orders list.
     *
     * @return void
     */
    public function handle_enable_wc_actions_column() {
        // Verify nonce
        if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'freefactu_enable_wc_actions' ) ) {
            wp_die( esc_html__( 'Acción no autorizada.', 'freefactu' ) );
        }

        // Check user capabilities
        if ( ! current_user_can( 'manage_woocommerce' ) ) {
            wp_die( esc_html__( 'No tienes permisos para realizar esta acción.', 'freefactu' ) );
        }

        $user_id = get_current_user_id();

        // Update both storage modes to ensure column is visible
        $meta_keys = array(
            'manageedit-shop_ordercolumnshidden',          // Legacy CPT storage
            'managewoocommerce_page_wc-orderscolumnshidden', // HPOS storage
        );

        foreach ( $meta_keys as $meta_key ) {
            $hidden_columns = get_user_meta( $user_id, $meta_key, true );

            if ( ! is_array( $hidden_columns ) ) {
                continue;
            }

            // Remove 'wc_actions' from the hidden columns array
            $key = array_search( 'wc_actions', $hidden_columns, true );
            if ( false !== $key ) {
                unset( $hidden_columns[ $key ] );
                $hidden_columns = array_values( $hidden_columns ); // Re-index array
                update_user_meta( $user_id, $meta_key, $hidden_columns );
            }
        }

        // Also clear the dismissed notice since the user took action
        delete_user_meta( $user_id, 'freefactu_wc_actions_notice_dismissed' );

        // Determine redirect URL (prefer HPOS, fallback to legacy)
        if ( class_exists( \Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController::class ) ) {
            $cot_controller = wc_get_container()->get( \Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController::class );
            if ( $cot_controller->custom_orders_table_usage_is_enabled() ) {
                $redirect_url = admin_url( 'admin.php?page=wc-orders' );
            } else {
                $redirect_url = admin_url( 'edit.php?post_type=shop_order' );
            }
        } else {
            $redirect_url = admin_url( 'edit.php?post_type=shop_order' );
        }

        // Add success message parameter
        $redirect_url = add_query_arg( 'freefactu_wc_actions_enabled', '1', $redirect_url );

        wp_safe_redirect( $redirect_url );
        exit;
    }

    /**
     * Render centralized Verifactu icon HTML with status badges.
     *
     * Generates consistent icon HTML across all display locations (order list,
     * order view, invoice list) in both standalone and integrated modes.
     *
     * SECURITY NOTE: This function is safe for unescaped output because:
     * - All dynamic values use esc_attr(), esc_url(), or esc_html()
     * - SVG badges are static hardcoded strings (get_alert_badge_svg, get_success_badge_svg)
     * - CSS classes are built from sanitized, controlled values
     *
     * @param WC_Order $order   The order object.
     * @param string   $context Display context: 'order_list', 'order_view', or 'invoice_list'.
     * @return string Complete HTML markup for the icon with status badges. Safe for direct output.
     */
    public static function render_verifactu_icon_html( $order, $context = 'order_list' ) {
        // Validate order object
        if ( ! $order || ! $order instanceof WC_Order ) {
            return '';
        }

        // Get button state using existing method
        $button_state = self::get_button_state( $order );
        $order_id = $order->get_id();

        // Get status from database
        $invoice = Freefactu_DB::get_invoice_by_order_id( $order_id );
        $status = $invoice ? $invoice->status : '';

        // Normalize status for data attribute
        if ( empty( $status ) ) {
            $status = 'normal';
        }

        // Determine CSS classes
        $classes = array( 'freefactu-invoice-action' );
        if ( ! empty( $button_state['classes'] ) ) {
            $classes[] = $button_state['classes'];
        }

        // Build data attributes
        $error_category = $invoice && isset( $invoice->error_category ) ? $invoice->error_category : '';
        $error_message = $invoice && isset( $invoice->error_message ) ? $invoice->error_message : '';
        
        $data_attrs = sprintf(
            'data-order-id="%d" data-status="%s" data-context="%s" data-error-category="%s" data-error-message="%s"',
            esc_attr( $order_id ),
            esc_attr( $status ),
            esc_attr( $context ),
            esc_attr( $error_category ),
            esc_attr( $error_message )
        );

        // Get PDF icon URL
        $pdf_icon_url = FREEFACTU_PLUGIN_URL . 'assets/images/freefactu-invoice.svg';

        // Build status badge SVG if needed
        $badge_svg = '';
        if ( $button_state['state'] === 'alert' || $button_state['state'] === 'subsanar' ) {
            $badge_svg = self::get_alert_badge_svg();
        } elseif ( $button_state['state'] === 'success' ) {
            $badge_svg = self::get_success_badge_svg();
        }

        // Build complete HTML structure
        // Use 'tips' class and 'data-tip' attribute to match WooCommerce's tooltip system
        $html = sprintf(
            '<a href="#" class="tips %s" %s data-tip="%s">
                <span class="freefactu-icon-wrapper">
                    <img src="%s" class="freefactu-pdf-icon" alt="PDF" />
                    %s
                </span>
                <span class="freefactu-icon-text">%s</span>
            </a>',
            esc_attr( implode( ' ', $classes ) ),
            $data_attrs,
            esc_attr( $button_state['tooltip'] ),
            esc_url( $pdf_icon_url ),
            $badge_svg,
            esc_html( $button_state['text'] )
        );

        return $html;
    }

    /**
     * Render Verifactu icon HTML from invoice data when order doesn't exist.
     *
     * This method is used when displaying invoices in the history list
     * for orders that have been deleted. It uses invoice database data
     * instead of the order object.
     *
     * @param object $invoice Invoice database object.
     * @param string $context Display context: 'order_list', 'order_view', or 'invoice_list'.
     * @return string Complete HTML markup for the icon with status badges.
     */
    public static function render_verifactu_icon_html_from_invoice( $invoice, $context = 'invoice_list' ) {
        // Validate invoice object
        if ( ! $invoice || ! isset( $invoice->order_id ) ) {
            return '';
        }

        $order_id = $invoice->order_id;
        $status = isset( $invoice->status ) ? $invoice->status : '';
        $invoice_number = isset( $invoice->invoice_number ) ? $invoice->invoice_number : '';

        // Normalize status for data attribute
        if ( empty( $status ) ) {
            $status = 'normal';
        }

        // Determine button state based on invoice status
        $button_state = array(
            'has_invoice' => ! empty( $invoice_number ),
            'state'       => 'no_invoice',
            'classes'     => '',
            'text'        => __( 'Ver Factura', 'freefactu' ),
            'tooltip'     => __( 'Ver factura (pedido eliminado)', 'freefactu' ),
        );

        if ( ! empty( $invoice_number ) ) {
            if ( $status === 'success' ) {
                $button_state['state'] = 'success';
                $button_state['classes'] = 'exists';
                $button_state['text'] = __( 'Ver Factura', 'freefactu' );
                $button_state['tooltip'] = __( 'Factura generada correctamente (pedido eliminado)', 'freefactu' );
            } elseif ( $status === 'alert_aeat' ) {
                // Check if error is subsanable
                $error_category = isset( $invoice->error_category ) ? $invoice->error_category : '';
                $aeat_status = isset( $invoice->aeat_final_status ) ? $invoice->aeat_final_status : 'pending';
                $subsanation_count = isset( $invoice->subsanation_count ) ? (int) $invoice->subsanation_count : 0;
                $max_attempts = apply_filters( 'freefactu_max_subsanation_attempts', 3, $invoice );
                
                $is_subsanable = (
                    $aeat_status !== 'accepted' && 
                    $subsanation_count < $max_attempts &&
                    ( empty( $error_category ) || Freefactu_Aeat_Error_Categories::is_subsanable_category( $error_category ) )
                );
                
                if ( $is_subsanable ) {
                    $button_state['state'] = 'subsanar';
                    $button_state['classes'] = 'alert subsanable';
                    $button_state['text'] = __( 'Subsanar', 'freefactu' );
                    $button_state['tooltip'] = sprintf(
                        /* translators: 1: Error category, 2: Current attempt, 3: Max attempts */
                        __( 'Error AEAT: %1$s - Clic para subsanar (intento %2$d/%3$d)', 'freefactu' ),
                        Freefactu_Aeat_Error_Categories::get_label( $error_category ),
                        $subsanation_count + 1,
                        $max_attempts
                    );
                } else {
                    $button_state['state'] = 'alert';
                    $button_state['classes'] = 'alert not-subsanable';
                    $button_state['text'] = __( 'Crear Rectificativa', 'freefactu' );
                    $button_state['tooltip'] = sprintf(
                        /* translators: %s: Error category */
                        __( 'Error no subsanable (%s). Debe crear una factura rectificativa.', 'freefactu' ),
                        Freefactu_Aeat_Error_Categories::get_label( $error_category )
                    );
                }
            } elseif ( $status === 'alert_qr' ) {
                $button_state['state'] = 'alert';
                $button_state['classes'] = 'alert';
                $button_state['text'] = __( 'Reintentar Factura', 'freefactu' );
                $button_state['tooltip'] = __( 'Factura con errores (pedido eliminado)', 'freefactu' );
            } elseif ( $status === 'alert_client' ) {
                // Client validation error - always retryable
                $button_state['state'] = 'alert';
                $button_state['classes'] = 'alert retry-client';
                $button_state['text'] = __( 'Reintentar', 'freefactu' );
                $button_state['tooltip'] = __( 'Error de validación (pedido eliminado)', 'freefactu' );
            }
        }

        // Determine CSS classes
        $classes = array( 'freefactu-invoice-action' );
        if ( ! empty( $button_state['classes'] ) ) {
            $classes[] = $button_state['classes'];
        }

        // Build data attributes
        $error_category = isset( $invoice->error_category ) ? $invoice->error_category : '';
        $error_message = isset( $invoice->error_message ) ? $invoice->error_message : '';
        
        $data_attrs = sprintf(
            'data-order-id="%d" data-status="%s" data-context="%s" data-error-category="%s" data-error-message="%s"',
            esc_attr( $order_id ),
            esc_attr( $status ),
            esc_attr( $context ),
            esc_attr( $error_category ),
            esc_attr( $error_message )
        );

        // Get PDF icon URL
        $pdf_icon_url = FREEFACTU_PLUGIN_URL . 'assets/images/freefactu-invoice.svg';

        // Build status badge SVG if needed
        $badge_svg = '';
        if ( $button_state['state'] === 'alert' || $button_state['state'] === 'subsanar' ) {
            $badge_svg = self::get_alert_badge_svg();
        } elseif ( $button_state['state'] === 'success' ) {
            $badge_svg = self::get_success_badge_svg();
        }

        // Build complete HTML structure
        $html = sprintf(
            '<a href="#" class="tips %s" %s data-tip="%s">
                <span class="freefactu-icon-wrapper">
                    <img src="%s" class="freefactu-pdf-icon" alt="PDF" />
                    %s
                </span>
                <span class="freefactu-icon-text">%s</span>
            </a>',
            esc_attr( implode( ' ', $classes ) ),
            $data_attrs,
            esc_attr( $button_state['tooltip'] ),
            esc_url( $pdf_icon_url ),
            $badge_svg,
            esc_html( $button_state['text'] )
        );

        return $html;
    }

    /**
     * Get success badge SVG (green checkmark).
     *
     * @return string SVG markup for success badge.
     */
    public static function get_success_badge_svg() {
        return '<svg class="freefactu-status-badge freefactu-badge-success" width="12" height="12" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
            <circle cx="12" cy="12" r="11" fill="#46b450"/>
            <path d="M9 12l2 2 4-4" stroke="#fff" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
        </svg>';
    }

    /**
     * Get alert badge SVG (yellow warning triangle).
     *
     * @return string SVG markup for alert badge.
     */
    private static function get_alert_badge_svg() {
        return '<svg class="freefactu-status-badge freefactu-badge-alert" width="12" height="12" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
            <path d="M1 21h22L12 2 1 21z" fill="#ffb900"/>
            <path d="M13 18h-2v-2h2v2zm0-4h-2v-4h2v4z" fill="#fff"/>
        </svg>';
    }

    /**
     * Determine button state from invoice database data.
     *
     * Centralizes logic for determining button appearance and behavior
     * based on invoice status. Used by both order list and meta box.
     *
     * @param WC_Order|null $order The order object.
     * @return array {
     *     @type bool   $has_invoice  Whether invoice exists
     *     @type string $state        Button state: 'no_invoice', 'success', or 'alert'
     *     @type string $classes      Space-separated CSS classes
     *     @type string $icon         Dashicon class name
     *     @type string $text         Button text
     *     @type string $tooltip      Button tooltip/title
     * }
     */
    public static function get_button_state( $order ) {
        // Default state for invalid order
        $default_state = array(
            'has_invoice' => false,
            'state'       => 'no_invoice',
            'classes'     => '',
            'icon'        => 'dashicons-media-document',
            'text'        => __( 'Crear Factura', 'freefactu' ),
            'tooltip'     => __( 'Crear nueva factura Verifactu', 'freefactu' ),
        );

        // Validate order object
        if ( ! $order || ! $order instanceof WC_Order ) {
            return $default_state;
        }

        // Get invoice data from database
        $invoice = Freefactu_DB::get_invoice_by_order_id( $order->get_id() );
        $invoice_number = $invoice ? $invoice->invoice_number : '';
        $status = $invoice ? $invoice->status : '';
        $has_invoice = ! empty( $invoice_number );

        // NOTE: PDF existence is NOT checked here anymore.
        // PDFs can be regenerated on-demand from snapshots. Missing PDFs are
        // only shown as informative in the debug invoice page.

        // Start with default state
        $state = $default_state;
        $state['has_invoice'] = $has_invoice;

        // Determine state based on invoice existence and status
        if ( $has_invoice ) {
            // Normalize empty or invalid status
            $valid_statuses = array( 'success', 'alert_aeat', 'alert_qr', 'alert_client' );
            if ( empty( $status ) || ! in_array( $status, $valid_statuses, true ) ) {
                // Unknown status - log warning and treat as no invoice
                if ( ! empty( $status ) && class_exists( 'Freefactu_Logger', false ) && method_exists( 'Freefactu_Logger', 'log' ) ) {
                    try {
                        Freefactu_Logger::log( 'Unknown invoice status: ' . $status . ' for order ' . $order->get_id(), 'warning' );
                    } catch ( \Exception $e ) {
                        // Silently fail if logging doesn't work (e.g., in tests)
                    }
                }
                $status = '';
            }

            // Set state based on status
            if ( $status === 'success' ) {
                $state['state'] = 'success';
                $state['classes'] = 'exists';
                $state['icon'] = 'dashicons-yes-alt';
                $state['text'] = __( 'Ver Factura', 'freefactu' );
                $state['tooltip'] = __( 'Factura generada correctamente', 'freefactu' );
            } elseif ( $status === 'alert_aeat' ) {
                // Check if error is subsanable
                $error_category = isset( $invoice->error_category ) ? $invoice->error_category : '';
                $aeat_status = isset( $invoice->aeat_final_status ) ? $invoice->aeat_final_status : 'pending';
                $subsanation_count = isset( $invoice->subsanation_count ) ? (int) $invoice->subsanation_count : 0;
                $max_attempts = apply_filters( 'freefactu_max_subsanation_attempts', 3, $invoice );
                
                $is_subsanable = (
                    $aeat_status !== 'accepted' && 
                    $subsanation_count < $max_attempts &&
                    ( empty( $error_category ) || Freefactu_Aeat_Error_Categories::is_subsanable_category( $error_category ) )
                );
                
                if ( $is_subsanable ) {
                    $state['state'] = 'subsanar';
                    $state['classes'] = 'alert subsanable';
                    $state['icon'] = 'dashicons-update';
                    $state['text'] = __( 'Subsanar', 'freefactu' );
                    $state['tooltip'] = sprintf(
                        /* translators: 1: Error category, 2: Current attempt, 3: Max attempts */
                        __( 'Error AEAT: %1$s - Clic para subsanar (intento %2$d/%3$d)', 'freefactu' ),
                        Freefactu_Aeat_Error_Categories::get_label( $error_category ),
                        $subsanation_count + 1,
                        $max_attempts
                    );
                    $state['error_category'] = $error_category;
                    $state['subsanation_count'] = $subsanation_count;
                } else {
                    // Not subsanable - show rectificativa prompt
                    $state['state'] = 'alert';
                    $state['classes'] = 'alert not-subsanable';
                    $state['icon'] = 'dashicons-warning';
                    $state['text'] = __( 'Crear Rectificativa', 'freefactu' );
                    $state['tooltip'] = sprintf(
                        /* translators: %s: Error category */
                        __( 'Error no subsanable (%s). Debe crear una factura rectificativa.', 'freefactu' ),
                        Freefactu_Aeat_Error_Categories::get_label( $error_category )
                    );
                    $state['error_category'] = $error_category;
                }
            } elseif ( $status === 'alert_qr' ) {
                $state['state'] = 'alert';
                $state['classes'] = 'alert';
                $state['icon'] = 'dashicons-warning';
                $state['text'] = __( 'Reintentar Factura', 'freefactu' );
                $state['tooltip'] = __( 'Error al generar factura - clic para reintentar', 'freefactu' );
            } elseif ( $status === 'alert_client' ) {
                // Client validation error - always retryable (unlimited attempts)
                $error_message = isset( $invoice->error_message ) ? $invoice->error_message : '';
                $human_error = class_exists( 'Freefactu_Client_Error_Helper' ) 
                    ? Freefactu_Client_Error_Helper::humanize( $error_message ) 
                    : $error_message;
                
                $state['state'] = 'alert';
                $state['classes'] = 'alert retry-client';
                $state['icon'] = 'dashicons-warning';
                $state['text'] = __( 'Reintentar', 'freefactu' );
                $state['tooltip'] = sprintf(
                    /* translators: %s: Error message */
                    __( 'Error de validación: %s - Clic para reintentar', 'freefactu' ),
                    $human_error
                );
            }
        }

        return $state;
    }



    /**
     * Render the invoice meta box content.
     *
     * Static method that handles the actual rendering logic. Can be called
     * directly for testing or via the instance method for WordPress hooks.
     *
     * @param WC_Order|null $order The order object.
     */
    public static function render_invoice_meta_box_content( $order ) {
        if ( ! $order ) {
            echo '<p>' . esc_html__( 'Pedido no encontrado.', 'freefactu' ) . '</p>';
            return;
        }

        $order_id = $order->get_id();

        // Check for auto-draft status (new unsaved order)
        if ( $order->get_status() === 'auto-draft' ) {
            echo '<div class="freefactu-meta-box-content">';
            
            echo '<p class="freefactu-warning-message" style="color: #d63638; margin-bottom: 12px;">' . 
                 esc_html__( 'Debes guardar el pedido para poder crear una factura Verifactu', 'freefactu' ) . 
                 '</p>';

            echo '<p class="freefactu-meta-box-buttons">';
            echo '<span class="freefactu-metabox-action">';
            
            $pdf_icon_url = FREEFACTU_PLUGIN_URL . 'assets/images/freefactu-invoice.svg';
            
            printf(
                '<a href="#" class="tips freefactu-invoice-action disabled" style="opacity: 0.5; cursor: not-allowed; pointer-events: none;" data-tip="%s">
                    <span class="freefactu-icon-wrapper">
                        <img src="%s" class="freefactu-pdf-icon" alt="PDF" />
                    </span>
                    <span class="freefactu-icon-text">%s</span>
                </a>',
                esc_attr__( 'Guarda el pedido primero', 'freefactu' ),
                esc_url( $pdf_icon_url ),
                esc_html__( 'Crear Factura', 'freefactu' )
            );

            echo '</span>';
            echo '</p>';
            echo '</div>';
            return;
        }

        $button_state = self::get_button_state( $order );

        echo '<div class="freefactu-meta-box-content">';

            if ( $button_state['has_invoice'] ) {
                // Show invoice information
                $invoice = Freefactu_DB::get_invoice_by_order_id( $order_id );
                $invoice_number = $invoice ? $invoice->invoice_number : '';

                echo '<div class="freefactu-invoice-info">';
                echo '<p><strong>' . esc_html__( 'Número de factura:', 'freefactu' ) . '</strong> ' . esc_html( $invoice_number ) . '</p>';
                echo '</div>';

                // Show error details for subsanable AEAT errors
                if ( $invoice && $invoice->status === 'alert_aeat' && $button_state['state'] === 'subsanar' ) {
                    $error_category = isset( $invoice->error_category ) ? $invoice->error_category : '';
                    $error_message = isset( $invoice->error_message ) ? $invoice->error_message : '';
                    $subsanation_count = isset( $invoice->subsanation_count ) ? (int) $invoice->subsanation_count : 0;
                    $max_attempts = apply_filters( 'freefactu_max_subsanation_attempts', 3, $invoice );

                    echo '<div class="freefactu-error-details" style="background: #fff8e5; border: 1px solid #ffb900; border-radius: 4px; padding: 12px; margin-bottom: 12px;">';
                    echo '<p style="margin: 0 0 8px; color: #856404; font-weight: 600;">';
                    echo '<span class="dashicons dashicons-warning" style="color: #ffb900; margin-right: 5px;"></span>';
                    esc_html_e( 'Error subsanable de AEAT', 'freefactu' );
                    echo '</p>';

                    if ( ! empty( $error_category ) ) {
                        echo '<p style="margin: 0 0 8px; color: #856404; font-size: 12px;"><strong>' . esc_html__( 'Categoría:', 'freefactu' ) . '</strong> ' . esc_html( Freefactu_Aeat_Error_Categories::get_label( $error_category ) ) . '</p>';
                    }

                    if ( ! empty( $error_message ) ) {
                        echo '<p style="margin: 0 0 8px; color: #856404; font-size: 12px;"><strong>' . esc_html__( 'Error:', 'freefactu' ) . '</strong> ' . esc_html( $error_message ) . '</p>';
                    }

                    echo '<p style="margin: 0 0 8px; color: #856404; font-size: 12px;">';
                    echo wp_kses_post( sprintf(
                        /* translators: 1: Remaining attempts, 2: Max attempts */
                        __( '<strong>Intentos:</strong> %1$d de %2$d restantes', 'freefactu' ),
                        $max_attempts - $subsanation_count,
                        $max_attempts
                    ) );
                    echo '</p>';

                    echo '<p style="margin: 0 0 8px; color: #856404; font-size: 12px; font-style: italic;">';
                    esc_html_e( 'Corrige los datos del pedido y haz clic en "Subsanar" para reintentar el envío.', 'freefactu' );
                    echo '</p>';

                    // Note: Hash chain input fields are now handled by the fix dialog
                    // which appears when clicking the Subsanar button for hash_chain errors
                    echo '</div>';
                }

                // Show error details for client validation errors
                if ( $invoice && $invoice->status === 'alert_client' ) {
                    $error_message = isset( $invoice->error_message ) ? $invoice->error_message : '';
                    $human_error = Freefactu_Client_Error_Helper::humanize( $error_message );

                    echo '<div class="freefactu-error-details" style="background: #fef0f0; border: 1px solid #d63638; border-radius: 4px; padding: 12px; margin-bottom: 12px;">';
                    echo '<p style="margin: 0 0 8px; color: #8b1c1c; font-weight: 600;">';
                    echo '<span class="dashicons dashicons-warning" style="color: #d63638; margin-right: 5px;"></span>';
                    esc_html_e( 'Error de validación', 'freefactu' );
                    echo '</p>';

                    echo '<p style="margin: 0 0 8px; color: #8b1c1c; font-size: 12px;">' . esc_html( $human_error ) . '</p>';

                    echo '<p style="margin: 0; color: #8b1c1c; font-size: 12px; font-style: italic;">';
                    esc_html_e( 'Corrige los datos del pedido y haz clic en "Reintentar" para volver a intentar.', 'freefactu' );
                    echo '</p>';
                    echo '</div>';
                }

                // Show error details for QR generation errors
                if ( $invoice && $invoice->status === 'alert_qr' ) {
                    $error_message = isset( $invoice->error_message ) ? $invoice->error_message : '';
                    
                    echo '<div class="freefactu-error-details" style="background: #fef0f0; border: 1px solid #d63638; border-radius: 4px; padding: 12px; margin-bottom: 12px;">';
                    echo '<p style="margin: 0 0 8px; color: #8b1c1c; font-weight: 600;">';
                    echo '<span class="dashicons dashicons-warning" style="color: #d63638; margin-right: 5px;"></span>';
                    esc_html_e( 'Error Generando Factura (QR)', 'freefactu' );
                    echo '</p>';

                    if ( ! empty( $error_message ) ) {
                        echo '<p style="margin: 0 0 8px; color: #8b1c1c; font-size: 12px;">' . esc_html( $error_message ) . '</p>';
                    }

                    echo '<p style="margin: 0; color: #8b1c1c; font-size: 12px; font-style: italic;">';
                    esc_html_e( 'La factura fue enviada a AEAT correctamente, pero falló la generación del QR o PDF. Haz clic en "Reintentar Factura" para intentar regenerarlos.', 'freefactu' );
                    echo '</p>';
                    echo '</div>';
                }

                // Show error details for non-subsanable AEAT errors
                if ( $invoice && $invoice->status === 'alert_aeat' && $button_state['state'] === 'alert' ) {
                    $error_category = isset( $invoice->error_category ) ? $invoice->error_category : '';
                    $error_message = isset( $invoice->error_message ) ? $invoice->error_message : '';

                    echo '<div class="freefactu-error-details" style="background: #fef0f0; border: 1px solid #d63638; border-radius: 4px; padding: 12px; margin-bottom: 12px;">';
                    echo '<p style="margin: 0 0 8px; color: #8b1c1c; font-weight: 600;">';
                    echo '<span class="dashicons dashicons-dismiss" style="color: #d63638; margin-right: 5px;"></span>';
                    esc_html_e( 'Error no subsanable de AEAT', 'freefactu' );
                    echo '</p>';

                    if ( ! empty( $error_category ) ) {
                        echo '<p style="margin: 0 0 8px; color: #8b1c1c; font-size: 12px;"><strong>' . esc_html__( 'Categoría:', 'freefactu' ) . '</strong> ' . esc_html( Freefactu_Aeat_Error_Categories::get_label( $error_category ) ) . '</p>';
                    }

                    if ( ! empty( $error_message ) ) {
                        echo '<p style="margin: 0 0 8px; color: #8b1c1c; font-size: 12px;"><strong>' . esc_html__( 'Error:', 'freefactu' ) . '</strong> ' . esc_html( $error_message ) . '</p>';
                    }

                    echo '<p style="margin: 0; color: #8b1c1c; font-size: 12px; font-style: italic;">';
                    esc_html_e( 'Este error no se puede subsanar. Debes crear una factura rectificativa.', 'freefactu' );
                    echo '</p>';
                    echo '</div>';
                }
            }

            echo '<p class="freefactu-meta-box-buttons">';
            echo '<span class="freefactu-metabox-action">';
            // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Function uses esc_attr/esc_url/esc_html for all dynamic values. See docblock in render_verifactu_icon_html().
            echo self::render_verifactu_icon_html( $order, 'order_view' );
            echo '</span>';
            echo '</p>';

        echo '</div>';
    }

    public function add_admin_menu() {
        add_submenu_page(
            'woocommerce',
            __( 'Facturas Freefactu', 'freefactu' ),
            __( 'Facturas Freefactu', 'freefactu' ),
            'manage_verifactu',
            'freefactu',
            array( $this, 'render_main_page' )
        );
    }

    public function register_settings() {
        // General Settings Group
        register_setting( 'freefactu_settings_group', 'freefactu_settings', array( $this, 'sanitize_settings' ) );

        // Numbering Settings Group (shares same option name for now, or split?)
        // If we use same option name 'freefactu_settings', we can register it twice or just once.
        // But settings_fields() outputs hidden fields for the option group.
        // Let's use 'freefactu_settings_group' for general and 'freefactu_numbering_group' for numbering.
        // But both save to 'freefactu_settings' option?
        // register_setting 2nd arg is option_name.
        // So both groups should sanitize and save to same option.
        register_setting( 'freefactu_numbering_group', 'freefactu_settings', array( $this, 'sanitize_settings' ) );

        // General Section
        add_settings_section(
            'freefactu_general_section',
            __( 'Configuración General', 'freefactu' ),
            null,
            'freefactu_settings_page'
        );

        add_settings_field(
            'environment',
            __( 'Entorno', 'freefactu' ),
            array( $this, 'render_environment_field' ),
            'freefactu_settings_page',
            'freefactu_general_section'
        );

        add_settings_field(
            'company_name',
            __( 'Razón Social', 'freefactu' ),
            array( $this, 'render_company_name_field' ),
            'freefactu_settings_page',
            'freefactu_general_section'
        );

        add_settings_field(
            'company_nif',
            __( 'NIF Empresa o Autónomo', 'freefactu' ),
            array( $this, 'render_company_nif_field' ),
            'freefactu_settings_page',
            'freefactu_general_section'
        );

        add_settings_field(
            'company_email',
            __( 'Correo electrónico', 'freefactu' ),
            array( $this, 'render_company_email_field' ),
            'freefactu_settings_page',
            'freefactu_general_section'
        );

        add_settings_field(
            'company_phone',
            __( 'Teléfono de contacto', 'freefactu' ),
            array( $this, 'render_company_phone_field' ),
            'freefactu_settings_page',
            'freefactu_general_section'
        );

        add_settings_field(
            'certificate_file',
            __( 'Certificado Digital (.p12 o .pfx)', 'freefactu' ),
            array( $this, 'render_certificate_upload_field' ),
            'freefactu_settings_page',
            'freefactu_general_section'
        );

        add_settings_field(
            'certificate_password',
            __( 'Contraseña del Certificado', 'freefactu' ),
            array( $this, 'render_password_field' ),
            'freefactu_settings_page',
            'freefactu_general_section'
        );

        add_settings_field(
            'cert_expiry_warning_days',
            __( 'Días de Aviso de Caducidad', 'freefactu' ),
            array( $this, 'render_cert_expiry_warning_days_field' ),
            'freefactu_settings_page',
            'freefactu_general_section'
        );

        add_settings_field(
            'ssl_verify',
            __( 'Verificación SSL', 'freefactu' ),
            array( $this, 'render_ssl_verify_field' ),
            'freefactu_settings_page',
            'freefactu_general_section'
        );

        add_settings_field(
            'clear_qr_cache',
            __( 'Caché de Códigos QR', 'freefactu' ),
            array( $this, 'render_clear_qr_cache_field' ),
            'freefactu_settings_page',
            'freefactu_general_section'
        );

        add_settings_field(
            'enable_nif_field',
            __( 'Añadir campo NIF/CIF', 'freefactu' ),
            array( $this, 'render_enable_nif_field' ),
            'freefactu_settings_page',
            'freefactu_general_section'
        );

        add_settings_field(
            'xsd_validation',
            __( 'Validación XML', 'freefactu' ),
            array( $this, 'render_xsd_validation_field' ),
            'freefactu_settings_page',
            'freefactu_general_section'
        );

        // Numbering Section
        add_settings_section(
            'freefactu_numbering_section',
            __( 'Numeración de Facturas', 'freefactu' ),
            null,
            'freefactu_numbering_page'
        );

        add_settings_field(
            'next_invoice_number',
            __( 'Próximo Número de Factura', 'freefactu' ),
            array( $this, 'render_next_invoice_number_field' ),
            'freefactu_numbering_page',
            'freefactu_numbering_section'
        );

        add_settings_field(
            'invoice_number_prefix',
            __( 'Prefijo', 'freefactu' ),
            array( $this, 'render_invoice_number_prefix_field' ),
            'freefactu_numbering_page',
            'freefactu_numbering_section'
        );

        add_settings_field(
            'invoice_number_suffix',
            __( 'Sufijo', 'freefactu' ),
            array( $this, 'render_invoice_number_suffix_field' ),
            'freefactu_numbering_page',
            'freefactu_numbering_section'
        );

        add_settings_field(
            'invoice_number_padding',
            __( 'Largo num. factura', 'freefactu' ),
            array( $this, 'render_invoice_number_padding_field' ),
            'freefactu_numbering_page',
            'freefactu_numbering_section'
        );

        // Rectificativa Numbering Section
        add_settings_section(
            'freefactu_rectificativa_numbering_section',
            __( 'Numeración de Facturas Rectificativas', 'freefactu' ),
            array( $this, 'render_rectificativa_numbering_section_description' ),
            'freefactu_numbering_page'
        );

        add_settings_field(
            'rectificativa_next_number',
            __( 'Próximo Número Rectificativa', 'freefactu' ),
            array( $this, 'render_rectificativa_next_number_field' ),
            'freefactu_numbering_page',
            'freefactu_rectificativa_numbering_section'
        );

        add_settings_field(
            'rectificativa_prefix',
            __( 'Prefijo Rectificativa', 'freefactu' ),
            array( $this, 'render_rectificativa_prefix_field' ),
            'freefactu_numbering_page',
            'freefactu_rectificativa_numbering_section'
        );

        add_settings_field(
            'rectificativa_suffix',
            __( 'Sufijo Rectificativa', 'freefactu' ),
            array( $this, 'render_rectificativa_suffix_field' ),
            'freefactu_numbering_page',
            'freefactu_rectificativa_numbering_section'
        );

        add_settings_field(
            'rectificativa_padding',
            __( 'Largo num. rectificativa', 'freefactu' ),
            array( $this, 'render_rectificativa_padding_field' ),
            'freefactu_numbering_page',
            'freefactu_rectificativa_numbering_section'
        );
    }

    public function render_main_page() {
        $active_tab = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : 'facturas'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        ?>
        <div class="wrap freefactu-admin-page">
            <h1><?php esc_html_e( 'Freefactu Toolkit', 'freefactu' ); ?></h1>

            <?php
            // Display settings saved message (custom styled, not using WordPress notice classes)
            // phpcs:ignore WordPress.Security.NonceVerification.Recommended
            if ( isset( $_GET['settings-updated'] ) && sanitize_text_field( wp_unslash( $_GET['settings-updated'] ) ) === 'true' ) {
                ?>
                <div class="freefactu-settings-saved-message" style="background: linear-gradient(135deg, #00a32a 0%, #007017 100%); color: #fff; padding: 12px 20px; border-radius: 6px; margin-bottom: 20px; display: flex; align-items: center; gap: 10px; box-shadow: 0 2px 8px rgba(0, 163, 42, 0.25);">
                    <span class="dashicons dashicons-yes-alt" style="font-size: 22px; width: 22px; height: 22px;"></span>
                    <span style="font-weight: 500;"><?php esc_html_e( 'Configuración guardada correctamente.', 'freefactu' ); ?></span>
                </div>
                <?php
            }
            ?>

            <nav class="nav-tab-wrapper" aria-label="<?php esc_attr_e( 'Navegación de secciones', 'freefactu' ); ?>">
                <a href="?page=freefactu&tab=facturas" class="nav-tab <?php echo esc_attr( $active_tab == 'facturas' ? 'nav-tab-active' : '' ); ?>">
                    <span class="dashicons dashicons-media-document"></span>
                    <?php esc_html_e( 'Facturas', 'freefactu' ); ?>
                </a>
                <a href="?page=freefactu&tab=numbering" class="nav-tab <?php echo esc_attr( $active_tab == 'numbering' ? 'nav-tab-active' : '' ); ?>">
                    <span class="dashicons dashicons-editor-ol"></span>
                    <?php esc_html_e( 'Series', 'freefactu' ); ?>
                </a>
                <a href="?page=freefactu&tab=extensions" class="nav-tab <?php echo esc_attr( $active_tab == 'extensions' ? 'nav-tab-active' : '' ); ?>">
                    <span class="dashicons dashicons-admin-plugins"></span>
                    <?php esc_html_e( 'Extras', 'freefactu' ); ?>
                </a>
                <a href="?page=freefactu&tab=settings" class="nav-tab <?php echo esc_attr( $active_tab == 'settings' ? 'nav-tab-active' : '' ); ?>">
                    <span class="dashicons dashicons-admin-settings"></span>
                    <?php esc_html_e( 'Ajustes', 'freefactu' ); ?>
                </a>
                <a href="?page=freefactu&tab=sif" class="nav-tab <?php echo esc_attr( $active_tab == 'sif' ? 'nav-tab-active' : '' ); ?>">
                    <span class="dashicons dashicons-shield"></span>
                    <?php esc_html_e( 'Info SIF', 'freefactu' ); ?>
                </a>
                <a href="?page=freefactu&tab=debug" class="nav-tab <?php echo esc_attr( $active_tab == 'debug' ? 'nav-tab-active' : '' ); ?>">
                    <span class="dashicons dashicons-admin-tools"></span>
                    <?php esc_html_e( 'Logs', 'freefactu' ); ?>
                </a>
                <a href="https://t.me/freefactu" target="_blank" rel="noopener noreferrer" class="freefactu-telegram-link" style="display: inline-flex; align-items: center; gap: 5px; padding: 6px 12px; margin-left: auto; color: #0088cc; text-decoration: none; font-size: 13px; font-weight: 500; border: none; background: none; align-self: center; opacity: 0.85; transition: opacity 0.2s;">
                    <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="flex-shrink: 0;"><path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/></svg>
                    <?php esc_html_e( 'Sugerencias / Bugs', 'freefactu' ); ?>
                    <span class="dashicons dashicons-external" style="font-size: 12px; width: 12px; height: 12px; line-height: 12px;"></span>
                </a>
            </nav>

            <?php $this->render_certificate_warning(); ?>

            <div class="tab-content">
                <?php
                try {
                    switch ( $active_tab ) {
                        case 'facturas':
                            if ( class_exists( 'Freefactu_History' ) ) {
                                Freefactu_History::render_history_page();
                            } else {
                                echo '<p>' . esc_html__( 'Error: Freefactu_History class not found.', 'freefactu' ) . '</p>';
                            }
                            break;
                        case 'numbering':
                            $this->render_numbering_tab();
                            break;
                        case 'settings':
                            $this->render_settings_tab();
                            break;
                        case 'debug':
                            if ( class_exists( 'Freefactu_Debug' ) ) {
                                Freefactu_Debug::render_debug_page();
                            } else {
                                echo '<p>' . esc_html__( 'Error: Freefactu_Debug class not found.', 'freefactu' ) . '</p>';
                            }
                            break;
                        case 'sif':
                            $this->render_sif_tab();
                            break;
                        case 'extensions':
                            if ( class_exists( 'Freefactu_Extensions' ) ) {
                                Freefactu_Extensions::render_extensions_page();
                            } else {
                                echo '<p>' . esc_html__( 'Error: Freefactu_Extensions class not found.', 'freefactu' ) . '</p>';
                            }
                            break;
                        default:
                            if ( class_exists( 'Freefactu_History' ) ) {
                                Freefactu_History::render_history_page();
                            }
                            break;
                    }
                } catch ( \Throwable $e ) {
                    echo '<div class="notice notice-error"><p>Critical Error: ' . esc_html( $e->getMessage() ) . '</p><pre>' . esc_html( $e->getTraceAsString() ) . '</pre></div>';
                }
                ?>
            </div>
        </div>
        <?php
    }

    public function render_settings_tab() {

        // Make admin instance available to the view
        global $freefactu_admin;
        $freefactu_admin = $this;

        // Include the settings page view
        include FREEFACTU_PLUGIN_DIR . 'includes/admin/views/settings-page.php';
    }

    /**
     * Render the SIF Information tab with Declaración Responsable management.
     *
     * This tab displays:
     * - Version certification warning (if current version not certified)
     * - List of existing declarations with PDF/ZIP downloads
     * - Modal workflow for creating new declarations
     *
     * @return void
     */
    public function render_sif_tab() {
        $options = get_option( 'freefactu_settings' );
        $company_name = isset( $options['company_name'] ) ? $options['company_name'] : '';
        $company_nif = isset( $options['company_nif'] ) ? $options['company_nif'] : '';
        
        // Ensure declarations table exists
        if ( ! Freefactu_DB::declarations_table_exists() ) {
            Freefactu_DB::create_declarations_table();
        }
        
        // Check if current version is certified
        $current_version = FREEFACTU_VERSION;
        $is_certified = Freefactu_DB::is_version_certified( $current_version );
        $declarations = Freefactu_DB::get_declarations( 50, 0 );
        $latest_declaration = Freefactu_DB::get_latest_declaration();
        
        // Calculate suggested version for new declaration
        $latest_version = Freefactu_DB::get_latest_declaration_version();
        if ( empty( $latest_version ) ) {
            // First declaration: suggest current plugin version
            $suggested_version = FREEFACTU_VERSION;
        } else {
            // Increment minor version (e.g., 1.0.0 -> 1.1.0, 2.3.5 -> 2.4.0)
            $parts = explode( '.', $latest_version );
            if ( count( $parts ) === 3 ) {
                $parts[1] = intval( $parts[1] ) + 1;  // Increment minor version
                $parts[2] = '0';  // Reset patch to 0
                $suggested_version = implode( '.', $parts );
            } else {
                $suggested_version = FREEFACTU_VERSION;
            }
        }
        
        // Get all existing versions for JavaScript validation
        $existing_versions = Freefactu_DB::get_all_declaration_versions();
        
        // Producer (vendor) data - defaults to company settings if not previously saved
        // These identify the entity that produces/develops the SIF software
        $producer_name = $company_name;
        $producer_nif = $company_nif;
        
        // Include the view file
        include FREEFACTU_PLUGIN_DIR . 'includes/admin/views/sif-page.php';
    }

    /**
     * AJAX handler for saving a new declaration.
     *
     * Uses the Declaration Manager to:
     * - Generate the declaration content if not provided
     * - Create PDF with the declaration text
     * - Generate SIF ZIP archive with plugin, uploads, and database
     *
     * @return void Outputs JSON response and exits.
     */
    public function ajax_save_declaration() {
        // Verify nonce
        if ( ! check_ajax_referer( 'freefactu_save_declaration', 'declaration_nonce', false ) ) {
            wp_send_json_error( __( 'Error de seguridad. Por favor, recarga la página.', 'freefactu' ), 403 );
        }

        // Verify capability
        if ( ! current_user_can( 'manage_verifactu' ) ) {
            wp_send_json_error( __( 'Permiso denegado.', 'freefactu' ), 403 );
        }

        // Sanitize and validate input
        $operator_name = isset( $_POST['operator_name'] ) ? sanitize_text_field( wp_unslash( $_POST['operator_name'] ) ) : '';
        $operator_nif = isset( $_POST['operator_nif'] ) ? sanitize_text_field( wp_unslash( $_POST['operator_nif'] ) ) : '';
        $software_name = isset( $_POST['software_name'] ) ? sanitize_text_field( wp_unslash( $_POST['software_name'] ) ) : '';
        $software_version = isset( $_POST['software_version'] ) ? sanitize_text_field( wp_unslash( $_POST['software_version'] ) ) : FREEFACTU_VERSION;
        $installation_number = isset( $_POST['installation_number'] ) ? sanitize_text_field( wp_unslash( $_POST['installation_number'] ) ) : '001';
        $declaration_content = isset( $_POST['declaration_content'] ) ? wp_kses_post( wp_unslash( $_POST['declaration_content'] ) ) : '';
        
        // Producer (vendor) data - the entity that produces/develops the SIF
        $producer_name = isset( $_POST['producer_name'] ) ? sanitize_text_field( wp_unslash( $_POST['producer_name'] ) ) : '';
        $producer_nif = isset( $_POST['producer_nif'] ) ? sanitize_text_field( wp_unslash( $_POST['producer_nif'] ) ) : '';
        
        // New AEAT-required fields
        $software_id_code = isset( $_POST['software_id_code'] ) ? sanitize_text_field( wp_unslash( $_POST['software_id_code'] ) ) : 'FF';
        $producer_address = isset( $_POST['producer_address'] ) ? sanitize_text_field( wp_unslash( $_POST['producer_address'] ) ) : '';
        $producer_postal_code = isset( $_POST['producer_postal_code'] ) ? sanitize_text_field( wp_unslash( $_POST['producer_postal_code'] ) ) : '';
        $producer_city = isset( $_POST['producer_city'] ) ? sanitize_text_field( wp_unslash( $_POST['producer_city'] ) ) : '';
        $producer_phone = isset( $_POST['producer_phone'] ) ? sanitize_text_field( wp_unslash( $_POST['producer_phone'] ) ) : '';
        $producer_email = isset( $_POST['producer_email'] ) ? sanitize_email( wp_unslash( $_POST['producer_email'] ) ) : '';
        $producer_website = isset( $_POST['producer_website'] ) ? esc_url_raw( wp_unslash( $_POST['producer_website'] ) ) : '';
        // Declaration place is derived from producer city
        $declaration_place = $producer_city;

        // Validate required fields (operator data comes from Settings)
        if ( empty( $operator_name ) || empty( $operator_nif ) ) {
            wp_send_json_error( __( 'Los datos del operador no están configurados. Por favor, complete los datos de la empresa en la pestaña Configuración.', 'freefactu' ), 400 );
        }

        // Validate producer fields
        if ( empty( $producer_name ) || empty( $producer_nif ) ) {
            wp_send_json_error( __( 'Por favor, complete los datos del productor del SIF (Nombre y NIF).', 'freefactu' ), 400 );
        }

        // Validate software name is provided
        if ( empty( $software_name ) ) {
            wp_send_json_error( __( 'Por favor, introduzca un nombre para el sistema.', 'freefactu' ), 400 );
        }

        // Validate software name doesn't contain 'freefactu'
        if ( stripos( $software_name, 'freefactu' ) !== false ) {
            wp_send_json_error( __( 'El nombre del sistema no puede contener "Freefactu". Por favor, use un nombre personalizado.', 'freefactu' ), 400 );
        }

        // Validate version format (should be X.Y.Z format, e.g., 1.0.0)
        if ( ! preg_match( '/^\d+\.\d+\.\d+$/', $software_version ) ) {
            wp_send_json_error( __( 'El formato de versión no es válido. Use el formato X.Y.Z (ej: 1.0.0, 1.1.0, 2.0.0).', 'freefactu' ), 400 );
        }

        // Check if version already exists (must be unique)
        $existing_versions = Freefactu_DB::get_all_declaration_versions();
        if ( in_array( $software_version, $existing_versions, true ) ) {
            wp_send_json_error( 
                sprintf( 
                    /* translators: %s: Software version */
                    __( 'La versión %s ya existe. Cada declaración debe tener una versión única.', 'freefactu' ), 
                    $software_version 
                ), 
                400 
            );
        }

        // Validate that version is incremental (greater than all existing versions)
        if ( ! empty( $existing_versions ) ) {
            $latest_version = Freefactu_DB::get_latest_declaration_version();
            if ( $latest_version && version_compare( $software_version, $latest_version, '<=' ) ) {
                wp_send_json_error( 
                    sprintf( 
                        /* translators: 1: New version, 2: Latest version */
                        __( 'La versión %1$s debe ser mayor que la última versión registrada (%2$s). Incremente el número de versión.', 'freefactu' ), 
                        $software_version,
                        $latest_version
                    ), 
                    400 
                );
            }
        }

        // Ensure declarations table exists
        if ( ! Freefactu_DB::declarations_table_exists() ) {
            Freefactu_DB::create_declarations_table();
        }

        // Prepare declaration data (including all AEAT-required fields)
        $declaration_data = array(
            'operator_name'        => $operator_name,
            'operator_nif'         => $operator_nif,
            'software_name'        => $software_name,
            'software_version'     => $software_version,
            'software_id_code'     => $software_id_code,
            'installation_number'  => $installation_number,
            'producer_name'        => $producer_name,
            'producer_nif'         => $producer_nif,
            'producer_address'     => $producer_address,
            'producer_postal_code' => $producer_postal_code,
            'producer_city'        => $producer_city,
            'producer_phone'       => $producer_phone,
            'producer_email'       => $producer_email,
            'producer_website'     => $producer_website,
            'declaration_place'    => $declaration_place,
        );

        // Generate content if not provided (when using the wizard's first step only)
        if ( empty( $declaration_content ) ) {
            $declaration_content = Freefactu_Declaration_Manager::generate_declaration_content( $declaration_data );
        }
        $declaration_data['declaration_content'] = $declaration_content;

        // Use Declaration Manager to create declaration with PDF and ZIP
        $result = Freefactu_Declaration_Manager::create_declaration( $declaration_data );

        if ( is_wp_error( $result ) ) {
            wp_send_json_error( $result->get_error_message(), 500 );
        }

        wp_send_json_success( array(
            'message' => __( 'Declaración creada correctamente. Se ha generado el PDF y el archivo SIF.', 'freefactu' ),
            'id'      => $result,
        ) );
    }

    /**
     * AJAX handler for getting a declaration by ID.
     *
     * @return void Outputs JSON response and exits.
     */
    public function ajax_get_declaration() {
        // Verify nonce for security.
        if ( ! check_ajax_referer( 'freefactu_ajax_nonce', 'nonce', false ) ) {
            wp_send_json_error( __( 'Error de seguridad. Por favor, recarga la página.', 'freefactu' ), 403 );
        }

        // Verify capability.
        if ( ! current_user_can( 'manage_verifactu' ) ) {
            wp_send_json_error( __( 'Permiso denegado.', 'freefactu' ), 403 );
        }

        $id = isset( $_GET['id'] ) ? absint( $_GET['id'] ) : 0;

        if ( ! $id ) {
            wp_send_json_error( __( 'ID de declaración no válido.', 'freefactu' ), 400 );
        }

        $declaration = Freefactu_DB::get_declaration_by_id( $id );

        if ( ! $declaration ) {
            wp_send_json_error( __( 'Declaración no encontrada.', 'freefactu' ), 404 );
        }

        // Build HTML content for display
        $html = '<div style="background: linear-gradient(135deg, #f0f7ff 0%, #e8f4fd 100%); border-left: 4px solid #2271b1; padding: 20px; margin-bottom: 20px; border-radius: 0 4px 4px 0;">';
        $html .= '<table style="width: 100%; border-collapse: collapse; margin-bottom: 15px;">';
        $html .= '<tr><td style="padding: 8px 0; font-weight: 600; width: 180px;">' . esc_html__( 'Fecha:', 'freefactu' ) . '</td>';
        $html .= '<td style="padding: 8px 0;">' . esc_html( date_i18n( get_option( 'date_format' ) . ' H:i', strtotime( $declaration->declaration_date ) ) ) . '</td></tr>';
        $html .= '<tr><td style="padding: 8px 0; font-weight: 600;">' . esc_html__( 'Operador:', 'freefactu' ) . '</td>';
        $html .= '<td style="padding: 8px 0;">' . esc_html( $declaration->operator_name ) . ' (' . esc_html( $declaration->operator_nif ) . ')</td></tr>';
        $html .= '<tr><td style="padding: 8px 0; font-weight: 600;">' . esc_html__( 'Software:', 'freefactu' ) . '</td>';
        $html .= '<td style="padding: 8px 0;">' . esc_html( $declaration->software_name ) . ' v' . esc_html( $declaration->software_version ) . '</td></tr>';
        $html .= '<tr><td style="padding: 8px 0; font-weight: 600;">' . esc_html__( 'Nº Instalación:', 'freefactu' ) . '</td>';
        $html .= '<td style="padding: 8px 0;"><code>' . esc_html( $declaration->installation_number ) . '</code></td></tr>';
        $html .= '<tr><td style="padding: 8px 0; font-weight: 600;">' . esc_html__( 'Productor:', 'freefactu' ) . '</td>';
        $html .= '<td style="padding: 8px 0;">' . esc_html( $declaration->producer_name ) . ' (' . esc_html( $declaration->producer_nif ) . ')</td></tr>';
        $html .= '</table>';
        $html .= '</div>';
        
        $html .= '<div style="background: #fff; border: 1px solid #ddd; padding: 20px; border-radius: 4px;">';
        $html .= '<h3 style="margin-top: 0;">' . esc_html__( 'Contenido de la Declaración', 'freefactu' ) . '</h3>';
        $html .= '<div style="font-family: monospace; font-size: 13px; line-height: 1.6; white-space: pre-wrap; background: #f9f9f9; padding: 15px; border-radius: 4px;">';
        $html .= esc_html( $declaration->declaration_content );
        $html .= '</div>';
        $html .= '</div>';

        wp_send_json_success( array(
            'html' => $html,
            'declaration' => array(
                'id' => $declaration->id,
                'date' => $declaration->declaration_date,
                'operator_name' => $declaration->operator_name,
                'operator_nif' => $declaration->operator_nif,
                'software_version' => $declaration->software_version,
            ),
        ) );
    }

    /**
     * Handle PDF download request for a declaration.
     *
     * @return void Outputs PDF file and exits.
     */
    public function handle_download_declaration_pdf() {
        // Verify capability
        if ( ! current_user_can( 'manage_verifactu' ) ) {
            wp_die( esc_html( __( 'Permiso denegado.', 'freefactu' ) ) );
        }

        $id = isset( $_GET['id'] ) ? absint( $_GET['id'] ) : 0;
        
        // Verify nonce
        if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'freefactu_download_declaration_' . $id ) ) {
            wp_die( esc_html__( 'Error de seguridad.', 'freefactu' ) );
        }

        if ( ! $id ) {
            wp_die( esc_html( __( 'ID de declaración no válido.', 'freefactu' ) ) );
        }

        $declaration = Freefactu_DB::get_declaration_by_id( $id );

        if ( ! $declaration ) {
            wp_die( esc_html( __( 'Declaración no encontrada.', 'freefactu' ) ) );
        }

        if ( empty( $declaration->pdf_path ) ) {
            wp_die( esc_html( __( 'El PDF de esta declaración no está disponible.', 'freefactu' ) ) );
        }

        // Get absolute path
        $upload_dir = wp_upload_dir();
        $pdf_path = $upload_dir['basedir'] . '/' . $declaration->pdf_path;

        if ( ! file_exists( $pdf_path ) ) {
            wp_die( esc_html__( 'El archivo PDF no existe.', 'freefactu' ) );
        }

        // Stream the PDF file
        header( 'Content-Type: application/pdf' );
        header( 'Content-Disposition: attachment; filename="' . sanitize_file_name( 'declaracion-responsable-' . $declaration->id . '.pdf' ) . '"' );
        header( 'Content-Length: ' . filesize( $pdf_path ) );
        header( 'Cache-Control: private, max-age=0, must-revalidate' );
        header( 'Pragma: public' );

        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile
        readfile( $pdf_path );
        exit;
    }

    /**
     * Handle ZIP download request for a declaration's SIF archive.
     *
     * @return void Outputs ZIP file and exits.
     */
    public function handle_download_declaration_zip() {
        // Verify capability
        if ( ! current_user_can( 'manage_verifactu' ) ) {
            wp_die( esc_html( __( 'Permiso denegado.', 'freefactu' ) ) );
        }

        $id = isset( $_GET['id'] ) ? absint( $_GET['id'] ) : 0;
        
        // Verify nonce
        if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'freefactu_download_declaration_' . $id ) ) {
            wp_die( esc_html__( 'Error de seguridad.', 'freefactu' ) );
        }

        if ( ! $id ) {
            wp_die( esc_html( __( 'ID de declaración no válido.', 'freefactu' ) ) );
        }

        $declaration = Freefactu_DB::get_declaration_by_id( $id );

        if ( ! $declaration ) {
            wp_die( esc_html( __( 'Declaración no encontrada.', 'freefactu' ) ) );
        }

        if ( empty( $declaration->zip_path ) ) {
            wp_die( esc_html( __( 'El archivo SIF de esta declaración no está disponible.', 'freefactu' ) ) );
        }

        // Get absolute path
        $upload_dir = wp_upload_dir();
        $zip_path = $upload_dir['basedir'] . '/' . $declaration->zip_path;

        if ( ! file_exists( $zip_path ) ) {
            wp_die( esc_html( __( 'El archivo SIF no existe.', 'freefactu' ) ) );
        }

        // Stream the ZIP file
        header( 'Content-Type: application/zip' );
        header( 'Content-Disposition: attachment; filename="' . sanitize_file_name( 'sif-' . $declaration->software_version . '-' . $declaration->id . '.zip' ) . '"' );
        header( 'Content-Length: ' . filesize( $zip_path ) );
        header( 'Cache-Control: private, max-age=0, must-revalidate' );
        header( 'Pragma: public' );

        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile
        readfile( $zip_path );
        exit;
    }

    /**
     * AJAX handler to generate declaration content based on operator data.
     *
     * Called when user moves from step 1 to step 2 in the wizard.
     *
     * @return void Outputs JSON response and exits.
     */
    public function ajax_generate_declaration_content() {
        // Verify nonce
        if ( ! check_ajax_referer( 'freefactu_save_declaration', 'declaration_nonce', false ) ) {
            wp_send_json_error( __( 'Error de seguridad.', 'freefactu' ), 403 );
        }

        // Verify capability
        if ( ! current_user_can( 'manage_verifactu' ) ) {
            wp_send_json_error( __( 'Permiso denegado.', 'freefactu' ), 403 );
        }

        // Get operator data from request
        $operator_name = isset( $_POST['operator_name'] ) ? sanitize_text_field( wp_unslash( $_POST['operator_name'] ) ) : '';
        $operator_nif = isset( $_POST['operator_nif'] ) ? sanitize_text_field( wp_unslash( $_POST['operator_nif'] ) ) : '';
        $software_name = isset( $_POST['software_name'] ) ? sanitize_text_field( wp_unslash( $_POST['software_name'] ) ) : 'Freefactu';
        $software_version = isset( $_POST['software_version'] ) ? sanitize_text_field( wp_unslash( $_POST['software_version'] ) ) : FREEFACTU_VERSION;
        $software_id_code = isset( $_POST['software_id_code'] ) ? sanitize_text_field( wp_unslash( $_POST['software_id_code'] ) ) : 'FF';
        $installation_number = isset( $_POST['installation_number'] ) ? sanitize_text_field( wp_unslash( $_POST['installation_number'] ) ) : '001';
        
        // Producer (vendor) data
        $producer_name = isset( $_POST['producer_name'] ) ? sanitize_text_field( wp_unslash( $_POST['producer_name'] ) ) : '';
        $producer_nif = isset( $_POST['producer_nif'] ) ? sanitize_text_field( wp_unslash( $_POST['producer_nif'] ) ) : '';
        $producer_address = isset( $_POST['producer_address'] ) ? sanitize_text_field( wp_unslash( $_POST['producer_address'] ) ) : '';
        $producer_postal_code = isset( $_POST['producer_postal_code'] ) ? sanitize_text_field( wp_unslash( $_POST['producer_postal_code'] ) ) : '';
        $producer_city = isset( $_POST['producer_city'] ) ? sanitize_text_field( wp_unslash( $_POST['producer_city'] ) ) : '';
        $producer_phone = isset( $_POST['producer_phone'] ) ? sanitize_text_field( wp_unslash( $_POST['producer_phone'] ) ) : '';
        $producer_email = isset( $_POST['producer_email'] ) ? sanitize_email( wp_unslash( $_POST['producer_email'] ) ) : '';
        $producer_website = isset( $_POST['producer_website'] ) ? esc_url_raw( wp_unslash( $_POST['producer_website'] ) ) : '';
        // Declaration place is derived from producer city
        $declaration_place = $producer_city;

        // Validate required fields (operator data comes from Settings)
        if ( empty( $operator_name ) || empty( $operator_nif ) ) {
            wp_send_json_error( __( 'Los datos del operador no están configurados. Por favor, complete los datos de la empresa en la pestaña Configuración.', 'freefactu' ), 400 );
        }

        // Generate declaration content with all AEAT-required fields
        $content = Freefactu_Declaration_Manager::generate_declaration_content( array(
            'operator_name'        => $operator_name,
            'operator_nif'         => $operator_nif,
            'software_name'        => $software_name,
            'software_version'     => $software_version,
            'software_id_code'     => $software_id_code,
            'installation_number'  => $installation_number,
            'producer_name'        => ! empty( $producer_name ) ? $producer_name : $operator_name,
            'producer_nif'         => ! empty( $producer_nif ) ? $producer_nif : $operator_nif,
            'producer_address'     => $producer_address,
            'producer_postal_code' => $producer_postal_code,
            'producer_city'        => $producer_city,
            'producer_phone'       => $producer_phone,
            'producer_email'       => $producer_email,
            'producer_website'     => $producer_website,
            'declaration_place'    => $declaration_place,
        ) );

        wp_send_json_success( array(
            'content' => $content,
        ) );
    }



    public function render_numbering_tab() {
        // Ensure series table exists and migrate if needed
        if ( ! Freefactu_Series_Store::table_exists() ) {
            Freefactu_Series_Store::create_table();
            Freefactu_Series_Store::migrate_from_legacy();
        }

        $normal_series = Freefactu_Series_Store::get_series_by_type( 'normal' );
        $rectificativa_series = Freefactu_Series_Store::get_series_by_type( 'rectificativa' );
        ?>
        <div class="freefactu-series-container" style="max-width: 1000px;">
            <!-- Normal Invoices Section -->
            <div class="freefactu-series-section" style="background: #fff; border: 1px solid #c3c4c7; border-radius: 4px; padding: 20px; margin-bottom: 20px;">
                <h2 style="margin-top: 0; border-bottom: 1px solid #eee; padding-bottom: 10px; display: flex; justify-content: space-between; align-items: center;">
                    <span><?php esc_html_e( 'Facturas de venta', 'freefactu' ); ?></span>
                    <button type="button" class="button button-primary freefactu-new-series-btn" data-type="normal">
                        <span class="dashicons dashicons-plus-alt2"></span>
                        <?php esc_html_e( 'Nueva Serie', 'freefactu' ); ?>
                    </button>
                </h2>
                
                <?php if ( empty( $normal_series ) ) : ?>
                <div style="background: #f6f7f7; border-radius: 4px; padding: 30px; text-align: center;">
                    <p style="margin: 0; color: #646970;">
                        <?php esc_html_e( 'No hay series de facturas. Crea una nueva serie para empezar a emitir facturas.', 'freefactu' ); ?>
                    </p>
                </div>
                <?php else : ?>
                <div class="freefactu-series-list" data-type="normal">
                    <?php foreach ( $normal_series as $series ) : 
                        $preview = Freefactu_Series_Store::get_preview( $series );
                        $invoice_count = Freefactu_Series_Store::count_invoices_for_series( $series->id );
                    ?>
                    <div class="freefactu-series-item <?php echo esc_attr( $series->is_active ? 'active' : '' ); ?>" 
                         style="display: flex; align-items: center; padding: 15px; border: 1px solid <?php echo esc_attr( $series->is_active ? '#2271b1' : '#dcdcde' ); ?>; border-radius: 4px; margin-bottom: 10px; background: <?php echo esc_attr( $series->is_active ? '#f0f6fc' : '#fff' ); ?>;">
                        <label style="display: flex; align-items: center; flex: 1; cursor: pointer;">
                            <input type="radio" name="freefactu_active_series_normal" value="<?php echo esc_attr( $series->id ); ?>" 
                                   <?php checked( $series->is_active, 1 ); ?> class="freefactu-series-radio" data-type="normal"
                                   style="margin-right: 15px; transform: scale(1.3);">
                            <div>
                                <strong style="font-size: 14px;"><?php echo esc_html( $series->name ); ?></strong>
                                <div style="color: #50575e; margin-top: 4px; font-family: 'SF Mono', Monaco, 'Courier New', monospace;">
                                    <?php esc_html_e( 'Próxima:', 'freefactu' ); ?> <code style="background: #f0f0f1; padding: 2px 6px; border-radius: 3px;"><?php echo esc_html( $preview ); ?></code>
                                </div>
                                <?php if ( $invoice_count > 0 ) : ?>
                                <small style="color: #757575; margin-top: 2px; display: block;">
                                    <?php /* translators: %d: Invoice count */
                                    echo esc_html( sprintf( _n( '%d factura emitida', '%d facturas emitidas', $invoice_count, 'freefactu' ), $invoice_count ) ); ?>
                                </small>
                                <?php endif; ?>
                            </div>
                        </label>
                        <?php if ( ! $series->is_active && $invoice_count === 0 ) : ?>
                        <button type="button" class="button freefactu-delete-series-btn" data-series-id="<?php echo esc_attr( $series->id ); ?>" 
                                style="margin-left: 10px;" title="<?php esc_attr_e( 'Eliminar serie', 'freefactu' ); ?>">
                            <span class="dashicons dashicons-trash" style="vertical-align: middle;"></span>
                        </button>
                        <?php elseif ( ! $series->is_active && $invoice_count > 0 ) : ?>
                        <span style="margin-left: 10px; background: #dcdcde; color: #50575e; padding: 4px 10px; border-radius: 3px; font-size: 11px;">
                            <?php esc_html_e( 'Serie usada', 'freefactu' ); ?>
                        </span>
                        <?php endif; ?>
                        <?php if ( $series->is_active ) : ?>
                        <span style="margin-left: 10px; background: #2271b1; color: #fff; padding: 4px 10px; border-radius: 3px; font-size: 11px; text-transform: uppercase;">
                            <?php esc_html_e( 'Activa', 'freefactu' ); ?>
                        </span>
                        <?php endif; ?>
                    </div>
                    <?php endforeach; ?>
                </div>
                <?php endif; ?>
            </div>

            <!-- Rectificativa Section -->
            <div class="freefactu-series-section" style="background: #fff; border: 1px solid #c3c4c7; border-radius: 4px; padding: 20px; margin-bottom: 20px;">
                <h2 style="margin-top: 0; border-bottom: 1px solid #eee; padding-bottom: 10px; display: flex; justify-content: space-between; align-items: center;">
                    <span><?php esc_html_e( 'Facturas rectificativas', 'freefactu' ); ?></span>
                    <button type="button" class="button freefactu-new-series-btn" data-type="rectificativa" style="background: linear-gradient(135deg, #f5af19 0%, #f12711 100%); border: none; color: #fff;">
                        <span class="dashicons dashicons-plus-alt2"></span>
                        <?php esc_html_e( 'Nueva Serie', 'freefactu' ); ?>
                    </button>
                </h2>
                
                <?php if ( empty( $rectificativa_series ) ) : ?>
                <div style="background: #f6f7f7; border-radius: 4px; padding: 30px; text-align: center;">
                    <p style="margin: 0; color: #646970;">
                        <?php esc_html_e( 'No hay series de rectificativas. Crea una nueva serie para emitir facturas rectificativas.', 'freefactu' ); ?>
                    </p>
                </div>
                <?php else : ?>
                <div class="freefactu-series-list" data-type="rectificativa">
                    <?php foreach ( $rectificativa_series as $series ) : 
                        $preview = Freefactu_Series_Store::get_preview( $series );
                        $invoice_count = Freefactu_Series_Store::count_invoices_for_series( $series->id );
                    ?>
                    <div class="freefactu-series-item <?php echo esc_attr( $series->is_active ? 'active' : '' ); ?>" 
                         style="display: flex; align-items: center; padding: 15px; border: 1px solid <?php echo esc_attr( $series->is_active ? '#f5a623' : '#dcdcde' ); ?>; border-radius: 4px; margin-bottom: 10px; background: <?php echo esc_attr( $series->is_active ? '#fff8e5' : '#fff' ); ?>;">
                        <label style="display: flex; align-items: center; flex: 1; cursor: pointer;">
                            <input type="radio" name="freefactu_active_series_rectificativa" value="<?php echo esc_attr( $series->id ); ?>" 
                                   <?php checked( $series->is_active, 1 ); ?> class="freefactu-series-radio" data-type="rectificativa"
                                   style="margin-right: 15px; transform: scale(1.3);">
                            <div>
                                <strong style="font-size: 14px;"><?php echo esc_html( $series->name ); ?></strong>
                                <div style="color: #50575e; margin-top: 4px; font-family: 'SF Mono', Monaco, 'Courier New', monospace;">
                                    <?php esc_html_e( 'Próxima:', 'freefactu' ); ?> <code style="background: #f0f0f1; padding: 2px 6px; border-radius: 3px;"><?php echo esc_html( $preview ); ?></code>
                                </div>
                                <?php if ( $invoice_count > 0 ) : ?>
                                <small style="color: #757575; margin-top: 2px; display: block;">
                                    <?php /* translators: %d: Invoice count */
                                    echo esc_html( sprintf( _n( '%d rectificativa emitida', '%d rectificativas emitidas', $invoice_count, 'freefactu' ), $invoice_count ) ); ?>
                                </small>
                                <?php endif; ?>
                            </div>
                        </label>
                        <?php if ( ! $series->is_active && $invoice_count === 0 ) : ?>
                        <button type="button" class="button freefactu-delete-series-btn" data-series-id="<?php echo esc_attr( $series->id ); ?>" 
                                style="margin-left: 10px;" title="<?php esc_attr_e( 'Eliminar serie', 'freefactu' ); ?>">
                            <span class="dashicons dashicons-trash" style="vertical-align: middle;"></span>
                        </button>
                        <?php elseif ( ! $series->is_active && $invoice_count > 0 ) : ?>
                        <span style="margin-left: 10px; background: #dcdcde; color: #50575e; padding: 4px 10px; border-radius: 3px; font-size: 11px;">
                            <?php esc_html_e( 'Serie usada', 'freefactu' ); ?>
                        </span>
                        <?php endif; ?>
                        <?php if ( $series->is_active ) : ?>
                        <span style="margin-left: 10px; background: #f5a623; color: #fff; padding: 4px 10px; border-radius: 3px; font-size: 11px; text-transform: uppercase;">
                            <?php esc_html_e( 'Activa', 'freefactu' ); ?>
                        </span>
                        <?php endif; ?>
                    </div>
                    <?php endforeach; ?>
                </div>
                <?php endif; ?>
            </div>

            <?php
            /**
             * Hook for extensions to add their own numbering/series sections.
             *
             * Extensions can use this hook to inject their own series management UI
             * into the Numeración tab. This allows for a modular numbering system
             * where extensions can add their own series (e.g., presupuestos, albaranes).
             *
             * @since 1.2.0
             */
            do_action( 'freefactu_numbering_sections' );
            ?>

            <!-- Help Section -->
            <div style="background: #f0f6fc; border: 1px solid #c3c4c7; border-left: 4px solid #2271b1; border-radius: 4px; padding: 15px 20px;">
                <strong style="color: #1d2327;"><span class="dashicons dashicons-info-outline" style="vertical-align: text-bottom;"></span> <?php esc_html_e( 'Información sobre series', 'freefactu' ); ?></strong>
                <ul style="margin: 10px 0 0 20px; color: #50575e;">
                    <li><?php esc_html_e( 'Cada serie tiene su propia numeración independiente.', 'freefactu' ); ?></li>
                    <li><?php esc_html_e( 'Solo la serie activa se usará para nuevas facturas.', 'freefactu' ); ?></li>
                    <li><?php esc_html_e( 'Las series con facturas emitidas no pueden ser eliminadas.', 'freefactu' ); ?></li>
                    <li><?php esc_html_e( 'Marcadores disponibles: [year], [month], [day]', 'freefactu' ); ?></li>
                </ul>
            </div>
        </div>

        <!-- New Series Dialog -->
        <div id="freefactu-series-dialog" style="display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 100000; justify-content: center; align-items: center;">
            <div style="background: #fff; border-radius: 8px; padding: 30px; max-width: 500px; width: 90%; box-shadow: 0 10px 40px rgba(0,0,0,0.2);">
                <h2 style="margin-top: 0; display: flex; justify-content: space-between; align-items: center;">
                    <span id="freefactu-dialog-title"><?php esc_html_e( 'Nueva Serie', 'freefactu' ); ?></span>
                    <button type="button" class="freefactu-close-dialog" style="background: none; border: none; font-size: 24px; cursor: pointer; color: #757575;">&times;</button>
                </h2>
                
                <form id="freefactu-series-form">
                    <input type="hidden" id="freefactu-series-type" name="type" value="normal">
                    
                    <!-- Live Preview (at the top) -->
                    <div id="freefactu-dialog-preview" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 8px; padding: 15px 20px; margin-bottom: 20px;">
                        <div style="color: rgba(255,255,255,0.8); font-size: 11px; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 5px;">
                            <?php esc_html_e( 'Vista previa', 'freefactu' ); ?>
                        </div>
                        <div id="freefactu-dialog-preview-number" style="color: #fff; font-size: 22px; font-weight: 600; font-family: 'SF Mono', Monaco, 'Courier New', monospace; letter-spacing: 1px;">
                            000001
                        </div>
                    </div>
                    
                    <p>
                        <label for="freefactu-series-name"><strong><?php esc_html_e( 'Nombre de la serie', 'freefactu' ); ?></strong></label>
                        <input type="text" id="freefactu-series-name" name="name" class="regular-text" style="width: 100%; margin-top: 5px;" 
                               placeholder="<?php esc_attr_e( 'Ej: FAC 2025', 'freefactu' ); ?>" required>
                    </p>
                    
                    <p>
                        <label for="freefactu-series-prefix"><strong><?php esc_html_e( 'Prefijo', 'freefactu' ); ?></strong></label>
                        <input type="text" id="freefactu-series-prefix" name="prefix" class="regular-text freefactu-series-input" style="width: 100%; margin-top: 5px;" 
                               placeholder="<?php esc_attr_e( 'Ej: FAC-[year]-', 'freefactu' ); ?>">
                        <span class="description"><?php esc_html_e( 'Marcadores: [year], [month], [day]', 'freefactu' ); ?></span>
                    </p>
                    
                    <p>
                        <label for="freefactu-series-suffix"><strong><?php esc_html_e( 'Sufijo', 'freefactu' ); ?></strong></label>
                        <input type="text" id="freefactu-series-suffix" name="suffix" class="regular-text freefactu-series-input" style="width: 100%; margin-top: 5px;" 
                               placeholder="<?php esc_attr_e( 'Ej: -A', 'freefactu' ); ?>">
                    </p>
                    
                    <p>
                        <label for="freefactu-series-padding"><strong><?php esc_html_e( 'Relleno de ceros', 'freefactu' ); ?></strong></label>
                        <input type="number" id="freefactu-series-padding" name="padding" class="small-text freefactu-series-input" 
                               min="0" max="10" value="6" style="margin-top: 5px;">
                        <span class="description"><?php esc_html_e( '(ej: 6 → 000001)', 'freefactu' ); ?></span>
                    </p>
                    
                    <div style="margin-top: 20px; padding-top: 16px; border-top: 1px solid #e0e0e0; display: flex; justify-content: flex-end;">
                        <button type="submit" class="button button-primary">
                            <span class="dashicons dashicons-plus-alt2"></span>
                            <?php esc_html_e( 'Crear Serie', 'freefactu' ); ?>
                        </button>
                    </div>
                </form>
            </div>
        </div>
        <?php
    }

    /**
     * Format invoice number preview with placeholders replaced
     */
    private function format_invoice_number_preview( $prefix, $number, $suffix, $padding ) {
        // Replace date placeholders
        $prefix = str_replace( '[year]', wp_date( 'Y' ), $prefix );
        $prefix = str_replace( '[month]', wp_date( 'm' ), $prefix );
        $prefix = str_replace( '[day]', wp_date( 'd' ), $prefix );

        $suffix = str_replace( '[year]', wp_date( 'Y' ), $suffix );
        $suffix = str_replace( '[month]', wp_date( 'm' ), $suffix );
        $suffix = str_replace( '[day]', wp_date( 'd' ), $suffix );

        // Apply padding
        $formatted_number = $padding > 0 ? str_pad( $number, $padding, '0', STR_PAD_LEFT ) : $number;

        return $prefix . $formatted_number . $suffix;
    }

    // ... (Keep existing helper methods: sanitize_settings, render_fields, etc.) ...

    public function render_password_field() {
        $options = get_option( $this->option_name );
        $has_password = ! empty( $options['certificate_password'] );

        // Never show the actual password - always show empty field
        echo '<input type="password" name="freefactu_settings[certificate_password]" value="" class="regular-text" autocomplete="new-password">';

        if ( $has_password ) {
            echo '<p class="description" style="color: #46b450;"><span class="dashicons dashicons-yes-alt"></span> ' . esc_html__( 'Contraseña guardada. Déjalo en blanco para mantener la contraseña actual.', 'freefactu' ) . '</p>';
        } else {
            echo '<p class="description">' . esc_html( __( 'Introduce la contraseña de tu certificado digital.', 'freefactu' ) ) . '</p>';
        }
    }

    public function render_cert_expiry_warning_days_field() {
        $options = get_option( $this->option_name );
        $value = isset( $options['cert_expiry_warning_days'] ) ? $options['cert_expiry_warning_days'] : self::DEFAULT_CERT_EXPIRY_WARNING_DAYS;
        echo '<input type="number" name="freefactu_settings[cert_expiry_warning_days]" value="' . esc_attr( $value ) . '" class="small-text" min="1">';
        echo '<p class="description">' . esc_html__( 'Mostrar aviso cuando el certificado caduque en estos días.', 'freefactu' ) . '</p>';
    }

    public function render_ssl_verify_field() {
        $options = get_option( $this->option_name );
        $value = isset( $options['ssl_verify'] ) ? $options['ssl_verify'] : true;
        ?>
        <label>
            <input type="checkbox" name="freefactu_settings[ssl_verify]" value="1" <?php checked( $value, true ); ?>>
            <?php esc_html_e( 'Verificar certificados SSL al conectar con AEAT', 'freefactu' ); ?>
        </label>
        <p class="description"><?php esc_html_e( 'Desactivar solo para pruebas. En producción siempre debe estar activado.', 'freefactu' ); ?></p>
        <?php
    }

    public function render_clear_qr_cache_field() {
        global $wpdb;

        // Count QR cache entries
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $cache_count = $wpdb->get_var(
            "SELECT COUNT(*) FROM {$wpdb->options}
             WHERE option_name LIKE '_transient_freefactu_qr%'"
        );

        ?>
        <div id="freefactu-qr-cache-container">
            <button type="button" class="button freefactu-clear-qr-cache">
                <span class="dashicons dashicons-trash"></span>
                <?php esc_html_e( 'Limpiar Caché de QR', 'freefactu' ); ?>
            </button>
            <p class="description">
                <?php
                if ( $cache_count > 0 ) {
                    /* translators: %d: QR cache count */
                    echo esc_html( sprintf( __( 'Hay %d códigos QR en caché. Limpia la caché para regenerar los códigos QR.', 'freefactu' ), intval( $cache_count ) ) );
                } else {
                    esc_html_e( 'No hay códigos QR en caché.', 'freefactu' );
                }
                ?>
            </p>
        </div>
        <?php
    }

    public function render_enable_nif_field() {
        $options = get_option( $this->option_name );
        $value = isset( $options['enable_nif_field'] ) ? $options['enable_nif_field'] : false;
        ?>
        <label>
            <input type="checkbox" name="freefactu_settings[enable_nif_field]" value="1" <?php checked( $value, true ); ?>>
            <?php esc_html_e( 'Añadir campo NIF/CIF/NIE en Checkout y Admin', 'freefactu' ); ?>
        </label>
        <p class="description"><?php esc_html_e( 'Habilita un campo adicional en el formulario de pago y en la edición de pedidos para capturar el NIF del cliente.', 'freefactu' ); ?></p>
        <?php
    }

    public function render_xsd_validation_field() {
        $options = get_option( $this->option_name );
        $value = isset( $options['xsd_validation'] ) ? $options['xsd_validation'] : 'basic';
        ?>
        <select name="freefactu_settings[xsd_validation]">
            <option value="basic" <?php selected( $value, 'basic' ); ?>><?php esc_html_e( 'Básica (estructural)', 'freefactu' ); ?></option>
            <option value="full" <?php selected( $value, 'full' ); ?>><?php esc_html_e( 'Completa (XSD)', 'freefactu' ); ?></option>
            <option value="disabled" <?php selected( $value, 'disabled' ); ?>><?php esc_html_e( 'Deshabilitada', 'freefactu' ); ?></option>
        </select>
        <p class="description">
            <?php esc_html_e( 'Valida el XML antes de enviarlo a AEAT. "Básica" verifica estructura y campos requeridos. "Completa" usa los esquemas XSD oficiales.', 'freefactu' ); ?>
        </p>
        <?php
    }

    public function render_company_name_field() {
		$options = get_option( $this->option_name );
		$value = isset( $options['company_name'] ) ? $options['company_name'] : '';
		echo '<input type="text" name="freefactu_settings[company_name]" value="' . esc_attr( $value ) . '" class="regular-text">';
	}

    public function render_company_nif_field() {
		$options = get_option( $this->option_name );
		$value = isset( $options['company_nif'] ) ? $options['company_nif'] : '';
		echo '<input type="text" name="freefactu_settings[company_nif]" value="' . esc_attr( $value ) . '" class="regular-text">';
	}

    public function render_company_email_field() {
		$options = get_option( $this->option_name );
		$value = isset( $options['company_email'] ) ? $options['company_email'] : '';
		echo '<input type="email" name="freefactu_settings[company_email]" value="' . esc_attr( $value ) . '" class="regular-text">';
	}

    public function render_company_phone_field() {
		$options = get_option( $this->option_name );
		$value = isset( $options['company_phone'] ) ? $options['company_phone'] : '';
		echo '<input type="text" name="freefactu_settings[company_phone]" value="' . esc_attr( $value ) . '" class="regular-text">';
	}

    public function render_environment_field() {
		$options = get_option( $this->option_name );
		$value = isset( $options['environment'] ) ? $options['environment'] : 'test';

		// Get endpoints from Environment classes using Factory
		$environment = Freefactu_Factory::create_environment( $value );
		$soap_endpoint = $environment->get_soap_endpoint();
		$qr_endpoint = $environment->get_qr_base_url();
		?>
        <select name="freefactu_settings[environment]">
            <option value="test" <?php selected( $value, 'test' ); ?>><?php esc_html_e( 'Pruebas', 'freefactu' ); ?></option>
            <option value="production" disabled><?php esc_html_e( 'Producción (pronto)', 'freefactu' ); ?></option>
        </select>
        <p class="description" style="margin-top: 8px;">
            <strong><?php esc_html_e( 'Endpoints actuales:', 'freefactu' ); ?></strong><br>
            <span style="font-size: 11px;">
                SOAP: <code><?php echo esc_html( $soap_endpoint ); ?></code><br>
                QR: <code><?php echo esc_html( $qr_endpoint ); ?></code>
            </span>
        </p>
        <?php
	}

    public function render_certificate_upload_field() {
        $options = get_option( $this->option_name );
        $cert_path = isset( $options['certificate_path'] ) ? $options['certificate_path'] : '';
        $file_exists = ! empty( $cert_path ) && Freefactu_Filesystem::exists( $cert_path );
        $has_cert = ! empty( $cert_path ) && $file_exists;
        
        // ALWAYS log certificate status for debugging persistence issue
        Freefactu_Logger::log( 
            sprintf( 
                'CERT DEBUG [render]: cert_path="%s", file_exists=%s, has_cert=%s, option_keys=%s', 
                $cert_path,
                $file_exists ? 'YES' : 'NO',
                $has_cert ? 'YES' : 'NO',
                is_array( $options ) ? implode( ',', array_keys( $options ) ) : 'NOT_ARRAY'
            ), 
            'debug' 
        );
        
        $expiry_info = $has_cert ? self::get_certificate_expiry( $cert_path, self::decrypt_password( $options['certificate_password'] ?? '' ) ) : null;

        echo '<div id="freefactu-cert-container">';
        $this->render_certificate_status_html( $has_cert, $expiry_info );
        echo '</div>';
    }

    /**
     * Render certificate status HTML (used for initial render and AJAX updates)
     */
    private function render_certificate_status_html( $has_cert, $expiry_info ) {
        $options = get_option( $this->option_name );
        $has_password = ! empty( $options['certificate_password'] );

        if ( $has_cert ) {
            // Determine status styling
            $status_bg = '#f0f9f0';
            $status_border = '#46b450';
            $status_icon = '<span class="dashicons dashicons-yes-alt"></span>';
            $status_color = '#2e7d32';
            $password_warning = false;

            // If certificate exists but we can't read expiry info, password might be wrong
            if ( ! $expiry_info && $has_password ) {
                $status_bg = '#fef7f1';
                $status_border = '#dc3232';
                $status_icon = '<span class="dashicons dashicons-warning"></span>';
                $status_color = '#dc3232';
                $password_warning = true;
            } elseif ( ! $expiry_info && ! $has_password ) {
                $status_bg = '#fff8e5';
                $status_border = '#ffb900';
                $status_icon = '<span class="dashicons dashicons-warning"></span>';
                $status_color = '#996800';
                $password_warning = true;
            } elseif ( $expiry_info ) {
                if ( $expiry_info['is_expired'] ) {
                    $status_bg = '#fef7f1';
                    $status_border = '#dc3232';
                    $status_icon = '<span class="dashicons dashicons-warning"></span>';
                    $status_color = '#dc3232';
                } elseif ( $expiry_info['days_remaining'] <= self::get_cert_expiry_warning_days() ) {
                    $status_bg = '#fff8e5';
                    $status_border = '#ffb900';
                    $status_icon = '<span class="dashicons dashicons-warning"></span>';
                    $status_color = '#996800';
                }
            }
            ?>
            <div class="freefactu-cert-card" style="background: <?php echo esc_attr( $status_bg ); ?>; border: 1px solid <?php echo esc_attr( $status_border ); ?>; border-radius: 4px; padding: 15px; max-width: 450px;">
                <div style="display: flex; align-items: center; margin-bottom: 10px;">
                    <span style="font-size: 18px; margin-right: 8px;"><?php echo wp_kses_post( $status_icon ); ?></span>
                    <strong style="color: <?php echo esc_attr( $status_color ); ?>;"><?php esc_html_e( 'Certificado instalado', 'freefactu' ); ?></strong>
                </div>

                <?php if ( $password_warning ) : ?>
                    <?php if ( ! $has_password ) : ?>
                        <p style="margin: 0 0 12px 0; color: #dc3232;">
                            <span class="dashicons dashicons-warning"></span> <?php esc_html_e( 'Falta la contraseña del certificado. Introdúcela abajo y guarda los cambios.', 'freefactu' ); ?>
                        </p>
                    <?php else : ?>
                        <p style="margin: 0 0 12px 0; color: #dc3232;">
                            <span class="dashicons dashicons-warning"></span> <?php esc_html_e( 'No se puede leer el certificado. Verifica que la contraseña sea correcta.', 'freefactu' ); ?>
                        </p>
                    <?php endif; ?>
                <?php elseif ( $expiry_info ) : ?>
                    <?php if ( $expiry_info['is_expired'] ) : ?>
                        <p style="margin: 0 0 12px 0; color: #dc3232;">
                            <?php /* translators: %s: HTML formatted Expiry date */ ?>
                            <?php echo wp_kses_post( sprintf( __( 'Caducado el: %s', 'freefactu' ), '<strong>' . esc_html( $expiry_info['expiry_date'] ) . '</strong>' ) ); ?>
                        </p>
                    <?php else : ?>
                        <p style="margin: 0 0 12px 0; color: #555;">
                            <?php /* translators: 1: HTML formatted Expiry date, 2: Days remaining */ ?>
                            <?php echo wp_kses_post( sprintf( __( 'Válido hasta: %1$s (%2$d días)', 'freefactu' ), '<strong>' . esc_html( $expiry_info['expiry_date'] ) . '</strong>', intval( $expiry_info['days_remaining'] ) ) ); ?>
                        </p>
                    <?php endif; ?>
                <?php endif; ?>

                <div style="display: flex; gap: 8px; align-items: center; flex-wrap: wrap;">
                    <button type="button" class="button freefactu-remove-cert">
                        <span class="dashicons dashicons-trash"></span>
                        <?php esc_html_e( 'Eliminar', 'freefactu' ); ?>
                    </button>
                    <label class="button freefactu-upload-cert-btn" style="cursor: pointer;">
                        <span class="dashicons dashicons-upload"></span>
                        <?php esc_html_e( 'Reemplazar', 'freefactu' ); ?>
                        <input type="file" class="freefactu-cert-input" accept=".p12,.pfx" style="display: none;">
                    </label>
                </div>
            </div>
            <?php
        } else {
            ?>
            <div class="freefactu-cert-card" style="background: #fff8e5; border: 1px solid #ffb900; border-radius: 4px; padding: 15px; max-width: 450px;">
                <div style="display: flex; align-items: center; margin-bottom: 10px;">
                    <span class="dashicons dashicons-warning" style="font-size: 18px; margin-right: 8px; color: #996800;"></span>
                    <strong style="color: #996800;"><?php esc_html_e( 'No hay certificado instalado', 'freefactu' ); ?></strong>
                </div>
                <p style="margin: 0 0 12px 0; color: #555;"><?php esc_html_e( 'Necesitas un certificado digital para firmar facturas Verifactu.', 'freefactu' ); ?></p>
                <label class="button button-primary freefactu-upload-cert-btn" style="cursor: pointer;">
                    <span class="dashicons dashicons-upload"></span>
                    <?php esc_html_e( 'Subir certificado', 'freefactu' ); ?>
                    <input type="file" class="freefactu-cert-input" accept=".p12,.pfx" style="display: none;">
                </label>
            </div>
            <?php
        }
    }

    /**
     * Render certificate expiration warning if certificate is expiring soon or expired
     */
    private function render_certificate_warning() {
        $options = get_option( $this->option_name );
        $has_cert = ! empty( $options['certificate_path'] ) && Freefactu_Filesystem::exists( $options['certificate_path'] );

        if ( ! $has_cert ) {
            return;
        }

        $expiry_info = self::get_certificate_expiry( $options['certificate_path'], self::decrypt_password( $options['certificate_password'] ?? '' ) );

        if ( ! $expiry_info ) {
            return;
        }

        $expiry_date = $expiry_info['expiry_date'];
        $is_expired = $expiry_info['is_expired'];
        $days_remaining = $expiry_info['days_remaining'];

        // Only show warning if expired or expiring within configured days
        if ( $is_expired ) {
            echo '<div class="notice notice-error" style="margin: 15px 0;">';
            /* translators: %s: Expiry date */
            echo '<p><strong><span class="dashicons dashicons-warning"></span> ' . wp_kses_post( sprintf( __( 'Certificado CADUCADO el: %s. Por favor, sube un nuevo certificado en Configuración.', 'freefactu' ), $expiry_date ) ) . '</strong></p>';
            echo '</div>';
        } elseif ( $days_remaining <= self::get_cert_expiry_warning_days() ) {
            echo '<div class="notice notice-warning" style="margin: 15px 0;">';
            /* translators: 1: Expiry date, 2: Days remaining */
            echo '<p><strong><span class="dashicons dashicons-warning"></span> ' . wp_kses_post( sprintf( __( 'El certificado caduca el: %1$s (%2$d días restantes). Por favor, renueva tu certificado pronto.', 'freefactu' ), $expiry_date, $days_remaining ) ) . ' <a target="_blank" href="https://sede.agenciatributaria.gob.es/Sede/ayuda/consultas-informaticas/firma-digital-sistema-clave-pin-tecnica/renovar-certificado-electronico.html">' . esc_html__( 'Renovar', 'freefactu' ) . '</a></strong></p>';
            echo '</div>';
        }
    }

    /**
     * Get certificate expiration information from a .p12/.pfx file
     *
     * @param string $cert_path Path to the certificate file
     * @param string $password Certificate password
     * @return array|false Array with expiry info or false on failure
     */
    public static function get_certificate_expiry( $cert_path, $password ) {
        if ( ! Freefactu_Filesystem::exists( $cert_path ) || ! function_exists( 'openssl_pkcs12_read' ) ) {
            return false;
        }

        $cert_content = Freefactu_Filesystem::get_contents( $cert_path );
        if ( ! $cert_content ) {
            return false;
        }

        $certs = array();
        if ( ! openssl_pkcs12_read( $cert_content, $certs, $password ) ) {
            return false;
        }

        if ( empty( $certs['cert'] ) ) {
            return false;
        }

        $cert_data = openssl_x509_parse( $certs['cert'] );
        if ( ! $cert_data || ! isset( $cert_data['validTo_time_t'] ) ) {
            return false;
        }

        $expiry_timestamp = $cert_data['validTo_time_t'];
        $now = time();
        $days_remaining = floor( ( $expiry_timestamp - $now ) / 86400 );

        return array(
            'expiry_date'    => date_i18n( get_option( 'date_format' ), $expiry_timestamp ),
            'expiry_timestamp' => $expiry_timestamp,
            'is_expired'     => $expiry_timestamp < $now,
            'days_remaining' => max( 0, $days_remaining ),
        );
    }

    public function render_next_invoice_number_field() {
        $series = Freefactu_Series_Store::get_active_series( 'normal' );
        $next = $series ? $series->next_number : 1;
        echo '<input type="number" id="freefactu_next_number" name="freefactu_settings[next_invoice_number]" value="' . esc_attr( $next ) . '" class="regular-text freefactu-numbering-field" min="1">';
        echo '<p class="description">' . esc_html__( 'El número que se usará para la próxima factura.', 'freefactu' ) . '</p>';
    }

    public function render_invoice_number_prefix_field() {
        $options = get_option( $this->option_name );
        $value = isset( $options['invoice_number_prefix'] ) ? $options['invoice_number_prefix'] : '';
        echo '<input type="text" id="freefactu_prefix" name="freefactu_settings[invoice_number_prefix]" value="' . esc_attr( $value ) . '" class="regular-text freefactu-numbering-field" placeholder="FAC-">';
        echo '<p class="description">' . esc_html__( 'Marcadores permitidos: [year], [month], [day]', 'freefactu' ) . '</p>';
    }

    public function render_invoice_number_suffix_field() {
        $options = get_option( $this->option_name );
        $value = isset( $options['invoice_number_suffix'] ) ? $options['invoice_number_suffix'] : '';
        echo '<input type="text" id="freefactu_suffix" name="freefactu_settings[invoice_number_suffix]" value="' . esc_attr( $value ) . '" class="regular-text freefactu-numbering-field" placeholder="-A">';
        echo '<p class="description">' . esc_html__( 'Marcadores permitidos: [year], [month], [day]', 'freefactu' ) . '</p>';
    }

    public function render_invoice_number_padding_field() {
        $options = get_option( $this->option_name );
        $value = isset( $options['invoice_number_padding'] ) ? $options['invoice_number_padding'] : '';
        echo '<input type="number" id="freefactu_padding" name="freefactu_settings[invoice_number_padding]" value="' . esc_attr( $value ) . '" class="small-text freefactu-numbering-field" min="0" max="10" placeholder="6">';
        echo '<p class="description">' . esc_html__( 'Se rellenará el resto con ceros (ej. 6 para 000001)', 'freefactu' ) . '</p>';
    }

    /**
     * Render description for rectificativa numbering section.
     */
    public function render_rectificativa_numbering_section_description() {
        echo '<p class="description">' . esc_html__( 'Configure la numeración separada para las facturas rectificativas. Esta serie es independiente de las facturas normales.', 'freefactu' ) . '</p>';
        
        // Preview card for rectificativa
        $series = Freefactu_Series_Store::get_active_series( 'rectificativa' );
        $next_number = $series ? $series->next_number : 1;
        $prefix = $series ? $series->prefix : 'R-';
        $suffix = $series ? $series->suffix : '';
        $padding = $series ? absint( $series->padding ) : 0;
        $preview = $this->format_invoice_number_preview( $prefix, $next_number, $suffix, $padding );
        ?>
        <div id="freefactu-rectificativa-preview" style="background: linear-gradient(135deg, #f5af19 0%, #f12711 100%); border-radius: 8px; padding: 20px 25px; margin: 20px 0; max-width: 500px; box-shadow: 0 4px 15px rgba(241, 39, 17, 0.3);">
            <div style="color: rgba(255,255,255,0.8); font-size: 12px; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px;">
                <?php esc_html_e( 'Vista previa de la próxima rectificativa', 'freefactu' ); ?>
            </div>
            <div id="freefactu-rectificativa-preview-number" style="color: #fff; font-size: 28px; font-weight: 600; font-family: 'SF Mono', Monaco, 'Courier New', monospace; letter-spacing: 2px;">
                <?php echo esc_html( $preview ); ?>
            </div>
        </div>
        <?php
    }

    /**
     * Render next rectificativa invoice number field.
     */
    public function render_rectificativa_next_number_field() {
        $series = Freefactu_Series_Store::get_active_series( 'rectificativa' );
        $next = $series ? $series->next_number : 1;
        echo '<input type="number" id="freefactu_rectificativa_next_number" name="freefactu_settings[rectificativa_next_number]" value="' . esc_attr( $next ) . '" class="regular-text freefactu-rectificativa-numbering-field" min="1">';
        echo '<p class="description">' . esc_html__( 'El número que se usará para la próxima factura rectificativa.', 'freefactu' ) . '</p>';
    }

    /**
     * Render rectificativa prefix field.
     */
    public function render_rectificativa_prefix_field() {
        $options = get_option( $this->option_name );
        $value = isset( $options['rectificativa_prefix'] ) ? $options['rectificativa_prefix'] : 'R-';
        echo '<input type="text" id="freefactu_rectificativa_prefix" name="freefactu_settings[rectificativa_prefix]" value="' . esc_attr( $value ) . '" class="regular-text freefactu-rectificativa-numbering-field" placeholder="R-">';
        echo '<p class="description">' . esc_html__( 'Marcadores permitidos: [year], [month], [day]', 'freefactu' ) . '</p>';
    }

    /**
     * Render rectificativa suffix field.
     */
    public function render_rectificativa_suffix_field() {
        $options = get_option( $this->option_name );
        $value = isset( $options['rectificativa_suffix'] ) ? $options['rectificativa_suffix'] : '';
        echo '<input type="text" id="freefactu_rectificativa_suffix" name="freefactu_settings[rectificativa_suffix]" value="' . esc_attr( $value ) . '" class="regular-text freefactu-rectificativa-numbering-field" placeholder="-R">';
        echo '<p class="description">' . esc_html__( 'Marcadores permitidos: [year], [month], [day]', 'freefactu' ) . '</p>';
    }

    /**
     * Render rectificativa padding field.
     */
    public function render_rectificativa_padding_field() {
        $options = get_option( $this->option_name );
        $value = isset( $options['rectificativa_padding'] ) ? $options['rectificativa_padding'] : '';
        echo '<input type="number" id="freefactu_rectificativa_padding" name="freefactu_settings[rectificativa_padding]" value="' . esc_attr( $value ) . '" class="small-text freefactu-rectificativa-numbering-field" min="0" max="10" placeholder="6">';
        echo '<p class="description">' . esc_html__( 'Se rellenará el resto con ceros (ej. 6 para 000001)', 'freefactu' ) . '</p>';
    }

    public function sanitize_settings( $input ) {
        // Explicit nonce verification to satisfy WordPress.org review requirements.
        // The Options API already verifies the nonce via check_admin_referer() before
        // calling this sanitize callback, but we add an explicit check here for clarity
        // and to ensure $_FILES and $_POST access is always protected.
        if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce(
            sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ),
            'freefactu_settings_group-options'
        ) ) {
            // If nonce verification fails, return existing options unchanged.
            // This should not normally happen since WP Options API checks first.
            return get_option( $this->option_name );
        }

        // Capability check for defense in depth.
        if ( ! current_user_can( 'manage_options' ) ) {
            return get_option( $this->option_name );
        }

        // Start with existing options to preserve values from other tabs
        $current_options = get_option( $this->option_name );
        $new_input = is_array( $current_options ) ? $current_options : array();

        // Handle certificate password - only update if a new value is provided
        if ( isset( $input['certificate_password'] ) && ! empty( $input['certificate_password'] ) ) {
            // Encrypt password before storing
            $new_input['certificate_password'] = self::encrypt_password( sanitize_text_field( $input['certificate_password'] ) );
        }
        // If password field exists but is empty, keep existing password (already in $new_input)

        // General settings - only update if present in input
        if ( isset( $input['company_name'] ) ) {
            $new_input['company_name'] = sanitize_text_field( $input['company_name'] );
        }
        if ( isset( $input['company_nif'] ) ) {
            $new_input['company_nif'] = sanitize_text_field( $input['company_nif'] );
        }
        if ( isset( $input['company_email'] ) ) {
            $new_input['company_email'] = sanitize_email( $input['company_email'] );
        }
        if ( isset( $input['company_phone'] ) ) {
            $new_input['company_phone'] = sanitize_text_field( $input['company_phone'] );
        }
        if ( isset( $input['environment'] ) ) {
            // Force test environment during beta phase
            $new_input['environment'] = 'test';
        }
        if ( isset( $input['cert_expiry_warning_days'] ) ) {
            $new_input['cert_expiry_warning_days'] = max( 1, absint( $input['cert_expiry_warning_days'] ) );
        }

        // Determine which settings page is being saved
        // This prevents checkboxes from being reset when saving from a different tab
        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified at top of sanitize_settings() via wp_verify_nonce().
        $is_settings_tab = isset( $_POST['option_page'] ) && sanitize_text_field( wp_unslash( $_POST['option_page'] ) ) === 'freefactu_settings_group';

        // SSL verification - checkbox handling (only update if saving from settings tab)
        if ( $is_settings_tab ) {
            $new_input['ssl_verify'] = ! empty( $input['ssl_verify'] );
        }

        // NIF Field - checkbox handling (only update if saving from settings tab)
        if ( $is_settings_tab ) {
            $new_input['enable_nif_field'] = ! empty( $input['enable_nif_field'] );
        }

        // XSD Validation - select field
        if ( isset( $input['xsd_validation'] ) ) {
            $valid_values = array( 'basic', 'full', 'disabled' );
            $new_input['xsd_validation'] = in_array( $input['xsd_validation'], $valid_values, true ) 
                ? $input['xsd_validation'] 
                : 'basic';
        }

        // Numbering Settings - only update if present in input
        if ( isset( $input['next_invoice_number'] ) ) {
            $new_input['next_invoice_number'] = absint( $input['next_invoice_number'] );
            // Update Series Store for normal invoices
            $series = Freefactu_Series_Store::get_active_series( 'normal' );
            if ( $series ) {
                global $wpdb;
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                $wpdb->update(
                    Freefactu_Series_Store::get_table_name(),
                    array( 'next_number' => $new_input['next_invoice_number'] ),
                    array( 'id' => $series->id ),
                    array( '%d' ),
                    array( '%d' )
                );
            }
        }
        if ( isset( $input['invoice_number_prefix'] ) ) {
            $new_input['invoice_number_prefix'] = sanitize_text_field( $input['invoice_number_prefix'] );
        }
        if ( isset( $input['invoice_number_suffix'] ) ) {
            $new_input['invoice_number_suffix'] = sanitize_text_field( $input['invoice_number_suffix'] );
        }
        if ( isset( $input['invoice_number_padding'] ) ) {
            $new_input['invoice_number_padding'] = absint( $input['invoice_number_padding'] );
        }

        // Rectificativa Numbering Settings
        if ( isset( $input['rectificativa_next_number'] ) ) {
            $new_input['rectificativa_next_number'] = absint( $input['rectificativa_next_number'] );
            // Update Series Store for rectificativa invoices
            $rect_series = Freefactu_Series_Store::get_active_series( 'rectificativa' );
            if ( $rect_series ) {
                global $wpdb;
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                $wpdb->update(
                    Freefactu_Series_Store::get_table_name(),
                    array( 'next_number' => $new_input['rectificativa_next_number'] ),
                    array( 'id' => $rect_series->id ),
                    array( '%d' ),
                    array( '%d' )
                );
            }
        }
        if ( isset( $input['rectificativa_prefix'] ) ) {
            $new_input['rectificativa_prefix'] = sanitize_text_field( $input['rectificativa_prefix'] );
        }
        if ( isset( $input['rectificativa_suffix'] ) ) {
            $new_input['rectificativa_suffix'] = sanitize_text_field( $input['rectificativa_suffix'] );
        }
        if ( isset( $input['rectificativa_padding'] ) ) {
            $new_input['rectificativa_padding'] = absint( $input['rectificativa_padding'] );
        }

        // Handle Certificate Upload
        // Nonce and capability have already been verified at the top of this function.
        if ( ! empty( $_FILES['freefactu_certificate']['name'] ) ) {
            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- File name is sanitized with sanitize_file_name; tmp_name is a PHP-internal path validated by is_uploaded_file().
            $file = $_FILES['freefactu_certificate'];

            // Preserve the PHP-internal tmp_name before any processing.
            // wp_unslash() must NOT be applied to tmp_name — WordPress's wp_magic_quotes()
            // does not add slashes to $_FILES, so wp_unslash() would strip real backslashes
            // from Windows temp paths (e.g., C:\Users\...\Temp\phpXXXX.tmp).
            $raw_tmp_name = $file['tmp_name'];

            // Sanitize user-controlled fields only
            $file['name']     = sanitize_file_name( wp_unslash( $file['name'] ) );
            $file['tmp_name'] = $raw_tmp_name; // Ensure tmp_name is never modified

            // Verify this is a genuine upload
            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- tmp_name is a PHP-internal server path validated by is_uploaded_file().
            if ( ! is_uploaded_file( $file['tmp_name'] ) ) {
                add_settings_error( 'freefactu_settings', 'upload_invalid', __( 'Subida de archivo no válida.', 'freefactu' ) );
            } elseif ( $file['error'] !== UPLOAD_ERR_OK ) {
                // Check for upload errors
                add_settings_error( 'freefactu_settings', 'upload_error', __( 'Error al subir el certificado.', 'freefactu' ) );
            } else {
                // Verify extension
                $file_ext = strtolower( pathinfo( $file['name'], PATHINFO_EXTENSION ) );
                if ( $file_ext !== 'p12' && $file_ext !== 'pfx' ) {
                     add_settings_error( 'freefactu_settings', 'invalid_type', __( 'Solo se permiten archivos .p12 o .pfx.', 'freefactu' ) );
                } else {
                    // Use WordPress's wp_handle_upload() via helper method
                    // This stores files in a protected directory outside the media library
                    $upload_result = self::handle_certificate_upload( $file );

                    if ( is_wp_error( $upload_result ) ) {
                        add_settings_error( 'freefactu_settings', 'move_error', $upload_result->get_error_message() );
                    } else {
                        $new_input['certificate_path'] = $upload_result['file'];
                        add_settings_error( 'freefactu_settings', 'upload_success', __( 'Certificado subido correctamente.', 'freefactu' ), 'updated' );
                    }
                }
            }
        }

        // Preserve certificate_path - check multiple sources in order of priority:
        // 1. Already set above from $_FILES upload
        // 2. Passed directly in $input (from AJAX update_option calls) - with validation
        // 3. From existing options (preserve current value)
        if ( empty( $new_input['certificate_path'] ) ) {
            // Check if certificate_path was passed directly in input (e.g., from AJAX handler)
            if ( ! empty( $input['certificate_path'] ) ) {
                // Validate the path is within the allowed certificate directory
                $upload_dir = wp_upload_dir();
                $allowed_dir = $upload_dir['basedir'] . '/freefactu-certs';
                $input_path = wp_normalize_path( $input['certificate_path'] );
                $allowed_dir_normalized = wp_normalize_path( $allowed_dir );
                
                // Security check: path must be within freefactu-certs directory
                // and have a valid certificate extension
                $is_valid_location = strpos( $input_path, $allowed_dir_normalized ) === 0;
                $is_valid_extension = preg_match( '/\.(p12|pfx)$/i', $input_path );
                
                if ( $is_valid_location && $is_valid_extension ) {
                    $new_input['certificate_path'] = $input['certificate_path'];
                }
            }
            
            // If still empty, preserve from existing options
            if ( empty( $new_input['certificate_path'] ) ) {
                $current_options = get_option( $this->option_name );
                if ( isset( $current_options['certificate_path'] ) ) {
                    $new_input['certificate_path'] = $current_options['certificate_path'];
                }
            }
        }

        // Handle uninstall data deletion options - checkbox handling (only update if saving from settings tab)
        // These checkboxes follow the standard pattern: only present in POST when checked
        if ( $is_settings_tab ) {
            $new_input['delete_invoices_on_uninstall'] = ! empty( $input['delete_invoices_on_uninstall'] );
            $new_input['delete_series_on_uninstall'] = ! empty( $input['delete_series_on_uninstall'] );
            $new_input['delete_declarations_on_uninstall'] = ! empty( $input['delete_declarations_on_uninstall'] );
            $new_input['delete_pdfs_on_uninstall'] = ! empty( $input['delete_pdfs_on_uninstall'] );
            $new_input['delete_certificates_on_uninstall'] = ! empty( $input['delete_certificates_on_uninstall'] );
            $new_input['delete_settings_on_uninstall'] = ! empty( $input['delete_settings_on_uninstall'] );
        }

		return $new_input;
	}



    /**
     * Check if a valid certificate is installed
     *
     * @return bool True if certificate exists, false otherwise
     */
    public static function has_certificate() {
        $options = get_option( 'freefactu_settings' );
        return ! empty( $options['certificate_path'] ) && Freefactu_Filesystem::exists( $options['certificate_path'] );
    }

    /**
     * Get certificate status including expiry information
     *
     * @return array Certificate status with has_certificate and is_expired flags
     */
    public static function get_certificate_status() {
        $options = get_option( 'freefactu_settings' );
        $has_cert = ! empty( $options['certificate_path'] ) && Freefactu_Filesystem::exists( $options['certificate_path'] );
        $is_expired = false;

        if ( $has_cert ) {
            $password = isset( $options['certificate_password'] ) ? self::decrypt_password( $options['certificate_password'] ) : '';
            $expiry_info = self::get_certificate_expiry( $options['certificate_path'], $password );

            if ( $expiry_info && $expiry_info['is_expired'] ) {
                $is_expired = true;
            }
        }

        return array(
            'has_certificate' => $has_cert,
            'is_expired' => $is_expired
        );
    }

    public function enqueue_admin_scripts( $hook ) {
        // Only load on relevant admin pages
        $screen = get_current_screen();
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        $is_freefactu_page = ( isset( $_GET['page'] ) && strpos( sanitize_text_field( wp_unslash( $_GET['page'] ) ), 'freefactu' ) !== false );
        $is_woocommerce_page = $screen && (
            strpos( $screen->id, 'woocommerce' ) !== false ||
            strpos( $screen->id, 'shop_order' ) !== false ||
            strpos( $screen->id, 'edit-shop_order' ) !== false ||
            $screen->post_type === 'shop_order'
        );

        // Only load scripts on Freefactu or WooCommerce pages
        if ( ! $is_freefactu_page && ! $is_woocommerce_page ) {
            return;
        }

        wp_enqueue_style( 'dashicons' );
        wp_enqueue_style( 'freefactu-admin', FREEFACTU_PLUGIN_URL . 'assets/css/freefactu-admin.css', array(), FREEFACTU_VERSION );
        wp_enqueue_script( 'freefactu-admin', FREEFACTU_PLUGIN_URL . 'assets/js/freefactu-admin.js', array( 'jquery' ), FREEFACTU_VERSION, true );

        // Enqueue modal styles and scripts for settings/SIF pages
        if ( $is_freefactu_page ) {
            wp_enqueue_style( 'freefactu-modals', FREEFACTU_PLUGIN_URL . 'assets/css/freefactu-modals.css', array(), FREEFACTU_VERSION );
            wp_enqueue_script( 'freefactu-modals', FREEFACTU_PLUGIN_URL . 'assets/js/freefactu-modals.js', array(), FREEFACTU_VERSION, true );

            // Extensions page script
            // phpcs:ignore WordPress.Security.NonceVerification.Recommended
            $current_tab = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : '';
            if ( $current_tab === 'extensions' ) {
                wp_enqueue_script( 'freefactu-extensions', FREEFACTU_PLUGIN_URL . 'assets/js/freefactu-extensions.js', array( 'jquery' ), FREEFACTU_VERSION, true );
                wp_localize_script( 'freefactu-extensions', 'freefactuExtensions', array(
                    'nonce' => wp_create_nonce( 'freefactu_toggle_extension' ),
                    'i18n'  => array(
                        'errorToggle'     => __( 'Error al cambiar el estado de la extensión.', 'freefactu' ),
                        'errorConnection' => __( 'Error de conexión. Por favor, inténtalo de nuevo.', 'freefactu' ),
                    ),
                ) );
            }
        }

        // Add inline CSS for spinner animation and loading states
        $spinner_url = admin_url( 'images/spinner.gif' );
        wp_add_inline_style( 'dashicons', '
            .freefactu-remove-cert .dashicons.spin,
            .freefactu-upload-cert-btn .dashicons.spin {
                animation: freefactu-spin 1s linear infinite;
            }
            @keyframes freefactu-spin {
                from { transform: rotate(0deg); }
                to { transform: rotate(360deg); }
            }

            /* ============================================================
               UNIFIED ICON SYSTEM - Inline SVG Badge Layout
               Requirements: 6.2, 6.3, 6.4, 7.1, 7.2, 7.3, 7.4, 7.5
               ============================================================ */

            .freefactu-debug-button {
                margin-top: 2px !important;
                height: 30px !important;
            }

            .freefactu-debug-button .dashicons {
                line-height: 1.6 !important;
            }

            /* Base styles for unified icon action link */
            .freefactu-invoice-action {
                display: inline-flex !important;
                align-items: center !important;
                gap: 5px !important;
                text-decoration: none !important;
                cursor: pointer !important;
                position: relative !important;
                vertical-align: middle !important;
                line-height: 1 !important;
                border: #2271b1 solid 1px;
                background: #f6f7f7;
                border-radius: 3px;
                padding: 6px 5px 6.5px;
                margin: 2px 0 2px 4px;
            }

            .freefactu-invoice-action:hover {
                background: #f0f0f1 !important;
                color: #0a4b78 !important;
                border-color: #0a4b78;
            }


            /* Focus state for accessibility */
            .freefactu-invoice-action:focus {
                outline: 2px solid #007cba !important;
                outline-offset: 2px !important;
            }

            /* ============================================================
               ICON WRAPPER - Flexbox Container (Requirement 6.2)
               Uses flexbox for layout with relative positioning for badge
               ============================================================ */
            /*
            .freefactu-icon-wrapper {
                display: inline-flex !important;
                align-items: center !important;
                justify-content: center !important;
                position: relative !important;
                width: 20px !important;
                height: 20px !important;
                flex-shrink: 0 !important;
            }
            */

            /* ============================================================
               PDF ICON - Main Icon Sizing (Requirement 6.2)
               Consistent sizing across all contexts
               ============================================================ */
            .freefactu-pdf-icon {
                width: 16px !important;
                height: 16px !important;
                display: block !important;
                flex-shrink: 0 !important;
                object-fit: contain !important;
            }

            /* ============================================================
               STATUS BADGE - Positioning (Requirement 6.2)
               Absolute positioning within the icon wrapper
               ============================================================ */
            .freefactu-status-badge {
                position: absolute !important;
                bottom: 2px !important;
                left: 12px !important;
                width: 12px !important;
                height: 12px !important;
                z-index: 10 !important;
                flex-shrink: 0 !important;
                pointer-events: none !important;
            }

            /* Success badge specific styling */
            .freefactu-badge-success {
                filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.2)) !important;
            }

            /* Alert badge specific styling */
            .freefactu-badge-alert {
                filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.2)) !important;
            }

            /* ============================================================
               RESPONSIVE BEHAVIOR (Requirement 6.4)
               Ensure icons scale appropriately on different screen sizes
               ============================================================ */
            @media screen and (max-width: 782px) {
                /* Slightly larger touch targets on mobile */
                .freefactu-invoice-action {
                    min-height: 32px !important;
                    min-width: 32px !important;
                    padding: 4px !important;
                }

                .freefactu-icon-wrapper {
                    width: 24px !important;
                    height: 24px !important;
                }

                .freefactu-pdf-icon {
                    width: 20px !important;
                    height: 20px !important;
                }

                .freefactu-status-badge {
                    width: 14px !important;
                    height: 14px !important;
                    bottom: 2px !important;
                    left: 8px !important;
                }
            }

            /* ============================================================
               CONTEXT-SPECIFIC STYLES (Requirement 6.3)
               Visual consistency across order list, order view, invoice list
               ============================================================ */

            /* Loading state - disable pointer events (Requirement 7.3) */
            .freefactu-invoice-action.loading {
                pointer-events: none !important;
                cursor: wait !important;
            }

            /* Loading state - hide PDF icon and show spinner (Requirement 7.2) */
            .freefactu-invoice-action.loading .freefactu-pdf-icon {
                visibility: hidden !important;
            }

            /* Loading state - hide status badge during loading */
            .freefactu-invoice-action.loading .freefactu-status-badge {
                display: none !important;
            }

            /* Loading state - spinner animation (Requirement 7.1, 7.2) */
            .freefactu-invoice-action.loading .freefactu-icon-wrapper::before {
                content: "" !important;
                display: block !important;
                position: absolute !important;
                top: 15px !important;
                left: 13px !important;
                width: 16px !important;
                height: 16px !important;
                margin-top: -8px !important;
                margin-left: -8px !important;
                background-image: url("' . esc_url( $spinner_url ) . '") !important;
                background-repeat: no-repeat !important;
                background-position: center !important;
                background-size: 16px 16px !important;
                z-index: 20 !important;
            }

            /* Icon text styling */
            .freefactu-icon-text {
                font-size: 12px !important;
                white-space: nowrap !important;
            }

        ' );
        // ==========================================================================
        // ONBOARDING DATA - Comprehensive prerequisite checks for invoice creation
        // ==========================================================================
        
        // 1. System Check (PHP version, required extensions)
        $system_check = array(
            'php_version_ok' => version_compare( PHP_VERSION, '8.2', '>=' ),
            'php_version' => PHP_VERSION,
            'openssl_ok' => extension_loaded( 'openssl' ),
            'dom_ok' => extension_loaded( 'dom' ),
            'gd_ok' => extension_loaded( 'gd' ) || extension_loaded( 'imagick' ),
        );
        $system_requirements_met = $system_check['php_version_ok'] && $system_check['openssl_ok'] && $system_check['dom_ok'];
        
        // Generate a fingerprint of current system requirements for tracking changes
        $system_fingerprint = md5( serialize( array(
            'php' => $system_check['php_version_ok'],
            'openssl' => $system_check['openssl_ok'],
            'dom' => $system_check['dom_ok'],
        ) ) );
        
        // Check if user has acknowledged the current system requirements
        $saved_ack = get_option( 'freefactu_system_requirements_ack', array() );
        $system_acknowledged = isset( $saved_ack['acknowledged'] ) 
            && $saved_ack['acknowledged'] === true 
            && isset( $saved_ack['fingerprint'] ) 
            && $saved_ack['fingerprint'] === $system_fingerprint;
        
        // System is only "ok" if requirements are met AND user has acknowledged
        $system_ok = $system_requirements_met && $system_acknowledged;
        
        // Include acknowledgment status and fingerprint in system check data for frontend
        $system_check['requirements_met'] = $system_requirements_met;
        $system_check['acknowledged'] = $system_acknowledged;
        $system_check['fingerprint'] = $system_fingerprint;
        $system_check['needs_acknowledgment'] = $system_requirements_met && ! $system_acknowledged;
        
        // 2. Series Check (need at least one active series for normal and rectificativa)
        $has_normal_series = false;
        $has_rectificativa_series = false;
        if ( class_exists( 'Freefactu_Series_Store' ) ) {
            $normal_series = Freefactu_Series_Store::get_active_series( 'normal' );
            $rectificativa_series = Freefactu_Series_Store::get_active_series( 'rectificativa' );
            $has_normal_series = ! empty( $normal_series );
            $has_rectificativa_series = ! empty( $rectificativa_series );
        }
        $series_ok = $has_normal_series && $has_rectificativa_series;
        
        // 3. Company Data + Certificate Check
        $options = get_option( 'freefactu_settings' );
        $has_company_name = ! empty( $options['company_name'] );
        $has_company_nif = ! empty( $options['company_nif'] );
        $cert_status = self::get_certificate_status();
        $company_ok = $has_company_name && $has_company_nif && $cert_status['has_certificate'] && ! $cert_status['is_expired'];
        
        // 4. Declaration Check
        $has_declaration = false;
        $declaration_mismatch = false;
        if ( class_exists( 'Freefactu_DB' ) ) {
            $latest_declaration = Freefactu_DB::get_latest_declaration();
            $has_declaration = ! empty( $latest_declaration );
            
            // Check for mismatch between current settings and declaration
            if ( $has_declaration ) {
                $current_name = isset( $options['company_name'] ) ? trim( $options['company_name'] ) : '';
                $current_nif = isset( $options['company_nif'] ) ? trim( $options['company_nif'] ) : '';
                
                $decl_name = trim( $latest_declaration->operator_name );
                $decl_nif = trim( $latest_declaration->operator_nif );
                
                // Compare (case insensitive)
                if ( strcasecmp( $current_name, $decl_name ) !== 0 || strcasecmp( $current_nif, $decl_nif ) !== 0 ) {
                    $declaration_mismatch = true;
                }
            }
        }
        
        // Build onboarding status object
        $onboarding = array(
            'all_complete' => $system_ok && $series_ok && $company_ok && $has_declaration && ! $declaration_mismatch,
            'steps' => array(
                array(
                    'id' => 'system',
                    'title' => __( 'Requisitos del Sistema', 'freefactu' ),
                    'complete' => $system_ok,
                    'data' => $system_check,
                    'url' => null,
                    'action' => null,
                ),
                array(
                    'id' => 'series',
                    'title' => __( 'Series de Numeración', 'freefactu' ),
                    'complete' => $series_ok,
                    'data' => array(
                        'has_normal' => $has_normal_series,
                        'has_rectificativa' => $has_rectificativa_series,
                    ),
                    'url' => admin_url( 'admin.php?page=freefactu&tab=numbering' ),
                    'action' => __( 'Configurar Series', 'freefactu' ),
                ),
                array(
                    'id' => 'company',
                    'title' => __( 'Datos de Empresa y Certificado', 'freefactu' ),
                    'complete' => $company_ok,
                    'data' => array(
                        'has_name' => $has_company_name,
                        'has_nif' => $has_company_nif,
                        'has_certificate' => $cert_status['has_certificate'],
                        'is_expired' => $cert_status['is_expired'],
                    ),
                    'url' => admin_url( 'admin.php?page=freefactu&tab=settings' ),
                    'action' => __( 'Ir a Configuración', 'freefactu' ),
                ),
                array(
                    'id' => 'declaration',
                    'title' => __( 'Declaración Responsable', 'freefactu' ),
                    'complete' => $has_declaration && ! $declaration_mismatch,
                    'data' => array(
                        'has_declaration' => $has_declaration,
                        'mismatch' => $declaration_mismatch,
                        'message' => $declaration_mismatch ? __( 'Los datos de la empresa han cambiado. Debes crear una nueva declaración.', 'freefactu' ) : '',
                    ),
                    'url' => admin_url( 'admin.php?page=freefactu&tab=sif' ),
                    'action' => __( 'Crear Declaración', 'freefactu' ),
                ),
            ),
        );

        wp_localize_script( 'freefactu-admin', 'freefactu_params', array(
            'unified_action_nonce' => wp_create_nonce( 'freefactu_unified_invoice_action' ),
            'remove_cert_nonce' => wp_create_nonce( 'freefactu_remove_certificate' ),
            'upload_cert_nonce' => wp_create_nonce( 'freefactu_upload_certificate' ),
            'clear_qr_cache_nonce' => wp_create_nonce( 'freefactu_clear_qr_cache' ),
            'standalone_pdf_nonce' => wp_create_nonce( 'freefactu_standalone_pdf' ),
            'debug_modal_nonce' => wp_create_nonce( 'freefactu_debug_invoice' ),
            // Legacy flags (kept for backward compatibility)
            'has_certificate' => $cert_status['has_certificate'],
            'is_expired' => $cert_status['is_expired'],
            'has_declaration' => $has_declaration,
            // New onboarding data
            'onboarding' => $onboarding,
            'is_standalone' => true,  // Always standalone mode (DOMPDF)
            'settings_url' => admin_url( 'admin.php?page=freefactu&tab=settings' ),
            'sif_url' => admin_url( 'admin.php?page=freefactu&tab=sif' ),
            'numbering_url' => admin_url( 'admin.php?page=freefactu&tab=numbering' ),
            'order_edit_url_base' => admin_url( 'admin.php?page=wc-orders&action=edit&id=' ),
            'no_cert_message' => __( 'No hay certificado instalado. Necesitas instalar un certificado digital para crear facturas Verifactu. ¿Deseas ir a la página de configuración?', 'freefactu' ),
            'expired_cert_message' => __( 'El certificado ha caducado. Necesitas renovar tu certificado digital para crear facturas Verifactu. ¿Deseas ir a la página de configuración?', 'freefactu' ),
            'no_declaration_message' => __( 'No hay una Declaración Responsable creada. Debes crear una declaración en la pestaña SIF antes de poder enviar facturas a AEAT.', 'freefactu' ),
            'rectificativa_nonce' => wp_create_nonce( 'freefactu_rectificativa' ),
            'series_nonce' => wp_create_nonce( 'freefactu_series_nonce' ),
            'ack_system_requirements_nonce' => wp_create_nonce( 'freefactu_ack_system_requirements' ),
            'i18n' => array(
                'uploading' => __( 'Subiendo...', 'freefactu' ),
                'removing' => __( 'Eliminando...', 'freefactu' ),
                'clearing' => __( 'Limpiando...', 'freefactu' ),
                'creating' => __( 'Creando...', 'freefactu' ),
                'upload_success' => __( 'Certificado subido correctamente.', 'freefactu' ),
                'remove_success' => __( 'Certificado eliminado correctamente.', 'freefactu' ),
                'confirm_remove' => __( '¿Estás seguro de que deseas eliminar el certificado?', 'freefactu' ),
                'confirm_clear_qr' => __( '¿Estás seguro de que deseas limpiar la caché de códigos QR? Los códigos se regenerarán automáticamente cuando sea necesario.', 'freefactu' ),
                'confirm_clear_logs' => __( '¿Estás seguro de que deseas borrar los logs?', 'freefactu' ),
                'no_qr_cache' => __( 'No hay códigos QR en caché.', 'freefactu' ),
                'error' => __( 'Error', 'freefactu' ),
                'select_file' => __( 'Selecciona un archivo .p12 o .pfx', 'freefactu' ),
                'confirm_create' => __( '¿Deseas crear una factura Verifactu para este pedido?', 'freefactu' ),
                'confirm_retry' => __( '¿Deseas reintentar la generación de la factura?', 'freefactu' ),
                'connection_error' => __( 'Error de conexión. Por favor, inténtalo de nuevo.', 'freefactu' ),
                'view_invoice' => __( 'Ver Factura', 'freefactu' ),
                'retry_invoice' => __( 'Reintentar Factura', 'freefactu' ),
                'error_title' => __( 'Error al crear factura', 'freefactu' ),
                'close' => __( 'Cerrar', 'freefactu' ),
                // Debug modal strings
                'debug_loading_title' => __( 'Cargando información...', 'freefactu' ),
                'debug_loading_text' => __( 'Cargando información de depuración...', 'freefactu' ),
                // Rectificativa dialog strings
                'invoice_type_title' => __( 'Nueva factura VeriFactu', 'freefactu' ),
                'new_invoice' => __( 'Nueva factura', 'freefactu' ),
                'rectificativa' => __( 'Factura rectificativa', 'freefactu' ),
                'rectificativa_title' => __( 'Crear factura rectificativa', 'freefactu' ),
                'select_original_invoice' => __( 'Seleccionar factura a rectificar', 'freefactu' ),
                'search_invoices' => __( 'Buscar por número o pedido...', 'freefactu' ),
                'tipo_rectificativa' => __( 'Tipo de rectificativa', 'freefactu' ),
                'tipo_r1' => __( 'R1 - Factura rectificativa (error fundado en derecho)', 'freefactu' ),
                'tipo_r2' => __( 'R2 - Factura rectificativa (artículo 80.Uno, Dos y Seis LIVA)', 'freefactu' ),
                'tipo_r3' => __( 'R3 - Factura rectificativa (artículo 80.Tres LIVA)', 'freefactu' ),
                'tipo_r4' => __( 'R4 - Factura rectificativa (artículo 80.Cuatro LIVA)', 'freefactu' ),
                'tipo_r5' => __( 'R5 - Factura rectificativa (otras)', 'freefactu' ),
                'metodo_rectificativa' => __( 'Método de rectificación', 'freefactu' ),
                'metodo_i' => __( 'Por diferencias', 'freefactu' ),
                'metodo_s' => __( 'Por sustitución', 'freefactu' ),
                'motivo' => __( 'Motivo de la rectificación (opcional)', 'freefactu' ),
                'no_invoices_found' => __( 'No se encontraron facturas', 'freefactu' ),
                'loading_invoices' => __( 'Cargando facturas...', 'freefactu' ),
                'confirm_rectificativa' => __( 'ATENCIÓN: Se creará una factura rectificativa que se enviará a VeriFactu. Esta acción no se puede deshacer. ¿Deseas continuar?', 'freefactu' ),
                'send_rectificativa' => __( 'Crear Rectificativa', 'freefactu' ),
                'cancel' => __( 'Cancelar', 'freefactu' ),
                // Error fix dialog strings
                'fix_dialog_title' => __( 'Corregir error', 'freefactu' ),
                'fix_dialog_hash_chain_title' => __( 'Subsanar error de encadenamiento', 'freefactu' ),
                'fix_dialog_client_title' => __( 'Corregir error de validación', 'freefactu' ),
                'fix_dialog_description' => __( 'Para corregir el encadenamiento de facturas, introduce los datos de la última factura registrada en Verifactu.', 'freefactu' ),
                'fix_confirm_subsanar' => __( 'Se aplicarán los datos introducidos y se reenviará a AEAT. ¿Continuar?', 'freefactu' ),
                'fix_confirm_retry' => __( 'Se reintentará el envío con los datos corregidos. ¿Continuar?', 'freefactu' ),
                'fix_submit_subsanar' => __( 'Subsanar', 'freefactu' ),
                'fix_submit_retry' => __( 'Reintentar', 'freefactu' ),
            ),
            // Fix fields configuration for error fix dialog
            'fix_fields' => class_exists( 'Freefactu_Error_Fix_Helper' ) ? Freefactu_Error_Fix_Helper::get_all_fix_fields() : array(),
            'fix_dialog_categories' => class_exists( 'Freefactu_Error_Fix_Helper' ) ? Freefactu_Error_Fix_Helper::get_dialog_required_categories() : array(),
        ) );

        // Add inline CSS for action buttons
        $icon_url = FREEFACTU_PLUGIN_URL . 'assets/images/freefactu-invoice.svg';
            wp_add_inline_style( 'dashicons', '
                /* WooCommerce order list button styling */
                .wc-action-button-freefactu_invoice {
                    background-repeat: no-repeat !important;
                    background-position: center !important;
                    background-size: 16px 16px !important;
                    background-image: url("' . esc_url( $icon_url ) . '") !important;
                    position: relative !important;
                }
                /* Loading state with WooCommerce spinner */
                .wc-action-button-freefactu_invoice.loading {
                    pointer-events: none !important;
                    background-image: url("' . esc_url( admin_url( 'images/spinner.gif' ) ) . '") !important;
                    background-size: 16px 16px !important;
                }

                /* Meta box button styles - matching order list appearance */
                .freefactu-meta-box-buttons {
                    margin-top: 10px !important;
                }
                .freefactu-meta-box-buttons .button {
                    margin-right: 5px !important;
                    margin-bottom: 5px !important;
                    display: inline-flex !important;
                    align-items: center !important;
                    gap: 3px !important;
                }
                .freefactu-meta-box-buttons .dashicons {
                    font-size: 16px !important;
                    width: 16px !important;
                    height: 16px !important;
                    vertical-align: middle !important;
                }

                /* Freefactu actions list */
                .freefactu-actions {
                    margin: 10px 0 0 0 !important;
                    padding: 0 !important;
                    list-style: none !important;
                }
                .freefactu-actions li {
                    margin: 0 0 5px 0 !important;
                    padding: 0 !important;
                }
                .freefactu-actions .button {
                    display: inline-flex !important;
                    align-items: center !important;
                    gap: 5px !important;
                }
                .freefactu-actions .button svg {
                    width: 14px !important;
                    height: 14px !important;
                    fill: #2aad2a !important;
                    vertical-align: middle !important;
                }

                /* Status indicators - matching order list colors */
                .freefactu-status-success {
                    color: #2aad2a !important;
                    display: flex !important;
                    align-items: center !important;
                    gap: 5px !important;
                }
                .freefactu-status-success .dashicons {
                    color: #2aad2a !important;
                }
                .freefactu-status-alert {
                    color: #ffb900 !important;
                    display: flex !important;
                    align-items: center !important;
                    gap: 5px !important;
                }
                .freefactu-status-alert .dashicons {
                    color: #ffb900 !important;
                }

                /* Meta box button with icon and text - matching order list style */
                .freefactu-meta-box-buttons .wc-action-button-freefactu_invoice {
                    display: inline-flex !important;
                    flex-direction: row !important;
                    align-items: center !important;
                    justify-content: flex-start !important;
                    gap: 8px !important;
                    padding-left: 10px !important;
                    padding-right: 10px !important;
                    min-width: auto !important;
                    width: auto !important;
                    height: auto !important;
                    line-height: normal !important;
                    background-image: none !important;
                    background: none !important;
                }
                /* Remove the ::before and ::after from the button itself in metabox */
                .freefactu-meta-box-buttons .wc-action-button-freefactu_invoice::before,
                .freefactu-meta-box-buttons .wc-action-button-freefactu_invoice::after {
                    content: none !important;
                    display: none !important;
                }
                .freefactu-meta-box-buttons .wc-action-button-freefactu_invoice .freefactu-button-icon {
                    display: inline-block !important;
                    width: 20px !important;
                    height: 20px !important;
                    background-repeat: no-repeat !important;
                    background-position: center !important;
                    background-size: 16px 16px !important;
                    background-image: url("' . esc_url( $icon_url ) . '") !important;
                    position: relative !important;
                    flex-shrink: 0 !important;
                }
                /* NOTE: Legacy pseudo-element badge overlays removed per Requirements 1.5, 6.1
                   Status badges are now rendered as inline SVG via render_verifactu_icon_html() */
                .freefactu-meta-box-buttons .wc-action-button-freefactu_invoice .freefactu-button-text {
                    display: inline-block !important;
                    white-space: nowrap !important;
                    background-image: none !important;
                    background: none !important;
                    width: auto !important;
                    height: auto !important;
                    line-height: inherit !important;
                    text-align: left !important;
                    font-size: inherit !important;
                    vertical-align: baseline !important;
                    flex-shrink: 1 !important;
                }
                /* Ensure text element never gets pseudo-elements */
                .freefactu-meta-box-buttons .wc-action-button-freefactu_invoice .freefactu-button-text::before,
                .freefactu-meta-box-buttons .wc-action-button-freefactu_invoice .freefactu-button-text::after {
                    content: none !important;
                    display: none !important;
                }
                .freefactu-meta-box-buttons .wc-action-button-freefactu_invoice.loading .freefactu-button-icon {
                    background-image: url("' . esc_url( admin_url( 'images/spinner.gif' ) ) . '") !important;
                }

                /* Alert button border styling - matching order list */
                .freefactu-meta-box-buttons .freefactu-create-invoice.alert {
                    border-color: #ffb900 !important;
                }
                .freefactu-meta-box-buttons .freefactu-create-invoice.exists {
                    border-color: #2aad2a !important;
                }

                /* Invoice info section */
                .freefactu-invoice-info {
                    margin-bottom: 10px !important;
                }
                .freefactu-invoice-info p {
                    margin: 5px 0 !important;
                }
            ' );
    }

    /**
     * Unified AJAX handler for all Verifactu invoice actions.
     *
     * Handles invoice creation, retry operations, and PDF viewing from all
     * display locations (order list, order view, invoice list) in both
     * standalone and integrated modes.
     *
     * Routes to appropriate operation based on invoice status:
     * - normal: Create new invoice (send to AEAT, generate QR, save PDF)
     * - alert_aeat: Retry AEAT submission
     * - alert_qr: Retry QR generation and PDF
     * - alert_pdf: Retry PDF generation only
     * - success: Return existing PDF URL
     *
     * @return void Outputs JSON response and exits.
     */
    public function handle_unified_invoice_action() {

        // Verify nonce - log and return user-friendly error
        if ( ! check_ajax_referer( 'freefactu_unified_invoice_action', 'security', false ) ) {
            Freefactu_Logger::log( 'Unified handler: Invalid security nonce', 'error' );
            wp_send_json_error( __( 'Enlace de seguridad inválido. Por favor, recarga la página e inténtalo de nuevo.', 'freefactu' ), 403 );
        }

        // Verify user capabilities - log and return user-friendly error
        if ( ! current_user_can( 'manage_woocommerce' ) ) {
            $current_user = wp_get_current_user();
            $user_info = $current_user->ID > 0 ? $current_user->user_login : 'anonymous';
            Freefactu_Logger::log( 'Unified handler: Permission denied for user ' . $user_info, 'error' );
            wp_send_json_error( __( 'No tienes permisos para realizar esta acción. Contacta con el administrador.', 'freefactu' ), 403 );
        }

        // Extract and validate order_id from POST data
        $raw_order_id = isset( $_POST['order_id'] ) ? sanitize_text_field( wp_unslash( $_POST['order_id'] ) ) : null;

        // Check if order_id is missing
        if ( $raw_order_id === null || $raw_order_id === '' ) {
            Freefactu_Logger::log( 'Unified handler: Missing order_id in request', 'error' );
            wp_send_json_error( __( 'Error: No se proporcionó el ID del pedido.', 'freefactu' ) );
        }

        // Sanitize and validate order_id
        $order_id = absint( $raw_order_id );
        if ( $order_id <= 0 ) {
            Freefactu_Logger::log( 'Unified handler: Invalid order_id provided: ' . sanitize_text_field( $raw_order_id ), 'error' );
            wp_send_json_error( __( 'Error: El ID del pedido no es válido.', 'freefactu' ) );
        }

        // Get current invoice status from database first
        $invoice = Freefactu_DB::get_invoice_by_order_id( $order_id );
        $status = $invoice ? $invoice->status : '';
        $invoice_number = $invoice ? $invoice->invoice_number : '';

        // Normalize empty status to 'normal'
        if ( empty( $status ) ) {
            $status = 'normal';
        }

        // Get order object - log detailed error if not found
        $order = wc_get_order( $order_id );
        $order_deleted = ! $order;

        if ( $order_deleted ) {
            Freefactu_Logger::log( 'Unified handler: Order not found with ID ' . $order_id, 'error' );

            // If invoice exists, allow certain operations even with deleted order
            if ( $invoice ) {
                // Allow viewing existing successful invoices
                if ( $status === 'success' ) {
                    $result = $this->unified_open_existing_from_invoice( $invoice );
                    if ( ! is_wp_error( $result ) ) {
                        wp_send_json_success( $result );
                    }
                }

                // Allow retry operations for failed invoices (will use snapshot)
                if ( $status === 'alert_qr' ) {
                    // Continue to retry logic below with null order
                    // The retry methods will handle snapshot-based regeneration
                } else {
                    // For other statuses with deleted orders, cannot proceed
                    wp_send_json_error( __( 'Error: No se encontró el pedido. Es posible que haya sido eliminado.', 'freefactu' ) );
                }
            } else {
                // No invoice and no order - cannot proceed
                wp_send_json_error( __( 'Error: No se encontró el pedido. Es posible que haya sido eliminado.', 'freefactu' ) );
            }
        }

        // Get context for logging
        $context = isset( $_POST['context'] ) ? sanitize_text_field( wp_unslash( $_POST['context'] ) ) : 'unknown';

        // Log the action being performed
        Freefactu_Logger::log( sprintf(
            'Unified handler: Processing order %d with status "%s" from context "%s"',
            $order_id,
            $status,
            $context
        ), 'info' );

        try {
            // Route to appropriate operation based on status
            switch ( $status ) {
                case 'normal':
                    // No invoice exists - create new invoice
                    $result = $this->unified_create_invoice( $order );
                    break;

                case 'alert_aeat':
                    // AEAT submission failed - retry full send
                    $result = $this->unified_retry_aeat( $order );
                    break;

                case 'alert_qr':
                    // QR generation failed - retry QR and PDF
                    $result = $this->unified_retry_qr( $order, $order_id );
                    break;

                case 'success':
                    // Invoice exists and is successful - return existing PDF URL
                    $result = $this->unified_open_existing( $order );
                    break;

                default:
                    // Unknown status - log and treat as normal
                    Freefactu_Logger::log( 'Unknown status in unified handler: ' . $status . ' for order ' . $order_id, 'warning' );
                    $result = $this->unified_create_invoice( $order );
                    break;
            }

            // Send response
            if ( is_wp_error( $result ) ) {
                $error_code = $result->get_error_code();
                $error_message = $result->get_error_message();

                // Log detailed error information
                Freefactu_Logger::log( sprintf(
                    'Unified handler error for order %d: [%s] %s (context: %s, status: %s)',
                    $order_id,
                    $error_code,
                    $error_message,
                    $context,
                    $status
                ), 'error' );

                // Return user-friendly error message based on error code
                $user_message = $this->get_user_friendly_error_message( $error_code, $error_message );
                wp_send_json_error( $user_message );
            } else {
                // Log success
                Freefactu_Logger::log( sprintf(
                    'Unified handler success for order %d: action=%s (context: %s)',
                    $order_id,
                    isset( $result['action_taken'] ) ? $result['action_taken'] : 'unknown',
                    $context
                ), 'info' );

                wp_send_json_success( $result );
            }

        } catch ( \Throwable $e ) {
            // Log detailed exception information
            Freefactu_Logger::log( sprintf(
                'Unified handler exception for order %d: %s in %s:%d (context: %s, status: %s)',
                $order_id,
                $e->getMessage(),
                $e->getFile(),
                $e->getLine(),
                $context,
                $status
            ), 'error' );

            // Return user-friendly error message
            wp_send_json_error( __( 'Ha ocurrido un error inesperado. Por favor, inténtalo de nuevo o contacta con soporte.', 'freefactu' ) );
        }
    }

    /**
     * AJAX handler for subsanation (retry with fresh snapshot).
     *
     * Called when user clicks "Subsanar" button after an AEAT rejection.
     * This differs from normal retry by:
     * 1. Creating a NEW snapshot with current (fixed) order data
     * 2. Sending to AEAT with isCorrection=true
     * 3. Incrementing subsanation_count
     *
     * @return void Outputs JSON response and exits.
     */
    public function ajax_subsanar_invoice() {
        check_ajax_referer( 'freefactu_admin_nonce', 'nonce' );

        if ( ! current_user_can( 'manage_verifactu' ) ) {
            wp_send_json_error( __( 'No tienes permisos para realizar esta acción.', 'freefactu' ) );
        }

        $order_id = isset( $_POST['order_id'] ) ? absint( $_POST['order_id'] ) : 0;

        if ( ! $order_id ) {
            wp_send_json_error( __( 'ID de pedido no válido.', 'freefactu' ) );
        }

        // Validate certificate before proceeding
        $cert_check = $this->validate_certificate_for_operation();
        if ( is_wp_error( $cert_check ) ) {
            wp_send_json_error( $cert_check->get_error_message() );
        }

        // Get existing invoice to verify it's in a subsanable state
        $invoice = Freefactu_DB::get_invoice_by_order_id( $order_id );
        if ( ! $invoice ) {
            wp_send_json_error( __( 'No se encontró una factura para este pedido.', 'freefactu' ) );
        }

        // Check if invoice is in a subsanable state
        if ( $invoice->status === 'success' ) {
            wp_send_json_error( __( 'Esta factura ya fue aceptada. Use una factura rectificativa para correcciones.', 'freefactu' ) );
        }

        // Check AEAT final status
        $aeat_status = isset( $invoice->aeat_final_status ) ? $invoice->aeat_final_status : 'pending';
        if ( $aeat_status === 'accepted' ) {
            wp_send_json_error( __( 'No se puede subsanar una factura ya aceptada por la AEAT.', 'freefactu' ) );
        }

        // Check error category - only subsanable categories allowed
        $error_category = isset( $invoice->error_category ) ? $invoice->error_category : '';
        if ( ! empty( $error_category ) && ! Freefactu_Aeat_Error_Categories::is_subsanable_category( $error_category ) ) {
            wp_send_json_error( 
                sprintf( 
                    /* translators: %s: Error category */
                    __( 'Este tipo de error (%s) no se puede subsanar. Debe crear una factura rectificativa.', 'freefactu' ),
                    Freefactu_Aeat_Error_Categories::get_label( $error_category )
                ) 
            );
        }

        try {
            // Create client and call with $is_subsanation = true
            $generator = Freefactu_Factory::create_generator();
            $environment = Freefactu_Factory::create_environment();
            $client = new Freefactu_Client( $generator, $environment );

            Freefactu_Logger::log( sprintf( 'Subsanation attempt for order #%d', $order_id ), 'info' );

            // Pass $is_subsanation = true to force snapshot recreation
            $result = $client->send_invoice_to_verifactu( $order_id, null, true );

            if ( is_wp_error( $result ) ) {
                $error_data = $result->get_error_data();
                $can_retry = isset( $error_data['can_subsanar'] ) && $error_data['can_subsanar'];
                
                wp_send_json_error( array(
                    'message'     => $result->get_error_message(),
                    'can_retry'   => $can_retry,
                    'next_action' => isset( $error_data['next_action'] ) ? $error_data['next_action'] : '',
                ) );
            }

            // Success - get PDF URL
            $updated_invoice = Freefactu_DB::get_invoice_by_order_id( $order_id );
            $pdf_url = '';
            if ( $updated_invoice && ! empty( $updated_invoice->pdf_path ) ) {
                $pdf_url = admin_url( 'admin-post.php?action=freefactu_download_invoice&order_id=' . $order_id );
            }

            wp_send_json_success( array(
                'message' => __( 'Subsanación completada. La factura ha sido aceptada por la AEAT.', 'freefactu' ),
                'pdf_url' => $pdf_url,
            ) );

        } catch ( Exception $e ) {
            Freefactu_Logger::log( 'Subsanation exception: ' . $e->getMessage(), 'error' );
            wp_send_json_error( $e->getMessage() );
        }
    }

    /**
     * AJAX handler to save order and create invoice in one seamless operation.
     *
     * Called from order_view context to ensure all modifications are saved
     * before creating a new invoice.
     *
     * @return void Outputs JSON response and exits.
     */
    public function ajax_save_and_create_invoice() {
        // Verify nonce
        if ( ! check_ajax_referer( 'freefactu_unified_invoice_action', 'security', false ) ) {
            Freefactu_Logger::log( 'Save and create: Invalid security nonce', 'error' );
            wp_send_json_error( __( 'Enlace de seguridad inválido. Por favor, recarga la página e inténtalo de nuevo.', 'freefactu' ), 403 );
        }

        // Verify capability
        if ( ! current_user_can( 'manage_verifactu' ) ) {
            wp_send_json_error( __( 'No tienes permisos para realizar esta acción.', 'freefactu' ) );
        }

        $order_id = isset( $_POST['order_id'] ) ? absint( $_POST['order_id'] ) : 0;

        if ( ! $order_id ) {
            wp_send_json_error( __( 'ID de pedido no válido.', 'freefactu' ) );
        }

        $order = wc_get_order( $order_id );
        if ( ! $order ) {
            wp_send_json_error( __( 'Pedido no encontrado.', 'freefactu' ) );
        }

        // Apply form data from browser to order before saving
        // This captures any changes the user made to billing fields (NIF, address, etc.)
        if ( isset( $_POST['form_data'] ) && is_array( $_POST['form_data'] ) ) {
            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized per-field in apply_form_data_to_order()
            $form_data = map_deep( wp_unslash( $_POST['form_data'] ), 'sanitize_text_field' );
            $this->apply_form_data_to_order( $order, $form_data );
            Freefactu_Logger::log( 'Save and create: Applied form data to order ' . $order_id, 'debug' );
        }

        // Save the order with the updated data
        try {
            $order->save();
            Freefactu_Logger::log( 'Save and create invoice: Order ' . $order_id . ' saved successfully', 'info' );
        } catch ( \Exception $e ) {
            Freefactu_Logger::log( 'Save and create invoice: Failed to save order ' . $order_id . ': ' . $e->getMessage(), 'error' );
            wp_send_json_error( __( 'Error al guardar el pedido: ', 'freefactu' ) . $e->getMessage() );
        }

        // Refresh the order object after saving
        $order = wc_get_order( $order_id );

        // Now create the invoice
        $result = $this->unified_create_invoice( $order );

        if ( is_wp_error( $result ) ) {
            $error_code = $result->get_error_code();
            $error_message = $result->get_error_message();

            // Re-get the order to ensure latest data for metabox
            $order = wc_get_order( $order_id );

            // Get the refreshed meta box HTML to show error in UI
            ob_start();
            self::render_invoice_meta_box_content( $order );
            $metabox_html = ob_get_clean();

            wp_send_json_error( array(
                'message'      => $this->get_user_friendly_error_message( $error_code, $error_message ),
                'metabox_html' => $metabox_html,
            ) );
        }

        wp_send_json_success( $result );
    }

    /**
     * AJAX handler to save order and retry a failed invoice in one seamless operation.
     *
     * Called from order_view context to ensure all modifications are saved
     * before retrying a failed invoice (alert_aeat, alert_client, alert_qr, alert_pdf).
     *
     * @return void Outputs JSON response and exits.
     */
    public function ajax_save_and_retry_invoice() {
        // Verify nonce
        if ( ! check_ajax_referer( 'freefactu_unified_invoice_action', 'security', false ) ) {
            Freefactu_Logger::log( 'Save and retry: Invalid security nonce', 'error' );
            wp_send_json_error( __( 'Enlace de seguridad inválido. Por favor, recarga la página e inténtalo de nuevo.', 'freefactu' ), 403 );
        }

        // Verify capability
        if ( ! current_user_can( 'manage_verifactu' ) ) {
            wp_send_json_error( __( 'No tienes permisos para realizar esta acción.', 'freefactu' ) );
        }

        $order_id = isset( $_POST['order_id'] ) ? absint( $_POST['order_id'] ) : 0;
        $status = isset( $_POST['status'] ) ? sanitize_text_field( wp_unslash( $_POST['status'] ) ) : '';

        // Log the incoming request for debugging
        Freefactu_Logger::log( sprintf(
            'Save and retry request: order_id=%d, status=%s, has_huella=%s, has_id=%s, has_fecha=%s',
            $order_id,
            $status,
            isset( $_POST['huella_anterior'] ) && ! empty( $_POST['huella_anterior'] ) ? 'YES' : 'NO',
            isset( $_POST['id_anterior'] ) && ! empty( $_POST['id_anterior'] ) ? 'YES' : 'NO',
            isset( $_POST['fecha_anterior'] ) && ! empty( $_POST['fecha_anterior'] ) ? 'YES' : 'NO'
        ), 'info' );

        // Capture huella_anterior for hash_chain error subsanation
        $huella_anterior = null;
        $id_anterior = null;
        $fecha_anterior = null;

        if ( isset( $_POST['huella_anterior'] ) && ! empty( $_POST['huella_anterior'] ) ) {
            $huella_anterior = sanitize_text_field( wp_unslash( $_POST['huella_anterior'] ) );
            // Validate SHA-256 hash format (64 hex characters)
            if ( ! preg_match( '/^[a-fA-F0-9]{64}$/', $huella_anterior ) ) {
                wp_send_json_error( __( 'El formato de la huella no es válido. Debe ser un hash SHA-256 (64 caracteres hexadecimales).', 'freefactu' ) );
            }
            Freefactu_Logger::log( 'Save and retry: Manual huella_anterior provided for order ' . $order_id, 'info' );
            
            // Capture optional ID and Date if Hash is provided
            if ( isset( $_POST['id_anterior'] ) && ! empty( $_POST['id_anterior'] ) ) {
                $id_anterior = sanitize_text_field( wp_unslash( $_POST['id_anterior'] ) );
            }
            
            if ( isset( $_POST['fecha_anterior'] ) && ! empty( $_POST['fecha_anterior'] ) ) {
                $fecha_anterior = sanitize_text_field( wp_unslash( $_POST['fecha_anterior'] ) );
            }
        }

        if ( ! $order_id ) {
            wp_send_json_error( __( 'ID de pedido no válido.', 'freefactu' ) );
        }

        $order = wc_get_order( $order_id );
        if ( ! $order ) {
            wp_send_json_error( __( 'Pedido no encontrado.', 'freefactu' ) );
        }

        // Apply form data from browser to order before saving
        // This captures any changes the user made to billing fields (NIF, address, etc.)
        if ( isset( $_POST['form_data'] ) && is_array( $_POST['form_data'] ) ) {
            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized per-field in apply_form_data_to_order()
            $form_data = map_deep( wp_unslash( $_POST['form_data'] ), 'sanitize_text_field' );
            $this->apply_form_data_to_order( $order, $form_data );
            Freefactu_Logger::log( 'Save and retry: Applied form data to order ' . $order_id, 'debug' );
        }

        // Save the order with the updated data
        try {
            $order->save();
            Freefactu_Logger::log( 'Save and retry invoice: Order ' . $order_id . ' saved successfully', 'info' );
        } catch ( \Exception $e ) {
            Freefactu_Logger::log( 'Save and retry invoice: Failed to save order ' . $order_id . ': ' . $e->getMessage(), 'error' );
            wp_send_json_error( __( 'Error al guardar el pedido: ', 'freefactu' ) . $e->getMessage() );
        }

        // Refresh the order object after saving
        $order = wc_get_order( $order_id );

        // Route retry based on status
        $result = null;

        switch ( $status ) {
            case 'alert_aeat':
                $result = $this->unified_retry_aeat( $order, $huella_anterior, $id_anterior, $fecha_anterior );
                break;

            case 'alert_client':
                // Client validation errors - if chain repair data provided, use retry_aeat
                // Otherwise just retry the full creation
                if ( ! empty( $huella_anterior ) ) {
                    // Chain repair for client chaining errors
                    $result = $this->unified_retry_aeat( $order, $huella_anterior, $id_anterior, $fecha_anterior );
                } else {
                    $result = $this->unified_create_invoice( $order );
                }
                break;

            case 'alert_qr':
                $result = $this->unified_retry_qr( $order );
                break;

            default:
                // Unknown status - try regular creation
                $result = $this->unified_create_invoice( $order );
                break;
        }

        if ( is_wp_error( $result ) ) {
            $error_code = $result->get_error_code();
            $error_message = $result->get_error_message();

            // Re-get the order to ensure latest data for metabox
            $order = wc_get_order( $order_id );

            // Get the refreshed meta box HTML to show error in UI
            ob_start();
            self::render_invoice_meta_box_content( $order );
            $metabox_html = ob_get_clean();

            wp_send_json_error( array(
                'message'      => $this->get_user_friendly_error_message( $error_code, $error_message ),
                'metabox_html' => $metabox_html,
            ) );
        }

        wp_send_json_success( $result );
    }

    /**
     * AJAX handler to refresh metabox content without page reload.
     *
     * Returns the current state of the invoice metabox HTML for updating
     * the UI after operations (errors, retries, etc).
     *
     * @return void Outputs JSON response and exits.
     */
    public function ajax_refresh_metabox() {
        // Verify nonce
        if ( ! check_ajax_referer( 'freefactu_unified_invoice_action', 'security', false ) ) {
            wp_send_json_error( __( 'Enlace de seguridad inválido.', 'freefactu' ), 403 );
        }

        // Verify capability
        if ( ! current_user_can( 'manage_verifactu' ) ) {
            wp_send_json_error( __( 'No tienes permisos para realizar esta acción.', 'freefactu' ) );
        }

        $order_id = isset( $_POST['order_id'] ) ? absint( $_POST['order_id'] ) : 0;

        if ( ! $order_id ) {
            wp_send_json_error( __( 'ID de pedido no válido.', 'freefactu' ) );
        }

        $order = wc_get_order( $order_id );
        if ( ! $order ) {
            wp_send_json_error( __( 'Pedido no encontrado.', 'freefactu' ) );
        }

        // Render the metabox content
        ob_start();
        self::render_invoice_meta_box_content( $order );
        $html = ob_get_clean();

        wp_send_json_success( array(
            'html' => $html,
        ) );
    }

    /**
     * Apply form data from browser to order object.
     *
     * Maps WooCommerce admin form field names to order properties and
     * updates the order object accordingly. This ensures that when the user
     * edits billing fields (like NIF) in the admin, those changes are applied
     * before saving and creating a new invoice snapshot.
     *
     * @param WC_Order $order     The order object to update.
     * @param array    $form_data Array of form field names and values.
     * @return void
     */
    private function apply_form_data_to_order( $order, $form_data ) {
        // Map of form field names to WC_Order setter methods
        $billing_field_map = array(
            '_billing_first_name' => 'set_billing_first_name',
            '_billing_last_name'  => 'set_billing_last_name',
            '_billing_company'    => 'set_billing_company',
            '_billing_address_1'  => 'set_billing_address_1',
            '_billing_address_2'  => 'set_billing_address_2',
            '_billing_city'       => 'set_billing_city',
            '_billing_state'      => 'set_billing_state',
            '_billing_postcode'   => 'set_billing_postcode',
            '_billing_country'    => 'set_billing_country',
            '_billing_email'      => 'set_billing_email',
            '_billing_phone'      => 'set_billing_phone',
        );

        $shipping_field_map = array(
            '_shipping_first_name' => 'set_shipping_first_name',
            '_shipping_last_name'  => 'set_shipping_last_name',
            '_shipping_company'    => 'set_shipping_company',
            '_shipping_address_1'  => 'set_shipping_address_1',
            '_shipping_address_2'  => 'set_shipping_address_2',
            '_shipping_city'       => 'set_shipping_city',
            '_shipping_state'      => 'set_shipping_state',
            '_shipping_postcode'   => 'set_shipping_postcode',
            '_shipping_country'    => 'set_shipping_country',
        );

        // Apply billing fields
        foreach ( $billing_field_map as $field_name => $setter ) {
            if ( isset( $form_data[ $field_name ] ) ) {
                $value = sanitize_text_field( $form_data[ $field_name ] );
                if ( method_exists( $order, $setter ) ) {
                    $order->$setter( $value );
                }
            }
        }

        // Apply shipping fields
        foreach ( $shipping_field_map as $field_name => $setter ) {
            if ( isset( $form_data[ $field_name ] ) ) {
                $value = sanitize_text_field( $form_data[ $field_name ] );
                if ( method_exists( $order, $setter ) ) {
                    $order->$setter( $value );
                }
            }
        }

        // Handle custom NIF field (used by Freefactu)
        // Check multiple possible field names for NIF
        $nif_field_names = array(
            '_billing_nif',
            '_billing_dni',
            '_billing_cif',
            '_billing_vat',
            'billing_nif',
            'billing_dni',
        );

        foreach ( $nif_field_names as $nif_field ) {
            if ( isset( $form_data[ $nif_field ] ) && ! empty( $form_data[ $nif_field ] ) ) {
                $nif_value = sanitize_text_field( $form_data[ $nif_field ] );
                $order->update_meta_data( '_billing_nif', $nif_value );
                Freefactu_Logger::log( 'Applied NIF value from form: ' . $nif_value, 'debug' );
                break; // Only apply the first found NIF field
            }
        }

        // Handle any other meta fields that start with _ (custom meta data)
        foreach ( $form_data as $field_name => $value ) {
            // Skip already processed billing/shipping fields
            if ( isset( $billing_field_map[ $field_name ] ) || isset( $shipping_field_map[ $field_name ] ) ) {
                continue;
            }

            // Skip non-meta fields (ones that don't start with underscore or billing_/shipping_)
            if ( strpos( $field_name, '_billing_' ) === 0 || strpos( $field_name, '_shipping_' ) === 0 ) {
                // This is a custom meta field, update it
                $sanitized_value = sanitize_text_field( $value );
                $order->update_meta_data( $field_name, $sanitized_value );
            }
        }
    }

    /**
     * Validate certificate is available and valid for invoice operations.
     *
     * Checks that a certificate is installed, not expired, and has a valid password.
     * Returns WP_Error with appropriate error code if validation fails.
     *
     * @return true|WP_Error True if certificate is valid, WP_Error otherwise.
     */
    private function validate_certificate_for_operation() {
        $options = get_option( 'freefactu_settings' );

        // Check if certificate file exists
        $cert_path = isset( $options['certificate_path'] ) ? $options['certificate_path'] : '';
        if ( empty( $cert_path ) || ! Freefactu_Filesystem::exists( $cert_path ) ) {
            return new \WP_Error(
                'certificate_missing',
                __( 'No hay certificado digital instalado.', 'freefactu' )
            );
        }

        // Check if password is set
        $cert_password = isset( $options['certificate_password'] ) ? $options['certificate_password'] : '';
        if ( empty( $cert_password ) ) {
            return new \WP_Error(
                'certificate_password',
                __( 'Falta la contraseña del certificado.', 'freefactu' )
            );
        }

        // Decrypt password and check certificate expiry
        $decrypted_password = self::decrypt_password( $cert_password );
        $expiry_info = self::get_certificate_expiry( $cert_path, $decrypted_password );

        // If we can't read expiry info, password might be wrong
        if ( ! $expiry_info ) {
            return new \WP_Error(
                'certificate_invalid',
                __( 'No se puede leer el certificado. Verifica que la contraseña sea correcta.', 'freefactu' )
            );
        }

        // Check if certificate is expired
        if ( $expiry_info['is_expired'] ) {
            return new \WP_Error(
                'certificate_expired',
                sprintf(
                    /* translators: %s: Expiry date */
                    __( 'El certificado ha caducado el %s.', 'freefactu' ),
                    $expiry_info['expiry_date']
                )
            );
        }

        return true;
    }

    /**
     * Get user-friendly error message based on error code.
     *
     * Translates internal error codes to user-friendly messages in Spanish.
     *
     * @param string $error_code    The error code from WP_Error.
     * @param string $error_message The original error message.
     * @return string User-friendly error message.
     */
    private function get_user_friendly_error_message( $error_code, $error_message ) {
        // Map error codes to user-friendly messages
        $error_messages = array(
            // AEAT submission errors
            'aeat_connection_error'    => __( 'Error de conexión con la AEAT. Por favor, verifica tu conexión a internet e inténtalo de nuevo.', 'freefactu' ),
            'aeat_timeout'             => __( 'La conexión con la AEAT ha excedido el tiempo de espera. Por favor, inténtalo de nuevo más tarde.', 'freefactu' ),
            'aeat_invalid_response'    => __( 'La AEAT ha devuelto una respuesta inválida. Por favor, inténtalo de nuevo.', 'freefactu' ),
            'aeat_rejected'            => __( 'La AEAT ha rechazado la factura. ', 'freefactu' ) . $error_message,
            'aeat_rejection'           => __( 'La AEAT ha rechazado la factura. ', 'freefactu' ) . $error_message,
            'no_csv'                   => __( 'La factura se envió pero no se recibió el código CSV de la AEAT.', 'freefactu' ),
            'already_sent'             => __( 'Esta factura ya ha sido enviada a Verifactu.', 'freefactu' ),

            // Certificate errors
            'certificate_missing'      => __( 'No hay certificado digital instalado. Ve a Configuración para subir tu certificado.', 'freefactu' ),
            'certificate_expired'      => __( 'El certificado digital ha caducado. Por favor, renueva tu certificado.', 'freefactu' ),
            'certificate_invalid'      => __( 'El certificado digital no es válido o la contraseña es incorrecta.', 'freefactu' ),
            'certificate_password'     => __( 'La contraseña del certificado es incorrecta o falta. Ve a Configuración para corregirla.', 'freefactu' ),

            // QR generation errors
            'qr_generation_failed'     => __( 'Error al generar el código QR. ', 'freefactu' ) . $error_message,
            'qr_save_failed'           => __( 'Error al guardar el código QR. Verifica los permisos de escritura.', 'freefactu' ),

            // PDF generation errors
            'pdf_generation_failed'    => __( 'Error al generar el PDF. ', 'freefactu' ) . $error_message,
            'pdf_save_failed'          => __( 'Error al guardar el PDF. Verifica los permisos de escritura.', 'freefactu' ),
            'pdf_template_missing'     => __( 'No se encontró la plantilla de factura PDF.', 'freefactu' ),

            // Order errors
            'order_not_found'          => __( 'No se encontró el pedido. Es posible que haya sido eliminado.', 'freefactu' ),
            'order_invalid'            => __( 'El pedido no es válido para generar factura.', 'freefactu' ),
            'invalid_order'            => __( 'El pedido no es válido o no existe.', 'freefactu' ),

            // Permission errors
            'permission_denied'        => __( 'No tienes permisos para realizar esta acción.', 'freefactu' ),

            // Exception/generic errors
            'exception'                => __( 'Ha ocurrido un error durante el proceso. ', 'freefactu' ) . $error_message,
            'unknown_error'            => __( 'Ha ocurrido un error desconocido. Por favor, inténtalo de nuevo.', 'freefactu' ),
        );

        // Return mapped message or original message if no mapping exists
        if ( isset( $error_messages[ $error_code ] ) ) {
            return $error_messages[ $error_code ];
        }

        // If no specific mapping, return the original message without prefix
        // (the JavaScript handler will add the "Error: " prefix)
        if ( ! empty( $error_message ) ) {
            return $error_message;
        }

        return $error_messages['unknown_error'];
    }

    /**
     * Create a new invoice (unified handler helper).
     *
     * @param WC_Order $order The order object.
     * @return array|WP_Error Response data or error.
     */
    private function unified_create_invoice( $order ) {
        $order_id = $order->get_id();

        // Validate certificate before attempting to create invoice
        $cert_validation = $this->validate_certificate_for_operation();
        if ( is_wp_error( $cert_validation ) ) {
            Freefactu_Logger::log( sprintf(
                'Certificate validation failed for order %d: %s',
                $order_id,
                $cert_validation->get_error_message()
            ), 'error' );
            return $cert_validation;
        }

        // Get client instance with proper dependencies
        $generator = Freefactu_Factory::create_generator();
        $environment = Freefactu_Factory::create_environment();
        $client = new Freefactu_Client( $generator, $environment );

        // Send invoice to Verifactu
        $result = $client->send_invoice_to_verifactu( $order_id );

        if ( is_wp_error( $result ) ) {
            // Log the AEAT submission error
            Freefactu_Logger::log( sprintf(
                'AEAT submission failed for order %d: [%s] %s',
                $order_id,
                $result->get_error_code(),
                $result->get_error_message()
            ), 'error' );
            return $result;
        }

        if ( ! isset( $result['csv'] ) || empty( $result['csv'] ) ) {
            $msg = isset( $result['message'] ) ? $result['message'] : __( 'Factura enviada pero no se recibió CSV.', 'freefactu' );
            Freefactu_Logger::log( sprintf(
                'No CSV received from AEAT for order %d: %s',
                $order_id,
                $msg
            ), 'error' );
            return new \WP_Error( 'no_csv', $msg );
        }

        // Get invoice data from database (already saved by client)
        $invoice = Freefactu_DB::get_invoice_by_order_id( $order_id );

        if ( ! $invoice ) {
            Freefactu_Logger::log( sprintf(
                'Invoice record not found in database after creation for order %d',
                $order_id
            ), 'error' );
            return new \WP_Error( 'invoice_not_found', __( 'Error: No se encontró el registro de factura.', 'freefactu' ) );
        }

        // Build PDF URL
        $pdf_url = $this->get_pdf_url_for_order( $order_id );

        return array(
            'pdf_url'        => $pdf_url,
            'invoice_number' => $invoice->invoice_number,
            'action_taken'   => 'created',
            'message'        => __( 'Factura creada correctamente.', 'freefactu' ),
        );
    }

    /**
     * Retry AEAT submission (unified handler helper).
     *
     * When retrying AEAT, we always use subsanation mode to create a fresh snapshot
     * from the current order data. This ensures any corrections made by the user
     * (e.g., NIF changes) are captured in the new submission.
     *
     * @param WC_Order $order The order object.
     * @param string|null $huella_anterior Optional manual previous hash for hash_chain error subsanation.
     * @param string|null $id_anterior Optional manual previous ID.
     * @param string|null $fecha_anterior Optional manual previous Date.
     * @return array|WP_Error Response data or error.
     */
    private function unified_retry_aeat( $order, $huella_anterior = null, $id_anterior = null, $fecha_anterior = null ) {
        $order_id = $order->get_id();

        // Validate certificate before attempting to retry AEAT submission
        $cert_validation = $this->validate_certificate_for_operation();
        if ( is_wp_error( $cert_validation ) ) {
            Freefactu_Logger::log( sprintf(
                'Certificate validation failed for AEAT retry on order %d: %s',
                $order_id,
                $cert_validation->get_error_message()
            ), 'error' );
            return $cert_validation;
        }

        // Get client instance with proper dependencies
        $generator = Freefactu_Factory::create_generator();
        $environment = Freefactu_Factory::create_environment();
        $client = new Freefactu_Client( $generator, $environment );

        // Retry full send to AEAT with $is_subsanation = true
        // This creates a fresh snapshot from the current order data, ensuring any
        // corrections (NIF, address, etc.) made by the user are captured
        // Pass the manual values if provided (for hash_chain error recovery)
        if ( ! empty( $huella_anterior ) ) {
             Freefactu_Logger::log( sprintf( 'Unified retry passing manual chain values: ID=%s, Date=%s, Hash=%s', $id_anterior, $fecha_anterior, substr($huella_anterior, 0, 10) . '...' ), 'info' );
        }
        
        $result = $client->send_invoice_to_verifactu( $order_id, null, true, $huella_anterior, $id_anterior, $fecha_anterior );

        if ( is_wp_error( $result ) ) {
            // Log the AEAT retry error
            Freefactu_Logger::log( sprintf(
                'AEAT retry failed for order %d: [%s] %s',
                $order_id,
                $result->get_error_code(),
                $result->get_error_message()
            ), 'error' );
            return $result;
        }

        if ( ! isset( $result['csv'] ) || empty( $result['csv'] ) ) {
            Freefactu_Logger::log( sprintf(
                'No CSV received from AEAT retry for order %d',
                $order_id
            ), 'error' );
            return new \WP_Error( 'no_csv', __( 'Error al enviar a AEAT.', 'freefactu' ) );
        }

        // Get invoice data from database
        $invoice = Freefactu_DB::get_invoice_by_order_id( $order_id );

        // Build PDF URL
        $pdf_url = $this->get_pdf_url_for_order( $order_id );

        return array(
            'pdf_url'        => $pdf_url,
            'invoice_number' => $invoice ? $invoice->invoice_number : '',
            'action_taken'   => 'regenerated',
            'message'        => __( 'Factura reenviada correctamente.', 'freefactu' ),
        );
    }

    /**
     * Retry QR generation and PDF (unified handler helper).
     *
     * @param WC_Order|null $order The order object (may be null if order was deleted).
     * @param int $order_id The order ID (used when order is deleted).
     * @return array|WP_Error Response data or error.
     */
    private function unified_retry_qr( $order, $order_id = 0 ) {
        // Use provided order_id or get from order object
        if ( ! $order_id && $order ) {
            $order_id = $order->get_id();
        }

        if ( ! $order_id ) {
            return new WP_Error( 'invalid_order_id', __( 'ID de pedido inválido.', 'freefactu' ) );
        }

        // Get client instance with proper dependencies
        $generator = Freefactu_Factory::create_generator();
        $environment = Freefactu_Factory::create_environment();
        $client = new Freefactu_Client( $generator, $environment );

        // Retry QR generation (works with order object or will fetch from DB)
        $qr_result = $client->generate_and_save_qr( $order );

        if ( is_wp_error( $qr_result ) ) {
            // Log the QR generation error
            Freefactu_Logger::log( sprintf(
                'QR generation failed for order %d: [%s] %s',
                $order_id,
                $qr_result->get_error_code(),
                $qr_result->get_error_message()
            ), 'error' );
            return $qr_result;
        }

        // Now try PDF - pass order_id to handle deleted orders
        $pdf_result = $client->save_pdf_snapshot_by_id( $order_id );

        if ( is_wp_error( $pdf_result ) ) {
            // QR succeeded but PDF failed - just log the error, don't set alert_pdf status
            // PDF can be regenerated on demand from snapshot
            Freefactu_Logger::log( sprintf(
                'PDF generation failed after QR success for order %d: [%s] %s (PDF can be regenerated later)',
                $order_id,
                $pdf_result->get_error_code(),
                $pdf_result->get_error_message()
            ), 'warning' );

            // Still return error but don't update database status
            return $pdf_result;
        }

        // Both succeeded - update status in database
        Freefactu_DB::update_invoice_status( $order_id, 'success', '' );

        // Get invoice data from database
        $invoice = Freefactu_DB::get_invoice_by_order_id( $order_id );

        // Build PDF URL
        $pdf_url = $this->get_pdf_url_for_order( $order_id );

        return array(
            'pdf_url'        => $pdf_url,
            'invoice_number' => $invoice ? $invoice->invoice_number : '',
            'action_taken'   => 'regenerated',
            'message'        => __( 'QR y PDF generados correctamente.', 'freefactu' ),
        );
    }

    /**
     * Retry PDF generation only (unified handler helper).
     *
     * @param WC_Order|null $order The order object (may be null if order was deleted).
     * @param int $order_id The order ID (used when order is deleted).
     * @return array|WP_Error Response data or error.
     */
    private function unified_retry_pdf( $order, $order_id = 0 ) {
        // Use provided order_id or get from order object
        if ( ! $order_id && $order ) {
            $order_id = $order->get_id();
        }

        if ( ! $order_id ) {
            return new WP_Error( 'invalid_order_id', __( 'ID de pedido inválido.', 'freefactu' ) );
        }

        // Get client instance with proper dependencies
        $generator = Freefactu_Factory::create_generator();
        $environment = Freefactu_Factory::create_environment();
        $client = new Freefactu_Client( $generator, $environment );

        // Retry PDF generation - pass order_id to handle deleted orders
        $pdf_result = $client->save_pdf_snapshot_by_id( $order_id );

        if ( is_wp_error( $pdf_result ) ) {
            // Log the PDF generation error
            Freefactu_Logger::log( sprintf(
                'PDF generation retry failed for order %d: [%s] %s',
                $order_id,
                $pdf_result->get_error_code(),
                $pdf_result->get_error_message()
            ), 'error' );
            return $pdf_result;
        }

        // Success - update status in database
        Freefactu_DB::update_invoice_status( $order_id, 'success', '' );

        // Get invoice data from database
        $invoice = Freefactu_DB::get_invoice_by_order_id( $order_id );

        // Build PDF URL
        $pdf_url = $this->get_pdf_url_for_order( $order_id );

        return array(
            'pdf_url'        => $pdf_url,
            'invoice_number' => $invoice ? $invoice->invoice_number : '',
            'action_taken'   => 'regenerated',
            'message'        => __( 'PDF generado correctamente.', 'freefactu' ),
        );
    }

    /**
     * Return existing PDF URL (unified handler helper).
     *
     * @param WC_Order $order The order object.
     * @return array Response data.
     */
    private function unified_open_existing( $order ) {
        $order_id = $order->get_id();

        // Get invoice data from database
        $invoice = Freefactu_DB::get_invoice_by_order_id( $order_id );

        // Check if PDF file exists, update status if missing
        $this->check_and_update_pdf_status( $invoice );

        // Build PDF URL
        $pdf_url = $this->get_pdf_url_for_order( $order_id );

        return array(
            'pdf_url'        => $pdf_url,
            'invoice_number' => $invoice ? $invoice->invoice_number : '',
            'action_taken'   => 'opened',
            'message'        => __( 'Abriendo factura existente.', 'freefactu' ),
        );
    }

    /**
     * Open existing invoice using invoice data (when order is deleted).
     *
     * @param object $invoice Invoice database object.
     * @return array Response data.
     */
    private function unified_open_existing_from_invoice( $invoice ) {
        $order_id = $invoice->order_id;

        // Check if PDF file exists, update status if missing
        $this->check_and_update_pdf_status( $invoice );

        // Build PDF URL
        $pdf_url = $this->get_pdf_url_for_order( $order_id );

        return array(
            'pdf_url'        => $pdf_url,
            'invoice_number' => $invoice->invoice_number,
            'action_taken'   => 'opened',
            'message'        => __( 'Abriendo factura existente (pedido eliminado).', 'freefactu' ),
        );
    }

    /**
     * Check if PDF file exists (for debugging/logging only).
     *
     * This method verifies that the PDF file referenced in the database
     * actually exists on the filesystem. It only logs a warning if missing,
     * it does NOT update the invoice status since PDFs can be regenerated.
     *
     * @deprecated 1.2.0 PDF existence no longer affects invoice status
     * @param object|null $invoice Invoice database object.
     * @return void
     */
    private function check_and_update_pdf_status( $invoice ) {
        // This method is deprecated and no longer updates status.
        // PDF existence is only checked for display in the debug invoice page.
        return;
    }

    /**
     * Get PDF URL for an order
     *
     * @param int $order_id Order ID
     * @return string PDF URL (decoded for JavaScript usage)
     */
    private function get_pdf_url_for_order( $order_id ) {
        // Use factory to get the appropriate generator and ask for PDF URL
        $generator = Freefactu_Factory::create_generator();
        $url = $generator->get_pdf_url( $order_id );

        // Decode HTML entities for JavaScript usage
        return html_entity_decode( $url );
    }

    /**
     * AJAX handler for certificate removal
     *
     * @return void Outputs JSON response and exits
     */
    public function ajax_remove_certificate() {
        // Verify nonce
        if ( ! check_ajax_referer( 'freefactu_remove_certificate', 'security', false ) ) {
            wp_send_json_error( __( 'Permiso denegado.', 'freefactu' ), 403 );
        }

        // Verify capability
        if ( ! current_user_can( 'manage_verifactu' ) ) {
            wp_send_json_error( __( 'Permiso denegado.', 'freefactu' ), 403 );
        }

        // Get current options
        $options = get_option( $this->option_name );

        // Check if certificate exists
        if ( empty( $options['certificate_path'] ) ) {
            wp_send_json_error( __( 'No hay certificado instalado.', 'freefactu' ), 400 );
        }

        // Delete certificate file if it exists
        if ( Freefactu_Filesystem::exists( $options['certificate_path'] ) ) {
            if ( ! Freefactu_Filesystem::delete( $options['certificate_path'] ) ) {
                wp_send_json_error( __( 'Error al eliminar el certificado.', 'freefactu' ), 500 );
            }
        }

        // Clear certificate_path from options
        $options['certificate_path'] = '';
        update_option( $this->option_name, $options );

        // Return rendered HTML for seamless update
        ob_start();
        $this->render_certificate_status_html( false, null );
        $html = ob_get_clean();

        wp_send_json_success( array(
            'message' => __( 'Certificado eliminado correctamente.', 'freefactu' ),
            'html' => $html
        ) );
    }

    /**
     * AJAX handler for certificate upload
     *
     * @return void Outputs JSON response and exits
     */
    public function ajax_upload_certificate() {
        // Verify nonce
        if ( ! check_ajax_referer( 'freefactu_upload_certificate', 'security', false ) ) {
            wp_send_json_error( __( 'Permiso denegado.', 'freefactu' ), 403 );
        }

        // Verify capability
        if ( ! current_user_can( 'manage_verifactu' ) ) {
            wp_send_json_error( __( 'Permiso denegado.', 'freefactu' ), 403 );
        }

        // Check if file was uploaded
        if ( empty( $_FILES['certificate'] ) || ! isset( $_FILES['certificate']['error'] ) || $_FILES['certificate']['error'] !== UPLOAD_ERR_OK ) {
            wp_send_json_error( __( 'Error al subir el archivo.', 'freefactu' ), 400 );
        }

        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- File name is sanitized with sanitize_file_name; tmp_name is a PHP-internal path validated by is_uploaded_file().
        $file = $_FILES['certificate'];

        // Preserve the PHP-internal tmp_name before any processing.
        // wp_unslash() must NOT be applied to tmp_name — WordPress's wp_magic_quotes()
        // does not add slashes to $_FILES, so wp_unslash() would strip real backslashes
        // from Windows temp paths (e.g., C:\Users\...\Temp\phpXXXX.tmp).
        $raw_tmp_name = $file['tmp_name'];

        // Sanitize user-controlled fields only
        $file['name']     = sanitize_file_name( wp_unslash( $file['name'] ) );
        $file['tmp_name'] = $raw_tmp_name; // Ensure tmp_name is never modified

        // Verify this is a genuine upload
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- tmp_name is a PHP-internal server path validated by is_uploaded_file().
        if ( ! is_uploaded_file( $file['tmp_name'] ) ) {
            wp_send_json_error( __( 'Subida de archivo no válida.', 'freefactu' ), 400 );
        }

        // Verify extension (redundant check already done above, but explicit after sanitization)
        if ( isset( $file['error'] ) && $file['error'] !== UPLOAD_ERR_OK ) {
             wp_send_json_error( __( 'Error en la subida.', 'freefactu' ), 400 );
        }
        
        $file_ext = strtolower( pathinfo( $file['name'], PATHINFO_EXTENSION ) );
        if ( $file_ext !== 'p12' && $file_ext !== 'pfx' ) {
            wp_send_json_error( __( 'Solo se permiten archivos .p12 o .pfx.', 'freefactu' ), 400 );
        }

        // Validate MIME type using WordPress function
        $file_type = wp_check_filetype_and_ext( $file['tmp_name'], $file['name'], array(
            'p12' => 'application/x-pkcs12',
            'pfx' => 'application/x-pkcs12',
        ) );

        // If WordPress couldn't determine the type, check manually
        if ( empty( $file_type['ext'] ) ) {
            // Read first bytes to check for PKCS#12 signature
            // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
            $handle = fopen( $file['tmp_name'], 'rb' );
            if ( $handle ) {
                // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread
                $header = fread( $handle, 4 );
                // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
                fclose( $handle );
                // PKCS#12 files start with ASN.1 SEQUENCE tag (0x30)
                if ( ord( $header[0] ) !== 0x30 ) {
                    wp_send_json_error( __( 'El archivo no parece ser un certificado PKCS#12 válido.', 'freefactu' ), 400 );
                }
            }
        }

        // Use WordPress's wp_handle_upload() via helper method
        // This stores files in a protected directory outside the media library
        $upload_result = self::handle_certificate_upload( $file );

        if ( is_wp_error( $upload_result ) ) {
            wp_send_json_error( $upload_result->get_error_message(), 500 );
        }

        $target_file = $upload_result['file'];
        
        // Log successful file save
        Freefactu_Logger::log( 
            sprintf( 'CERT DEBUG [upload]: File saved to "%s", file_exists=%s', $target_file, file_exists( $target_file ) ? 'YES' : 'NO' ), 
            'debug' 
        );

        // Update options
        $options = get_option( $this->option_name );
        $options_before = is_array( $options ) ? $options : array();
        if ( ! is_array( $options ) ) {
            $options = array();
        }
        $options['certificate_path'] = $target_file;
        $update_result = update_option( $this->option_name, $options );
        
        // Always log the update attempt
        Freefactu_Logger::log( 
            sprintf( 
                'CERT DEBUG [upload]: update_option result=%s, cert_path_before="%s", cert_path_after="%s"', 
                $update_result ? 'SUCCESS' : 'FALSE',
                isset( $options_before['certificate_path'] ) ? $options_before['certificate_path'] : 'NOT_SET',
                $target_file
            ), 
            'debug' 
        );
        
        // Verify the option was saved by reading it back
        $verify_options = get_option( $this->option_name );
        $verify_path = isset( $verify_options['certificate_path'] ) ? $verify_options['certificate_path'] : 'NOT_SET';
        Freefactu_Logger::log( 
            sprintf( 'CERT DEBUG [upload]: Verification read-back: cert_path="%s"', $verify_path ), 
            'debug' 
        );

        // Get expiry info for response
        $password = isset( $options['certificate_password'] ) ? self::decrypt_password( $options['certificate_password'] ) : '';
        $expiry_info = self::get_certificate_expiry( $target_file, $password );

        // Return rendered HTML for seamless update
        ob_start();
        $this->render_certificate_status_html( true, $expiry_info );
        $html = ob_get_clean();

        wp_send_json_success( array(
            'message' => __( 'Certificado subido correctamente.', 'freefactu' ),
            'html' => $html
        ) );
    }

    /**
     * AJAX handler for clearing QR code cache
     *
     * @return void Outputs JSON response and exits
     */
    public function ajax_clear_qr_cache() {
        // Verify nonce
        if ( ! check_ajax_referer( 'freefactu_clear_qr_cache', 'security', false ) ) {
            wp_send_json_error( __( 'Permiso denegado.', 'freefactu' ), 403 );
        }

        // Verify capability
        if ( ! current_user_can( 'manage_verifactu' ) ) {
            wp_send_json_error( __( 'Permiso denegado.', 'freefactu' ), 403 );
        }

        global $wpdb;

        // Count QR cache entries before deleting
        // We only count the actual data entries (not the timeout entries) to report accurate number
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $count = $wpdb->get_var(
            "SELECT COUNT(*) FROM {$wpdb->options}
             WHERE option_name LIKE '_transient_freefactu_qr%'"
        );

        // Delete all QR code transients
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $deleted = $wpdb->query(
            "DELETE FROM {$wpdb->options}
             WHERE option_name LIKE '_transient_freefactu_qr%'
             OR option_name LIKE '_transient_timeout_freefactu_qr%'"
        );

        if ( $deleted === false ) {
            wp_send_json_error( __( 'Error al limpiar la caché.', 'freefactu' ), 500 );
        }

        wp_send_json_success( array(
            /* translators: %d: QR cache count */
            'message' => sprintf( __( 'Se han eliminado %d entradas de caché de códigos QR.', 'freefactu' ), intval( $count ) ),
            'deleted' => intval( $count )
        ) );
    }

    public function handle_download_invoice() {
        if ( ! current_user_can( 'manage_woocommerce' ) ) {
            wp_die( esc_html( __( 'Permiso denegado.', 'freefactu' ) ) );
        }

        $invoice_id = isset( $_GET['id'] ) ? absint( $_GET['id'] ) : 0;
        
        // Verify nonce
        if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'freefactu_download_invoice_' . $invoice_id ) ) {
            wp_die( esc_html__( 'Error de seguridad.', 'freefactu' ) );
        }
        if ( ! $invoice_id ) {
            wp_die( esc_html__( 'ID de factura inválido.', 'freefactu' ) );
        }

        $invoice = Freefactu_DB::get_invoice( $invoice_id );
        // Convert stored relative path to absolute for file operations
        $absolute_path = Freefactu_Filesystem::to_absolute_path( $invoice->pdf_path ?? '' );
        if ( ! $invoice || empty( $invoice->pdf_path ) || ! Freefactu_Filesystem::exists( $absolute_path ) ) {
            wp_die( esc_html__( 'PDF no encontrado.', 'freefactu' ) );
        }

        // Serve the file
        header( 'Content-Type: application/pdf' );
        header( 'Content-Disposition: inline; filename="' . sanitize_file_name( 'verifactu-' . $invoice->invoice_number . '.pdf' ) . '"' );
        header( 'Content-Length: ' . filesize( $absolute_path ) );
        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile
        readfile( $absolute_path );
        exit;
    }

    /**
     * AJAX handler for creating rectificativa (correction) invoices.
     *
     * Creates a new rectificativa invoice that references and corrects an original invoice.
     * Required for VeriFactu compliance - corrections must be new chain entries.
     *
     * @return void Outputs JSON response and exits.
     */
    public function ajax_create_rectificativa() {
        // Verify nonce
        check_ajax_referer( 'freefactu_rectificativa', 'security' );

        // Verify capability
        if ( ! current_user_can( 'manage_verifactu' ) ) {
            wp_send_json_error( __( 'Permiso denegado.', 'freefactu' ) );
        }

        // Get parameters
        $original_invoice_id = isset( $_POST['invoice_id'] ) ? absint( wp_unslash( $_POST['invoice_id'] ) ) : 0;
        $order_id = isset( $_POST['order_id'] ) ? absint( wp_unslash( $_POST['order_id'] ) ) : 0;
        $tipo_rectificativa = isset( $_POST['tipo_rectificativa'] ) ? sanitize_text_field( wp_unslash( $_POST['tipo_rectificativa'] ) ) : 'R1';
        $metodo_rectificativa = isset( $_POST['metodo_rectificativa'] ) ? sanitize_text_field( wp_unslash( $_POST['metodo_rectificativa'] ) ) : 'I';
        $motivo = isset( $_POST['motivo'] ) ? sanitize_textarea_field( wp_unslash( $_POST['motivo'] ) ) : '';

        // Validate required fields
        if ( ! $original_invoice_id ) {
            wp_send_json_error( __( 'ID de factura original no válido.', 'freefactu' ) );
        }

        // Get original invoice
        $original_invoice = Freefactu_DB::get_invoice( $original_invoice_id );
        if ( ! $original_invoice ) {
            wp_send_json_error( __( 'Factura original no encontrada.', 'freefactu' ) );
        }

        // Get the order
        $order = wc_get_order( $order_id ? $order_id : $original_invoice->order_id );
        if ( ! $order ) {
            wp_send_json_error( __( 'Pedido no encontrado.', 'freefactu' ) );
        }

        // Validate tipo_rectificativa
        $valid_tipos = array( 'R1', 'R2', 'R3', 'R4', 'R5' );
        if ( ! in_array( $tipo_rectificativa, $valid_tipos, true ) ) {
            wp_send_json_error( __( 'Tipo de rectificativa no válido.', 'freefactu' ) );
        }

        // Validate metodo_rectificativa
        if ( ! in_array( $metodo_rectificativa, array( 'I', 'S' ), true ) ) {
            $metodo_rectificativa = 'I'; // Default to "Por diferencias"
        }

        try {
            // Create the client using Factory pattern
            $generator = Freefactu_Factory::create_generator();
            $environment = Freefactu_Factory::create_environment();
            $client = new Freefactu_Client( $generator, $environment );

            // Prepare rectificativa data
            $rectificativa_data = array(
                'tipo_rectificativa' => $tipo_rectificativa,
                'metodo_rectificativa' => $metodo_rectificativa,
                'original_invoice' => $original_invoice,
                'motivo' => $motivo,
            );

            //wp_send_json_error( print_r( $order->get_id(), true ) );

            // Send to Verifactu
            $result = $client->send_rectificativa_invoice( $order, $rectificativa_data );

            if ( is_wp_error( $result ) ) {
                wp_send_json_error( $result->get_error_message() );
            }

            // Get the invoice from DB to build PDF URL
            $invoice = Freefactu_DB::get_invoice_by_number( $result['invoice_number'] );
            $pdf_url = '';
            if ( $invoice ) {
                $pdf_url = wp_nonce_url(
                    admin_url( 'admin-post.php?action=freefactu_download_invoice&id=' . $invoice->id ),
                    'freefactu_download_invoice_' . $invoice->id
                );
                $pdf_url = html_entity_decode( $pdf_url );
            }

            wp_send_json_success( array(
                'message' => __( 'Factura rectificativa creada correctamente.', 'freefactu' ),
                'invoice_number' => $result['invoice_number'],
                'pdf_url' => $pdf_url,
            ) );

        } catch ( Exception $e ) {
            Freefactu_Logger::log( 'Rectificativa creation error: ' . $e->getMessage(), 'error' );
            wp_send_json_error( $e->getMessage() );
        }
    }

    /**
     * AJAX handler for searching invoices to rectify.
     *
     * Returns a list of successful invoices that can be used as the base for a rectificativa.
     * Includes both regular invoices (F1) and rectificativas (R1-R5).
     *
     * @return void Outputs JSON response and exits.
     */
    public function ajax_search_invoices() {
        // Verify nonce
        check_ajax_referer( 'freefactu_unified_invoice_action', 'security' );

        // Verify capability
        if ( ! current_user_can( 'manage_verifactu' ) ) {
            wp_send_json_error( __( 'Permiso denegado.', 'freefactu' ) );
        }

        $search = isset( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : '';
        $page = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 1;
        $per_page = 10;

        global $wpdb;
        $table_name = Freefactu_DB::get_table_name();

        // Build search query - include both regular invoices (F1/null) and rectificativas (R1-R5)
        // All successful invoices can be rectified
        // Build search query - include both regular invoices (F1/null) and rectificativas (R1-R5)
        // All successful invoices can be rectified
        
        // Use separate query paths to avoid dynamic SQL interpolation warnings
        if ( ! empty( $search ) ) {
            $like = '%' . $wpdb->esc_like( $search ) . '%';
            
            // Count query

            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
            $total = $wpdb->get_var( $wpdb->prepare(
                "SELECT COUNT(*) FROM " . esc_sql( $table_name ) . " WHERE status = 'success' AND (invoice_number LIKE %s OR order_id LIKE %s)",
                $like,
                $like
            ) );

            // Results query
            $offset = ( $page - 1 ) * $per_page;
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
            $invoices = $wpdb->get_results( $wpdb->prepare(
                "SELECT id, order_id, invoice_number, invoice_date, total, csv, tipo_factura 
                 FROM " . esc_sql( $table_name ) . " 
                 WHERE status = 'success' AND (invoice_number LIKE %s OR order_id LIKE %s)
                 ORDER BY invoice_date DESC 
                 LIMIT %d OFFSET %d",
                 $like,
                 $like,
                 $per_page,
                 $offset
            ) );

        } else {
            // Count query - simplified

            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
            $total = $wpdb->get_var( "SELECT COUNT(*) FROM " . esc_sql( $table_name ) . " WHERE status = 'success'" );

            // Results query
            $offset = ( $page - 1 ) * $per_page;
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
            $invoices = $wpdb->get_results( $wpdb->prepare(
                "SELECT id, order_id, invoice_number, invoice_date, total, csv, tipo_factura 
                 FROM " . esc_sql( $table_name ) . " 
                 WHERE status = 'success'
                 ORDER BY invoice_date DESC 
                 LIMIT %d OFFSET %d",
                 $per_page,
                 $offset
            ) );
        }

        // Format for response
        $results = array();
        foreach ( $invoices as $invoice ) {
            $order = wc_get_order( $invoice->order_id );
            $customer_name = $order ? $order->get_billing_first_name() . ' ' . $order->get_billing_last_name() : __( 'Pedido eliminado', 'freefactu' );
            
            // Check if this is a rectificativa (tipo_factura starts with 'R')
            $is_rectificativa = ! empty( $invoice->tipo_factura ) && strpos( $invoice->tipo_factura, 'R' ) === 0;
            
            $results[] = array(
                'id'               => $invoice->id,
                'order_id'         => $invoice->order_id,
                'invoice_number'   => $invoice->invoice_number,
                'date'             => date_i18n( get_option( 'date_format' ), strtotime( $invoice->invoice_date ) ),
                'total'            => number_format( $invoice->total, 2, ',', '.' ) . ' ' . get_woocommerce_currency_symbol(),
                'customer'         => $customer_name,
                'csv'              => $invoice->csv,
                'is_rectificativa' => $is_rectificativa,
                'tipo_factura'     => $invoice->tipo_factura,
            );
        }

        wp_send_json_success( array(
            'invoices'   => $results,
            'total'      => (int) $total,
            'page'       => $page,
            'per_page'   => $per_page,
            'total_pages' => ceil( $total / $per_page ),
        ) );
    }

    /**
     * AJAX handler for standalone PDF generation.
     *
     * Creates a Verifactu invoice and returns success/error response.
     * Used when clicking "Create Invoice" button in standalone mode.
     *
     * @return void Outputs JSON response and exits.
     */
    public function handle_standalone_pdf_request() {
        // Verify nonce
        check_ajax_referer( 'freefactu_standalone_pdf', 'security' );

        // Verify capability
        if ( ! current_user_can( 'manage_woocommerce' ) ) {
            wp_send_json_error( __( 'Permiso denegado.', 'freefactu' ) );
        }

        // Get order ID
        $order_id = isset( $_POST['order_id'] ) ? absint( wp_unslash( $_POST['order_id'] ) ) : 0;
        if ( ! $order_id ) {
            wp_send_json_error( __( 'ID de pedido inválido.', 'freefactu' ) );
        }

        // Check if invoice exists in database first
        $invoice = Freefactu_DB::get_invoice_by_order_id( $order_id );

        // If invoice exists and is successful, return view URL (works even if order is deleted)
        if ( $invoice && ! empty( $invoice->invoice_number ) && $invoice->status === 'success' ) {
            // Invoice exists and is successful, return success with view URL
            $view_url = wp_nonce_url(
                admin_url( 'admin-post.php?action=freefactu_standalone_pdf&order_id=' . $order_id . '&output=inline' ),
                'freefactu_standalone_pdf_' . $order_id
            );
            // Decode HTML entities for JavaScript usage
            $view_url = html_entity_decode( $view_url );
            wp_send_json_success( array(
                'message'        => __( 'Factura ya existe.', 'freefactu' ),
                'invoice_number' => $invoice->invoice_number,
                'view_url'       => $view_url,
            ) );
        }

        // Get order (required for creating/retrying invoice)
        $order = wc_get_order( $order_id );
        if ( ! $order ) {
            // Order deleted - can only view existing invoice, cannot create/retry
            if ( $invoice ) {
                wp_send_json_error( __( 'El pedido ha sido eliminado. Solo se puede ver la factura existente, no se puede regenerar.', 'freefactu' ) );
            } else {
                wp_send_json_error( __( 'Pedido no encontrado.', 'freefactu' ) );
            }
        }

        // If status is alert, we'll retry the operation below

        // Use the Client to generate and send to Verifactu
        // Get client instance with proper dependencies
        $generator = Freefactu_Factory::create_generator();
        $environment = Freefactu_Factory::create_environment();
        $client = new Freefactu_Client( $generator, $environment );

        try {
            $result = $client->send_invoice_to_verifactu( $order_id );

            if ( is_wp_error( $result ) ) {
                wp_send_json_error( $result->get_error_message() );
            }

            if ( isset( $result['csv'] ) && ! empty( $result['csv'] ) ) {
                // Get invoice data from database (already saved by client)
                $invoice = Freefactu_DB::get_invoice_by_order_id( $order_id );

                // Build view URL - use html_entity_decode to ensure proper URL for JavaScript
                $view_url = wp_nonce_url(
                    admin_url( 'admin-post.php?action=freefactu_standalone_pdf&order_id=' . $order_id . '&output=inline' ),
                    'freefactu_standalone_pdf_' . $order_id
                );
                // Decode HTML entities for JavaScript usage
                $view_url = html_entity_decode( $view_url );

                wp_send_json_success( array(
                    'message'        => __( 'Factura enviada correctamente.', 'freefactu' ),
                    'csv'            => $result['csv'],
                    'invoice_number' => $invoice ? $invoice->invoice_number : '',
                    'view_url'       => $view_url,
                ) );
            } else {
                $msg = isset( $result['message'] ) ? $result['message'] : __( 'Factura enviada pero no se recibió CSV.', 'freefactu' );
                wp_send_json_error( $msg );
            }
        } catch ( \Throwable $e ) {
            Freefactu_Logger::log( 'Standalone PDF Error: ' . $e->getMessage(), 'error' );
            wp_send_json_error( 'Error: ' . $e->getMessage() );
        }
    }

    /**
     * Handle standalone PDF output (view or download).
     *
     * Outputs the PDF directly to the browser for viewing or downloading.
     * Checks for stored PDF snapshot first, generates new if not exists.
     *
     * @return void Outputs PDF and exits.
     */
    public function handle_standalone_pdf_output() {
        // Verify capability
        if ( ! current_user_can( 'manage_woocommerce' ) ) {
            wp_die( esc_html( __( 'Permiso denegado.', 'freefactu' ) ) );
        }

        // Get order ID
        $order_id = isset( $_GET['order_id'] ) ? absint( $_GET['order_id'] ) : 0;
        if ( ! $order_id ) {
            wp_die( esc_html__( 'ID de pedido inválido.', 'freefactu' ) );
        }

        // Verify nonce
        if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'freefactu_standalone_pdf_' . $order_id ) ) {
            wp_die( esc_html__( 'Enlace de seguridad inválido.', 'freefactu' ) );
        }

        // Check for invoice in database first
        $invoice = Freefactu_DB::get_invoice_by_order_id( $order_id );
        if ( ! $invoice ) {
            wp_die( esc_html__( 'Factura no encontrada.', 'freefactu' ) );
        }

        // Get order (may be null if deleted)
        $order = wc_get_order( $order_id );

        // Determine output mode (sanitized to only allow 'download' or default to 'inline')
        $output_mode = isset( $_GET['output'] ) && sanitize_key( wp_unslash( $_GET['output'] ) ) === 'download' ? 'download' : 'inline';

        // Check for stored PDF path in database
        $stored_pdf_path = $invoice->pdf_path;
        // Convert stored relative path to absolute for file operations
        $absolute_stored_path = Freefactu_Filesystem::to_absolute_path( $stored_pdf_path );

        if ( ! empty( $stored_pdf_path ) && Freefactu_Filesystem::exists( $absolute_stored_path ) ) {
            // Serve stored PDF
            $this->serve_pdf_file_from_invoice( $absolute_stored_path, $invoice, $output_mode );
        } else {
            // PDF file is missing - check if invoice was sent to AEAT
            // Only use snapshot if invoice was successfully sent to AEAT
            $invoice_sent_to_aeat = ! empty( $invoice->csv );
            $has_snapshot = ! empty( $invoice->invoice_snapshot );

            if ( $invoice_sent_to_aeat && $has_snapshot ) {
                // Invoice sent to AEAT with snapshot - use snapshot to maintain consistency
                Freefactu_Logger::log( "PDF missing for order #{$order_id}, invoice sent to AEAT, regenerating from snapshot", 'info' );
                
                $pdf_content = $this->regenerate_pdf_from_snapshot( $invoice );

                if ( is_wp_error( $pdf_content ) ) {
                    wp_die( esc_html__( 'Error al regenerar el PDF desde snapshot: ', 'freefactu' ) . esc_html( $pdf_content->get_error_message() ) );
                }

                // Save the regenerated PDF
                $upload_dir = wp_upload_dir();
                $base_dir = $upload_dir['basedir'] . '/freefactu/invoices';

                if ( ! Freefactu_Filesystem::exists( $base_dir ) ) {
                    Freefactu_Filesystem::mkdir( $base_dir );
                    Freefactu_Filesystem::put_contents( $base_dir . '/.htaccess', 'Deny from all' );
                    Freefactu_Filesystem::put_contents( $base_dir . '/index.php', '<?php // Silence is golden' );
                }

                $safe_number = str_replace( array( '/', '\\', ' ' ), '-', $invoice->invoice_number );
                $pdf_path = $base_dir . '/verifactu-' . $safe_number . '.pdf';

                $result = Freefactu_Filesystem::put_contents( $pdf_path, $pdf_content );

                if ( $result === false ) {
                    wp_die( esc_html__( 'No se pudo guardar el PDF regenerado.', 'freefactu' ) );
                }

                // Update database with relative PDF path for portability
                $relative_path = Freefactu_Filesystem::to_relative_path( $pdf_path );
                Freefactu_DB::update_invoice_pdf( $order_id, $relative_path );

                Freefactu_Logger::log( "PDF regenerado desde snapshot para pedido #{$order_id}", 'info' );

                // Serve the newly saved PDF
                $this->serve_pdf_file_from_invoice( $pdf_path, $invoice, $output_mode );
            } elseif ( ! $order ) {
                // Order deleted - cannot regenerate
                if ( $has_snapshot ) {
                    wp_die( esc_html__( 'El PDF no existe y el pedido ha sido eliminado. La factura no fue enviada a AEAT, no se puede regenerar.', 'freefactu' ) );
                } else {
                    wp_die( esc_html__( 'El PDF no existe, el pedido ha sido eliminado y no hay snapshot disponible para regenerar el PDF.', 'freefactu' ) );
                }
            } else {
                // Order exists but invoice not sent to AEAT yet - regenerate from current order
                Freefactu_Logger::log( "PDF missing for order #{$order_id}, invoice not sent to AEAT, regenerating from current order", 'info' );
                
                $pdf_generator = Freefactu_Factory::create_generator( $order );

                // Save the PDF first (save_pdf already stores relative path)
                $pdf_path = $pdf_generator->save_pdf();

                if ( is_wp_error( $pdf_path ) ) {
                    wp_die( esc_html( $pdf_path->get_error_message() ) );
                }

                // Note: save_pdf() already updates the database with relative path

                // Now serve the newly saved PDF (pdf_path is absolute from save_pdf return)
                $this->serve_pdf_file_from_invoice( $pdf_path, $invoice, $output_mode );
            }
        }
    }

    /**
     * Serve a PDF file to the browser.
     *
     * @param string   $file_path   Path to the PDF file.
     * @param WC_Order $order       The order object.
     * @param string   $output_mode Output mode: 'inline' or 'download'.
     * @return void
     */
    private function serve_pdf_file( $file_path, $order, $output_mode = 'inline' ) {
        // Clean output buffer
        if ( ob_get_length() ) {
            ob_end_clean();
        }

        // Get filename from database
        $invoice = Freefactu_DB::get_invoice_by_order_id( $order->get_id() );
        $invoice_number = $invoice ? $invoice->invoice_number : '';
        if ( empty( $invoice_number ) ) {
            $invoice_number = $order->get_id();
        }
        $safe_number = str_replace( array( '/', '\\', ' ' ), '-', $invoice_number );
        $filename = 'verifactu-' . $safe_number . '.pdf';

        // Set headers
        header( 'Content-Type: application/pdf' );
        header( 'Content-Length: ' . filesize( $file_path ) );

        if ( $output_mode === 'download' ) {
            header( 'Content-Disposition: attachment; filename="' . sanitize_file_name( $filename ) . '"' );
        } else {
            header( 'Content-Disposition: inline; filename="' . sanitize_file_name( $filename ) . '"' );
        }

        header( 'Cache-Control: private, max-age=0, must-revalidate' );
        header( 'Pragma: public' );

        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile
        readfile( $file_path );
        exit;
    }

    /**
     * Serve a PDF file to the browser using invoice data.
     *
     * Used when order may be deleted but invoice data exists.
     *
     * @param string $file_path   Path to the PDF file.
     * @param object $invoice     The invoice database object.
     * @param string $output_mode Output mode: 'inline' or 'download'.
     * @return void
     */
    private function serve_pdf_file_from_invoice( $file_path, $invoice, $output_mode = 'inline' ) {
        // Clean output buffer
        if ( ob_get_length() ) {
            ob_end_clean();
        }

        // Get filename from invoice
        $invoice_number = $invoice->invoice_number;
        if ( empty( $invoice_number ) ) {
            $invoice_number = $invoice->order_id;
        }
        $safe_number = str_replace( array( '/', '\\', ' ' ), '-', $invoice_number );
        $filename = 'verifactu-' . $safe_number . '.pdf';

        // Set headers
        header( 'Content-Type: application/pdf' );
        header( 'Content-Length: ' . filesize( $file_path ) );

        if ( $output_mode === 'download' ) {
            header( 'Content-Disposition: attachment; filename="' . sanitize_file_name( $filename ) . '"' );
        } else {
            header( 'Content-Disposition: inline; filename="' . sanitize_file_name( $filename ) . '"' );
        }

        header( 'Cache-Control: private, max-age=0, must-revalidate' );
        header( 'Pragma: public' );

        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile
        readfile( $file_path );
        exit;
    }

    /**
     * Regenerate PDF from snapshot data when order is deleted.
     *
     * @param object $invoice The invoice database object with snapshot data.
     * @return string|WP_Error PDF content or WP_Error on failure.
     */
    private function regenerate_pdf_from_snapshot( $invoice ) {
        // Check if snapshot exists
        if ( empty( $invoice->invoice_snapshot ) ) {
            return new WP_Error( 'no_snapshot', __( 'No hay snapshot disponible.', 'freefactu' ) );
        }

        // Decode snapshot data
        $snapshot = json_decode( $invoice->invoice_snapshot, true );
        if ( ! $snapshot || ! is_array( $snapshot ) ) {
            return new WP_Error( 'invalid_snapshot', __( 'Snapshot de factura inválido.', 'freefactu' ) );
        }

        // For rectificativa invoices: add original invoice info and inherit NIF if needed
        if ( ! empty( $invoice->rectified_invoice_id ) ) {
            $original_invoice = Freefactu_DB::get_invoice( $invoice->rectified_invoice_id );
            
            // Add rectificativa metadata to snapshot for PDF display
            $snapshot['is_rectificativa'] = true;
            $snapshot['tipo_factura'] = $invoice->tipo_factura ?? ''; // R1, R2, R3, R4, R5
            $snapshot['metodo_rectificativa'] = $invoice->tipo_rectificativa ?? ''; // S (Sustitución) or I (Diferencias)
            
            if ( $original_invoice ) {
                $snapshot['rectified_invoice'] = array(
                    'invoice_number' => $original_invoice->invoice_number,
                    'invoice_date'   => $original_invoice->invoice_date,
                );
            }
            
            // If current snapshot has no customer NIF but original invoice does,
            // use the customer NIF from the original invoice (same logic as when sending to AEAT)
            $current_customer_nif = isset( $snapshot['customer']['nif'] ) 
                ? strtoupper( preg_replace( '/[^A-Z0-9]/i', '', $snapshot['customer']['nif'] ) ) 
                : '';
            
            if ( empty( $current_customer_nif ) && $original_invoice && ! empty( $original_invoice->invoice_snapshot ) ) {
                $orig_snapshot = json_decode( $original_invoice->invoice_snapshot, true );
                if ( $orig_snapshot && isset( $orig_snapshot['customer']['nif'] ) ) {
                    $orig_customer_nif = strtoupper( preg_replace( '/[^A-Z0-9]/i', '', $orig_snapshot['customer']['nif'] ) );
                    if ( ! empty( $orig_customer_nif ) ) {
                        // Inherit customer NIF from original invoice
                        $snapshot['customer']['nif'] = $orig_customer_nif;
                        Freefactu_Logger::log( 
                            sprintf( 
                                'Rectificativa PDF (regenerate): Inherited customer NIF %s from original invoice #%d', 
                                $orig_customer_nif,
                                $invoice->rectified_invoice_id
                            ), 
                            'debug' 
                        );
                    }
                }
            }
        }

        // Create a generator (without order since it's deleted)
        $pdf_generator = Freefactu_Factory::create_generator();

        // Generate PDF from snapshot data
        return $pdf_generator->generate_from_snapshot( $invoice->order_id, $snapshot );
    }

    /**
     * AJAX handler for creating a new series.
     *
     * @return void Outputs JSON response and exits.
     */
    public function ajax_create_series() {
        // Verify nonce
        if ( ! check_ajax_referer( 'freefactu_series_nonce', 'security', false ) ) {
            wp_send_json_error( __( 'Error de seguridad. Por favor, recarga la página.', 'freefactu' ), 403 );
        }

        // Verify capability
        if ( ! current_user_can( 'manage_verifactu' ) ) {
            wp_send_json_error( __( 'Permiso denegado.', 'freefactu' ), 403 );
        }

        // Validate required fields
        $name = isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : '';
        $type = isset( $_POST['type'] ) ? sanitize_text_field( wp_unslash( $_POST['type'] ) ) : '';

        if ( empty( $name ) || empty( $type ) ) {
            wp_send_json_error( __( 'Nombre y tipo son requeridos.', 'freefactu' ), 400 );
        }

        $data = array(
            'name'    => $name,
            'type'    => $type,
            'prefix'  => isset( $_POST['prefix'] ) ? sanitize_text_field( wp_unslash( $_POST['prefix'] ) ) : '',
            'suffix'  => isset( $_POST['suffix'] ) ? sanitize_text_field( wp_unslash( $_POST['suffix'] ) ) : '',
            'padding' => isset( $_POST['padding'] ) ? absint( $_POST['padding'] ) : 0,
        );

        $result = Freefactu_Series_Store::create_series( $data );

        if ( is_wp_error( $result ) ) {
            wp_send_json_error( $result->get_error_message(), 500 );
        }

        wp_send_json_success( array(
            'message'   => __( 'Serie creada correctamente.', 'freefactu' ),
            'series_id' => $result,
        ) );
    }

    /**
     * AJAX handler for setting active series.
     *
     * @return void Outputs JSON response and exits.
     */
    public function ajax_set_active_series() {
        // Verify nonce
        if ( ! check_ajax_referer( 'freefactu_series_nonce', 'security', false ) ) {
            wp_send_json_error( __( 'Error de seguridad. Por favor, recarga la página.', 'freefactu' ), 403 );
        }

        // Verify capability
        if ( ! current_user_can( 'manage_verifactu' ) ) {
            wp_send_json_error( __( 'Permiso denegado.', 'freefactu' ), 403 );
        }

        $series_id = isset( $_POST['series_id'] ) ? absint( wp_unslash( $_POST['series_id'] ) ) : 0;

        if ( ! $series_id ) {
            wp_send_json_error( __( 'ID de serie no válido.', 'freefactu' ), 400 );
        }

        $result = Freefactu_Series_Store::set_active_series( $series_id );

        if ( is_wp_error( $result ) ) {
            wp_send_json_error( $result->get_error_message(), 500 );
        }

        wp_send_json_success( array(
            'message' => __( 'Serie activa actualizada.', 'freefactu' ),
        ) );
    }

    /**
     * AJAX handler for deleting a series.
     *
     * @return void Outputs JSON response and exits.
     */
    public function ajax_delete_series() {
        // Verify nonce
        if ( ! check_ajax_referer( 'freefactu_series_nonce', 'security', false ) ) {
            wp_send_json_error( __( 'Error de seguridad. Por favor, recarga la página.', 'freefactu' ), 403 );
        }

        // Verify capability
        if ( ! current_user_can( 'manage_verifactu' ) ) {
            wp_send_json_error( __( 'Permiso denegado.', 'freefactu' ), 403 );
        }

        $series_id = isset( $_POST['series_id'] ) ? absint( wp_unslash( $_POST['series_id'] ) ) : 0;

        if ( ! $series_id ) {
            wp_send_json_error( __( 'ID de serie no válido.', 'freefactu' ), 400 );
        }

        $result = Freefactu_Series_Store::delete_series( $series_id );

        if ( is_wp_error( $result ) ) {
            wp_send_json_error( $result->get_error_message(), 500 );
        }

        wp_send_json_success( array(
            'message' => __( 'Serie eliminada correctamente.', 'freefactu' ),
        ) );
    }

    /**
     * AJAX handler for acknowledging system requirements.
     *
     * Saves the user's acknowledgment that they have verified the system
     * requirements are met. Includes a fingerprint to invalidate the
     * acknowledgment if requirements change.
     *
     * @return void Outputs JSON response and exits.
     */
    public function ajax_ack_system_requirements() {
        // Verify nonce
        if ( ! check_ajax_referer( 'freefactu_ack_system_requirements', 'security', false ) ) {
            wp_send_json_error( __( 'Error de seguridad. Por favor, recarga la página.', 'freefactu' ), 403 );
        }

        // Verify capability
        if ( ! current_user_can( 'manage_verifactu' ) ) {
            wp_send_json_error( __( 'Permiso denegado.', 'freefactu' ), 403 );
        }

        // Get fingerprint from request
        $fingerprint = isset( $_POST['fingerprint'] ) ? sanitize_text_field( wp_unslash( $_POST['fingerprint'] ) ) : '';

        if ( empty( $fingerprint ) ) {
            wp_send_json_error( __( 'Fingerprint no proporcionado.', 'freefactu' ), 400 );
        }

        // Verify that all system requirements are currently met
        $system_check = array(
            'php_version_ok' => version_compare( PHP_VERSION, '8.2', '>=' ),
            'openssl_ok' => extension_loaded( 'openssl' ),
            'dom_ok' => extension_loaded( 'dom' ),
        );
        
        $current_fingerprint = md5( serialize( array(
            'php' => $system_check['php_version_ok'],
            'openssl' => $system_check['openssl_ok'],
            'dom' => $system_check['dom_ok'],
        ) ) );

        // Verify fingerprint matches (prevents acknowledging outdated state)
        if ( $fingerprint !== $current_fingerprint ) {
            wp_send_json_error( __( 'Los requisitos del sistema han cambiado. Por favor, recarga la página.', 'freefactu' ), 400 );
        }

        // Verify all requirements are actually met
        if ( ! $system_check['php_version_ok'] || ! $system_check['openssl_ok'] || ! $system_check['dom_ok'] ) {
            wp_send_json_error( __( 'No puedes confirmar los requisitos del sistema porque no se cumplen todos.', 'freefactu' ), 400 );
        }

        // Save the acknowledgment
        $ack_data = array(
            'acknowledged' => true,
            'fingerprint' => $fingerprint,
            'user_id' => get_current_user_id(),
            'timestamp' => time(),
        );

        update_option( 'freefactu_system_requirements_ack', $ack_data );

        wp_send_json_success( array(
            'message' => __( 'Requisitos del sistema confirmados.', 'freefactu' ),
        ) );
    }
}
