<?php
/**
 * Freefactu Base PDF Generator
 *
 * Abstract base class containing shared functionality for PDF generators.
 *
 * @package Freefactu
 * @since 0.5.0
 */

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

/**
 * Abstract Class Freefactu_Base_Generator
 *
 * Contains common functionality for PDF generation.
 * Child classes implement the specific PDF generation logic.
 */
abstract class Freefactu_Base_Generator implements Freefactu_Generator_Interface {

	/**
	 * The WooCommerce order object.
	 *
	 * @var WC_Order|null
	 */
	protected $order = null;

	/**
	 * The generated PDF content.
	 *
	 * @var string|null
	 */
	protected $pdf_content = null;

	/**
	 * Environment instance for QR generation.
	 *
	 * @var Freefactu_Environment_Interface|null
	 */
	protected $environment = null;

	/**
	 * Constructor - initializes generator with order and environment.
	 *
	 * What this function does:
	 * - Accepts order (object or ID) and environment instance
	 * - Sets order if provided
	 * - Stores environment for QR generation
	 * - Initializes PDF content to null
	 *
	 * Where it's called from:
	 * - class-freefactu-factory.php: create_generator() method
	 * - Standalone_Generator class
	 *
	 * @param WC_Order|int|null $order WooCommerce order object or order ID.
	 * @param Freefactu_Environment_Interface|null $environment Environment instance for QR generation.
	 */
	public function __construct( $order = null, $environment = null ) {
		if ( $order ) {
			$this->set_order( $order );
		}
		$this->environment = $environment;
	}

	/**
	 * Set the order for PDF generation.
	 *
	 * What this function does:
	 * - Accepts order object or order ID
	 * - Converts ID to order object if needed
	 * - Validates order is valid WC_Order instance
	 * - Resets PDF content cache
	 * - Calls reset_after_order_change() hook
	 * - Returns true on success, false on failure
	 *
	 * Where it's called from:
	 * - class-freefactu-client.php: save_pdf_snapshot() method
	 * - class-freefactu-client.php: create_invoice_snapshot() method
	 * - abstract-base-generator.php: __construct() method
	 *
	 * @param WC_Order|int $order WooCommerce order object or order ID.
	 * @return bool True on success, false on failure.
	 */
	public function set_order( $order ): bool {
		if ( is_numeric( $order ) ) {
			$order = wc_get_order( $order );
		}

		if ( ! $order instanceof WC_Order ) {
			return false;
		}

		$this->order = $order;
		$this->pdf_content = null;
		$this->reset_after_order_change();
		return true;
	}

	/**
	 * Hook for child classes to reset their state after order change.
	 *
	 * @return void
	 */
	protected function reset_after_order_change() {
		// Override in child classes if needed
	}

	/**
	 * Set the environment instance.
	 *
	 * What this function does:
	 * - Sets the environment instance for QR generation
	 * - Allows changing environment after construction
	 * - Used when generator is created without environment
	 *
	 * Where it's called from:
	 * - class-freefactu-client.php: save_pdf_snapshot_by_id() method
	 *
	 * @param Freefactu_Environment_Interface $environment Environment instance.
	 * @return void
	 */
	public function set_environment( $environment ) {
		$this->environment = $environment;
	}

	/**
	 * Get PDF content as string.
	 *
	 * @return string|WP_Error PDF content or WP_Error on failure.
	 */
	public function get_pdf_content() {
		if ( $this->pdf_content === null ) {
			$result = $this->generate();
			if ( is_wp_error( $result ) ) {
				return $result;
			}
		}

		return $this->pdf_content;
	}

	/**
	 * Save PDF to file.
	 *
	 * @param string|null $path Optional custom path.
	 * @return string|WP_Error File path on success or WP_Error on failure.
	 */
	public function save_pdf( ?string $path = null ) {
		// Generate PDF if not already generated
		if ( $this->pdf_content === null ) {
			$result = $this->generate();
			if ( is_wp_error( $result ) ) {
				return $result;
			}
		}

		if ( empty( $this->pdf_content ) ) {
			return new WP_Error( 'empty_pdf', __( 'No se pudo generar el PDF.', 'freefactu' ) );
		}

		// Determine file path
		if ( $path === null ) {
			$upload_dir = wp_upload_dir();
			$base_dir = $upload_dir['basedir'] . '/freefactu/invoices';

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

			$path = $base_dir . '/' . $this->get_filename();
		}

		// Save file
		$result = Freefactu_Filesystem::put_contents( $path, $this->pdf_content );

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

		// Convert to relative path for database storage (portable across hosting environments)
		$relative_path = Freefactu_Filesystem::to_relative_path( $path );

		// Update invoice table with relative PDF path
		$db_result = Freefactu_DB::update_invoice_pdf( $this->order->get_id(), $relative_path );

		if ( $db_result === false ) {
			Freefactu_Logger::log(
				sprintf( 'Failed to update PDF path in database for order %d', $this->order->get_id() ),
				'error'
			);
			return new WP_Error( 'db_update_failed', __( 'No se pudo actualizar la ruta del PDF en la base de datos.', 'freefactu' ) );
		}

		return $path;
	}

	/**
	 * Get the filename for the PDF.
	 *
	 * @return string Filename.
	 */
	protected function get_filename() {
		// Get invoice number from database
		$invoice_db = Freefactu_DB::get_invoice_by_order_id( $this->order->get_id() );
		$invoice_number = $invoice_db ? $invoice_db->invoice_number : '';

		if ( empty( $invoice_number ) ) {
			$invoice_number = $this->order->get_id();
		}

		// Sanitize for filename
		$safe_number = str_replace( array( '/', '\\', ' ' ), '-', $invoice_number );

		return 'verifactu-' . $safe_number . '.pdf';
	}

	/**
	 * Prepare Verifactu data (CSV, hash, QR) from database.
	 *
	 * @param object|null $invoice_db Invoice database record.
	 * @param string $invoice_number Invoice number.
	 * @param WC_Order|null $order Optional order object (for deleted orders, pass null).
	 * @param array|null $snapshot Optional snapshot data (for deleted orders).
	 * @return array Verifactu data array.
	 */
	protected function prepare_verifactu_data( $invoice_db, $invoice_number, $order = null, $snapshot = null ) {
		$csv = $invoice_db ? ( $invoice_db->csv ?? '' ) : '';
		$hash = $invoice_db ? ( $invoice_db->hash ?? '' ) : '';

		$verifactu = array(
			'csv'      => $csv,
			'hash'     => $hash,
			'qr_url'   => '',
			'qr_image' => '',
		);

		// Use provided order or fall back to $this->order
		$order_to_use = $order !== null ? $order : $this->order;

		// Generate QR code if CSV exists and environment is available
		if ( ! empty( $verifactu['csv'] ) && $this->environment ) {
			$options = get_option( 'freefactu_settings' );
			$company_nif = isset( $options['company_nif'] ) ? $options['company_nif'] : '';

			// If we have an order, use the normal method
			if ( $order_to_use ) {
				$verifactu['qr_url'] = $this->environment->generate_qr_url( $order_to_use, $invoice_number, $company_nif );
			} 
			// If no order but we have snapshot data, build QR URL manually
			elseif ( $snapshot && is_array( $snapshot ) ) {
				$invoice_date = '';
				$invoice_total = '';

				// Get total from snapshot
				if ( isset( $snapshot['totals']['total'] ) ) {
					$invoice_total = number_format( $snapshot['totals']['total'], 2, '.', '' );
				}

				// Get date from invoice_db
				if ( $invoice_db && ! empty( $invoice_db->invoice_date ) ) {
					$timestamp = is_numeric( $invoice_db->invoice_date )
						? $invoice_db->invoice_date
						: strtotime( $invoice_db->invoice_date );
					$invoice_date = gmdate( 'd-m-Y', $timestamp );
				}

				// Build QR URL manually if we have all required data
				if ( ! empty( $invoice_date ) && ! empty( $invoice_total ) ) {
					$params = array(
						'nif'      => $company_nif,
						'numserie' => $invoice_number,
						'fecha'    => $invoice_date,
						'importe'  => $invoice_total,
					);
					$verifactu['qr_url'] = $this->environment->get_qr_base_url() . '?' . http_build_query( $params );
				}
			}

			// Generate QR image if we have a URL
			if ( ! empty( $verifactu['qr_url'] ) ) {
				$verifactu['qr_image'] = $this->environment->generate_qr_image( $verifactu['qr_url'] );
			}
		}

		return $verifactu;
	}

	/**
	 * Get invoice data from database.
	 *
	 * @return array Array with invoice_number and invoice_date.
	 */
	protected function get_invoice_data_from_db() {
		$invoice_db = Freefactu_DB::get_invoice_by_order_id( $this->order->get_id() );

		$invoice_number = $invoice_db ? $invoice_db->invoice_number : '';
		$invoice_date_meta = $invoice_db ? $invoice_db->invoice_date : '';

		if ( $invoice_date_meta ) {
			$invoice_date = is_numeric( $invoice_date_meta )
				? date_i18n( get_option( 'date_format' ), $invoice_date_meta )
				: date_i18n( get_option( 'date_format' ), strtotime( $invoice_date_meta ) );
		} else {
			// Fallback to order creation date, or current date if order has no creation date
			$date_created = $this->order->get_date_created();
			$invoice_date = $date_created 
				? $date_created->date_i18n( get_option( 'date_format' ) )
				: date_i18n( get_option( 'date_format' ) );
		}

		return array(
			'invoice_db'     => $invoice_db,
			'invoice_number' => $invoice_number,
			'invoice_date'   => $invoice_date,
		);
	}

	/**
	 * Prepare template data from the order.
	 *
	 * What this function does:
	 * - Checks if snapshot exists and should be used
	 * - If snapshot exists and force_real_data is false, returns snapshot data
	 * - Otherwise builds fresh data from current order state
	 * - Includes: company data, customer data, line items, totals, tax breakdown
	 * - Adds Verifactu data (CSV, hash, QR)
	 * - Returns complete array for template rendering
	 *
	 * Where it's called from:
	 * - class-freefactu-client.php: create_invoice_snapshot() method
	 * - class-freefactu-snapshot-utility.php: create_snapshot_for_order() method
	 * - class-freefactu-snapshot-utility.php: verify_snapshot() method
	 * - Child generator classes for PDF rendering
	 *
	 * @param bool $force_real_data If true, use real order data instead of snapshot.
	 * @return array Template data array.
	 */
	public function prepare_template_data( bool $force_real_data = false ): array {
		if ( ! $this->order ) {
			return array();
		}

		$invoice_db = Freefactu_DB::get_invoice_by_order_id( $this->order->get_id() );

		// Check if we have a snapshot and should use it
		if ( ! $force_real_data && $invoice_db && ! empty( $invoice_db->invoice_snapshot ) ) {
			$snapshot = json_decode( $invoice_db->invoice_snapshot, true );
			if ( $snapshot && is_array( $snapshot ) ) {
				$invoice_number = $invoice_db->invoice_number ?? '';
				
				// Ensure invoice number is in the snapshot (it might be empty in the stored snapshot for rectificativas)
				$snapshot['invoice_number'] = $invoice_number;
				
				// Inject company email and phone from current settings if missing in snapshot
				$options = get_option( 'freefactu_settings' );
				if ( empty( $snapshot['company']['email'] ) ) {
					$snapshot['company']['email'] = isset( $options['company_email'] ) ? $options['company_email'] : get_option( 'woocommerce_email_from_address', get_option( 'admin_email' ) );
				}
				if ( empty( $snapshot['company']['phone'] ) ) {
					$snapshot['company']['phone'] = isset( $options['company_phone'] ) ? $options['company_phone'] : get_option( 'woocommerce_store_phone', '' );
				}

				$snapshot['verifactu'] = $this->prepare_verifactu_data( $invoice_db, $invoice_number );
				return $snapshot;
			}
		}

		// Build complete template data from real order data
		$options = get_option( 'freefactu_settings' );

		// Get invoice data from database
		$invoice_data = $this->get_invoice_data_from_db();
		$invoice_db = $invoice_data['invoice_db'];
		$invoice_number = $invoice_data['invoice_number'];
		$invoice_date = $invoice_data['invoice_date'];

		// Company data
		$company = array(
			'site_name' => get_bloginfo( 'name' ),
			'name'    => isset( $options['company_name'] ) ? $options['company_name'] : '',
			'nif'     => isset( $options['company_nif'] ) ? $options['company_nif'] : '',
			'address' => $this->get_store_address(),
			'email'   => isset( $options['company_email'] ) ? $options['company_email'] : get_option( 'woocommerce_email_from_address', get_option( 'admin_email' ) ),
			'phone'   => isset( $options['company_phone'] ) ? $options['company_phone'] : get_option( 'woocommerce_store_phone', '' ),
			'logo'    => $this->get_site_logo(),
		);

		// Customer data
		$customer = $this->get_customer_data( $this->order );

		// Line items
		$items = $this->get_line_items( $this->order );

		// Totals
		$totals = array(
			'subtotal'  => $this->order->get_subtotal(),
			'tax_total' => $this->order->get_total_tax(),
			'total'     => $this->order->get_total(),
		);

		// Tax breakdown
		$tax_breakdown = $this->get_tax_breakdown( $this->order );

		// Safely get order date, fallback to current date if order has no creation date
		$date_created = $this->order->get_date_created();
		$order_date = $date_created 
			? $date_created->date_i18n( get_option( 'date_format' ) )
			: date_i18n( get_option( 'date_format' ) );

		return array(
			'invoice_number' => $invoice_number,
			'invoice_date'   => $invoice_date,
			'company'        => $company,
			'customer'       => $customer,
			'items'          => $items,
			'totals'         => $totals,
			'tax_breakdown'  => $tax_breakdown,
			'verifactu'      => $this->prepare_verifactu_data( $invoice_db, $invoice_number ),
			'order_id'       => $this->order->get_id(),
			'order_date'     => $order_date,
		);
	}

	/**
	 * Get customer data from order.
	 *
	 * What this function does:
	 * - Extracts customer billing information from order
	 * - Tries to find DNI/NIF from common custom field names
	 * - Returns array with name, company, NIF, address, email, phone
	 * - Handles missing data gracefully
	 *
	 * Where it's called from:
	 * - abstract-base-generator.php: prepare_template_data() method
	 *
	 * @param WC_Order $order WooCommerce order.
	 * @return array Customer data array.
	 */
	protected function get_customer_data( $order ) {
		// Try to get DNI/NIF from common custom field names
		$customer_nif = $order->get_meta( '_billing_nif' );
		if ( empty( $customer_nif ) ) {
			$customer_nif = $order->get_meta( '_billing_dni' );
		}
		if ( empty( $customer_nif ) ) {
			$customer_nif = $order->get_meta( '_billing_vat' );
		}
		if ( empty( $customer_nif ) ) {
			$customer_nif = $order->get_meta( 'billing_nif' );
		}
		if ( empty( $customer_nif ) ) {
			$customer_nif = $order->get_meta( 'billing_dni' );
		}

		return array(
			'name'      => $order->get_billing_first_name() . ' ' . $order->get_billing_last_name(),
			'company'   => $order->get_billing_company(),
			'nif'       => $customer_nif,
			'address_1' => $order->get_billing_address_1(),
			'address_2' => $order->get_billing_address_2(),
			'city'      => $order->get_billing_city(),
			'postcode'  => $order->get_billing_postcode(),
			'country'   => WC()->countries->countries[ $order->get_billing_country() ] ?? $order->get_billing_country(),
			'email'     => $order->get_billing_email(),
			'phone'     => $order->get_billing_phone(),
		);
	}

	/**
	 * Get line items from order.
	 *
	 * What this function does:
	 * - Iterates through order items
	 * - Calculates unit price from total and quantity
	 * - Gets tax rate for each item
	 * - Returns array of items with name, quantity, unit_price, total, tax_rate
	 *
	 * Where it's called from:
	 * - abstract-base-generator.php: prepare_template_data() method
	 *
	 * @param WC_Order $order WooCommerce order.
	 * @return array Line items array.
	 */
	protected function get_line_items( $order ) {
		$items = array();
		foreach ( $order->get_items() as $item ) {
			$quantity = $item->get_quantity();
			$total = $item->get_total();

			// Calculate unit price (without tax)
			$unit_price = $quantity > 0 ? $total / $quantity : 0;

			$items[] = array(
				'name'       => $item->get_name(),
				'quantity'   => $quantity,
				'unit_price' => $unit_price,
				'total'      => $total,
				'tax_rate'   => $this->get_item_tax_rate( $item ),
			);
		}
		return $items;
	}

	/**
	 * Get the store address from WooCommerce settings.
	 *
	 * What this function does:
	 * - Reads store address from WooCommerce settings
	 * - Combines address line 1, line 2, postcode, and city
	 * - Filters out empty parts
	 * - Returns comma-separated formatted address
	 *
	 * Where it's called from:
	 * - abstract-base-generator.php: prepare_template_data() method
	 *
	 * @return string Formatted store address.
	 */
	protected function get_store_address() {
		$address_parts = array_filter( array(
			get_option( 'woocommerce_store_address' ),
			get_option( 'woocommerce_store_address_2' ),
			get_option( 'woocommerce_store_postcode' ) . ' ' . get_option( 'woocommerce_store_city' ),
		) );

		return implode( ', ', $address_parts );
	}

	/**
	 * Get the site logo URL.
	 *
	 * What this function does:
	 * - Tries WooCommerce email header image first
	 * - Falls back to WordPress custom logo
	 * - Falls back to site icon
	 * - Returns logo URL or empty string if none found
	 * - Prioritizes WooCommerce branding over WordPress branding
	 *
	 * Where it's called from:
	 * - abstract-base-generator.php: prepare_template_data() method
	 *
	 * @return string Logo URL or empty string if not found.
	 */
	protected function get_site_logo() {
		// Try WooCommerce email header image first
		$wc_logo = get_option( 'woocommerce_email_header_image' );
		if ( ! empty( $wc_logo ) ) {
			return $wc_logo;
		}

		// Try WordPress custom logo
		$custom_logo_id = get_theme_mod( 'custom_logo' );
		if ( $custom_logo_id ) {
		$logo_url = wp_get_attachment_image_url( $custom_logo_id, 'full' );
			if ( $logo_url ) {
				return $logo_url;
			}
		}

		// Try site icon as fallback
		$site_icon_id = get_option( 'site_icon' );
		if ( $site_icon_id ) {
			$icon_url = wp_get_attachment_image_url( $site_icon_id, 'full' );
			if ( $icon_url ) {
				return $icon_url;
			}
		}

		return '';
	}

	/**
	 * Get tax rate for an order item.
	 *
	 * What this function does:
	 * - Gets tax data from order item
	 * - Calculates rate from tax amount and item total
	 * - Rounds to nearest common Spanish tax rate (0%, 4%, 10%, 21%)
	 * - Returns tax rate percentage
	 *
	 * Where it's called from:
	 * - abstract-base-generator.php: get_line_items() method
	 *
	 * @param WC_Order_Item $item Order item.
	 * @return float Tax rate percentage.
	 */
	protected function get_item_tax_rate( $item ) {
		$taxes = $item->get_taxes();

		if ( empty( $taxes['total'] ) ) {
			return 0;
		}

		$total = $item->get_total();
		$tax_total = array_sum( $taxes['total'] );

		if ( abs( $total ) < 0.000001 ) {
			return 0;
		}

		// Calculate rate from amounts
		$rate = ( $tax_total / $total ) * 100;

		// Round to common tax rates (4%, 10%, 21%)
		$common_rates = array( 0, 4, 10, 21 );
		$closest = $common_rates[0];
		$min_diff = abs( $rate - $closest );

		foreach ( $common_rates as $common_rate ) {
			$diff = abs( $rate - $common_rate );
			if ( $diff < $min_diff ) {
				$min_diff = $diff;
				$closest = $common_rate;
			}
		}

		return $closest;
	}

	/**
	 * Get tax breakdown for the order.
	 *
	 * What this function does:
	 * - Gets tax totals from order
	 * - Extracts tax rate percentage for each tax
	 * - Calculates base amount from tax amount and rate
	 * - Returns array of tax breakdowns with rate, base, and amount
	 * - Handles orders with no taxes (0% rate)
	 *
	 * Where it's called from:
	 * - abstract-base-generator.php: prepare_template_data() method
	 *
	 * @param WC_Order $order WooCommerce order.
	 * @return array Tax breakdown array.
	 */
	protected function get_tax_breakdown( $order ) {
		$tax_breakdown = array();
		$tax_totals = $order->get_tax_totals();

		// Calculate taxable bases by summing up line items
		// This avoids rounding errors that occur when back-calculating from tax amounts
		$bases_by_rate = array();
		$zero_tax_base = 0.0;

		// Helper to process taxes for an item
		$process_item_taxes = function( $total, $taxes ) use ( &$bases_by_rate, &$zero_tax_base ) {
			$found_tax = false;
			if ( ! empty( $taxes['total'] ) ) {
				foreach ( $taxes['total'] as $rate_id => $amount ) {
					// Skip spurious zero amounts for non-zero rates
					if ( (float) $amount === 0.0 ) {
						$rate_data = WC_Tax::_get_tax_rate( $rate_id );
						$rate_percent = isset( $rate_data['tax_rate'] ) ? (float) $rate_data['tax_rate'] : 0.0;
						if ( $rate_percent > 0.0 ) {
							continue;
						}
					}

					if ( ! isset( $bases_by_rate[ $rate_id ] ) ) {
						$bases_by_rate[ $rate_id ] = 0.0;
					}
					$bases_by_rate[ $rate_id ] += $total;
					$found_tax = true;
				}
			}

			// If no valid tax rate was found for this item, add to zero tax base
			if ( ! $found_tax ) {
				$zero_tax_base += $total;
			}
		};

		// 1. Product lines
		foreach ( $order->get_items() as $item ) {
			$process_item_taxes( (float) $item->get_total(), $item->get_taxes() );
		}

		// 2. Fees
		foreach ( $order->get_fees() as $item ) {
			$process_item_taxes( (float) $item->get_total(), $item->get_taxes() );
		}

		// 3. Shipping
		foreach ( $order->get_items( 'shipping' ) as $item ) {
			$process_item_taxes( (float) $item->get_total(), $item->get_taxes() );
		}

		if ( empty( $tax_totals ) && $zero_tax_base > 0 ) {
			// No defined taxes but we have base amounts
			$tax_breakdown[] = array(
				'name'   => __( 'Exento', 'freefactu' ),
				'rate'   => 0,
				'base'   => $order->get_total(), // Using order total to be safe, or could use $zero_tax_base
				'amount' => 0,
			);
			return $tax_breakdown;
		} elseif ( empty( $tax_totals ) ) {
			// Fallback for completely empty order or error
			$tax_breakdown[] = array(
				'name'   => __( 'Exento', 'freefactu' ),
				'rate'   => 0,
				'base'   => $order->get_subtotal(),
				'amount' => 0,
			);
			return $tax_breakdown;
		}

		foreach ( $tax_totals as $tax_code => $tax ) {
			// Get tax rate from the tax class
			$rate_id = $tax->rate_id;
			$rate_percent = 0;
			$tax_name = isset( $tax->label ) ? $tax->label : '';

			if ( $rate_id ) {
				$rate_data = WC_Tax::_get_tax_rate( $rate_id );
				if ( $rate_data ) {
					$rate_percent = floatval( $rate_data['tax_rate'] );
					// Use tax rate name if label not available
					if ( empty( $tax_name ) && isset( $rate_data['tax_rate_name'] ) ) {
						$tax_name = $rate_data['tax_rate_name'];
					}
				}
			}

			// Use calculated base if available, otherwise fallback to back-calculation
			$tax_amount = floatval( $tax->amount );
			
			if ( isset( $bases_by_rate[ $rate_id ] ) ) {
				$base = $bases_by_rate[ $rate_id ];
			} else {
				$base = $rate_percent > 0 ? ( $tax_amount / $rate_percent ) * 100 : 0;
			}

			$tax_breakdown[] = array(
				'name'   => $tax_name,
				'rate'   => $rate_percent,
				'base'   => $base,
				'amount' => $tax_amount,
			);
		}

		// Add any zero-rated base that wasn't covered by existing tax totals
		if ( $zero_tax_base > 0.001 ) {
			// Check if we already have a 0% rate in the breakdown
			$merged = false;
			foreach ( $tax_breakdown as &$breakdown ) {
				if ( (float) $breakdown['rate'] === 0.0 ) {
					$breakdown['base'] += $zero_tax_base;
					$merged = true;
					break;
				}
			}
			unset( $breakdown );

			if ( ! $merged ) {
				$tax_breakdown[] = array(
					'name'   => __( 'Exento', 'freefactu' ),
					'rate'   => 0,
					'base'   => $zero_tax_base,
					'amount' => 0,
				);
			}
		}

		return $tax_breakdown;
	}

	/**
	 * Generate PDF from snapshot data (for deleted orders).
	 *
	 * What this function does:
	 * - Validates snapshot data exists and is valid array
	 * - Retrieves invoice data from database
	 * - Adds Verifactu data (CSV, hash, QR) to snapshot
	 * - Renders template using snapshot data
	 * - Generates PDF from rendered HTML
	 * - Returns PDF content or WP_Error
	 * - Allows PDF regeneration even when order is deleted
	 *
	 * Where it's called from:
	 * - abstract-base-generator.php: get_saved_pdf() method
	 * - class-freefactu-client.php: save_pdf_snapshot_by_id() method
	 *
	 * @param int   $order_id Order ID.
	 * @param array $snapshot Snapshot data array.
	 * @return string|WP_Error PDF content or WP_Error on failure.
	 */
	public function generate_from_snapshot( $order_id, $snapshot ) {

		if ( empty( $snapshot ) || ! is_array( $snapshot ) ) {
			return new WP_Error( 'invalid_snapshot', __( 'Snapshot de factura inválido.', 'freefactu' ) );
		}

		// Get invoice data from database to add verifactu information
		$invoice_db = Freefactu_DB::get_invoice_by_order_id( $order_id );
		$invoice_number = $invoice_db ? $invoice_db->invoice_number : '';

		// Inject invoice number into snapshot if not already present (it might be empty in the stored snapshot)
		if ( empty( $snapshot['invoice_number'] ) ) {
			$snapshot['invoice_number'] = $invoice_number;
		}

		// Add verifactu data to snapshot (pass null for order and snapshot data for QR generation)
		$snapshot['verifactu'] = $this->prepare_verifactu_data( $invoice_db, $invoice_number, null, $snapshot );

		// Render template using snapshot data
		$template_path = $this->get_snapshot_template_path();

		if ( ! Freefactu_Filesystem::exists( $template_path ) ) {
			return new WP_Error(
				'template_not_found',
				__( 'Plantilla de factura no encontrada.', 'freefactu' )
			);
		}

		// Prepare data variable for template
		$data = $snapshot;

		// Render template
		ob_start();
		include $template_path;
		$html = ob_get_clean();

		// Generate PDF from HTML (implementation depends on child class)
		return $this->generate_pdf_from_html( $html );
	}

	/**
	 * Get template path for snapshot rendering.
	 * Can be overridden by child classes.
	 *
	 * @return string Template path.
	 */
	protected function get_snapshot_template_path() {
		// Allow theme/child theme override
		$theme_template = get_stylesheet_directory() . '/freefactu/invoice.php';
		if ( Freefactu_Filesystem::exists( $theme_template ) ) {
			return $theme_template;
		}

		// Default plugin template
		return FREEFACTU_PLUGIN_DIR . 'templates/invoice.php';
	}

	/**
	 * Generate PDF from HTML content.
	 * Must be implemented by child classes.
	 *
	 * @param string $html HTML content.
	 * @return string|WP_Error PDF content or WP_Error on failure.
	 */
	abstract protected function generate_pdf_from_html( $html );

	/**
	 * Get saved PDF from database if it exists.
	 * This method is environment-agnostic and works in both integrated and standalone modes.
	 *
	 * @param int $order_id Order ID.
	 * @return string|null PDF content if found, null otherwise.
	 */
	protected function get_saved_pdf( $order_id ) {
		// Get PDF path from database
		$invoice = Freefactu_DB::get_invoice_by_order_id( $order_id );
		$saved_pdf_path = $invoice ? $invoice->pdf_path : '';

		// Convert stored relative path to absolute for file operations
		$absolute_path = Freefactu_Filesystem::to_absolute_path( $saved_pdf_path );

		if ( $absolute_path && file_exists( $absolute_path ) ) {
			return Freefactu_Filesystem::get_contents( $absolute_path );
		}

		// Check if order still exists
		$order = wc_get_order( $order_id );
		$order_exists = ! empty( $order ) && $order instanceof WC_Order;

		// If order doesn't exist, try to regenerate from snapshot
		if ( ! $order_exists && $invoice && ! empty( $invoice->invoice_snapshot ) ) {
			$snapshot = json_decode( $invoice->invoice_snapshot, true );
			if ( ! $snapshot || ! is_array( $snapshot ) ) {
				if ( class_exists( 'Freefactu_Logger' ) ) {
					Freefactu_Logger::log( sprintf(
						'Invalid snapshot for deleted order %d',
						$invoice->order_id
					), 'error' );
				}
				return null;
			}

			$pdf = $this->generate_from_snapshot( $invoice->order_id, $snapshot );

			if ( is_wp_error( $pdf ) ) {
				if ( class_exists( 'Freefactu_Logger' ) ) {
					Freefactu_Logger::log( sprintf(
						'Failed to regenerate PDF from snapshot for deleted order %d: %s',
						$invoice->order_id,
						$pdf->get_error_message()
					), 'error' );
				}
				return null;
			}

			// Save the regenerated PDF
			$this->save_regenerated_pdf( $invoice, $pdf );

			return $pdf;
		}

		return null;
	}

	/**
	 * Save PDF if it was missing from the filesystem.
	 * This method is environment-agnostic and works in both integrated and standalone modes.
	 *
	 * @param int    $order_id Order ID.
	 * @param string $pdf_content PDF content to save.
	 * @return void
	 */
	protected function save_pdf_if_missing( $order_id, $pdf_content ) {
		$invoice = Freefactu_DB::get_invoice_by_order_id( $order_id );
		$saved_pdf_path = $invoice ? $invoice->pdf_path : '';

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

		// Only save if PDF path was set but file is missing
		if ( ! empty( $saved_pdf_path ) && ! file_exists( $absolute_path ) ) {
			$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 = $invoice ? $invoice->invoice_number : '';
			if ( ! $invoice_number ) {
				$invoice_number = $order_id;
			}
			$safe_invoice_number = str_replace( array( '/', '\\' ), '-', $invoice_number );
			$filename = 'verifactu-' . $safe_invoice_number . '.pdf';
			$new_pdf_path = $base_dir . '/' . $filename;

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

			if ( $result !== false ) {
				// Store relative path for portability
				$relative_path = Freefactu_Filesystem::to_relative_path( $new_pdf_path );
				Freefactu_DB::update_invoice_pdf( $order_id, $relative_path );

				if ( class_exists( 'Freefactu_Logger' ) ) {
					Freefactu_Logger::log( sprintf(
						'PDF file was missing for order %d, regenerated and saved to: %s',
						$order_id,
						$new_pdf_path
					), 'info' );
				}
			}
		}
	}

	/**
	 * Save regenerated PDF from snapshot.
	 * This method is environment-agnostic and works in both integrated and standalone modes.
	 *
	 * @param object $invoice Invoice database record.
	 * @param string $pdf PDF content.
	 * @return void
	 */
	protected function save_regenerated_pdf( $invoice, $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' );
		}

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

		$result = Freefactu_Filesystem::put_contents( $new_pdf_path, $pdf );

		if ( $result !== false ) {
			// Store relative path for portability
			$relative_path = Freefactu_Filesystem::to_relative_path( $new_pdf_path );
			Freefactu_DB::update_invoice_pdf( $invoice->order_id, $relative_path );

			if ( class_exists( 'Freefactu_Logger' ) ) {
				Freefactu_Logger::log( sprintf(
					'PDF regenerated from snapshot for deleted order %d',
					$invoice->order_id
				), 'info' );
			}
		}
	}
}
