<?php
/**
 * Freefactu Client Class
 *
 * Handles invoice submission to AEAT Verifactu system, PDF generation,
 * and QR code management. This class is mode-agnostic and environment-agnostic,
 * using dependency injection to receive Generator and Environment implementations.
 *
 * @package Freefactu
 * @since 0.5.0
 */

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

use Freefactu_Vendor\GuzzleHttp\HandlerStack;
use Freefactu_Vendor\GuzzleHttp\Middleware;

use Freefactu_Vendor\josemmo\Verifactu\Services\AeatClient;
use Freefactu_Vendor\josemmo\Verifactu\Models\ComputerSystem;
use Freefactu_Vendor\josemmo\Verifactu\Models\Records\RegistrationRecord;
use Freefactu_Vendor\josemmo\Verifactu\Models\Records\InvoiceIdentifier;
use Freefactu_Vendor\josemmo\Verifactu\Models\Records\FiscalIdentifier;
use Freefactu_Vendor\josemmo\Verifactu\Models\Records\BreakdownDetails;
use Freefactu_Vendor\josemmo\Verifactu\Models\Records\InvoiceType;
use Freefactu_Vendor\josemmo\Verifactu\Models\Records\CorrectiveType;
use Freefactu_Vendor\josemmo\Verifactu\Models\Records\RegimeType;
use Freefactu_Vendor\josemmo\Verifactu\Models\Records\OperationType;
use Freefactu_Vendor\josemmo\Verifactu\Models\Records\TaxType;

/**
 * Class Freefactu_Client
 *
 * Core client for Verifactu invoice operations. Uses the Factory Pattern
 * through dependency injection to remain independent of mode (Standalone/Integrated)
 * and environment (Production/Test).
 *
 * Dependency Injection Pattern:
 * Instead of creating Generator and Environment instances internally,
 * this class receives them through its constructor. This allows:
 * - Easy testing with mock implementations
 * - Clean separation of concerns
 * - No mode or environment conditionals in business logic
 * - Flexibility to swap implementations without modifying this class
 *
 * Example usage:
 * ```php
 * // Factory creates appropriate implementations based on configuration
 * $generator = Freefactu_Factory::create_generator( $order );
 * $environment = Freefactu_Factory::create_environment();
 *
 * // Inject dependencies into client
 * $client = new Freefactu_Client( $generator, $environment );
 *
 * // Client uses abstractions without knowing which implementations it has
 * $client->send_invoice_to_verifactu( $order_id );
 * ```
 */
class Freefactu_Client {

	/**
	 * Generator instance for PDF generation.
	 *
	 * Injected via constructor. Uses Standalone_Generator for DOMPDF-based
	 * PDF generation. Client doesn't need to know implementation details.
	 *
	 * @var Freefactu_Generator_Interface
	 */
	private $generator;

	/**
	 * Environment instance for environment-specific operations.
	 *
	 * Injected via constructor. Can be Production_Environment or Test_Environment
	 * depending on settings. Client doesn't need to know which.
	 *
	 * @var Freefactu_Environment_Interface
	 */
	private $environment;

	/**
	 * Constructor.
	 *
	 * What this function does:
	 * - Receives generator and environment via dependency injection
	 * - Stores dependencies for use throughout the class
	 * - Registers admin_post action for manual invoice sending
	 * - Makes class mode-agnostic and environment-agnostic
	 *
	 * Where it's called from:
	 * - freefactu.php: __construct() method during plugin initialization
	 *
	 * @param Freefactu_Generator_Interface   $generator   Generator implementation (Standalone or Integrated).
	 * @param Freefactu_Environment_Interface $environment Environment implementation (Production or Test).
	 */
	public function __construct( $generator, $environment ) {
		$this->generator   = $generator;
		$this->environment = $environment;

		// Hook into WooCommerce order completion - REMOVED for manual submission
		// add_action( 'woocommerce_order_status_completed', array( $this, 'send_invoice_to_verifactu' ), 10, 1 );

		// Manual Submission Handler
		add_action( 'admin_post_freefactu_send_invoice', array( $this, 'handle_manual_send' ) );

		// Hook into PDF Invoices - REMOVED to avoid duplication, handled in main class
		// add_action( 'wpo_wcpdf_after_order_details', array( $this, 'add_qr_to_pdf' ), 10, 2 );
	}

    /**
     * Send invoice to Verifactu (AEAT).
     *
     * Main method for submitting invoices to the Spanish Tax Agency (AEAT).
     * Handles the complete flow: XML generation, signing, AEAT submission,
     * QR code generation, and PDF creation.
     *
     * Uses injected Generator and Environment abstractions to remain mode-agnostic
     * and environment-agnostic. The method doesn't know or care whether it's using
     * Standalone or Integrated mode, or Production or Test environment.
     *
     * @param int $order_id WooCommerce order ID.
     * @return array|WP_Error Array with success status and CSV on success, WP_Error on failure.
     */
    public function send_invoice_to_verifactu( $order_id, $rectificativa_data = null, $is_subsanation = false, $manual_previous_hash = null, $manual_previous_id = null, $manual_previous_date = null ) {
        $order = wc_get_order( $order_id );
        if ( ! $order ) {
            return new WP_Error( 'invalid_order', __( 'Order not found.', 'freefactu' ) );
        }

        $is_rectificativa = ! empty( $rectificativa_data );

        if ( $is_rectificativa ) {
            Freefactu_Logger::log( sprintf( 'Creating rectificativa invoice for order #%d', $order_id ), 'info' );
        }

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

        // Check if already sent successfully - read from database (only for standard invoices)
        $existing_invoice = Freefactu_DB::get_invoice_by_order_id( $order_id );
        
        if ( ! $is_rectificativa ) {
            $existing_csv = $existing_invoice ? $existing_invoice->csv : '';
            $existing_status = $existing_invoice ? $existing_invoice->status : '';
            $existing_aeat_status = $existing_invoice && isset( $existing_invoice->aeat_final_status ) 
                ? $existing_invoice->aeat_final_status 
                : 'pending';

            // Block if already accepted (unless we're retrying QR/PDF)
            if ( $existing_csv && $existing_status === 'success' ) {
                return new WP_Error( 'already_sent', __( 'Invoice already sent to Verifactu.', 'freefactu' ) );
            }

            // Block subsanation if previously accepted
            if ( $is_subsanation && $existing_aeat_status === 'accepted' ) {
                return new WP_Error( 
                    'cannot_subsanar_accepted', 
                    __( 'No se puede subsanar una factura ya aceptada por la AEAT. Use una factura rectificativa para hacer correcciones.', 'freefactu' ) 
                );
            }

            if ( $existing_csv && ! $is_subsanation ) {
                return $this->retry_qr_pdf_generation( $order );
            }
        }

        // Get Chaining Data
        $chaining_data = $this->get_chaining_data_from_db();

        // Get Invoice Number
        $invoice_number = '';
        $invoice_number_plain_raw = null; // For rectificativas: store the raw sequential number
        if ( $is_rectificativa ) {
            // Use the series store directly for rectificativa numbering
            $series = Freefactu_Series_Store::get_active_series( 'rectificativa' );
            
            if ( ! $series ) {
                // No active series - create a default one
                Freefactu_Logger::log( 'No active rectificativa series found. Creating default.', 'warning' );
                Freefactu_Series_Store::create_series( array(
                    'name'    => 'Serie Rectificativas',
                    'type'    => 'rectificativa',
                    'prefix'  => 'R-',
                    'suffix'  => '',
                    'padding' => 6,
                ) );
                $series = Freefactu_Series_Store::get_active_series( 'rectificativa' );
            }

            // Increment series counter and get new number
            $number = Freefactu_Series_Store::increment( $series->id );
            
            if ( is_wp_error( $number ) ) {
                Freefactu_Logger::log( 'Failed to increment rectificativa series: ' . $number->get_error_message(), 'error' );
                $number = time();
            }

            // Format using series settings
            $invoice_number = Freefactu_Series_Store::format_invoice_number( $series, $number, $order );
            $invoice_number_plain_raw = $number; // Store the raw sequential number
        } else {
            $invoice_number = $existing_invoice && ! empty( $existing_invoice->invoice_number ) ? $existing_invoice->invoice_number : '';
            if ( ! $invoice_number ) {
                $invoice_number = $this->get_formatted_invoice_number( $order );
                if ( is_wp_error( $invoice_number ) ) {
                    return $invoice_number;
                }
            }
        }

        // --- 0. Create/Get Snapshot ---
        // For SUBSANATION: Always recreate snapshot to pick up corrected data
        // For normal retry: Use existing snapshot if available
        $snapshot = '';
        $force_new_snapshot = $is_subsanation; // Subsanation must use fresh data
        
        if ( ! $force_new_snapshot && $existing_invoice && ! empty( $existing_invoice->invoice_snapshot ) ) {
            $snapshot = $existing_invoice->invoice_snapshot;
            Freefactu_Logger::log( sprintf( 'Using existing snapshot for order #%d', $order_id ), 'debug' );
        }

        if ( empty( $snapshot ) || $force_new_snapshot ) {
            $snapshot = $this->create_invoice_snapshot( $order );
            if ( empty( $snapshot ) ) {
                Freefactu_Logger::log( sprintf( 'Failed to create snapshot for order #%d', $order_id ), 'error' );
                return new WP_Error( 'snapshot_failed', __( 'Failed to create invoice snapshot. The invoice was not created.', 'freefactu' ) );
            }
            
            if ( $is_subsanation ) {
                Freefactu_Logger::log( sprintf( 'Created NEW snapshot for subsanation of order #%d', $order_id ), 'info' );
            }
        }

        $snapshot_data = json_decode( $snapshot, true );
        if ( ! $snapshot_data ) {
             Freefactu_Logger::log( sprintf( 'Invalid snapshot data for order #%d', $order_id ), 'error' );
             return new WP_Error( 'invalid_snapshot', __( 'Invalid invoice snapshot data.', 'freefactu' ) );
        }

        Freefactu_Logger::log( sprintf( 'Snapshot ready for order #%d before AEAT submission', $order_id ), 'debug' );

        try {
            // --- 1. Configure Client ---
            $options = get_option( 'freefactu_settings' );
            $is_prod = ( isset( $options['environment'] ) && $options['environment'] === 'production' );
            $company_nif = isset( $options['company_nif'] ) ? $options['company_nif'] : '';
            $company_name = isset( $options['company_name'] ) ? $options['company_name'] : '';

            // 1a. Computer System - Get SIF data from the latest declaration
            $latest_declaration = Freefactu_DB::get_latest_declaration();
            if ( ! $latest_declaration ) {
                throw new \Exception( 
                    __( 'No existe una Declaración Responsable. Debe crear una declaración en la pestaña "SIF / Declaraciones" antes de poder enviar facturas a AEAT.', 'freefactu' )
                );
            }
            
            $system = new ComputerSystem();
            $system->name = $latest_declaration->software_name;
            $system->id = '01'; // Sequential ID within the SIF
            $system->version = $latest_declaration->software_version;
            $system->installationNumber = $latest_declaration->installation_number;
            $system->onlySupportsVerifactu = true;
            $system->supportsMultipleTaxpayers = false;
            $system->hasMultipleTaxpayers = false;
            // Producer (vendor) data from declaration - the entity that produces/develops the SIF
            $system->vendorNif = $latest_declaration->producer_nif;
            $system->vendorName = $latest_declaration->producer_name;

            // 1b. Taxpayer (Emisor) - Constructor is (name, nif)
            $taxpayer = new FiscalIdentifier( $company_name, $company_nif );

            // 1c. Create custom Guzzle client with SSL settings
            $ssl_verify = isset( $options['ssl_verify'] ) && $options['ssl_verify'] === '1';
            
            // Setup history middleware to capture raw XML response
            $container = array();
            $history = Middleware::history( $container );
            $stack = HandlerStack::create();
            $stack->push( $history );

            $httpClient = new \Freefactu_Vendor\GuzzleHttp\Client( array(
                'verify' => $ssl_verify,
                'handler' => $stack,
            ) );

            // 1d. Instantiate Client - Constructor is (ComputerSystem, FiscalIdentifier, ?GuzzleClient)
            $client = new AeatClient( $system, $taxpayer, $httpClient );
            $client->setProduction( $is_prod );

            // 1d. Set Certificate
            $cert_path = isset( $options['certificate_path'] ) ? $options['certificate_path'] : '';
            $cert_pass = isset( $options['certificate_password'] ) ? Freefactu_Admin::decrypt_password( $options['certificate_password'] ) : '';
            
            if ( empty( $cert_path ) || ! file_exists( $cert_path ) ) {
                throw new Exception( __( 'Certificate file not found or not configured.', 'freefactu' ) );
            }
            $client->setCertificate( $cert_path, $cert_pass );

            // --- 2. Create Invoice Record ---
            $record = new RegistrationRecord();

            // ============================================
            // Veri*Factu Date Handling (AEAT Rules):
            // ============================================
            // 1. FechaOperacion (operationDate) = When operation occurred (order date)
            // 2. FechaExpedicionFactura (issueDate) = When invoice is issued (NOW)
            // 3. FechaHoraHusoGenRegistro (hashedAt) = Technical timestamp (NOW)
            //
            // Rules: operationDate <= issueDate <= hashedAt
            // By using NOW() for issueDate, we guarantee issueDate >= operationDate
            // ============================================

            // FechaExpedicionFactura: Always use current date when invoice is generated
            // This is the ISSUE DATE - when the invoice is created/expedited
            $issue_date_immutable = new \DateTimeImmutable( 'now', new \DateTimeZone( wp_timezone_string() ) );
            $record->invoiceId = new InvoiceIdentifier( $company_nif, $invoice_number, $issue_date_immutable );

            // FechaOperacion: When the operation/sale occurred
            // For rectificativa: use current date (the correction is being made now)
            // For normal invoices: use order completion date, fallback to creation date, then current
            if ( $is_rectificativa ) {
                // For rectificativas, the operation is the correction itself
                $record->operationDate = $issue_date_immutable;
            } else {
                // For normal invoices, operation date is when the sale occurred
                $date_completed = $order->get_date_completed();
                $date_created = $order->get_date_created();
                
                if ( $date_completed ) {
                    // Prefer completion date (when order was marked complete = sale finalized)
                    $operation_date_str = $date_completed->format( 'Y-m-d' );
                } elseif ( $date_created ) {
                    // Fallback to creation date
                    $operation_date_str = $date_created->format( 'Y-m-d' );
                } else {
                    // Ultimate fallback: current date (same as issue date)
                    $operation_date_str = $issue_date_immutable->format( 'Y-m-d' );
                }
                
                $operation_date = new \DateTimeImmutable( $operation_date_str );
                
                // Only set operationDate if it differs from issue date
                // If dates match, AEAT doesn't require explicit operationDate
                if ( $operation_date->format( 'Y-m-d' ) !== $issue_date_immutable->format( 'Y-m-d' ) ) {
                    $record->operationDate = $operation_date;
                }
            }

            // Required properties
            $record->issuerName = $company_name;
            $record->recipients = array(); // Default to empty, will be set if customer has NIF
            $record->correctedInvoices = array();
            $record->replacedInvoices = array();

            // Customer (From Snapshot)
            $customer_nif = '';
            if ( isset( $snapshot_data['customer']['nif'] ) ) {
                $customer_nif = strtoupper( preg_replace( '/[^A-Z0-9]/i', '', $snapshot_data['customer']['nif'] ) );
            }

            if ( ! empty( $customer_nif ) ) {
                $customer_name = $snapshot_data['customer']['name'];
                if ( empty( trim( $customer_name ) ) && isset( $snapshot_data['customer']['company'] ) ) {
                    $customer_name = $snapshot_data['customer']['company'];
                }
                $record->recipients = array( new FiscalIdentifier( $customer_name, $customer_nif ) );
            }

            if ( $is_rectificativa ) {
                // Rectificativa Logic
                $tipo_rect = $rectificativa_data['tipo_rectificativa']; // R1..R5
                $record->invoiceType = InvoiceType::from( $tipo_rect );

                $method = $rectificativa_data['metodo_rectificativa']; // S or I
                // IMPORTANT: isCorrection (Subsanación) should be FALSE for rectificativa invoices!
                // isCorrection=true means we're re-submitting a previously rejected registration record (subsanación).
                // Rectificativas are NEW invoice records with type R1-R5, not corrections of existing records.
                // Setting isCorrection=true causes AEAT error "No existe el registro de facturación" because
                // AEAT looks for the previous record to correct but can't find it.
                $record->isCorrection = false;
                $record->correctiveType = $method === 'S' ? CorrectiveType::Substitution : CorrectiveType::Differences;
                
                // For rectificativas, get customer from the ORIGINAL invoice, not the current order
                $original = $rectificativa_data['original_invoice'];
                
                // Get customer data from original invoice snapshot
                if ( ! empty( $original->invoice_snapshot ) ) {
                    $orig_snapshot = json_decode( $original->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 ) ) {
                            $orig_customer_name = isset( $orig_snapshot['customer']['name'] ) ? $orig_snapshot['customer']['name'] : '';
                            if ( empty( trim( $orig_customer_name ) ) && isset( $orig_snapshot['customer']['company'] ) ) {
                                $orig_customer_name = $orig_snapshot['customer']['company'];
                            }
                            $record->recipients = array( new FiscalIdentifier( $orig_customer_name, $orig_customer_nif ) );
                        }
                    }
                }
                
                // R5 (Rectificativa Simplificada) doesn't require recipients
                // R1-R4 require recipients like regular invoices
                if ( $tipo_rect === 'R5' ) {
                    $record->recipients = array();
                } elseif ( empty( $record->recipients ) ) {
                    // If R1-R4 was requested but no recipient is available (original was simplified invoice),
                    // we must use R5 instead, as R1-R4 require recipients
                    Freefactu_Logger::log( 
                        sprintf( 
                            'Rectificativa type changed from %s to R5: Original invoice had no customer NIF (was simplified invoice)', 
                            $tipo_rect 
                        ), 
                        'warning' 
                    );
                    $tipo_rect = 'R5';
                    $record->invoiceType = InvoiceType::from( 'R5' );
                }
                
                $orig_date_immutable = new \DateTimeImmutable( gmdate( 'Y-m-d', strtotime( $original->invoice_date ) ) );
                $record->correctedInvoices = array(
                    new InvoiceIdentifier( $company_nif, $original->invoice_number, $orig_date_immutable )
                );

                // For substitution method, we need to provide the amounts being corrected
                if ( $method === 'S' ) {
                    // Get original invoice totals from snapshot or use stored values
                    $original_base = 0.0;
                    $original_tax = 0.0;
                    
                    if ( ! empty( $original->invoice_snapshot ) ) {
                        $orig_snapshot = json_decode( $original->invoice_snapshot, true );
                        if ( $orig_snapshot && isset( $orig_snapshot['tax_breakdown'] ) ) {
                            foreach ( $orig_snapshot['tax_breakdown'] as $tax_item ) {
                                $original_base += (float) $tax_item['base'];
                                $original_tax += (float) $tax_item['amount'];
                            }
                        }
                    }
                    
                    // If no snapshot, calculate from total (approximate)
                    if ( $original_base == 0 && ! empty( $original->total ) ) {
                        // Assume 21% IVA for approximation if no breakdown available
                        $original_base = (float) $original->total / 1.21;
                        $original_tax = (float) $original->total - $original_base;
                    }
                    
                    $record->correctedBaseAmount = number_format( $original_base, 2, '.', '' );
                    $record->correctedTaxAmount = number_format( $original_tax, 2, '.', '' );
                }

                $motivo = ! empty( $rectificativa_data['motivo'] ) ? $rectificativa_data['motivo'] : __( 'Rectificación', 'freefactu' );
                $record->description = $motivo;
            } else {
                // Standard Logic
                if ( ! empty( $customer_nif ) ) {
                    $record->invoiceType = InvoiceType::Factura; // F1 - Factura Completa
                } else {
                    $record->invoiceType = InvoiceType::Simplificada; // F2 - Factura Simplificada
                }
                
                // For subsanation with chain repair (error 3000/2007 - invoice already registered):
                // Must send isCorrection=true (Subsanación) instead of new Alta
                // This tells AEAT we're correcting an existing record, not registering a new one
                if ( $is_subsanation && ! empty( $manual_previous_hash ) ) {
                    $record->isCorrection = true;
                    Freefactu_Logger::log( sprintf( 'Chain repair subsanation for order #%d: setting isCorrection=true', $order_id ), 'info' );
                } else {
                    $record->isCorrection = false;
                }
                
                $record->description = apply_filters( 'freefactu_operation_description', __( 'Venta de mercaderías', 'freefactu' ), $order );
            }


            // Taxes (Breakdown from Snapshot)
            $breakdown = array();
            $total_tax_amount = 0.0;
            
            if ( ! empty( $snapshot_data['tax_breakdown'] ) && is_array( $snapshot_data['tax_breakdown'] ) ) {
                foreach ( $snapshot_data['tax_breakdown'] as $tax_item ) {
                    $rate_percent = (float) $tax_item['rate'];
                    $base = (float) $tax_item['base'];
                    $cuota = (float) $tax_item['amount'];
                    
                    // Skip if base is 0 (optional, but good practice)
                    // if ( $base <= 0 ) continue; 

                    $total_tax_amount += $cuota;

                    // Create Breakdown Detail
                    $detail = new BreakdownDetails();
                    $detail->taxType = TaxType::IVA; // 01
                    $detail->regimeType = RegimeType::C01; // General
                    $detail->operationType = OperationType::Subject; // Sujeta y No Exenta
                    $detail->taxRate = number_format( $rate_percent, 2, '.', '' );
                    $detail->baseAmount = number_format( $base, 2, '.', '' );
                    $detail->taxAmount = number_format( $cuota, 2, '.', '' );
                    
                    $breakdown[] = $detail;
                }
            }
            $record->breakdown = $breakdown;
            $record->totalTaxAmount = number_format( $total_tax_amount, 2, '.', '' );

            // Totals - must be strings (From Snapshot)
            // Use snapshot total to ensure it matches PDF
            $snapshot_total = isset( $snapshot_data['totals']['total'] ) ? $snapshot_data['totals']['total'] : $order->get_total();
            $record->totalAmount = number_format( (float) $snapshot_total, 2, '.', '' );


            // Chaining - per library docs: set previousInvoiceId and previousHash
            // If manual_previous_hash is provided (for hash_chain error subsanation), use it instead
            if ( ! empty( $manual_previous_hash ) ) {
                // Manual hash override - used for hash_chain error recovery
                
                // If we have manual ID and Date, construct the full InvoiceIdentifier
                // This is required by the library when a previous hash is present
                if ( ! empty( $manual_previous_id ) && ! empty( $manual_previous_date ) ) {
                    try {
                        $prev_date_immutable = new \DateTimeImmutable( $manual_previous_date );
                        // InvoiceIdentifier requires (IssuerNIF, InvoiceNumber, IssueDate)
                        $record->previousInvoiceId = new InvoiceIdentifier( $company_nif, $manual_previous_id, $prev_date_immutable );
                        Freefactu_Logger::log( sprintf( 'Using manual previous ID (%s) and Date (%s) for chain repair', $manual_previous_id, $manual_previous_date ), 'info' );
                    } catch ( \Exception $e ) {
                        // Fallback if date invalid
                         Freefactu_Logger::log( 'Invalid manual previous date: ' . $e->getMessage(), 'error' );
                         $record->previousInvoiceId = null;
                    }
                } else {
                     // Fallback to null ID if incomplete data (will likely throw error in library but we try)
                     $record->previousInvoiceId = null;
                }

                $record->previousHash = $manual_previous_hash;
                Freefactu_Logger::log( sprintf( 'Using manual previous hash for order #%d: %s', $order_id, substr( $manual_previous_hash, 0, 16 ) . '...' ), 'info' );
            } elseif ( ! empty( $chaining_data ) && isset( $chaining_data['hash'] ) ) {
                $prev_date_immutable = new \DateTimeImmutable( gmdate( 'Y-m-d', strtotime( $chaining_data['date'] ) ) );
                $record->previousInvoiceId = new InvoiceIdentifier( $company_nif, $chaining_data['series'], $prev_date_immutable );
                $record->previousHash = $chaining_data['hash'];
            } else {
                // First record in chain - explicitly set to null per library docs
                $record->previousInvoiceId = null;
                $record->previousHash = null;
            }

            // Calculate hash - MUST set hashedAt BEFORE calling calculateHash() per library docs
            $record->hashedAt = new \DateTimeImmutable( 'now', new \DateTimeZone( wp_timezone_string() ) );
            $record->hash = $record->calculateHash();
            $record->validate();

            // Log the Veri*Factu dates for debugging
            Freefactu_Logger::log( 
                sprintf( 
                    'Veri*Factu dates for order #%d: FechaOperacion=%s, FechaExpedicion=%s, FechaRemision=%s', 
                    $order_id,
                    $record->operationDate ? $record->operationDate->format( 'd-m-Y' ) : '(same as expedicion)',
                    $issue_date_immutable->format( 'd-m-Y' ),
                    $record->hashedAt->format( 'd-m-Y H:i:s' )
                ), 
                'debug' 
            );

            // --- 3. Send to AEAT ---
            $aeatResponse = $client->send( array( $record ) )->wait();

            // Capture XML request and response
            $xml_response = '';
            $xml_request = '';
            // Get the last transaction from the container
            if ( ! empty( $container ) ) {
                $transaction = end( $container );
                if ( isset( $transaction['response'] ) ) {
                    $xml_response = (string) $transaction['response']->getBody();
                }
                if ( isset( $transaction['request'] ) ) {
                    $xml_request = (string) $transaction['request']->getBody();
                }
            }
            
            // If empty, try to see if the response object has it (fallback)
            if ( empty( $xml_response ) && isset( $aeatResponse->raw ) ) {
                 $xml_response = $aeatResponse->raw;
            }

            // Check response status per library docs
            if ( $aeatResponse->status !== \Freefactu_Vendor\josemmo\Verifactu\Models\Responses\ResponseStatus::Correct ) {
                // Get error from first item
                $error_msg = isset( $aeatResponse->items[0] ) ? $aeatResponse->items[0]->errorDescription : __( 'Unknown error', 'freefactu' );
                
                // Save XML even on error if we have it
                // We will use $xml_response in the catch block if available
                
                throw new Exception( $error_msg );
            }

            // Success - get CSV from response
            $csv = $aeatResponse->csv;
            $hash = $record->hash; // Already calculated above

            // --- 4. Database & QR ---
            // Snapshot was already created before AEAT submission
            $invoice_date = current_time( 'mysql' );

            if ( $is_rectificativa ) {
                 // Add rectificativa metadata to snapshot for PDF display (at insert time)
                 $snapshot_data['is_rectificativa'] = true;
                 $snapshot_data['tipo_factura'] = $tipo_rect;
                 $snapshot_data['metodo_rectificativa'] = $rectificativa_data['metodo_rectificativa'];
                 $snapshot_data['motivo_rectificativa'] = ! empty( $rectificativa_data['motivo'] ) ? $rectificativa_data['motivo'] : '';
                 
                 // Add rectified invoice info to snapshot
                 $original_invoice = $rectificativa_data['original_invoice'];
                 $snapshot_data['rectified_invoice'] = array(
                     'invoice_number' => $original_invoice->invoice_number,
                     'invoice_date'   => $original_invoice->invoice_date,
                 );
                 
                 // Re-encode snapshot with the new metadata
                 $snapshot = wp_json_encode( $snapshot_data );
                 
                // Rectificativa specific save logic - use the raw sequential number
                 $db_result = Freefactu_DB::insert_invoice( array(
                    'order_id' => $order_id,
                    'invoice_number' => $invoice_number,
                    'invoice_number_plain' => (string) $invoice_number_plain_raw,
                    'invoice_date' => $invoice_date,
                    'total' => $order->get_total(),
                    'csv' => $csv,
                    'hash' => $hash,
                    'pdf_path' => '',
                    'status' => 'success',
                    'error_message' => '',
                    'error_category' => '',
                    'subsanation_count' => 0,
                    'aeat_final_status' => 'accepted',
                    'invoice_snapshot' => $snapshot,
                    'aeat_xml_response' => $xml_response,
                    'aeat_xml_request' => $xml_request,
                    'tipo_factura' => $tipo_rect, // Use potentially modified tipo_rect (may have changed to R5)
                    'tipo_rectificativa' => $rectificativa_data['metodo_rectificativa'],
                    'rectified_invoice_id' => $rectificativa_data['original_invoice']->id,
                ) );

                if ( is_wp_error( $db_result ) ) {
                    Freefactu_Logger::log( sprintf( 'Database insert failed for rectificativa order #%d: %s', $order_id, $db_result->get_error_message() ), 'error' );
                    return $db_result;
                }

                /* translators: %s: Invoice number */
                $order->add_order_note( sprintf( __( 'Factura rectificativa %s creada.', 'freefactu' ), $invoice_number ) );
                
                // Generate QR/PDF for rectificativa
                $qr_result = $this->generate_and_save_qr( $order );
                if ( is_wp_error( $qr_result ) ) {
                    // Use invoice_number to update only the rectificativa, not all invoices for this order
                    Freefactu_DB::update_invoice_status_by_number( $invoice_number, 'alert_qr', $qr_result->get_error_message() );
                    // Don't return error - AEAT submission succeeded, just log the QR issue
                    Freefactu_Logger::log( sprintf( 'Rectificativa QR generation failed for order #%d: %s', $order_id, $qr_result->get_error_message() ), 'warning' );
                }

                // Use save_pdf_for_invoice to generate PDF with the correct rectificativa invoice number
                $pdf_result = $this->save_pdf_for_invoice( $order, $invoice_number );
                if ( is_wp_error( $pdf_result ) ) {
                    // Don't set alert_pdf status - just log the warning. PDF can be regenerated later.
                    Freefactu_Logger::log( sprintf( 'Rectificativa PDF generation failed for order #%d (invoice %s): %s (can be regenerated)', $order_id, $invoice_number, $pdf_result->get_error_message() ), 'warning' );
                }
                
                // Return format expected by caller of send_rectificativa_invoice
                return array(
                    'success' => true,
                    'invoice_number' => $invoice_number,
                    'csv' => $csv,
                    'hash' => $hash,
                );

            } else {
                // Standard save logic
                $db_result = Freefactu_DB::upsert_invoice( array(
                    'order_id' => $order->get_id(),
                    'invoice_number' => $invoice_number,
                    'invoice_date' => $invoice_date,
                    'total' => $order->get_total(),
                    'csv' => $csv,
                    'hash' => $hash,
                    'pdf_path' => '',
                    'status' => 'success',
                    'error_message' => '',
                    'error_category' => '',
                    'subsanation_count' => 0,
                    'aeat_final_status' => 'accepted',
                    'invoice_snapshot' => $snapshot,
                    'aeat_xml_response' => $xml_response,
                    'aeat_xml_request' => $xml_request,
                ) );

                if ( is_wp_error( $db_result ) ) {
                    Freefactu_Logger::log( sprintf( 'Database upsert failed for order #%d: %s', $order_id, $db_result->get_error_message() ), 'error' );
                    return $db_result;
                }

                // Generate QR/PDF
                $qr_result = $this->generate_and_save_qr( $order );
                if ( is_wp_error( $qr_result ) ) {
                    Freefactu_DB::update_invoice_status( $order_id, 'alert_qr', $qr_result->get_error_message() );
                    return $qr_result;
                }

                $pdf_result = $this->save_pdf_snapshot( $order );
                if ( is_wp_error( $pdf_result ) ) {
                    // Don't set alert_pdf status - just log the warning. PDF can be regenerated later.
                    Freefactu_Logger::log( sprintf( 'PDF generation failed for order #%d: %s (can be regenerated)', $order_id, $pdf_result->get_error_message() ), 'warning' );
                }

                return array( 'success' => true, 'csv' => $csv );
            }

        } catch ( \Freefactu_Vendor\josemmo\Verifactu\Exceptions\InvalidModelException $e ) {
            // ============================================
            // CLIENT ERROR - Library validation before AEAT call
            // Unlimited retries, no snapshot save, no subsanation_count
            // ============================================
            $error_msg = $e->getMessage();
            $log_prefix = $is_rectificativa ? 'Rectificativa' : 'Freefactu Client';
            
            Freefactu_Logger::log( "$log_prefix validation error: " . $error_msg, 'error' );
            
            // CLIENT errors use alert_client status and CLIENT category
            $error_category = Freefactu_Aeat_Error_Categories::CLIENT;
            $status = 'alert_client';
            
            // Create response for decision (CLIENT error, no AEAT response)
            $normalized_response = Freefactu_Aeat_Response::rejected( '', $error_msg, '' );
            $normalized_response = $normalized_response->with_category( $error_category );
            
            $invoice_context = array(
                'invoice_id'        => $existing_invoice ? $existing_invoice->id : 0,
                'order_id'          => $order->get_id(),
                'tipo_factura'      => $is_rectificativa ? ( isset( $tipo_rect ) ? $tipo_rect : 'F3' ) : 'F1',
                'aeat_final_status' => 'pending', // Never reached AEAT
                'subsanation_count' => 0, // CLIENT errors don't use subsanation count
            );
            
            $decision = Freefactu_Aeat_Next_Action_Decider::decide( $invoice_context, $normalized_response );
            
            // Log classification
            Freefactu_Logger::log( 
                sprintf( 
                    'Client validation error for order #%d: category=%s, next_action=%s', 
                    $order->get_id(), 
                    $error_category, 
                    $decision['next_action'] 
                ), 
                'info' 
            );
            
            // Add order note
            $order->add_order_note( 
                sprintf( 
                    /* translators: %s: Error message */
                    __( 'VeriFactu Error de validación: %s', 'freefactu' ), 
                    $error_msg
                ) 
            );
            
            // Prepare database data - CLIENT errors do NOT save snapshot
            $data = array(
                'order_id'           => $order->get_id(),
                'invoice_number'     => $invoice_number,
                'invoice_date'       => current_time( 'mysql' ),
                'total'              => $order->get_total(),
                'csv'                => '',
                'hash'               => '',
                'pdf_path'           => '',
                'status'             => $status,
                'error_message'      => $decision['user_message'],
                'error_category'     => $error_category,
                'subsanation_count'  => 0, // CLIENT errors: unlimited retries, no increment
                'aeat_final_status'  => 'pending', // Never reached AEAT
                'invoice_snapshot'   => '', // NO snapshot for CLIENT errors
                'aeat_xml_response'  => '',
            );

            if ( $is_rectificativa ) {
                $data['invoice_number_plain'] = (string) $invoice_number_plain_raw;
                $data['tipo_factura'] = isset( $tipo_rect ) ? $tipo_rect : 'F3';
                $data['tipo_rectificativa'] = $rectificativa_data['metodo_rectificativa'];
                $data['rectified_invoice_id'] = $rectificativa_data['original_invoice']->id;
                Freefactu_DB::insert_invoice( $data );
            } else {
                Freefactu_DB::upsert_invoice( $data );
            }
            
            return new WP_Error( 
                'client_error', 
                $decision['user_message'],
                array(
                    'next_action'    => $decision['next_action'],
                    'error_category' => $error_category,
                    'can_retry'      => true, // CLIENT errors always allow retry
                    'can_subsanar'   => false, // Not subsanation, just retry
                )
            );
            
        } catch ( Exception $e ) {
            // ============================================
            // AEAT ERROR - Error from AEAT response or connection
            // Follows subsanation rules, saves snapshot, increments count
            // ============================================
            $error_msg = $e->getMessage();
            $log_prefix = $is_rectificativa ? 'Rectificativa' : 'Freefactu Library';
            
            Freefactu_Logger::log( "$log_prefix Error: " . $error_msg, 'error' );
            if ( $is_rectificativa ) {
                Freefactu_Logger::log( 'Rectificativa failed: ' . $error_msg, 'error' );
            }
            
            // --- Use Subsanation Service for Error Classification ---
            $tipo_factura = $is_rectificativa ? ( isset( $tipo_rect ) ? $tipo_rect : 'F3' ) : 'F1';
            $subsanation_count = $existing_invoice && isset( $existing_invoice->subsanation_count ) 
                ? (int) $existing_invoice->subsanation_count 
                : 0;
            
            // Classify the error
            $error_code = '';
            if ( isset( $aeatResponse ) && isset( $aeatResponse->items[0] ) && isset( $aeatResponse->items[0]->errorCode ) ) {
                $error_code = $aeatResponse->items[0]->errorCode;
            }
            $error_category = Freefactu_Aeat_Error_Classifier::classify( $error_code, $error_msg );
            
            // Determine next action
            $invoice_context = array(
                'invoice_id'        => $existing_invoice ? $existing_invoice->id : 0,
                'order_id'          => $order->get_id(),
                'tipo_factura'      => $tipo_factura,
                'aeat_final_status' => $existing_invoice && isset( $existing_invoice->aeat_final_status ) 
                    ? $existing_invoice->aeat_final_status 
                    : 'pending',
                'subsanation_count' => $is_subsanation ? $subsanation_count + 1 : $subsanation_count,
            );
            
            // Create normalized response for decision
            $normalized_response = Freefactu_Aeat_Response::rejected( $error_code, $error_msg, isset( $xml_response ) ? $xml_response : '' );
            $normalized_response = $normalized_response->with_category( $error_category );
            
            $decision = Freefactu_Aeat_Next_Action_Decider::decide( $invoice_context, $normalized_response );
            
            // Log the classification result
            Freefactu_Logger::log( 
                sprintf( 
                    'AEAT Error classified for order #%d: category=%s, next_action=%s', 
                    $order->get_id(), 
                    $error_category, 
                    $decision['next_action'] 
                ), 
                'info' 
            );
            
            // Add order note with user-friendly message
            $order->add_order_note( 
                sprintf( 
                    /* translators: 1: Error message, 2: Error category */
                    __( 'VeriFactu Error: %1$s (%2$s)', 'freefactu' ), 
                    $decision['user_message'],
                    Freefactu_Aeat_Error_Categories::get_label( $error_category )
                ) 
            );
            
            // Prepare database data with new fields
            $status = 'alert_aeat';
            $new_subsanation_count = $is_subsanation ? $subsanation_count + 1 : $subsanation_count;
            
            $data = array(
                'order_id'           => $order->get_id(),
                'invoice_number'     => $invoice_number,
                'invoice_date'       => current_time( 'mysql' ),
                'total'              => $order->get_total(),
                'csv'                => '',
                'hash'               => '',
                'pdf_path'           => '',
                'status'             => $status,
                'error_message'      => $decision['user_message'],
                'error_category'     => $error_category,
                'subsanation_count'  => $new_subsanation_count,
                'aeat_final_status'  => 'rejected',
                'invoice_snapshot'   => $snapshot,
                'aeat_xml_response'  => isset( $xml_response ) ? $xml_response : '',
                'aeat_xml_request'   => isset( $xml_request ) ? $xml_request : '',
            );

            if ( $is_rectificativa ) {
                 // Rectificativa specific error save - use the raw sequential number
                 $data['invoice_number_plain'] = (string) $invoice_number_plain_raw;
                 $data['tipo_factura'] = $tipo_rect; // Use potentially modified tipo_rect (may have changed to R5)
                 $data['tipo_rectificativa'] = $rectificativa_data['metodo_rectificativa'];
                 $data['rectified_invoice_id'] = $rectificativa_data['original_invoice']->id;
                 Freefactu_DB::insert_invoice( $data );
                 
                 return new WP_Error( 
                     'aeat_error', 
                     $decision['user_message'],
                     array(
                         'next_action'    => $decision['next_action'],
                         'error_category' => $error_category,
                         'can_subsanar'   => $decision['next_action'] === Freefactu_Aeat_Next_Action::RETRY_SUBSANATION,
                     )
                 );
            } else {
                 Freefactu_DB::upsert_invoice( $data );
                 return new WP_Error( 
                     'aeat_error', 
                     $decision['user_message'],
                     array(
                         'next_action'    => $decision['next_action'],
                         'error_category' => $error_category,
                         'can_subsanar'   => $decision['next_action'] === Freefactu_Aeat_Next_Action::RETRY_SUBSANATION,
                     )
                 );
            }
        }
    }

    /**
     * Retry QR and PDF generation without re-sending to AEAT
     * Used when AEAT submission was successful but QR/PDF generation failed
     *
     * @param WC_Order $order
     * @return array|WP_Error
     */
    private function retry_qr_pdf_generation( $order ) {
        $order_id = $order->get_id();

        // Read from database
        $existing_invoice = Freefactu_DB::get_invoice_by_order_id( $order_id );
        $csv = $existing_invoice ? $existing_invoice->csv : '';
        $current_status = $existing_invoice ? $existing_invoice->status : '';

        // Verify we have CSV from AEAT
        if ( empty( $csv ) ) {
            return new WP_Error( 'no_csv', __( 'No CSV available from AEAT. Cannot retry.', 'freefactu' ) );
        }

        $order->add_order_note( __( 'Retrying QR/PDF generation (AEAT data already exists).', 'freefactu' ) );

        // If status is alert_qr, regenerate QR first
        if ( $current_status === 'alert_qr' ) {
            $qr_result = $this->generate_and_save_qr( $order );
            if ( is_wp_error( $qr_result ) ) {
                // QR generation failed again
                $error_msg = $qr_result->get_error_message();

                Freefactu_DB::update_invoice_status( $order_id, 'alert_qr', $error_msg );

                return $qr_result;
            }
        }

        // Use Generator abstraction to generate and save PDF
        $this->generator->set_order( $order );
        $pdf_result = $this->save_pdf_snapshot( $order );
        if ( is_wp_error( $pdf_result ) ) {
            // PDF generation failed - just log, don't set alert_pdf status
            Freefactu_Logger::log( sprintf( 'PDF generation retry failed for order #%d: %s (can be regenerated)', $order_id, $pdf_result->get_error_message() ), 'warning' );

            return $pdf_result;
        }

        // Everything succeeded - mark as success
        Freefactu_DB::update_invoice_status( $order_id, 'success', '' );

        $order->add_order_note( __( 'QR and PDF regenerated successfully.', 'freefactu' ) );

        return array( 'success' => true, 'csv' => $csv, 'regenerated' => true );
    }

    /**
     * Send a rectificativa (correction) invoice to Verifactu.
     *
     * What this function does:
     * - Creates a new invoice that rectifies/corrects an existing invoice
     * - Uses special TipoFactura codes (R1-R5) in XML
     * - References the original invoice in FacturasRectificadas
     * - Creates new chain link (rectificativas are always new records)
     *
     * Where it's called from:
     * - class-freefactu-admin.php: ajax_create_rectificativa() method
     *
     * @param WC_Order $order WooCommerce order object.
     * @param array $rectificativa_data {
     *     @type string $tipo_rectificativa R1, R2, R3, R4, or R5.
     *     @type string $metodo_rectificativa I (differences) or S (substitution).
     *     @type object $original_invoice Original invoice from database.
     *     @type string $motivo Optional reason for rectification.
     * }
     * @return array|WP_Error Array with invoice data on success, WP_Error on failure.
     */
    public function send_rectificativa_invoice( $order, $rectificativa_data ) {
        return $this->send_invoice_to_verifactu( $order->get_id(), $rectificativa_data );
    }

    /**
     * Get chaining data from the last invoice in database.
     *
     * What this function does:
     * - Queries database for the most recent invoice with a hash
     * - Returns hash, date, and invoice number for chaining
     * - Returns empty array if no previous invoice exists
     * - Used for automatic invoice chaining (Verifactu requirement)
     *
     * Where it's called from:
     * - class-freefactu-client.php: send_invoice_to_verifactu() method
     *
     * @return array Chaining data array with hash, date, and series.
     */
    private function get_chaining_data_from_db() {
        // Find the last invoice from freefactu_invoices table that has a hash
        global $wpdb;
        $table_name = Freefactu_DB::get_table_name();

        // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $last_invoice = $wpdb->get_row(
            "SELECT invoice_number, invoice_date, hash
             FROM $table_name
             WHERE hash != '' AND hash IS NOT NULL
             ORDER BY invoice_date DESC, id DESC
             LIMIT 1"
        );
        // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

        if ( $last_invoice ) {
            return array(
                'hash' => $last_invoice->hash,
                'date' => gmdate( 'd-m-Y', strtotime( $last_invoice->invoice_date ) ),
                'series' => $last_invoice->invoice_number
            );
        }

        return array();
    }



    /**
     * Add QR code to PDF (legacy hook, currently unused).
     *
     * What this function does:
     * - Checks if document type is 'invoice'
     * - Calls render_qr_code() to output QR code HTML
     * - Only renders for invoice documents, not packing slips
     *
     * Where it's called from:
     * - Not currently hooked (commented out in __construct)
     * - Kept for potential future integrations
     *
     * @param string $template_type Document type (invoice, packing-slip, etc).
     * @param WC_Order $order WooCommerce order object.
     * @return void
     */
    public function add_qr_to_pdf( $template_type, $order ) {
        if ( $template_type !== 'invoice' ) {
            return;
        }
        $this->render_qr_code( $order );
    }

    /**
     * Render QR code HTML for invoice.
     *
     * What this function does:
     * - Retrieves invoice data from database
     * - Gets CSV, invoice number, date, and total from snapshot if available
     * - Generates QR URL using environment abstraction
     * - Generates QR image as base64
     * - Outputs HTML with QR code following AEAT styling requirements
     * - Includes "QR tributario:" header and "VERI*FACTU" footer
     *
     * Where it's called from:
     * - class-freefactu-client.php: add_qr_to_pdf() method
     * - Standalone/Integrated generators for PDF rendering
     *
     * @param WC_Order $order WooCommerce order object.
     * @return void
     */
    public function render_qr_code( $order ) {
        // Read from database
        $existing_invoice = Freefactu_DB::get_invoice_by_order_id( $order->get_id() );
        $csv = $existing_invoice ? $existing_invoice->csv : '';

        if ( ! $csv ) {
            return;
        }

        $options = get_option( 'freefactu_settings' );
        $company_nif = isset( $options['company_nif'] ) ? $options['company_nif'] : '';

        // Construct parameters using snapshot data from database
        $invoice_number = $existing_invoice && ! empty( $existing_invoice->invoice_number ) ? $existing_invoice->invoice_number : $order->get_id();

        // Get date and total from snapshot if available, otherwise use current order data
        $invoice_date = '';
        $invoice_total = '';

        if ( $existing_invoice && ! empty( $existing_invoice->invoice_snapshot ) ) {
            $snapshot = json_decode( $existing_invoice->invoice_snapshot, true );
            if ( $snapshot && is_array( $snapshot ) ) {
                // Use snapshot data for QR parameters (preserves original invoice data)
                if ( isset( $snapshot['totals']['total'] ) ) {
                    $invoice_total = number_format( $snapshot['totals']['total'], 2, '.', '' );
                }
                // Date from invoice_date field in database
                if ( ! empty( $existing_invoice->invoice_date ) ) {
                    $timestamp = is_numeric( $existing_invoice->invoice_date )
                        ? $existing_invoice->invoice_date
                        : strtotime( $existing_invoice->invoice_date );
                    $invoice_date = gmdate( 'd-m-Y', $timestamp );
                }
            }
        }

        // Fallback to current order data if snapshot not available
        if ( empty( $invoice_date ) ) {
            $date_created = $order->get_date_created();
            $invoice_date = $date_created ? $date_created->date( 'd-m-Y' ) : gmdate( 'd-m-Y' );
        }
        if ( empty( $invoice_total ) ) {
            $invoice_total = number_format( $order->get_total(), 2, '.', '' );
        }

        // Use Environment abstraction to generate QR URL
        $qr_url = $this->environment->generate_qr_url( $order, $invoice_number, $company_nif );

        // Use Environment abstraction to generate QR image
        $img_src = $this->environment->generate_qr_image( $qr_url, 150 );

        // AEAT Styling Requirements
        // Placement: Top, centered.
        // Header: "QR tributario:"
        // Footer: "VERI*FACTU"
        // Size: 30x30mm to 40x40mm.
        // Margins: At least 2mm (recommended 6mm).
        // Separator line.

        // Standard flow to push header down
        echo '<div style="text-align: center; width: 100%; margin-bottom: 20px;">';
        echo '<div style="font-size: 10pt; font-weight: bold; margin-bottom: 2mm;">QR tributario:</div>';
        echo '<img src="' . esc_attr( $img_src ) . '" alt="Verifactu QR" style="width: 35mm; height: 35mm; margin: 2mm;" />';
        echo '<div style="font-size: 10pt; font-weight: bold; margin-top: 2mm;">VERI*FACTU</div>';
        echo '</div>';
        echo '<div style="border-bottom: 1px dashed #999; margin-bottom: 20px; width: 100%;"></div>';
    }



    /**
     * Handle manual invoice submission from admin interface.
     *
     * What this function does:
     * - Checks user permissions (manage_options)
     * - Verifies nonce for security
     * - Gets order_id from request
     * - Calls send_invoice_to_verifactu()
     * - Redirects back to history page with success/error message
     *
     * Where it's called from:
     * - WordPress: admin_post_freefactu_send_invoice action hook
     * - Triggered when admin clicks "Send to Verifactu" button
     *
     * @return void
     */
    public function handle_manual_send() {
        if ( ! current_user_can( 'manage_verifactu' ) ) {
            wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'freefactu' ) );
        }

        check_admin_referer( 'freefactu_send_invoice' );

        $order_id = isset( $_GET['order_id'] ) ? intval( $_GET['order_id'] ) : 0;
        if ( ! $order_id ) {
            wp_safe_redirect( admin_url( 'admin.php?page=freefactu-history&error=invalid_order' ) );
            exit;
        }

        $this->send_invoice_to_verifactu( $order_id );

        wp_safe_redirect( admin_url( 'admin.php?page=freefactu-history&success=sent' ) );
        exit;
    }

    /**
     * Get or generate formatted invoice number for order.
     *
     * What this function does:
     * - Checks if invoice number already assigned in database
     * - If not, gets the active series for normal invoices
     * - Generates new sequential number using Series_Store
     * - Applies prefix, suffix, and padding from series settings
     * - Replaces date placeholders ([year], [month], [day])
     * - Saves invoice number to database
     * - Returns formatted invoice number
     *
     * Where it's called from:
     * - class-freefactu-client.php: send_invoice_to_verifactu() method
     *
     * @param WC_Order $order WooCommerce order object.
     * @return string Formatted invoice number.
     */
    public function get_formatted_invoice_number( $order ) {
        // Check if already assigned in database
        $existing_invoice = Freefactu_DB::get_invoice_by_order_id( $order->get_id() );
        if ( $existing_invoice && ! empty( $existing_invoice->invoice_number ) ) {
            return $existing_invoice->invoice_number;
        }

        // Get active series for normal invoices
        $series = Freefactu_Series_Store::get_active_series( 'normal' );
        
        if ( ! $series ) {
            // No active series - create a default one
            Freefactu_Logger::log( 'No active normal invoice series found. Creating default series.', 'warning' );
            Freefactu_Series_Store::create_series( array(
                'name' => 'Serie Principal',
                'type' => 'normal',
                'prefix' => 'FAC-',
                'suffix' => '',
                'padding' => 6,
            ) );
            $series = Freefactu_Series_Store::get_active_series( 'normal' );
        }

        // Retry loop to handle duplicate invoice number collisions.
        // This can happen when the series counter is behind the actual max number
        // in the database (e.g., orders from a previous year with [year] prefix
        // already used certain numbers).
        $max_attempts = 5;
        $last_error = null;

        for ( $attempt = 1; $attempt <= $max_attempts; $attempt++ ) {
            // Increment series counter and get new number
            $number = Freefactu_Series_Store::increment( $series->id );
            
            if ( is_wp_error( $number ) ) {
                Freefactu_Logger::log( 'Failed to increment series: ' . $number->get_error_message(), 'error' );
                $number = time(); // Fallback
            }

            // Format using series settings
            $formatted_number = Freefactu_Series_Store::format_invoice_number( $series, $number, $order );

            // Save to database using upsert_invoice
            $invoice_date = current_time( 'mysql' );

            $result = Freefactu_DB::upsert_invoice( array(
                'order_id' => $order->get_id(),
                'invoice_number' => $formatted_number,
                'invoice_number_plain' => (string) $number,
                'invoice_date' => $invoice_date,
                'total' => $order->get_total(),
                'csv' => '',
                'hash' => '',
                'pdf_path' => '',
                'status' => '',
                'error_message' => ''
            ) );

            if ( ! is_wp_error( $result ) ) {
                if ( $attempt > 1 ) {
                    Freefactu_Logger::log( 
                        sprintf( 'Invoice number collision resolved after %d attempts. Using: %s', $attempt, $formatted_number ),
                        'info'
                    );
                }
                return $formatted_number;
            }

            $last_error = $result;

            // Check if this is a duplicate key error (collision)
            $error_data = $result->get_error_data();
            $wpdb_error = isset( $error_data['wpdb_error'] ) ? $error_data['wpdb_error'] : '';

            if ( stripos( $wpdb_error, 'Duplicate entry' ) !== false ) {
                Freefactu_Logger::log( 
                    sprintf( 'Invoice number collision (attempt %d/%d): %s already exists. Retrying with next number.', $attempt, $max_attempts, $formatted_number ),
                    'warning'
                );
                continue; // Try next number
            }

            // Not a duplicate error, break immediately
            break;
        }

        // All attempts failed
        Freefactu_Logger::log( 'Failed to save formatted invoice number after ' . $max_attempts . ' attempts: ' . $last_error->get_error_message(), 'error' );
        return $last_error;
    }

    /**
     * Generate and save QR code for the invoice.
     *
     * Uses the injected Environment abstraction to generate QR codes.
     * This method demonstrates environment-agnostic code: it doesn't know
     * whether it's using Production or Test environment URLs. The Environment
     * implementation handles all environment-specific details.
     *
     * @param WC_Order $order WooCommerce order object.
     * @return true|WP_Error True on success, WP_Error on failure.
     */
    public function generate_and_save_qr( $order ) {
        // Read from database
        $existing_invoice = Freefactu_DB::get_invoice_by_order_id( $order->get_id() );
        $csv = $existing_invoice ? $existing_invoice->csv : '';

        if ( ! $csv ) {
            return new WP_Error( 'no_csv', __( 'No CSV available for QR generation.', 'freefactu' ) );
        }

        // Get company NIF from settings
        $options = get_option( 'freefactu_settings' );
        $company_nif = isset( $options['company_nif'] ) ? $options['company_nif'] : '';

        $invoice_number = $existing_invoice && ! empty( $existing_invoice->invoice_number ) ? $existing_invoice->invoice_number : $order->get_id();

        // Use Environment abstraction to generate QR URL
        // The client doesn't know which environment's URL it's using
        $qr_url = $this->environment->generate_qr_url( $order, $invoice_number, $company_nif );

        // Check if QR code already exists in cache
        $cache_key = 'freefactu_qr_v2_' . md5( $qr_url . 150 );
        $cached_qr = get_transient( $cache_key );

        if ( $cached_qr !== false && strpos( $cached_qr, 'data:image/png;base64,' ) === 0 ) {
            // QR already created, skip generation
            return true;
        }

        // Use Environment abstraction to generate QR image
        // Again, environment-specific details are abstracted away
        $img_src = $this->environment->generate_qr_image( $qr_url, 150 );

        if ( empty( $img_src ) ) {
            return new WP_Error( 'qr_generation_failed', __( 'Failed to generate QR code.', 'freefactu' ) );
        }

        return true;
    }

    /**
     * Save PDF snapshot for an order.
     *
     * Generates and saves a PDF invoice using the injected Generator abstraction.
     * This method demonstrates dependency injection: it uses the Standalone_Generator
     * (DOMPDF) for PDF generation. The Generator interface provides a unified API.
     *
     * @param WC_Order $order WooCommerce order object.
     * @return true|WP_Error True on success, WP_Error on failure.
     */
    public function save_pdf_snapshot( $order ) {
        try {
            // Check if PDF was already created successfully - read from database
            $existing_invoice = Freefactu_DB::get_invoice_by_order_id( $order->get_id() );
            $existing_pdf_path = $existing_invoice ? $existing_invoice->pdf_path : '';

            // Convert stored relative path to absolute for file existence check
            $absolute_pdf_path = Freefactu_Filesystem::to_absolute_path( $existing_pdf_path );

            if ( $existing_pdf_path && Freefactu_Filesystem::exists( $absolute_pdf_path ) ) {
                // PDF already exists, skip generation
                return true;
            }

            // Use Generator abstraction to generate PDF
            // The client doesn't know which generator implementation it's using
            $this->generator->set_order( $order );
            $pdf_content = $this->generator->generate( $order );

            if ( is_wp_error( $pdf_content ) ) {
                return $pdf_content;
            }

            if ( empty( $pdf_content ) ) {
                return new WP_Error( 'empty_pdf', __( 'PDF content is empty.', 'freefactu' ) );
            }

            // Use Generator abstraction to save PDF
            // Again, the client doesn't care about implementation details
            $file_path = $this->generator->save_pdf();

            if ( is_wp_error( $file_path ) ) {
                return $file_path;
            }

            // Update existing record in Custom DB with PDF path and success status
            $existing_invoice = Freefactu_DB::get_invoice_by_order_id( $order->get_id() );
            if ( $existing_invoice ) {
                // Convert to relative path for database storage
                $relative_file_path = Freefactu_Filesystem::to_relative_path( $file_path );

                // Update existing invoice with relative PDF path
                global $wpdb;
                $table_name = Freefactu_DB::get_table_name();

                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                $wpdb->update(
                    $table_name,
                    array(
                        'pdf_path' => $relative_file_path,
                        'status' => 'success',
                        'error_message' => '',
                    ),
                    array( 'order_id' => $order->get_id() ),
                    array( '%s', '%s', '%s' ),
                    array( '%d' )
                );
            }

            return true;

        } catch ( Exception $e ) {
            Freefactu_Logger::log( 'Freefactu PDF Snapshot Error: ' . $e->getMessage(), 'error' );
            return new WP_Error( 'pdf_exception', $e->getMessage() );
        }
    }

    /**
     * Save PDF for a specific invoice by invoice number.
     *
     * This is used for rectificativas where the order has multiple invoices.
     * Unlike save_pdf_snapshot which gets the first invoice for an order,
     * this method generates the PDF for a specific invoice identified by its number.
     *
     * @param WC_Order $order WooCommerce order object.
     * @param string $invoice_number The specific invoice number to generate PDF for.
     * @return true|WP_Error True on success, WP_Error on failure.
     */
    public function save_pdf_for_invoice( $order, $invoice_number ) {
        try {
            // Get the specific invoice by invoice_number
            $invoice = Freefactu_DB::get_invoice_by_number( $invoice_number );
            
            if ( ! $invoice ) {
                /* translators: %s: Invoice number */
                return new WP_Error( 'invoice_not_found', sprintf( __( 'Invoice %s not found.', 'freefactu' ), $invoice_number ) );
            }

            // Check if PDF was already created for this specific invoice
            // Convert stored relative path to absolute for file existence check
            $absolute_pdf_path = Freefactu_Filesystem::to_absolute_path( $invoice->pdf_path ?? '' );
            if ( ! empty( $invoice->pdf_path ) && Freefactu_Filesystem::exists( $absolute_pdf_path ) ) {
                return true;
            }

            // Set the order for the generator
            $this->generator->set_order( $order );
            
            // Set the specific invoice for the generator (if supported)
            if ( method_exists( $this->generator, 'set_invoice' ) ) {
                $this->generator->set_invoice( $invoice );
            }

            // Generate PDF - use snapshot if available
            $pdf_content = null;
            if ( ! empty( $invoice->invoice_snapshot ) ) {
                $snapshot = json_decode( $invoice->invoice_snapshot, true );
                if ( $snapshot && is_array( $snapshot ) ) {
                    // Add verifactu data to snapshot
                    $snapshot['invoice_number'] = $invoice->invoice_number;
                    $snapshot['verifactu'] = array(
                        'csv' => $invoice->csv ?? '',
                        'hash' => $invoice->hash ?? '',
                        'qr_url' => '',
                        'qr_image' => '',
                    );
                    
                    // 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: Inherited customer NIF %s from original invoice #%d', 
                                            $orig_customer_nif,
                                            $invoice->rectified_invoice_id
                                        ), 
                                        'debug' 
                                    );
                                }
                            }
                        }
                    }
                    
                    // Generate QR if environment is available
                    if ( $this->environment && ! empty( $invoice->csv ) ) {
                        $options = get_option( 'freefactu_settings' );
                        $company_nif = isset( $options['company_nif'] ) ? $options['company_nif'] : '';
                        $snapshot['verifactu']['qr_url'] = $this->environment->generate_qr_url( $order, $invoice->invoice_number, $company_nif );
                        if ( ! empty( $snapshot['verifactu']['qr_url'] ) ) {
                            $snapshot['verifactu']['qr_image'] = $this->environment->generate_qr_image( $snapshot['verifactu']['qr_url'] );
                        }
                    }
                    
                    // Generate PDF from snapshot
                    $pdf_content = $this->generator->generate_from_snapshot( $order->get_id(), $snapshot );
                }
            }
            
            // Fallback to normal generation if snapshot didn't work
            if ( $pdf_content === null || is_wp_error( $pdf_content ) ) {
                $pdf_content = $this->generator->generate( $order );
            }

            if ( is_wp_error( $pdf_content ) ) {
                return $pdf_content;
            }

            if ( empty( $pdf_content ) ) {
                return new WP_Error( 'empty_pdf', __( 'PDF content is empty.', 'freefactu' ) );
            }

            // Generate filename based on invoice number
            $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_number );
            $filename = 'verifactu-' . $safe_number . '.pdf';
            $file_path = $base_dir . '/' . $filename;
            
            // Save the PDF file
            $result = Freefactu_Filesystem::put_contents( $file_path, $pdf_content );
            
            if ( $result === false ) {
                return new WP_Error( 'save_failed', __( 'No se pudo guardar el archivo PDF.', 'freefactu' ) );
            }

            // Update the specific invoice record with relative PDF path using invoice_number
            $relative_file_path = Freefactu_Filesystem::to_relative_path( $file_path );

            global $wpdb;
            $table_name = Freefactu_DB::get_table_name();
            
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $wpdb->update(
                $table_name,
                array(
                    'pdf_path' => $relative_file_path,
                    'status' => 'success',
                    'error_message' => '',
                ),
                array( 'invoice_number' => $invoice_number ),
                array( '%s', '%s', '%s' ),
                array( '%s' )
            );

            return true;

        } catch ( Exception $e ) {
            Freefactu_Logger::log( 'Freefactu PDF Invoice Error: ' . $e->getMessage(), 'error' );
            return new WP_Error( 'pdf_exception', $e->getMessage() );
        }
    }

    /**
     * Save PDF snapshot by order ID (handles both existing and deleted orders)
     * 
     * IMPORTANT: This method ALWAYS uses the snapshot data if it exists, regardless
     * of whether the order still exists. This ensures that regenerated PDFs match
     * the original invoice that was sent to AEAT.
     *
     * @param int $order_id Order ID
     * @return true|WP_Error True on success, WP_Error on failure
     */
    public function save_pdf_snapshot_by_id( $order_id ) {
        // Get existing invoice to check for snapshot
        $existing_invoice = Freefactu_DB::get_invoice_by_order_id( $order_id );

        if ( ! $existing_invoice ) {
            return new WP_Error( 'no_invoice', __( 'No se encontró la factura en la base de datos.', 'freefactu' ) );
        }

        // Only use snapshot if invoice was successfully sent to AEAT
        // This ensures regenerated PDFs match the original invoice sent to AEAT
        // For new/unsent invoices, regenerate from current order data
        $invoice_sent_to_aeat = ! empty( $existing_invoice->csv );
        $has_snapshot = ! empty( $existing_invoice->invoice_snapshot );

        if ( ! $invoice_sent_to_aeat || ! $has_snapshot ) {
            // Invoice not sent to AEAT yet, or no snapshot - generate from order
            $order = wc_get_order( $order_id );
            if ( $order ) {
                return $this->save_pdf_snapshot( $order );
            }
            // Order deleted and no valid snapshot
            return new WP_Error( 'no_snapshot', __( 'No hay snapshot disponible para regenerar el PDF.', 'freefactu' ) );
        }

        // Check if PDF already exists
        $existing_pdf_path = $existing_invoice->pdf_path;
        // Convert stored relative path to absolute for file existence check
        $absolute_pdf_path = Freefactu_Filesystem::to_absolute_path( $existing_pdf_path );
        if ( $existing_pdf_path && Freefactu_Filesystem::exists( $absolute_pdf_path ) ) {
            // PDF already exists, skip generation
            Freefactu_Logger::log( sprintf(
                'PDF already exists for order %d, skipping regeneration',
                $order_id
            ), 'info' );
            return true;
        }

        // Decode snapshot data
        $snapshot = json_decode( $existing_invoice->invoice_snapshot, true );
        if ( ! $snapshot || ! is_array( $snapshot ) ) {
            return new WP_Error( 'invalid_snapshot', __( 'El snapshot de la factura no es válido.', 'freefactu' ) );
        }

        // Use standalone generator for snapshot regeneration
        // The standalone generator uses DOMPDF directly for snapshot data
        if ( ! class_exists( 'Freefactu_Standalone_Generator' ) ) {
            require_once FREEFACTU_PLUGIN_DIR . 'includes/generators/standalone/class-standalone-generator.php';
        }

        $environment = Freefactu_Factory::create_environment();
        $generator = new Freefactu_Standalone_Generator( null, $environment );

        // Log that we're using snapshot data with standalone generator
        $order = wc_get_order( $order_id );
        $order_status = $order ? 'exists' : 'deleted';
        Freefactu_Logger::log( sprintf(
            'Regenerating PDF from snapshot for order %d (order %s, using standalone generator)',
            $order_id,
            $order_status
        ), 'info' );

        // Generate PDF from snapshot data using standalone generator
        $pdf_content = $generator->generate_from_snapshot( $order_id, $snapshot );

        if ( is_wp_error( $pdf_content ) ) {
            return $pdf_content;
        }

        if ( empty( $pdf_content ) ) {
            return new WP_Error( 'empty_pdf', __( 'El contenido del PDF está vacío.', 'freefactu' ) );
        }

        // Save the PDF file
        $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' );
        }

        $invoice_number = $existing_invoice->invoice_number;
        $safe_invoice_number = str_replace( array( '/', '\\' ), '-', $invoice_number );
        $filename = 'verifactu-' . $safe_invoice_number . '.pdf';
        $file_path = $base_dir . '/' . $filename;

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

        if ( $result === false ) {
            return new WP_Error( 'pdf_save_failed', __( 'No se pudo guardar el archivo PDF.', 'freefactu' ) );
        }

        // Update database with relative PDF path for portability
        $relative_file_path = Freefactu_Filesystem::to_relative_path( $file_path );

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

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $wpdb->update(
            $table_name,
            array(
                'pdf_path' => $relative_file_path,
                'status' => 'success',
                'error_message' => '',
            ),
            array( 'order_id' => $order_id ),
            array( '%s', '%s', '%s' ),
            array( '%d' )
        );

        Freefactu_Logger::log( sprintf(
            'PDF regenerated from snapshot for deleted order %d',
            $order_id
        ), 'info' );

        return true;
    }



    /**
     * Create a snapshot of the invoice data at the time of sending to Verifactu.
     *
     * What this function does:
     * - Uses generator to prepare complete template data
     * - Captures order state at time of AEAT submission
     * - Encodes data as JSON
     * - Returns JSON string for database storage
     * - Allows PDF regeneration even if order is later modified or deleted
     *
     * Where it's called from:
     * - class-freefactu-client.php: send_invoice_to_verifactu() method
     *
     * @param WC_Order $order The order to snapshot
     * @return string JSON encoded snapshot data
     */
    private function create_invoice_snapshot( $order ) {
        // Use the Generator abstraction to prepare template data
        $this->generator->set_order( $order );
        $template_data = $this->generator->prepare_template_data( true );

        // Return JSON encoded snapshot
        return wp_json_encode( $template_data );
    }

}
