<?php
/**
 * Plugin Name: OrderFlow PDF Documents
 * Description: Generates PDF packing slips for WooCommerce orders with admin download links.
 * Version: 1.0.4
 * Requires at least: 6.0
 * Requires PHP: 7.4
 * WC requires at least: 8.0
 * WC tested up to: 10.4.2
 * Author:      Digital Lychee Creative Limited
 * Author URI: https://digitallychee.com
 * License:     GPLv3 or later
 * License URI: https://www.gnu.org/licenses/gpl-3.0.html
 * Text Domain: dl-orderflow-pdf-documents
 * Domain Path: /languages
 * Requires Plugins: woocommerce
 * 
 * Copyright (C) 2025 Digital Lychee
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

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

/**
 * Declare HPOS (High-Performance Order Storage) compatibility for WooCommerce.
 */
add_action(
	'before_woocommerce_init',
	function () {
		if (class_exists('\Automattic\WooCommerce\Utilities\FeaturesUtil')) {
			\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility(
				'custom_order_tables',
				__FILE__,
				true
			);
		}
	}
);

/**
 * Try to load Composer autoloader for FPDI/FPDF.
 */
if (!class_exists('\setasign\Fpdi\Fpdi') && file_exists(__DIR__ . '/vendor/autoload.php')) {
	require_once __DIR__ . '/vendor/autoload.php';
} elseif (!class_exists('\setasign\Fpdi\Fpdi')) {
	// Hard-fail gracefully in admin if vendor is missing AND FPDI is not already loaded by something else.
	add_action(
		'admin_notices',
		function () {
			echo '<div class="notice notice-error"><p><strong>OrderFlow PDF Documents</strong>: '
				. 'missing <code>vendor/</code>. Run <code>composer install</code> (or <code>composer require setasign/fpdi setasign/fpdf</code>) inside the plugin folder.</p></div>';
		}
	);
	// No return; plugin still loads and just won’t generate PDFs until dependencies exist.
}

if (!class_exists('DL_OrderFlow_Pdf', false)) {
	require_once __DIR__ . '/includes/class-orderflow-pdf.php';
}

/**
 * Always show "Settings" + "Upgrade to Pro" links under the plugin name.
 *
 * @param string[] $links Existing plugin action links.
 * @return string[]
 */
function dl_orderflow_pdf_documents_free_action_links($links)
{

	// WooCommerce settings tab for the FREE version.
	$settings_url = admin_url(
		'admin.php?page=wc-settings&tab=' . DL_OrderFlow_PDF_Documents::SETTINGS_TAB_ID
	);

	// Pro upgrade URL (Freemius etc).
	$upgrade_url = 'https://checkout.freemius.com/plugin/22068/plan/36875/';

	$upgrade_link = sprintf(
		'<a href="%1$s">%2$s</a>',
		esc_url($upgrade_url),
		esc_html__('Upgrade to Pro', 'dl-orderflow-pdf-documents')
	);

	$settings_link = sprintf(
		'<a href="%1$s">%2$s</a>',
		esc_url($settings_url),
		esc_html__('Settings', 'dl-orderflow-pdf-documents')
	);

	// Put Settings first, Upgrade second (or swap if you prefer).
	array_unshift($links, $upgrade_link);
	array_unshift($links, $settings_link);

	return $links;
}
add_filter(
	'plugin_action_links_' . plugin_basename(__FILE__),
	'dl_orderflow_pdf_documents_free_action_links'
);

/**
 * Inline row notice on Plugins page if WooCommerce is not active.
 */
add_action(
	'after_plugin_row_' . plugin_basename(__FILE__),
	function ($plugin_file, $plugin_data, $status) {
		if (class_exists('\WooCommerce')) {
			return;
		}
		if (!current_user_can('activate_plugins')) {
			return;
		}

		$install_url = esc_url('https://digitallycheeplugins.com/wordpress-plugins/orderflow-pdf-documents/documentation/');

		echo '<tr class="plugin-update-tr"><td colspan="3" class="plugin-update colspanchange">';
		echo '<div class="update-message notice inline notice-error"><p>';
		echo esc_html__('WooCommerce is required for OrderFlow PDF Documents.', 'dl-orderflow-pdf-documents') . ' ';

		$linked_message = sprintf(
			/* translators: %1$s is a URL to the WooCommerce installation docs. */
			__('See <a href="%1$s">how to install WooCommerce</a>.', 'dl-orderflow-pdf-documents'),
			$install_url
		);
		echo wp_kses(
			$linked_message,
			array(
				'a' => array(
					'href' => array(),
				),
			)
		);

		echo '</p></div></td></tr>';
	},
	10,
	3
);

/**
 * Main plugin class (Free build).
 */
final class DL_OrderFlow_PDF_Documents
{

	const OPTION_KEY = 'dl_orderflow_pdf_documents_settings_v1';
	const BLANK_TEMPLATE_PATH = '/assets/blank-a4.pdf';
	const SETTINGS_TAB_ID = 'dl_orderflow_pdf_documents';

	/**
	 * Last error message from PDF generation.
	 *
	 * @var string
	 */
	private $last_error = '';

	/**
	 * Constructor: hook everything.
	 */
	public function __construct()
	{

		if (!class_exists('\WooCommerce')) {
			return;
		}

		// WooCommerce Settings tab integration
		add_filter('woocommerce_settings_tabs_array', [$this, 'add_settings_tab'], 50);
		add_action('woocommerce_settings_tabs_' . self::SETTINGS_TAB_ID, [$this, 'render_settings_tab']);
		add_action('woocommerce_update_options_' . self::SETTINGS_TAB_ID, [$this, 'save_settings_tab']);

		// Assets
		add_action('admin_enqueue_scripts', array($this, 'admin_enqueue_media'));
		add_action('admin_enqueue_scripts', array($this, 'admin_enqueue_assets'));

		// Order admin UI: dedicated meta box on RHS (classic + HPOS screens).
		add_action('add_meta_boxes', array($this, 'add_order_metabox'), 10, 2);

		// Customer account area: buttons on order details page.
		add_action('woocommerce_order_details_after_order_table', array($this, 'render_customer_buttons'), 20, 1);

		// AJAX download handler (admin + customers).
		add_action('wp_ajax_ofpd_download_document', array($this, 'download_document_handler'));
		add_action('wp_ajax_nopriv_ofpd_download_document', array($this, 'download_document_handler'));
	}

	/**
	 * Default settings, including per-document configuration.
	 *
	 * Free version:
	 * - Only packing slip is enabled and visible.
	 * - Invoice and credit note are Pro-only and forced disabled.
	 *
	 * @return array
	 */
	private function get_default_settings()
	{
		return array(
			'logo_id' => 0,
			'logo_width' => 40, // mm.
			'show_store_name' => 1,
			'show_tagline' => 0,
			'show_website' => 1,
			'show_physical_address' => 0,
			'delete_on_uninstall' => 0,

			// FREE VERSION: Only one document allowed.
			'documents' => array(
				'packing_slip' => array(
					'enabled' => 1,
					'label' => __('Packing Slip', 'dl-orderflow-pdf-documents'),
					'show_in_admin' => 1,
					'show_in_customer' => 0,
					'columns' => array('product', 'qty'),
					'show_totals' => 0,
					'attach_to_admin_new_order' => 0,
					'attach_to_customer_emails' => 0,
				),
			),
		);
	}

	/**
	 * Get current settings merged with defaults.
	 *
	 * @return array
	 */
	private function get_settings()
	{
		$defaults = $this->get_default_settings();
		$raw = get_option(self::OPTION_KEY, array());

		if (!is_array($raw)) {
			$raw = array();
		}

		// Merge DB options over defaults, then run through
		// sanitize_settings() to enforce free-version rules.
		$merged = array_merge($defaults, $raw);

		return $this->sanitize_settings($merged);
	}

	/**
	 * Get a logo path that FPDI can handle (PNG/JPEG only).
	 *
	 * @param int $logo_id Attachment ID.
	 * @return string|false Absolute file path or false if unsupported.
	 */
	private function get_supported_logo_path($logo_id)
	{
		$logo_id = (int) $logo_id;
		if (!$logo_id) {
			return false;
		}

		$mime = get_post_mime_type($logo_id);
		if (!$mime) {
			return false;
		}

		$allowed = array(
			'image/jpeg',
			'image/jpg',
			'image/png',
		);

		if (!in_array($mime, $allowed, true)) {
			$this->set_last_error('Unsupported logo mime type: ' . $mime . '. Only JPEG and PNG are supported.');
			return false;
		}

		$path = get_attached_file($logo_id);
		if (!$path || !file_exists($path)) {
			return false;
		}

		return $path;
	}

	public function add_settings_tab($tabs)
	{
		$tabs[self::SETTINGS_TAB_ID] = __('OrderFlow PDF Documents', 'dl-orderflow-pdf-documents-pro');
		return $tabs;
	}

	/**
	 * Sanitize settings on save.
	 *
	 * Free build rules:
	 * - Only packing_slip can be enabled/visible.
	 * - Invoice + credit_note forced disabled/invisible.
	 * - No email attachments (all forced false).
	 * - Labels not editable (forced to defaults).
	 *
	 * @param array $input Raw input.
	 * @return array
	 */
	public function sanitize_settings($input)
	{
		$defaults = $this->get_default_settings();
		$output = $defaults;

		$output['logo_id'] = isset($input['logo_id']) ? absint($input['logo_id']) : 0;
		$output['logo_width'] = isset($input['logo_width']) ? absint($input['logo_width']) : $defaults['logo_width'];
		$output['show_store_name'] = !empty($input['show_store_name']) ? 1 : 0;
		$output['show_tagline'] = !empty($input['show_tagline']) ? 1 : 0;
		$output['show_website'] = !empty($input['show_website']) ? 1 : 0;
		$output['show_physical_address'] = !empty($input['show_physical_address']) ? 1 : 0;
		$output['delete_on_uninstall'] = !empty($input['delete_on_uninstall']) ? 1 : 0;

		// Back-compat: old key "show_address" -> now store name + website.
		if (
			isset($input['show_address'])
			&& !isset($input['show_store_name'], $input['show_tagline'], $input['show_website'], $input['show_physical_address'])
		) {
			$on = !empty($input['show_address']) ? 1 : 0;
			$output['show_store_name'] = $on;
			$output['show_website'] = $on;
		}

		$allowed_cols = array('product', 'sku', 'qty', 'price', 'total');

		// Only packing slip exists in free.
		$doc_key = 'packing_slip';
		$doc_default = $defaults['documents'][$doc_key];

		$in_doc = array();
		if (isset($input['documents'][$doc_key]) && is_array($input['documents'][$doc_key])) {
			$in_doc = $input['documents'][$doc_key];
		}

		$doc = $doc_default;

		$doc['enabled'] = !empty($in_doc['enabled']) ? 1 : 0;
		$doc['show_in_admin'] = !empty($in_doc['show_in_admin']) ? 1 : 0;
		$doc['show_in_customer'] = !empty($in_doc['show_in_customer']) ? 1 : 0;
		$doc['show_totals'] = !empty($in_doc['show_totals']) ? 1 : 0;

		// Allow label editing in free (avoids “greyed out” read-only pro prompt).
		if (isset($in_doc['label'])) {
			$label = sanitize_text_field($in_doc['label']);
			$doc['label'] = ('' !== trim($label)) ? $label : $doc_default['label'];
		}

		// Columns
		$cols = array();
		if (isset($in_doc['columns']) && is_array($in_doc['columns'])) {
			foreach ($in_doc['columns'] as $col) {
				$col = sanitize_key($col);
				if (in_array($col, $allowed_cols, true)) {
					$cols[] = $col;
				}
			}
		}
		$doc['columns'] = !empty($cols) ? array_values(array_unique($cols)) : $doc_default['columns'];

		$output['documents'] = array(
			$doc_key => $doc,
		);

		return $output;
	}

	/**
	 * Enqueue media library on our settings page for the logo uploader.
	 *
	 * @param string $hook Current admin page hook.
	 */
	public function admin_enqueue_media($hook)
	{
		if ('woocommerce_page_wc-settings' !== $hook) {
			return;
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$raw_tab = isset($_GET['tab'])
			? sanitize_text_field(wp_unslash($_GET['tab']))
			: '';

		if (self::SETTINGS_TAB_ID !== $raw_tab) {
			return;
		}

		wp_enqueue_media();
	}

	/**
	 * Render settings page.
	 */
	public function render_settings_tab()
	{
		if (!current_user_can('manage_woocommerce')) {
			return;
		}

		$settings = $this->get_settings();
		$logo_id = !empty($settings['logo_id']) ? (int) $settings['logo_id'] : 0;
		$logo_url = $logo_id ? wp_get_attachment_image_url($logo_id, 'medium') : '';
		$doc = isset($settings['documents']['packing_slip']) ? $settings['documents']['packing_slip'] : array();
		?>

		<div class="notice notice-info" style="margin-top:15px;">
			<p>
				<?php esc_html_e('Need invoices, credit notes and email attachments? Upgrade to OrderFlow PDF Documents Pro.', 'dl-orderflow-pdf-documents'); ?>
				<a href="https://checkout.freemius.com/plugin/22068/plan/36875/" target="_blank" rel="noopener noreferrer">
					<?php esc_html_e('Learn more', 'dl-orderflow-pdf-documents'); ?>
				</a>
			</p>
		</div>

		<h2><?php esc_html_e('Global Settings', 'dl-orderflow-pdf-documents'); ?></h2>
		<table class="form-table" role="presentation">
			<tbody>
				<tr>
					<th scope="row"><?php esc_html_e('Logo', 'dl-orderflow-pdf-documents'); ?></th>
					<td>
						<div id="ofpd-logo-preview" style="margin-bottom:10px;">
							<?php if ($logo_url): ?>
								<img src="<?php echo esc_url($logo_url); ?>"
									style="max-width:200px;height:auto;border:1px solid #ccc;padding:4px;" alt="" />
							<?php endif; ?>
						</div>

						<input type="hidden" name="<?php echo esc_attr(self::OPTION_KEY); ?>[logo_id]" id="ofpd-logo-id"
							value="<?php echo esc_attr($logo_id); ?>" />

						<button type="button" class="button" id="ofpd-logo-select">
							<?php echo $logo_url ? esc_html__('Change logo', 'dl-orderflow-pdf-documents') : esc_html__('Select logo', 'dl-orderflow-pdf-documents'); ?>
						</button>

						<button type="button" class="button" id="ofpd-logo-remove" <?php disabled(!$logo_url); ?>>
							<?php esc_html_e('Remove logo', 'dl-orderflow-pdf-documents'); ?>
						</button>

						<p class="description">
							<?php esc_html_e('Upload or choose a logo to display at the top of documents. If no logo is set, the site name will be used.', 'dl-orderflow-pdf-documents'); ?>
						</p>
					</td>
				</tr>

				<tr>
					<th scope="row"><?php esc_html_e('Logo Max Width (mm)', 'dl-orderflow-pdf-documents'); ?></th>
					<td>
						<input type="number" min="10" max="100" name="<?php echo esc_attr(self::OPTION_KEY); ?>[logo_width]"
							value="<?php echo esc_attr($settings['logo_width']); ?>" class="small-text" />
						<p class="description">
							<?php esc_html_e('Measured in millimetres on the A4 page. As a guide, 30–50 mm is a fairly typical logo width.', 'dl-orderflow-pdf-documents'); ?>
						</p>
					</td>
				</tr>

				<tr>
					<th scope="row">
						<?php esc_html_e('Header Contact Details', 'dl-orderflow-pdf-documents'); ?>
					</th>
					<td>
						<fieldset>
							<label style="display:block; margin-bottom:4px;">
								<input type="checkbox" name="<?php echo esc_attr(self::OPTION_KEY); ?>[show_store_name]"
									value="1" <?php checked(!empty($settings['show_store_name'])); ?> />
								<?php esc_html_e('Show store/site name', 'dl-orderflow-pdf-documents'); ?>
							</label>

							<label style="display:block; margin-bottom:4px;">
								<input type="checkbox" name="<?php echo esc_attr(self::OPTION_KEY); ?>[show_tagline]" value="1"
									<?php checked(!empty($settings['show_tagline'])); ?> />
								<?php esc_html_e('Show site tagline', 'dl-orderflow-pdf-documents'); ?>
							</label>

							<label style="display:block; margin-bottom:4px;">
								<input type="checkbox" name="<?php echo esc_attr(self::OPTION_KEY); ?>[show_website]" value="1"
									<?php checked(!empty($settings['show_website'])); ?> />
								<?php esc_html_e('Show website URL', 'dl-orderflow-pdf-documents'); ?>
							</label>

							<label style="display:block; margin-bottom:4px;">
								<input type="checkbox" name="<?php echo esc_attr(self::OPTION_KEY); ?>[show_physical_address]"
									value="1" <?php checked(!empty($settings['show_physical_address'])); ?> />
								<?php esc_html_e('Show store address (from WooCommerce settings)', 'dl-orderflow-pdf-documents'); ?>
							</label>

							<p class="description">
								<?php esc_html_e('These details appear beneath the logo/title on each document. The store address uses your WooCommerce → Settings → General → Store Address.', 'dl-orderflow-pdf-documents'); ?>
							</p>
						</fieldset>
					</td>
				</tr>

				<tr>
					<th scope="row"><?php esc_html_e('Clean Up on Uninstall', 'dl-orderflow-pdf-documents'); ?></th>
					<td>
						<label>
							<input type="checkbox" name="<?php echo esc_attr(self::OPTION_KEY); ?>[delete_on_uninstall]"
								value="1" <?php checked(!empty($settings['delete_on_uninstall'])); ?> />
							<?php esc_html_e('Delete all plugin data when this plugin is uninstalled.', 'dl-orderflow-pdf-documents'); ?>
						</label>
						<p class="description">
							<?php esc_html_e('Includes plugin settings and generated PDF files in the uploads/orderflow-pdf-documents folder.', 'dl-orderflow-pdf-documents'); ?>
						</p>
					</td>
				</tr>
			</tbody>
		</table>

		<h2><?php esc_html_e('Packing Slip', 'dl-orderflow-pdf-documents'); ?></h2>

		<?php
		$all_cols_labels = array(
			'product' => __('Product', 'dl-orderflow-pdf-documents'),
			'sku'     => __('SKU', 'dl-orderflow-pdf-documents'),
			'qty'     => __('Quantity', 'dl-orderflow-pdf-documents'),
			'price'   => __('Price', 'dl-orderflow-pdf-documents'),
			'total'   => __('Line Total', 'dl-orderflow-pdf-documents'),
		);

		$doc_label = isset($doc['label']) ? $doc['label'] : __('Packing Slip', 'dl-orderflow-pdf-documents');
		$columns = isset($doc['columns']) && is_array($doc['columns']) ? $doc['columns'] : array('product', 'qty');
		?>

		<table class="form-table" role="presentation" style="margin-bottom:25px;">
			<tbody>
				<tr>
					<th scope="row"><?php esc_html_e('Enabled', 'dl-orderflow-pdf-documents'); ?></th>
					<td>
						<label>
							<input type="checkbox"
								name="<?php echo esc_attr(self::OPTION_KEY); ?>[documents][packing_slip][enabled]"
								value="1" <?php checked(!empty($doc['enabled'])); ?> />
							<?php esc_html_e('Enable packing slips.', 'dl-orderflow-pdf-documents'); ?>
						</label>
					</td>
				</tr>

				<tr>
					<th scope="row"><?php esc_html_e('Label', 'dl-orderflow-pdf-documents'); ?></th>
					<td>
						<input type="text"
							name="<?php echo esc_attr(self::OPTION_KEY); ?>[documents][packing_slip][label]"
							value="<?php echo esc_attr($doc_label); ?>" class="regular-text" />
						<p class="description">
							<?php esc_html_e('This label appears as the document title and on download buttons.', 'dl-orderflow-pdf-documents'); ?>
						</p>
					</td>
				</tr>

				<tr>
					<th scope="row"><?php esc_html_e('Where available', 'dl-orderflow-pdf-documents'); ?></th>
					<td>
						<label style="display:block;">
							<input type="checkbox"
								name="<?php echo esc_attr(self::OPTION_KEY); ?>[documents][packing_slip][show_in_admin]"
								value="1" <?php checked(!empty($doc['show_in_admin'])); ?> />
							<?php esc_html_e('Show in admin order screen (meta box).', 'dl-orderflow-pdf-documents'); ?>
						</label>
						<label style="display:block;">
							<input type="checkbox"
								name="<?php echo esc_attr(self::OPTION_KEY); ?>[documents][packing_slip][show_in_customer]"
								value="1" <?php checked(!empty($doc['show_in_customer'])); ?> />
							<?php esc_html_e('Show in customer account order details.', 'dl-orderflow-pdf-documents'); ?>
						</label>
					</td>
				</tr>

				<tr>
					<th scope="row"><?php esc_html_e('Columns', 'dl-orderflow-pdf-documents'); ?></th>
					<td>
						<?php foreach ($all_cols_labels as $col_key => $col_label): ?>
							<label style="display:block; margin-bottom:4px;">
								<input type="checkbox"
									name="<?php echo esc_attr(self::OPTION_KEY); ?>[documents][packing_slip][columns][]"
									value="<?php echo esc_attr($col_key); ?>"
									<?php checked(in_array($col_key, $columns, true)); ?> />
								<?php echo esc_html($col_label); ?>
							</label>
						<?php endforeach; ?>
						<p class="description">
							<?php esc_html_e('For a packing slip you might use Product + Quantity only.', 'dl-orderflow-pdf-documents'); ?>
						</p>
					</td>
				</tr>

				<tr>
					<th scope="row"><?php esc_html_e('Totals', 'dl-orderflow-pdf-documents'); ?></th>
					<td>
						<label>
							<input type="checkbox"
								name="<?php echo esc_attr(self::OPTION_KEY); ?>[documents][packing_slip][show_totals]"
								value="1" <?php checked(!empty($doc['show_totals'])); ?> />
							<?php esc_html_e('Show order totals section at the bottom (subtotal, shipping, tax, total, etc.).', 'dl-orderflow-pdf-documents'); ?>
						</label>
					</td>
				</tr>
			</tbody>
		</table>

		<?php
	}

	public function save_settings_tab()
	{
		if (!current_user_can('manage_woocommerce')) {
			return;
		}

		// This is the WC settings form nonce.
		check_admin_referer('woocommerce-settings');

		if (!isset($_POST[self::OPTION_KEY]) || !is_array($_POST[self::OPTION_KEY])) {
			return;
		}

		$raw = isset($_POST[self::OPTION_KEY])
			? wp_unslash($_POST[self::OPTION_KEY]) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			: array();

		// Light sanitisation to satisfy Plugin Check.
		$raw = map_deep($raw, 'sanitize_text_field');

		// Full deep sanitisation.
		$san = $this->sanitize_settings($raw);
		update_option(self::OPTION_KEY, $san);
	}

	/**
	 * Set last error.
	 *
	 * @param string $msg Error message.
	 */
	private function set_last_error($msg)
	{
		$this->last_error = (string) $msg;
	}

	/**
	 * Get last error.
	 *
	 * @return string
	 */
	private function get_last_error()
	{
		return $this->last_error;
	}

	/**
	 * Generate (or regenerate) PDF document for an order using FPDI and a blank template.
	 *
	 * @param WC_Order $order   Order object.
	 * @param string   $doc_key Document type key (e.g. packing_slip).
	 * @param array    $doc     Document settings.
	 * @return string|false     Full path to PDF or false on failure.
	 */
	/**
	 * Generate (or regenerate) PDF document for an order using FPDI and a blank template.
	 *
	 * @param WC_Order $order   Order object.
	 * @param string   $doc_key Document type key (e.g. packing_slip).
	 * @param array    $doc     Document settings.
	 * @return string|false     Full path to PDF or false on failure.
	 */
	private function generate_pdf_for_order(WC_Order $order, $doc_key, array $doc)
	{
		if (!class_exists('DL_OrderFlow_Pdf')) {
			$this->set_last_error('DL_OrderFlow_Pdf / FPDI class not found. Is the composer autoloader loaded?');
			return false;
		}

		$uploads = wp_upload_dir();
		if (!empty($uploads['error'])) {
			$this->set_last_error('wp_upload_dir error: ' . $uploads['error']);
			return false;
		}

		$dir = trailingslashit($uploads['basedir']) . 'orderflow-pdf-documents/';

		if (!is_dir($dir) && !wp_mkdir_p($dir)) {
			$this->set_last_error('Cannot create directory: ' . $dir);
			return false;
		}

		$file_path = $dir . sprintf(
			'orderflow-%s-%d.pdf',
			sanitize_key($doc_key),
			$order->get_id()
		);

		$settings = $this->get_settings();
		$logo_id = !empty($settings['logo_id']) ? (int) $settings['logo_id'] : 0;
		$logo_width = isset($settings['logo_width']) ? (int) $settings['logo_width'] : 80;

		// New granular header settings with sensible defaults if missing.
		$show_store_name = isset($settings['show_store_name']) ? !empty($settings['show_store_name']) : true;
		$show_tagline = !empty($settings['show_tagline']);
		$show_website = !empty($settings['show_website']);
		$show_physical_address = !empty($settings['show_physical_address']);

		$columns = isset($doc['columns']) && is_array($doc['columns']) ? $doc['columns'] : array('product', 'qty');
		$show_totals = !empty($doc['show_totals']);
		$doc_label = isset($doc['label']) ? $doc['label'] : ucfirst(str_replace('_', ' ', $doc_key));

		$blank_tpl_path = __DIR__ . self::BLANK_TEMPLATE_PATH;

		try {
			$orientation = 'P';
			$pageW = 210;
			$pageH = 297;

			// --- Create PDF & (optionally) apply blank template ---
			if ($blank_tpl_path && file_exists($blank_tpl_path) && is_readable($blank_tpl_path)) {
				$probe = new DL_OrderFlow_Pdf('P', 'mm');
				$probe->setSourceFile($blank_tpl_path);
				$tplId = $probe->importPage(1);
				$ts = $probe->getTemplateSize($tplId);

				$pageW = isset($ts['width']) ? (float) $ts['width'] : 210;
				$pageH = isset($ts['height']) ? (float) $ts['height'] : 297;
				$orientation = ($pageW > $pageH) ? 'L' : 'P';

				$pdf = new DL_OrderFlow_Pdf($orientation, 'mm', array($pageW, $pageH));
				$pdf->AliasNbPages();
				$pdf->SetMargins(10, 10, 10);
				//$pdf->SetAutoPageBreak(true, 20);
				$pdf->SetAutoPageBreak(false);
				$pdf->AddPage($orientation, array($pageW, $pageH));
				$pdf->setSourceFile($blank_tpl_path);
				$tpl = $pdf->importPage(1);
				$pdf->useTemplate($tpl, 0, 0, $pageW, $pageH, true);
			} else {
				$pdf = new DL_OrderFlow_Pdf('P', 'mm', array(210, 297));
				$pdf->AliasNbPages();
				$pdf->SetMargins(10, 10, 10);
				//$pdf->SetAutoPageBreak(true, 20);
				$pdf->SetAutoPageBreak(false);
				$pdf->AddPage('P', array(210, 297));
				$pageW = 210;
				$pageH = 297;
			}

			// --- Dynamic column widths ---
			$left_margin = 10;
			$right_margin = 10;
			$pageW = $pdf->GetPageWidth();
			$usable_width = $pageW - $left_margin - $right_margin;

			$col_weights = array(
				'product' => 4,
				'sku' => 2,
				'qty' => 1,
				'price' => 2,
				'total' => 2,
			);

			$active_weights = 0;
			foreach ($columns as $col) {
				$active_weights += isset($col_weights[$col]) ? $col_weights[$col] : 1;
			}

			$col_widths = array();
			foreach ($columns as $col) {
				$w = isset($col_weights[$col]) ? $col_weights[$col] : 1;
				$col_widths[$col] = ($usable_width * $w) / max(1, $active_weights);
			}

			// --- Header: logo or site name ---
			if ($logo_id) {
				$logo_path = $this->get_supported_logo_path($logo_id);
				if ($logo_path) {
					$pdf->Image($logo_path, 10, 10, $logo_width);

					$logo_h_mm = 0;
					$meta = wp_get_attachment_metadata($logo_id);
					if (is_array($meta) && !empty($meta['width']) && !empty($meta['height'])) {
						$ratio = $logo_width / max(1, (float) $meta['width']);
						$logo_h_mm = (float) $meta['height'] * $ratio;
					} else {
						$logo_h_mm = $logo_width * 0.6;
					}

					$pdf->SetXY(10, 10 + $logo_h_mm + 5);
				} else {
					$pdf->SetFont('Arial', 'B', 18);
					$pdf->Cell(0, 10, $this->safe_text(get_bloginfo('name')), 0, 1, 'L');
				}
			} else {
				$pdf->SetFont('Arial', 'B', 18);
				$pdf->Cell(0, 10, $this->safe_text(get_bloginfo('name')), 0, 1, 'L');
			}

			// --- Title ---
			$pdf->SetFont('Arial', 'B', 16);
			$title = sprintf('%s - Order #%d', $doc_label, (int) $order->get_id());
			$safe_title = $this->safe_text($title);
			$pdf->SetTitle($safe_title, true);
			$pdf->Cell(0, 10, $safe_title, 0, 1, 'L');

			// --- Company address ---
// --- Company / store contact block beneath header ---
			$address_lines = array();

			if ($show_store_name) {
				$name = get_bloginfo('name');
				if ($name) {
					$address_lines[] = $name;
				}
			}

			if ($show_tagline) {
				$tagline = get_bloginfo('description');
				if ($tagline) {
					$address_lines[] = $tagline;
				}
			}

			if ($show_website) {
				$address_lines[] = home_url();
			}

			if ($show_physical_address && function_exists('WC') && WC()->countries) {
				$countries = WC()->countries;

				$addr1 = $countries->get_base_address();
				$addr2 = $countries->get_base_address_2();
				$city = $countries->get_base_city();
				$postcode = $countries->get_base_postcode();
				$country = $countries->get_base_country();

				if ($addr1) {
					$address_lines[] = $addr1;
				}
				if ($addr2) {
					$address_lines[] = $addr2;
				}

				$city_line = trim($city . ' ' . $postcode);
				if ($city_line !== '') {
					$address_lines[] = $city_line;
				}

				if ($country) {
					$country_name = isset($countries->countries[$country])
						? $countries->countries[$country]
						: $country;
					$address_lines[] = $country_name;
				}
			}

			if (!empty($address_lines)) {
				$pdf->SetFont('Arial', '', 10);

				foreach ($address_lines as $line) {
					$line = trim(wp_strip_all_tags($line));
					if ('' !== $line) {
						$pdf->Cell(0, 5, $this->safe_text($line), 0, 1, 'L');
					}
				}

				$pdf->Ln(5);
			}


			// --- Customer info ---
			$pdf->SetFont('Arial', '', 12);
			$pdf->Cell(0, 8, $this->safe_text(__('Customer', 'dl-orderflow-pdf-documents')), 0, 1, 'L');

			$billing_html = $order->get_formatted_billing_address();
			$billing_html = preg_replace('#<br\s*/?>#i', "\n", (string) $billing_html);
			$billing_text = wp_strip_all_tags($billing_html);
			$billing_lines = preg_split('/\r\n|\r|\n/', (string) $billing_text);

			foreach ($billing_lines as $line) {
				$line = trim($line);
				if ('' !== $line) {
					$pdf->Cell(0, 6, $this->safe_text($line), 0, 1, 'L');
				}
			}
			$pdf->Ln(6);

			// --- Table header ---
			$this->draw_table_header($pdf, $columns, $col_widths);

			// --- Table rows (wrapped) ---
			$pdf->SetFont('Arial', '', 12);
			$currency = $order->get_currency();

			foreach ($order->get_items() as $item) {
				$product = $item->get_product();
				$qty = $item->get_quantity();
				$line_total = (float) $item->get_total();
				$line_subtotal = (float) $item->get_subtotal();
				$unit_price = $qty > 0 ? $line_subtotal / $qty : $line_subtotal;

				// --- Product name & variation / meta lines ---

				// Start from the line item name. In WooCommerce this usually includes the variation,
				// e.g. "T-Shirt – Blue, Large".
				$product_name = $item->get_name();
				// Normalise the string a bit so our replacements actually match.
				$product_name = html_entity_decode((string) $product_name, ENT_QUOTES, 'UTF-8');

				// Replace non-breaking spaces with normal spaces (in case WooCommerce is using &nbsp;).
				$product_name = str_replace("\xC2\xA0", ' ', $product_name);

				// 1) Put a line break between product name and variation part.
				//    Match " something – something" OR " something - something", just once.
				$product_name = preg_replace('/\s[–-]\s/u', "\n", $product_name, 1);

				// 2) Then break comma-separated attributes onto their own lines.
				//    "Blue, Large" → "Blue\nLarge".
				$product_name = str_replace(', ', "\n", $product_name);

				// Safety fallback: if for some reason it's empty, fall back to the product name.
				if ('' === trim((string) $product_name) && $product) {
					$product_name = $product->get_name();
				}

				$sku = $product ? $product->get_sku() : '';

				// Build the "product" cell: main name + visible meta on new lines.
				$product_cell_lines = array();
				$product_cell_lines[] = $product_name;

				// Visible item meta: attributes, gift message, custom options, etc.
				$formatted_meta = $item->get_formatted_meta_data(''); // '' = include all visible meta.
				if (!empty($formatted_meta)) {
					foreach ($formatted_meta as $meta) {
						$label = wp_strip_all_tags($meta->display_key);
						$value = wp_strip_all_tags($meta->display_value);

						if ('' === $value) {
							continue;
						}

						$product_cell_lines[] = $label . ': ' . $value;
					}
				}

				// Join everything with "\n" so each goes to its own line in the product cell.
				$product_cell_text = implode("\n", $product_cell_lines);

				$row_values = array();
				foreach ($columns as $col) {
					switch ($col) {
						case 'product':
							$row_values[$col] = $product_cell_text;
							break;

						case 'sku':
							$row_values[$col] = $sku;
							break;

						case 'qty':
							$row_values[$col] = (string) $qty;
							break;

						case 'price':
							$price_html = wc_price($unit_price, array('currency' => $currency));
							$val = wp_strip_all_tags($price_html);
							$val = html_entity_decode($val, ENT_QUOTES, 'UTF-8');
							$row_values[$col] = $val;
							break;

						case 'total':
							$total_html = wc_price($line_total, array('currency' => $currency));
							$val = wp_strip_all_tags($total_html);
							$val = html_entity_decode($val, ENT_QUOTES, 'UTF-8');
							$row_values[$col] = $val;
							break;
					}
				}

				$this->render_wrapped_row($pdf, $columns, $col_widths, $row_values);
			}

			// --- Totals section (if enabled) ---
			if ($show_totals) {
				$pdf->Ln(5);
				$pdf->SetFont('Arial', 'B', 12);
				$pdf->Cell(0, 8, $this->safe_text(__('Order Totals', 'dl-orderflow-pdf-documents')), 0, 1, 'L');

				$pdf->SetFont('Arial', '', 12);
				$totals = $order->get_order_item_totals();
				if (!empty($totals)) {
					foreach ($totals as $total) {
						$label = isset($total['label']) ? wp_strip_all_tags($total['label']) : '';
						$value = isset($total['value'])
							? html_entity_decode(wp_strip_all_tags($total['value']), ENT_QUOTES, 'UTF-8')
							: '';

						$pdf->Cell(80, 6, $this->safe_text($label), 0, 0, 'L');
						$pdf->Cell(40, 6, $this->safe_text($value), 0, 1, 'L');
					}
				}
			}

			// Clean old PDF if it exists (ensures no stale content remains).
			if (file_exists($file_path)) {
				@unlink($file_path);
			}

			$pdf->Output('F', $file_path);

			return $file_path;

		} catch (\Throwable $e) {
			$this->set_last_error(
				'FPDI error while generating document "'
				. $doc_key
				. '" for order '
				. $order->get_id()
				. ': ' . $e->getMessage()
				. ' in ' . $e->getFile() . ':' . $e->getLine()
			);
			return false;
		}
	}


	/**
	 * Get existing or generate PDF for an order + document type.
	 *
	 * @param WC_Order $order   Order object.
	 * @param string   $doc_key Document key.
	 * @param array    $doc     Document settings.
	 * @return string|false
	 */
	private function get_pdf_path_for_order(WC_Order $order, $doc_key, array $doc)
	{
		$uploads = wp_upload_dir();
		if (!empty($uploads['error'])) {
			return false;
		}

		$dir = trailingslashit($uploads['basedir']) . 'orderflow-pdf-documents-pro/';
		$file_path = $dir . sprintf(
			'orderflow-%s-%d.pdf',
			sanitize_key($doc_key),
			$order->get_id()
		);

		if (file_exists($file_path)) {
			// DEV: always regenerate while you're tweaking layout.
			return $this->generate_pdf_for_order($order, $doc_key, $doc);
			//return $file_path;
		}

		return $this->generate_pdf_for_order($order, $doc_key, $doc);
	}

	/**
	 * Register OrderFlow PDF Documents meta box on order screens.
	 *
	 * Works for both classic "shop_order" editor and the HPOS wc-orders screen.
	 *
	 * @param string       $post_type Current post type or screen ID.
	 * @param WP_Post|mixed $post     Post object (classic) or WC order / other (HPOS).
	 */
	public function add_order_metabox($post_type, $post)
	{
		$screen_ids = array(
			'shop_order',
			'woocommerce_page_wc-orders',
		);

		$screen = function_exists('get_current_screen') ? get_current_screen() : null;
		$screen_id = $screen ? $screen->id : $post_type;

		if (!in_array($screen_id, $screen_ids, true)) {
			return;
		}

		add_meta_box(
			'ofpd_order_documents',
			__('OrderFlow PDF Documents', 'dl-orderflow-pdf-documents'),
			array($this, 'render_order_metabox'),
			$screen_id,
			'side',
			'default'
		);
	}

	/**
	 * Render the OrderFlow PDF Documents meta box in admin order screen.
	 *
	 * @param mixed $object WP_Post (classic) or WC_Order / other (HPOS).
	 */
	public function render_order_metabox($object)
	{
		if (!current_user_can('manage_woocommerce')) {
			echo '<p>' . esc_html__('You do not have permission to download documents.', 'dl-orderflow-pdf-documents') . '</p>';
			return;
		}

		if ($object instanceof WC_Order) {
			$order = $object;
		} elseif (is_object($object) && isset($object->ID)) {
			$order = wc_get_order($object->ID);
		} else {
			$order = null;
		}

		if (!($order instanceof WC_Order)) {
			echo '<p>' . esc_html__('Order not found.', 'dl-orderflow-pdf-documents') . '</p>';
			return;
		}

		$settings = $this->get_settings();
		$docs = isset($settings['documents']) && is_array($settings['documents']) ? $settings['documents'] : array();

		if (empty($docs)) {
			echo '<p>' . esc_html__('No documents configured.', 'dl-orderflow-pdf-documents') . '</p>';
			return;
		}

		echo '<p>';
		foreach ($docs as $key => $doc) {
			if (empty($doc['enabled']) || empty($doc['show_in_admin'])) {
				continue;
			}

			$label = isset($doc['label']) ? $doc['label'] : ucfirst(str_replace('_', ' ', $key));

			$url = wp_nonce_url(
				admin_url(
					'admin-ajax.php?action=ofpd_download_document&order_id=' .
					$order->get_id() .
					'&doc_type=' . urlencode($key)
				),
				'ofpd_download_document_' . $order->get_id() . '_' . $key
			);

			echo '<a class="button button-secondary" style="margin-bottom:5px;display:block;" href="' . esc_url($url) . '" target="_blank">';
			/* translators: %s: document label (e.g. Packing Slip). */
			echo esc_html(sprintf(__('Download %s (PDF)', 'dl-orderflow-pdf-documents'), $label));
			echo '</a>';
		}
		echo '</p>';
	}

	/**
	 * Render buttons in customer account order details (front-end).
	 *
	 * @param WC_Order $order Order object.
	 */
	public function render_customer_buttons($order)
	{
		if (!($order instanceof WC_Order)) {
			return;
		}

		// Only logged-in users who own this order.
		if (!is_user_logged_in()) {
			return;
		}
		if ((int) $order->get_user_id() !== (int) get_current_user_id()) {
			return;
		}

		$settings = $this->get_settings();
		$docs = isset($settings['documents']) && is_array($settings['documents']) ? $settings['documents'] : array();

		$buttons = array();

		foreach ($docs as $key => $doc) {
			if (empty($doc['enabled']) || empty($doc['show_in_customer'])) {
				continue;
			}

			$label = isset($doc['label']) ? $doc['label'] : ucfirst(str_replace('_', ' ', $key));

			$url = wp_nonce_url(
				add_query_arg(
					array(
						'action' => 'ofpd_download_document',
						'order_id' => $order->get_id(),
						'doc_type' => $key,
					),
					admin_url('admin-ajax.php')
				),
				'ofpd_download_document_' . $order->get_id() . '_' . $key
			);

			$buttons[] = sprintf(
				'<a class="button" style="margin-right:5px;margin-bottom:5px;" href="%s" target="_blank">%s</a>',
				esc_url($url),
				/* translators: %s: The document label, e.g. "Invoice", "Packing Slip", or "Credit Note". */
				esc_html(sprintf(__('Download %s (PDF)', 'dl-orderflow-pdf-documents-pro'), $label))
			);
		}

		if (!empty($buttons)) {
			echo '<section class="ofpd-documents" style="margin-top:20px;">';
			echo '<h2>' . esc_html__('Documents', 'dl-orderflow-pdf-documents-pro') . '</h2>';
			echo '<p>';
			// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
			echo implode(' ', $buttons);
			echo '</p>';
			echo '</section>';
		}
	}

	/**
	 * AJAX handler: stream PDF to the browser (admin + customers).
	 */
	public function download_document_handler()
	{
		$order_id = isset($_GET['order_id']) ? absint($_GET['order_id']) : 0;
		$doc_key = isset($_GET['doc_type']) ? sanitize_key($_GET['doc_type']) : '';

		if (!$order_id || !$doc_key) {
			wp_die(esc_html__('Invalid request.', 'dl-orderflow-pdf-documents-pro'), '', array('response' => 400));
		}

		check_admin_referer('ofpd_download_document_' . $order_id . '_' . $doc_key);

		$order = wc_get_order($order_id);
		if (!($order instanceof WC_Order)) {
			wp_die(esc_html__('Order not found.', 'dl-orderflow-pdf-documents-pro'), '', array('response' => 404));
		}

		$settings = $this->get_settings();
		$docs = isset($settings['documents']) && is_array($settings['documents']) ? $settings['documents'] : array();

		if (!isset($docs[$doc_key])) {
			wp_die(esc_html__('Document type not found.', 'dl-orderflow-pdf-documents-pro'), '', array('response' => 400));
		}

		$doc = $docs[$doc_key];
		$doc_label = isset($doc['label']) ? $doc['label'] : ucfirst(str_replace('_', ' ', $doc_key));
		$filename = sanitize_title($doc_label) . '-' . $order->get_order_number() . '.pdf';
		if (empty($doc['enabled'])) {
			wp_die(esc_html__('This document type is disabled.', 'dl-orderflow-pdf-documents-pro'), '', array('response' => 403));
		}

		$is_admin = current_user_can('manage_woocommerce');

		if ($is_admin) {
			if (empty($doc['show_in_admin'])) {
				wp_die(esc_html__('This document type is not available in admin.', 'dl-orderflow-pdf-documents-pro'), '', array('response' => 403));
			}
		} else {
			// Customer: must be logged in and own the order.
			if (!is_user_logged_in()) {
				wp_die(esc_html__('You must be logged in to view this document.', 'dl-orderflow-pdf-documents-pro'), '', array('response' => 403));
			}
			if ((int) $order->get_user_id() !== (int) get_current_user_id()) {
				wp_die(esc_html__('You do not have permission to view this document.', 'dl-orderflow-pdf-documents-pro'), '', array('response' => 403));
			}
			if (empty($doc['show_in_customer'])) {
				wp_die(esc_html__('This document type is not available for customers.', 'dl-orderflow-pdf-documents-pro'), '', array('response' => 403));
			}
		}

		$pdf_path = $this->get_pdf_path_for_order($order, $doc_key, $doc);
		if (!$pdf_path) {
			$extra = $this->get_last_error();
			if ($is_admin && !empty($extra)) {
				wp_die(
					esc_html__('Could not generate PDF document.', 'dl-orderflow-pdf-documents-pro') . ' ' . esc_html($extra),
					'',
					array('response' => 500)
				);
			}

			wp_die(
				esc_html__('Could not generate PDF document.', 'dl-orderflow-pdf-documents-pro'),
				'',
				array('response' => 500)
			);
		}

		// Resolve to real paths and enforce directory + non-stream.
		$uploads = wp_upload_dir();
		$base_dir = trailingslashit($uploads['basedir']) . 'orderflow-pdf-documents/';
		$real_base = realpath($base_dir);
		$real_pdf = realpath($pdf_path);

		// Bail if paths are invalid or outside our directory.
		if (false === $real_base || false === $real_pdf || 0 !== strpos($real_pdf, $real_base)) {
			wp_die(
				esc_html__('Invalid document path.', 'dl-orderflow-pdf-documents-pro'),
				'',
				array('response' => 400)
			);
		}

		// Block non-local streams such as phar:// just to satisfy static analysis.
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_readable
		if (function_exists('wp_is_stream') && wp_is_stream($pdf_path)) {
			wp_die(
				esc_html__('Invalid document stream.', 'dl-orderflow-pdf-documents-pro'),
				'',
				array('response' => 400)
			);
		}

		// Also ensure the file actually exists and is readable.
		if (!file_exists($real_pdf) || !is_readable($real_pdf)) {
			wp_die(
				esc_html__('Document file not found.', 'dl-orderflow-pdf-documents-pro'),
				'',
				array('response' => 404)
			);
		}

		while (ob_get_level() > 0) {
			ob_end_clean();
		}

		header('Content-Type: application/pdf');
		header(
			'Content-Disposition: inline; filename="' . $filename . '"; filename*=UTF-8\'\'' . rawurlencode($filename)
		);
		header('Content-Length: ' . filesize($real_pdf));

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

	}

	/**
	 * Normalise text for FPDF/FPDI (strip tags, replace fancy punctuation, and convert encoding).
	 *
	 * @param string $text Text.
	 * @return string
	 */
	private function safe_text($text)
	{
		$text = wp_strip_all_tags((string) $text);

		$replacements = array(
			'–' => '-',
			'—' => '-',
			'“' => '"',
			'”' => '"',
			'‘' => "'",
			'’' => "'",
			'•' => '-',
		);
		$text = strtr($text, $replacements);

		// Convert UTF-8 to ISO-8859-1 where possible.
		if (function_exists('iconv')) {
			$converted = @iconv('UTF-8', 'ISO-8859-1//TRANSLIT', $text);
			if (false !== $converted) {
				$text = $converted;
			}
		} elseif (function_exists('mb_convert_encoding')) {
			$converted = @mb_convert_encoding($text, 'ISO-8859-1', 'UTF-8');
			if (false !== $converted) {
				$text = $converted;
			}
		}
		// Otherwise: leave $text as-is – better to keep valid UTF-8
// than risk using deprecated or unsafe conversions.

		return $text;
	}

	/**
	 * Render a table row with wrapped text per cell.
	 *
	 * @param DL_OrderFlow_Pdf $pdf       PDF instance.
	 * @param array                                $columns   Column keys in order.
	 * @param array                                $col_widths Associative: col_key => width in mm.
	 * @param array                                $values    Associative: col_key => cell text.
	 */
	/**
	 * Render a table row with wrapped text per cell.
	 *
	 * Uses MultiCell for each cell and keeps the row height in sync across columns,
	 * without touching FPDF internals.
	 *
	 * @param DL_OrderFlow_PDF_Documents_Fpdi_Pro|DL_OrderFlow_PDF_Documents_Fpdi $pdf
	 * @param array         $columns    Column keys in order.
	 * @param array         $col_widths Associative: col_key => width in mm.
	 * @param array         $values     Associative: col_key => cell text.
	 */
	private function render_wrapped_row($pdf, array $columns, array $col_widths, array $values)
	{
		$line_height = 6;
		$bottom_margin = 20;

		// 1) Compute max number of lines needed for this row.
		$max_lines = 1;

		foreach ($columns as $col) {
			$w = isset($col_widths[$col]) ? $col_widths[$col] : 30;
			$txt = isset($values[$col]) ? $values[$col] : '';
			$txt = $this->safe_text($txt);

			$nb = $this->ofpd_nb_lines($pdf, $w, $txt);
			if ($nb > $max_lines) {
				$max_lines = $nb;
			}
		}

		$row_height = $line_height * $max_lines;

		// 2) Check if the row fits on the current page.
		$page_height = $pdf->GetPageHeight();
		$max_y = $page_height - $bottom_margin;
		$current_y = $pdf->GetY();

		if ($current_y + $row_height > $max_y) {
			// Put the "continued" note just above where FPDF would auto-break.
			$note_y = $max_y - 5; // 5mm high note

			if ($current_y < $note_y) {
				$pdf->SetY($note_y);
			}
			$pdf->SetFont('Arial', 'I', 8);
			$pdf->Cell(0, 5, $this->safe_text('Continued on next page…'), 0, 0, 'R');

			// New page + redraw table header.
			$pdf->AddPage();
			$this->draw_table_header($pdf, $columns, $col_widths);
		}

		// 3) Now draw the row, knowing it fits on this page.
		$x_start = $pdf->GetX();
		$y_start = $pdf->GetY();

		// Make sure body font is set (table header already does this, but safe to enforce).
		$pdf->SetFont('Arial', '', 12);

		foreach ($columns as $col) {
			$w = isset($col_widths[$col]) ? $col_widths[$col] : 30;
			$txt = isset($values[$col]) ? $values[$col] : '';
			$txt = $this->safe_text($txt);

			$x = $pdf->GetX();
			$y = $pdf->GetY();

			// Draw the cell border as a single rectangle with full row height.
			$pdf->Rect($x, $y, $w, $row_height);

			// Output the text inside that box with MultiCell (no border).
			$pdf->MultiCell($w, $line_height, $txt, 0, 'L');

			// Move to the top of the next cell in the row.
			$pdf->SetXY($x + $w, $y);
		}

		// Move cursor to the beginning of the next row.
		$pdf->SetXY($x_start, $y_start + $row_height);
	}

	/**
	 * Estimate number of lines MultiCell will use for a given width/text,
	 * based on the original FPDF::NbLines logic.
	 *
	 * @param float  $w   Cell width in mm.
	 * @param string $txt Cell text.
	 * @return int
	 */
	private function ofpd_nb_lines($pdf, $w, $txt)
	{
		$txt = (string) $txt;
		$txt = str_replace("\r", '', $txt);
		$lines = explode("\n", $txt);

		// Safety fallback for width.
		if ($w <= 0) {
			$page_width = $pdf->GetPageWidth();
			$left = 10;
			$right = 10;
			$w = $page_width - $left - $right;
		}

		$max_width = (float) $w;
		$total = 0;

		foreach ($lines as $line) {
			$line = trim($line);

			// Empty line still counts as one.
			if ('' === $line) {
				$total++;
				continue;
			}

			$words = preg_split('/\s+/', $line);
			$current_line = '';

			foreach ($words as $word) {
				$test = ('' === $current_line) ? $word : $current_line . ' ' . $word;

				if ($pdf->GetStringWidth($test) <= $max_width) {
					$current_line = $test;
				} else {
					// Current line is full, start a new line.
					$total++;
					$current_line = $word;

					// If a single word itself is wider than the max width, force-break by characters.
					if ($pdf->GetStringWidth($word) > $max_width) {
						$chars = preg_split('//u', $word, -1, PREG_SPLIT_NO_EMPTY);
						$chunk = '';
						foreach ($chars as $ch) {
							$test_chunk = $chunk . $ch;
							if ($pdf->GetStringWidth($test_chunk) <= $max_width) {
								$chunk = $test_chunk;
							} else {
								$total++;
								$chunk = $ch;
							}
						}
						if ('' !== $chunk) {
							$current_line = $chunk;
						}
					}
				}
			}

			if ('' !== $current_line) {
				$total++;
			}
		}

		return max(1, $total);
	}

	/**
	 * Draw the table header row for the items table.
	 *
	 * @param \FPDF|\setasign\Fpdi\Fpdi $pdf
	 * @param array                     $columns
	 * @param array                     $col_widths
	 */
	private function draw_table_header($pdf, array $columns, array $col_widths)
	{
		$pdf->SetFont('Arial', 'B', 12);

		foreach ($columns as $col) {
			switch ($col) {
				case 'product':
					$label = 'Product';
					break;
				case 'sku':
					$label = 'SKU';
					break;
				case 'qty':
					$label = 'Qty';
					break;
				case 'price':
					$label = 'Price';
					break;
				case 'total':
					$label = 'Total';
					break;
				default:
					$label = ucfirst($col);
					break;
			}

			$width = isset($col_widths[$col]) ? $col_widths[$col] : 30;
			$pdf->Cell($width, 8, $this->safe_text($label), 1, 0);
		}

		$pdf->Ln();

		// Reset to body font for rows
		$pdf->SetFont('Arial', '', 12);
	}

	public function admin_enqueue_assets($hook)
	{
		if ('woocommerce_page_wc-settings' !== $hook) {
			return;
		}

		$allowed_tabs = array(self::SETTINGS_TAB_ID);

		// Get the current WooCommerce settings tab in a safe way.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading 'tab' to select settings view only, no state changes.
		$raw_tab = filter_input(INPUT_GET, 'tab', FILTER_UNSAFE_RAW);
		$current_tab = is_string($raw_tab)
			? sanitize_text_field(wp_unslash($raw_tab))
			: '';

		if (self::SETTINGS_TAB_ID !== $current_tab) {
			return;
		}

		// Media library for logo picker.
		wp_enqueue_media();

		// Our admin settings script.
		wp_enqueue_script(
			'orderflow-pdf-documents-admin-settings',
			plugin_dir_url(__FILE__) . 'assets/js/orderflow-pdf-documents-admin-settings.js',
			array('jquery', 'wp-util', 'media-editor', 'media-views'),
			'1.0.5',
			true
		);

		// Pass data + translated strings to JS instead of echoing inline.
		wp_localize_script(
			'orderflow-pdf-documents-admin-settings',
			'ofpdSettings',
			array(
				'ajaxUrl' => admin_url('admin-ajax.php'),
				'i18n' => array(
					'selectLogoTitle' => __('Select logo', 'dl-orderflow-pdf-documents'),
					'selectLogoButton' => __('Use this logo', 'dl-orderflow-pdf-documents'),
					'changeLogo' => __('Change logo', 'dl-orderflow-pdf-documents'),
					'selectLogo' => __('Select logo', 'dl-orderflow-pdf-documents'),
					'logoMimeError' => __('Only JPEG and PNG logos are supported for PDF documents.', 'dl-orderflow-pdf-documents'),
					'sendingTest' => __('Sending test email...', 'dl-orderflow-pdf-documents'),
					'testSent' => __('Test email sent.', 'dl-orderflow-pdf-documents'),
					'testError' => __('Error sending test email.', 'dl-orderflow-pdf-documents'),
					'testAjaxError' => __('Error sending test email (AJAX request failed).', 'dl-orderflow-pdf-documents'),
				),
			)
		);
	}

}

/**
 * Uninstall handler: optionally delete all plugin data.
 */
function dl_orderflow_pdf_documents_uninstall()
{
	// Get settings to see if user requested cleanup.
	$option_key = DL_OrderFlow_PDF_Documents::OPTION_KEY;
	$settings = get_option($option_key);

	$delete_data = is_array($settings) && !empty($settings['delete_on_uninstall']);

	if ($delete_data) {
		// 1. Delete main option.
		delete_option($option_key);

		// 2. Delete generated PDFs directory.
		$uploads = wp_upload_dir();
		if (empty($uploads['error'])) {
			$dir = trailingslashit($uploads['basedir']) . 'orderflow-pdf-documents-pro/';
			$real_dir = realpath($dir);

			if (is_dir($real_dir)) {
				// Recursively delete files and subdirectories.
				$iterator = new \RecursiveIteratorIterator(
					new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
					\RecursiveIteratorIterator::CHILD_FIRST
				);

				foreach ($iterator as $fileinfo) {
					$path = $fileinfo->getRealPath();
					$real_path = $path ? realpath($path) : false;

					// Ensure every path we touch stays under our base directory.
					if (!$real_path || 0 !== strpos($real_path, $real_dir)) {
						continue;
					}

					if ($fileinfo->isDir()) {
						// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir
						@rmdir($real_path);
					} else {
						// phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink
						@unlink($real_path);
					}
				}

				// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir
				@rmdir($dir);
			}
		}
	}
}

register_uninstall_hook(__FILE__, 'dl_orderflow_pdf_documents_uninstall');



/**
 * Late bootstrap: only instantiate when WooCommerce is active.
 */
add_action(
	'plugins_loaded',
	function () {
		if (class_exists('\WooCommerce')) {
			new DL_OrderFlow_PDF_Documents();
		}
	},
	11
);
