<?php
/**
 * PDF Invoices & Packing Slips module.
 *
 * @package Kramar
 * @since   1.0.0
 */

declare( strict_types=1 );

namespace Kramar\Modules\Invoices;

defined( 'ABSPATH' ) || exit;

/**
 * Main module — hooks into WC orders, emails, and admin UI.
 *
 * @since 1.0.0
 */
final class Pdf_Invoices extends \Kramar\Module {

	private const DOWNLOAD_ACTION  = 'kramar_download_pdf';
	private const NONCE_ACTION     = 'kramar_pdf_nonce';
	private const BUILDER_SLUG     = 'kramar-invoice-builder';
	private const REGENERATE_ACTION = 'kramar_regenerate_invoices';

	private string $builder_hook = '';

	/** {@inheritDoc} */
	public function get_slug(): string {
		return 'pdf-invoices';
	}

	/** {@inheritDoc} */
	public function get_category(): string {
		return 'invoices';
	}

	/** {@inheritDoc} */
	public function get_name(): string {
		return __( 'PDF Invoices & Packing Slips', 'kramar' );
	}

	/** {@inheritDoc} */
	public function get_description(): string {
		return __( 'Generate EU-compliant PDF invoices, packing slips, and credit notes. Auto-attach to emails, encrypted storage, sequential numbering.', 'kramar' );
	}

	/** {@inheritDoc} */
	public function get_default_settings(): array {
		return [
			'enabled'              => false,
			'auto_generate_status' => 'processing',
			'number_prefix'        => '',
			'number_suffix'        => '',
			'number_padding'       => 4,
			'paper_size'           => 'a4',
			'show_sku'             => false,
			'show_product_image'   => false,
			'show_customer_note'   => true,
			'show_shipping_address' => true,
			'show_email'           => false,
			'show_phone'           => true,
			'accent_color'         => '#2563eb',
			'footer_text'          => '',
			'attach_invoice_to'    => [ 'customer_processing_order', 'customer_completed_order', 'customer_invoice' ],
			'attach_packing_to'    => [ 'customer_completed_order' ],
			'attach_credit_to'     => [ 'customer_refunded_order' ],
		];
	}

	/** {@inheritDoc} */
	public function sanitize_settings( array $input ): array {
		return [
			'enabled'              => ! empty( $input['enabled'] ),
			'auto_generate_status' => sanitize_key( $input['auto_generate_status'] ?? 'processing' ),
			'number_prefix'        => sanitize_text_field( $input['number_prefix'] ?? '' ),
			'number_suffix'        => sanitize_text_field( $input['number_suffix'] ?? '' ),
			'number_padding'       => max( 1, min( 10, (int) ( $input['number_padding'] ?? 4 ) ) ),
			'paper_size'           => in_array( $input['paper_size'] ?? '', [ 'a4', 'letter' ], true ) ? $input['paper_size'] : 'a4',
			'show_sku'             => ! empty( $input['show_sku'] ),
			'show_product_image'   => ! empty( $input['show_product_image'] ),
			'show_customer_note'   => ! empty( $input['show_customer_note'] ),
			'show_shipping_address' => ! empty( $input['show_shipping_address'] ),
			'show_email'           => ! empty( $input['show_email'] ),
			'show_phone'           => ! empty( $input['show_phone'] ),
			'accent_color'         => sanitize_hex_color( $input['accent_color'] ?? '#2563eb' ) ?: '#2563eb',
			'footer_text'          => sanitize_textarea_field( $input['footer_text'] ?? '' ),
			'attach_invoice_to'    => array_map( 'sanitize_key', (array) ( $input['attach_invoice_to'] ?? [] ) ),
			'attach_packing_to'    => array_map( 'sanitize_key', (array) ( $input['attach_packing_to'] ?? [] ) ),
			'attach_credit_to'     => array_map( 'sanitize_key', (array) ( $input['attach_credit_to'] ?? [] ) ),
		];
	}

	// -------------------------------------------------------------------------
	// Bootstrap
	// -------------------------------------------------------------------------

	/** {@inheritDoc} */
	protected function run(): void {
		Number_Store::create_table();
		Storage::ensure_directory();

		// Auto-generate invoice on status change.
		$status = $this->get_setting( 'auto_generate_status' ) ?: 'processing';
		add_action( 'woocommerce_order_status_' . $status, [ $this, 'auto_generate_invoice' ], 10, 2 );

		// WC orders page — icon buttons.
		add_action( 'woocommerce_admin_order_actions_end', [ $this, 'render_order_list_buttons' ] );

		// Invoice number column (HPOS + CPT).
		add_filter( 'manage_woocommerce_page_wc-orders_columns', [ $this, 'add_invoice_column' ], 20 );
		add_action( 'manage_woocommerce_page_wc-orders_custom_column', [ $this, 'render_invoice_column' ], 10, 2 );
		add_filter( 'manage_edit-shop_order_columns', [ $this, 'add_invoice_column' ], 20 );
		add_action( 'manage_shop_order_posts_custom_column', [ $this, 'render_invoice_column_cpt' ], 10, 2 );

		// Order edit metabox.
		add_action( 'add_meta_boxes', [ $this, 'register_metabox' ] );

		// Download handler (admin + frontend/guest via HMAC token).
		add_action( 'admin_post_' . self::DOWNLOAD_ACTION, [ $this, 'handle_download' ] );
		add_action( 'admin_post_nopriv_' . self::DOWNLOAD_ACTION, [ $this, 'handle_download' ] );

		// Email attachments.
		add_filter( 'woocommerce_email_attachments', [ $this, 'attach_pdf_to_email' ], 99, 4 );

		// Per-email PDF attachment checkboxes in the Email builder.
		add_action( 'kramar_email_builder_after_subject', [ $this, 'render_email_attachment_field' ], 10, 2 );

		// Auto-generate + store the credit note when an order is refunded.
		add_action( 'woocommerce_order_refunded', [ $this, 'auto_generate_credit_note' ], 10, 2 );

		// Merge fields for Email Manager.
		add_filter( 'kramar_email_merge_fields', [ $this, 'register_merge_fields' ], 10, 2 );
		add_filter( 'kramar_email_merge_field_values', [ $this, 'resolve_merge_fields' ], 10, 2 );

		// Admin styles.
		add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_pdf_btn_styles' ] );

		// My Account download button.
		add_filter( 'woocommerce_my_account_my_orders_actions', [ $this, 'add_my_account_button' ], 10, 2 );

		// Builder sub-page.
		add_action( 'admin_menu', [ $this, 'register_builder_page' ] );
		add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_builder_assets' ] );
		add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_email_builder_assets' ] );

		// AJAX: save template, live canvas render, full preview, regenerate all.
		add_action( 'wp_ajax_kramar_invoice_save_email_attachments', [ $this, 'ajax_save_email_attachments' ] );
		add_action( 'wp_ajax_kramar_invoice_save_template', [ $this, 'ajax_save_template' ] );
		add_action( 'wp_ajax_kramar_invoice_render', [ $this, 'ajax_render' ] );
		add_action( 'wp_ajax_kramar_invoice_preview', [ $this, 'ajax_preview' ] );
		add_action( 'wp_ajax_' . self::REGENERATE_ACTION, [ $this, 'ajax_regenerate_all' ] );

		// Bulk actions on WC orders.
		add_filter( 'bulk_actions-woocommerce_page_wc-orders', [ $this, 'register_bulk_actions' ], 20 );
		add_filter( 'bulk_actions-edit-shop_order', [ $this, 'register_bulk_actions' ], 20 );
		add_filter( 'handle_bulk_actions-woocommerce_page_wc-orders', [ $this, 'handle_bulk_action' ], 10, 3 );
		add_filter( 'handle_bulk_actions-edit-shop_order', [ $this, 'handle_bulk_action' ], 10, 3 );
	}

	// -------------------------------------------------------------------------
	// Helpers
	// -------------------------------------------------------------------------

	private function get_setting( string $key ): mixed {
		return $this->plugin->get_setting( $this->get_category(), $this->get_slug(), $key );
	}

	private function get_all_settings(): array {
		$settings = [];
		foreach ( $this->get_default_settings() as $key => $default ) {
			$val = $this->get_setting( $key );
			$settings[ $key ] = ( null === $val || '' === $val ) ? $default : $val;
		}
		return $settings;
	}

	private function get_download_url( int $order_id, string $type ): string {
		return wp_nonce_url(
			admin_url( 'admin-post.php?action=' . self::DOWNLOAD_ACTION . '&order_id=' . $order_id . '&type=' . $type ),
			self::NONCE_ACTION
		);
	}

	private function get_frontend_download_url( \WC_Order $order, string $type ): string {
		$token = hash_hmac( 'sha256', $order->get_id() . '|' . $order->get_billing_email() . '|' . $type, wp_salt( 'auth' ) );
		return add_query_arg(
			[
				'action'   => self::DOWNLOAD_ACTION,
				'order_id' => $order->get_id(),
				'type'     => $type,
				'token'    => $token,
			],
			admin_url( 'admin-post.php' )
		);
	}

	// -------------------------------------------------------------------------
	// Auto-Generate Invoice
	// -------------------------------------------------------------------------

	public function auto_generate_invoice( int $order_id, \WC_Order $order ): void {
		$existing = $order->get_meta( '_kramar_invoice_file' );
		if ( '' !== $existing && false !== $existing ) {
			return;
		}

		$invoice = new Invoice( $order, $this->get_all_settings() );
		$invoice->generate_and_store();
	}

	// -------------------------------------------------------------------------
	// WC Orders Page — Icon Buttons
	// -------------------------------------------------------------------------

	public function render_order_list_buttons( \WC_Order $order ): void {
		$has_invoice = '' !== (string) $order->get_meta( '_kramar_invoice_file' );
		$invoice_url = $this->get_download_url( $order->get_id(), 'invoice' );
		$packing_url = $this->get_download_url( $order->get_id(), 'packing-slip' );

		$inv_class = $has_invoice ? 'kramar-pdf-btn kramar-pdf-btn--exists' : 'kramar-pdf-btn';

		echo '<a href="' . esc_url( $invoice_url ) . '" class="' . esc_attr( $inv_class ) . '" title="' . esc_attr__( 'PDF Invoice', 'kramar' ) . '" target="_blank">';
		echo '<span class="dashicons dashicons-media-text"></span>';
		if ( $has_invoice ) {
			echo '<span class="kramar-pdf-check">✓</span>';
		}
		echo '</a>';

		echo '<a href="' . esc_url( $packing_url ) . '" class="kramar-pdf-btn" title="' . esc_attr__( 'Packing Slip', 'kramar' ) . '" target="_blank">';
		echo '<span class="dashicons dashicons-clipboard"></span>';
		echo '</a>';
	}

	// -------------------------------------------------------------------------
	// Invoice Number Column
	// -------------------------------------------------------------------------

	public function add_invoice_column( array $columns ): array {
		$new = [];
		foreach ( $columns as $key => $label ) {
			$new[ $key ] = $label;
			if ( 'order_number' === $key ) {
				$new['kramar_invoice'] = __( 'Invoice', 'kramar' );
			}
		}
		return $new;
	}

	public function render_invoice_column( string $column, $order ): void {
		if ( 'kramar_invoice' !== $column ) {
			return;
		}
		if ( ! $order instanceof \WC_Order ) {
			$order = wc_get_order( $order );
		}
		if ( ! $order ) {
			return;
		}
		$number = (string) $order->get_meta( '_kramar_invoice_number' );
		echo '' !== $number ? esc_html( $number ) : '<span style="color:#9ca3af;">—</span>';
	}

	public function render_invoice_column_cpt( string $column, int $post_id ): void {
		if ( 'kramar_invoice' !== $column ) {
			return;
		}
		$order = wc_get_order( $post_id );
		if ( ! $order ) {
			return;
		}
		$number = (string) $order->get_meta( '_kramar_invoice_number' );
		echo '' !== $number ? esc_html( $number ) : '<span style="color:#9ca3af;">—</span>';
	}

	// -------------------------------------------------------------------------
	// Order Edit Metabox
	// -------------------------------------------------------------------------

	public function register_metabox(): void {
		$screens = [ 'shop_order', 'woocommerce_page_wc-orders' ];
		foreach ( $screens as $screen ) {
			add_meta_box(
				'kramar-pdf-invoices',
				__( 'PDF Documents', 'kramar' ),
				[ $this, 'render_metabox' ],
				$screen,
				'side',
				'default'
			);
		}
	}

	public function render_metabox( $post_or_order ): void {
		// HPOS passes a WC_Order; the legacy CPT screen passes a WP_Post.
		if ( $post_or_order instanceof \WC_Order ) {
			$order = $post_or_order;
		} else {
			$order = wc_get_order( $post_or_order instanceof \WP_Post ? $post_or_order->ID : (int) $post_or_order );
		}
		if ( ! $order ) {
			return;
		}
		$order_id = $order->get_id();

		$inv_number = (string) $order->get_meta( '_kramar_invoice_number' );
		$inv_date   = (string) $order->get_meta( '_kramar_invoice_date' );
		$inv_file   = (string) $order->get_meta( '_kramar_invoice_file' );

		if ( '' !== $inv_number ) {
			echo '<p><strong>' . esc_html__( 'Invoice', 'kramar' ) . ':</strong> ' . esc_html( $inv_number );
			if ( '' !== $inv_date ) {
				$dt = \DateTime::createFromFormat( 'Y-m-d H:i:s', $inv_date );
				if ( $dt ) {
					echo ' <span style="color:#6b7280;">(' . esc_html( wp_date( wc_date_format(), $dt->getTimestamp() ) ) . ')</span>';
				}
			}
			echo '</p>';
		}

		echo '<p style="display:flex;gap:6px;flex-wrap:wrap;">';

		if ( '' !== $inv_file ) {
			echo '<a href="' . esc_url( $this->get_download_url( $order_id, 'invoice' ) ) . '" class="button button-small" target="_blank">'
				. esc_html__( 'Download Invoice', 'kramar' ) . '</a>';
		} else {
			echo '<a href="' . esc_url( $this->get_download_url( $order_id, 'invoice' ) ) . '" class="button button-primary button-small" target="_blank">'
				. esc_html__( 'Generate Invoice', 'kramar' ) . '</a>';
		}

		echo '<a href="' . esc_url( $this->get_download_url( $order_id, 'packing-slip' ) ) . '" class="button button-small" target="_blank">'
			. esc_html__( 'Packing Slip', 'kramar' ) . '</a>';

		// Credit note — one button per refund. A single button could only ever
		// reach the newest refund's document, leaving every earlier credit note
		// generated and stored but with no way to download it.
		$refunds = $order->get_refunds();
		if ( ! empty( $refunds ) && '' !== $inv_number ) {
			$multiple = count( $refunds ) > 1;

			foreach ( $refunds as $refund ) {
				$label = $multiple
					? sprintf(
						/* translators: %s: the refund amount, e.g. "€12.50" */
						__( 'Credit Note (%s)', 'kramar' ),
						html_entity_decode( wp_strip_all_tags( wc_price( abs( (float) $refund->get_amount() ) ) ), ENT_QUOTES, 'UTF-8' )
					)
					: __( 'Credit Note', 'kramar' );

				$url = add_query_arg( 'refund_id', (int) $refund->get_id(), $this->get_download_url( $order_id, 'credit-note' ) );

				echo '<a href="' . esc_url( $url ) . '" class="button button-small" target="_blank">'
					. esc_html( $label ) . '</a> ';
			}
		}

		echo '</p>';

		if ( '' !== $inv_file ) {
			echo '<p><a href="' . esc_url( $this->get_download_url( $order_id, 'regenerate' ) ) . '" class="button button-small" onclick="return confirm(\'' . esc_js( __( 'Regenerate invoice? This will overwrite the existing PDF.', 'kramar' ) ) . '\');">'
				. esc_html__( 'Regenerate', 'kramar' ) . '</a></p>';
		}
	}

	// -------------------------------------------------------------------------
	// Download Handler
	// -------------------------------------------------------------------------

	public function handle_download(): void {
		$order_id = absint( $_GET['order_id'] ?? 0 );
		$type     = sanitize_key( $_GET['type'] ?? '' );

		// Admin download (nonce-verified).
		$is_admin = isset( $_GET['_wpnonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), self::NONCE_ACTION );

		// Frontend download (HMAC-verified).
		$is_frontend = false;
		if ( ! $is_admin && isset( $_GET['token'] ) ) {
			$order = wc_get_order( $order_id );
			if ( $order ) {
				$expected = hash_hmac( 'sha256', $order->get_id() . '|' . $order->get_billing_email() . '|' . $type, wp_salt( 'auth' ) );
				$is_frontend = hash_equals( $expected, sanitize_text_field( wp_unslash( $_GET['token'] ) ) );
			}
		}

		if ( ! $is_admin && ! $is_frontend ) {
			wp_die( esc_html__( 'Unauthorized.', 'kramar' ), 403 );
		}

		if ( $is_admin && ! current_user_can( 'manage_woocommerce' ) ) {
			wp_die( esc_html__( 'Unauthorized.', 'kramar' ), 403 );
		}

		$order = wc_get_order( $order_id );
		if ( ! $order ) {
			wp_die( esc_html__( 'Order not found.', 'kramar' ), 404 );
		}

		$settings = $this->get_all_settings();

		if ( 'regenerate' === $type ) {
			$old_file = $order->get_meta( '_kramar_invoice_file' );
			if ( '' !== $old_file ) {
				Storage::delete( (string) $old_file );
			}
			// The issue date is deliberately kept. Regenerate exists to rebuild
			// the PDF after a template or company-details change; the document it
			// replaces is the same invoice, and art. 226(3) of Directive
			// 2006/112/EC requires the date it was issued. Deleting the meta made
			// assign_number() stamp today, so regenerating a six-month-old invoice
			// silently re-dated it to today — with the number, correctly, unchanged.
			$order->delete_meta_data( '_kramar_invoice_number' );
			$order->delete_meta_data( '_kramar_invoice_number_raw' );
			$order->delete_meta_data( '_kramar_invoice_file' );
			$order->save_meta_data();

			$invoice  = new Invoice( $order, $settings );
			$rel_path = $invoice->generate_and_store();
			Storage::serve( $rel_path, $invoice->get_filename() );
			return;
		}

		if ( 'invoice' === $type ) {
			$rel_path = (string) $order->get_meta( '_kramar_invoice_file' );
			if ( '' === $rel_path ) {
				$invoice  = new Invoice( $order, $settings );
				$rel_path = $invoice->generate_and_store();
			}
			$invoice = new Invoice( $order, $settings );
			Storage::serve( $rel_path, $invoice->get_filename() );
			return;
		}

		if ( 'packing-slip' === $type ) {
			$slip = new Packing_Slip( $order, $settings );
			$pdf  = $slip->generate();

			nocache_headers();
			header( 'Content-Type: application/pdf' );
			header( 'Content-Disposition: inline; filename="' . sanitize_file_name( $slip->get_filename() ) . '"' );
			header( 'Content-Length: ' . strlen( $pdf ) );
			// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
			echo $pdf;
			exit;
		}

		if ( 'credit-note' === $type ) {
			$refunds = $order->get_refunds();
			if ( empty( $refunds ) ) {
				wp_die( esc_html__( 'No refunds found for this order.', 'kramar' ), 404 );
			}

			// An order can have several refunds, each with its own credit note
			// and its own sequential number. get_refunds() is ordered newest
			// first, so always taking [0] made every older credit note
			// unreachable — the document existed on disk with no way to get at
			// it. Accept an explicit refund id and fall back to the newest.
			// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce for this download is verified above.
			$wanted = isset( $_GET['refund_id'] ) ? absint( $_GET['refund_id'] ) : 0;
			$refund = $refunds[0];

			if ( $wanted > 0 ) {
				$match = null;
				foreach ( $refunds as $candidate ) {
					if ( (int) $candidate->get_id() === $wanted ) {
						$match = $candidate;
						break;
					}
				}
				if ( null === $match ) {
					wp_die( esc_html__( 'That refund does not belong to this order.', 'kramar' ), 404 );
				}
				$refund = $match;
			}
			$cn     = new Credit_Note( $order, $refund, $settings );

			$rel_path = (string) $refund->get_meta( '_kramar_credit_note_file' );
			if ( '' === $rel_path ) {
				$rel_path = $cn->generate_and_store();
			}

			Storage::serve( $rel_path, $cn->get_filename() );
			return;
		}

		wp_die( esc_html__( 'Invalid document type.', 'kramar' ), 400 );
	}

	// -------------------------------------------------------------------------
	// Email Attachments
	// -------------------------------------------------------------------------

	public function attach_pdf_to_email( array $attachments, string $email_id, $order, $email = null ): array {
		if ( ! $order instanceof \WC_Order ) {
			return $attachments;
		}

		$settings = $this->get_all_settings();

		// WooCommerce reuses one WC_Email_Customer_Refunded_Order instance for
		// both refund notifications, switching its id to
		// 'customer_partially_refunded_order' only at trigger time. The settings
		// screen therefore only ever offers 'customer_refunded_order', so a
		// partial refund — the common case — matched nothing here and its
		// customer notification went out with no credit note attached, with no
		// way for an admin to turn it on. Match the partial variant on what the
		// admin was actually able to tick.
		$match_ids = [ $email_id ];
		if ( 'customer_partially_refunded_order' === $email_id ) {
			$match_ids[] = 'customer_refunded_order';
		}

		$per_email = [];
		foreach ( $match_ids as $match_id ) {
			$per_email = array_merge( $per_email, $this->get_email_pdf_attachments( $match_id ) );
		}
		$per_email = array_values( array_unique( $per_email ) );

		// Invoice.
		$attach_invoice = (array) ( $settings['attach_invoice_to'] ?? [] );
		if ( array_intersect( $match_ids, $attach_invoice ) || in_array( 'invoice', $per_email, true ) ) {
			$rel_path = (string) $order->get_meta( '_kramar_invoice_file' );
			if ( '' === $rel_path ) {
				$invoice  = new Invoice( $order, $settings );
				$rel_path = $invoice->generate_and_store();
			}
			$tmp = Storage::to_temp_file( $rel_path, 'invoice.pdf' );
			if ( $tmp ) {
				$attachments[] = $tmp;
			}
		}

		// Packing slip.
		$attach_packing = (array) ( $settings['attach_packing_to'] ?? [] );
		if ( array_intersect( $match_ids, $attach_packing ) || in_array( 'packing-slip', $per_email, true ) ) {
			$slip = new Packing_Slip( $order, $settings );
			$pdf  = $slip->generate();
			$tmp  = wp_tempnam( 'packing-slip.pdf' );
			file_put_contents( $tmp, $pdf );
			$attachments[] = $tmp;

			register_shutdown_function( static function () use ( $tmp ): void {
				if ( file_exists( $tmp ) ) {
					wp_delete_file( $tmp );
				}
			} );
		}

		// Credit note.
		$attach_credit = (array) ( $settings['attach_credit_to'] ?? [] );
		if ( array_intersect( $match_ids, $attach_credit ) || in_array( 'credit-note', $per_email, true ) ) {
			$refunds = $order->get_refunds();
			if ( ! empty( $refunds ) ) {
				// Attach the credit note for the refund this email is actually
				// about. WooCommerce passes the refund on the refund notification,
				// and get_refunds() is newest-first — so resending an older
				// refund's email would otherwise attach the newest refund's
				// document instead of its own.
				$refund = null;

				if ( $email instanceof \WC_Email && isset( $email->refund ) && $email->refund instanceof \WC_Order_Refund ) {
					$refund = $email->refund;
				} elseif ( $email instanceof \WC_Email && ! empty( $email->refund_id ) ) {
					foreach ( $refunds as $candidate ) {
						if ( (int) $candidate->get_id() === (int) $email->refund_id ) {
							$refund = $candidate;
							break;
						}
					}
				}

				if ( ! $refund instanceof \WC_Order_Refund ) {
					$refund = $refunds[0];
				}

				$rel_path = (string) $refund->get_meta( '_kramar_credit_note_file' );
				if ( '' === $rel_path ) {
					$cn       = new Credit_Note( $order, $refund, $settings );
					$rel_path = $cn->generate_and_store();
				}
				$tmp = Storage::to_temp_file( $rel_path, 'credit-note.pdf' );
				if ( $tmp ) {
					$attachments[] = $tmp;
				}
			}
		}

		return $attachments;
	}

	/**
	 * Per-email PDF attachment selection (set in the Email builder).
	 *
	 * @since 1.0.0
	 *
	 * @return list<string> Document types selected for this email.
	 */
	private function get_email_pdf_attachments( string $email_id ): array {
		$overrides = get_option( 'kramar_email_overrides', [] );
		$list      = is_array( $overrides ) ? ( $overrides[ $email_id ]['pdf_attachments'] ?? [] ) : [];
		return is_array( $list )
			? array_values( array_intersect( $list, [ 'invoice', 'packing-slip', 'credit-note' ] ) )
			: [];
	}

	/**
	 * Render PDF attachment checkboxes below the subject in the Email builder.
	 *
	 * @since 1.0.0
	 *
	 * @param string $email_id The email being edited.
	 * @param string $context  The email context.
	 */
	public function render_email_attachment_field( string $email_id, string $context ): void {
		if ( 'order' !== $context ) {
			return;
		}

		$selected = $this->get_email_pdf_attachments( $email_id );
		$types    = [
			'invoice'      => __( 'Invoice', 'kramar' ),
			'packing-slip' => __( 'Packing Slip', 'kramar' ),
			'credit-note'  => __( 'Credit Note', 'kramar' ),
		];
		$nonce = wp_create_nonce( 'kramar_invoice_email_attachments' );
		?>
		<div class="kramar-eb-canvas__attachments" style="max-width:600px;margin:0 auto 16px;background:#fff;border:1px solid #e4e4e7;border-radius:8px;padding:14px 18px;">
			<strong style="display:block;font-size:13px;margin-bottom:8px;color:#1d2327;">
				<span class="dashicons dashicons-media-document" style="color:#2271b1;vertical-align:text-bottom;"></span>
				<?php esc_html_e( 'PDF attachments', 'kramar' ); ?>
			</strong>
			<div style="display:flex;gap:18px;flex-wrap:wrap;">
				<?php foreach ( $types as $type => $label ) : ?>
					<label style="display:flex;align-items:center;gap:6px;font-size:13px;cursor:pointer;">
						<input type="checkbox" class="kramar-inv-attach" value="<?php echo esc_attr( $type ); ?>" <?php checked( in_array( $type, $selected, true ) ); ?> />
						<?php echo esc_html( $label ); ?>
					</label>
				<?php endforeach; ?>
				<span class="kramar-inv-attach-status" style="font-size:12px;color:#646970;align-self:center;"></span>
			</div>
			<p style="margin:8px 0 0;font-size:11px;color:#646970;">
				<?php esc_html_e( 'Attached when this email is sent. Also configurable globally under Kramar → Modules → PDF Invoices.', 'kramar' ); ?>
			</p>
		</div>
		<?php
	}

	/**
	 * Save the per-email PDF attachment selection.
	 *
	 * @since 1.0.0
	 */
	public function ajax_save_email_attachments(): void {
		check_ajax_referer( 'kramar_invoice_email_attachments' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( 'Unauthorized' );
		}

		$email_id = sanitize_key( wp_unslash( $_POST['email_id'] ?? '' ) );
		if ( '' === $email_id ) {
			wp_send_json_error( 'Invalid email' );
		}

		$raw   = isset( $_POST['attachments'] ) ? array_map( 'sanitize_key', (array) wp_unslash( $_POST['attachments'] ) ) : [];
		$clean = array_values( array_intersect( $raw, [ 'invoice', 'packing-slip', 'credit-note' ] ) );

		$overrides = get_option( 'kramar_email_overrides', [] );
		if ( ! is_array( $overrides ) ) {
			$overrides = [];
		}
		if ( ! isset( $overrides[ $email_id ] ) || ! is_array( $overrides[ $email_id ] ) ) {
			$overrides[ $email_id ] = [];
		}
		$overrides[ $email_id ]['pdf_attachments'] = $clean;
		update_option( 'kramar_email_overrides', $overrides );

		wp_send_json_success( [ 'attachments' => $clean ] );
	}

	/**
	 * Generate and store the credit note when an order is refunded.
	 *
	 * @since 1.0.0
	 *
	 * @param int $order_id  The refunded order ID.
	 * @param int $refund_id The refund ID.
	 */
	public function auto_generate_credit_note( int $order_id, int $refund_id ): void {
		$order  = wc_get_order( $order_id );
		$refund = wc_get_order( $refund_id );
		if ( ! $order instanceof \WC_Order || ! $refund instanceof \WC_Order_Refund ) {
			return;
		}

		if ( '' !== (string) $refund->get_meta( '_kramar_credit_note_file' ) ) {
			return;
		}

		$cn = new Credit_Note( $order, $refund, $this->get_all_settings() );
		$cn->generate_and_store();
	}

	// -------------------------------------------------------------------------
	// Email Manager — Merge Fields
	// -------------------------------------------------------------------------

	public function register_merge_fields( array $fields, string $context ): array {
		if ( 'order' !== $context ) {
			return $fields;
		}

		if ( isset( $fields['kramar']['fields'] ) ) {
			$fields['kramar']['fields']['{invoice_number}'] = __( 'Invoice Number', 'kramar' );
			$fields['kramar']['fields']['{invoice_date}']   = __( 'Invoice Date', 'kramar' );
			$fields['kramar']['fields']['{invoice_url}']    = __( 'Invoice Download URL', 'kramar' );
		}

		return $fields;
	}

	public function resolve_merge_fields( array $replacements, \WC_Order $order ): array {
		$inv_number = (string) $order->get_meta( '_kramar_invoice_number' );
		$inv_date   = (string) $order->get_meta( '_kramar_invoice_date' );

		$formatted_date = '';
		if ( '' !== $inv_date ) {
			$dt = \DateTime::createFromFormat( 'Y-m-d H:i:s', $inv_date );
			if ( $dt ) {
				$formatted_date = wp_date( wc_date_format(), $dt->getTimestamp() );
			}
		}

		$replacements['{invoice_number}'] = $inv_number;
		$replacements['{invoice_date}']   = $formatted_date;
		$replacements['{invoice_url}']    = '' !== $inv_number
			? $this->get_frontend_download_url( $order, 'invoice' )
			: '';

		return $replacements;
	}

	// -------------------------------------------------------------------------
	// My Account
	// -------------------------------------------------------------------------

	public function add_my_account_button( array $actions, \WC_Order $order ): array {
		$inv_file = (string) $order->get_meta( '_kramar_invoice_file' );
		if ( '' === $inv_file ) {
			return $actions;
		}

		$actions['kramar-invoice'] = [
			'url'  => $this->get_frontend_download_url( $order, 'invoice' ),
			'name' => __( 'Invoice', 'kramar' ),
		];

		return $actions;
	}

	// -------------------------------------------------------------------------
	// Builder Sub-Page
	// -------------------------------------------------------------------------

	public function register_builder_page(): void {
		$this->builder_hook = (string) add_submenu_page(
			'kramar',
			__( 'Invoices', 'kramar' ),
			__( 'Invoices', 'kramar' ),
			'manage_woocommerce',
			self::BUILDER_SLUG,
			[ $this, 'render_builder_page' ]
		);
	}

	public function enqueue_builder_assets( string $hook ): void {
		if ( $hook !== $this->builder_hook ) {
			return;
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$doc_type = isset( $_GET['doc_type'] ) ? sanitize_key( wp_unslash( $_GET['doc_type'] ) ) : '';
		if ( ! in_array( $doc_type, [ 'invoice', 'packing-slip', 'credit-note' ], true ) ) {
			// Overview page — enqueue base style for card hover rule.
			wp_enqueue_style( 'kramar-admin', KRAMAR_URL . 'assets/css/admin.css', [], KRAMAR_VERSION );
			wp_add_inline_style( 'kramar-admin', '.kramar-ib-doc-card:hover{border-color:#2271b1;box-shadow:0 2px 10px rgba(0,0,0,.08);}' );
			return;
		}

		wp_enqueue_style( 'kramar-email-builder', KRAMAR_URL . 'assets/css/email-builder.css', [], KRAMAR_VERSION );
		wp_enqueue_style( 'kramar-invoice-builder', KRAMAR_URL . 'assets/css/invoice-builder.css', [], KRAMAR_VERSION );
		wp_enqueue_script( 'kramar-invoice-builder', KRAMAR_URL . 'assets/js/invoice-builder.js', [], KRAMAR_VERSION, true );

		$blocks = Block_Renderer::get_template_blocks( $doc_type );

		// Add IDs to blocks for JS state management.
		foreach ( $blocks as &$block ) {
			if ( ! isset( $block['id'] ) ) {
				$block['id'] = 'b' . wp_rand( 100000, 999999 );
			}
		}
		unset( $block );

		$block_types = [];
		foreach ( $this->get_available_block_types() as $type => $info ) {
			$block_types[ $type ] = [
				'label'    => $info['label'],
				'icon'     => $info['icon'],
				'defaults' => $info['defaults'] ?? [],
			];
		}

		$has_order = $this->get_preview_order() instanceof \WC_Order;

		wp_localize_script( 'kramar-invoice-builder', 'kramarInvoiceBuilder', [
			'ajaxUrl'    => admin_url( 'admin-ajax.php' ),
			'nonce'      => wp_create_nonce( 'kramar_invoice_builder' ),
			'docType'    => $doc_type,
			'blocks'     => $blocks,
			'blockTypes' => $block_types,
			'locked'     => [ 'title', 'addresses', 'line-items', 'totals' ],
			'hasOrder'   => $has_order,
			'i18n'       => [
				'saved'           => __( 'Template saved.', 'kramar' ),
				'error'           => __( 'Error saving template.', 'kramar' ),
				'locked'          => __( 'Required by EU law', 'kramar' ),
				'noBlockSelected' => __( 'Select a block to edit its settings.', 'kramar' ),
				'clickToEdit'     => __( 'Click to Edit', 'kramar' ),
				'dragToReorder'   => __( 'Drag to Reorder', 'kramar' ),
				'unsavedChanges'  => __( 'You have unsaved changes. Leave anyway?', 'kramar' ),
				'noOrderData'     => __( 'Create a WooCommerce order to preview this template with real data.', 'kramar' ),
				'noRefundData'    => __( 'Refund an order to preview the credit note with real refunded amounts.', 'kramar' ),
				'rendering'       => __( 'Rendering…', 'kramar' ),
			],
		] );
	}

	/**
	 * Enqueue invoice attachment JS on the Email Builder page.
	 *
	 * @since 1.0.0
	 *
	 * @param string $hook Current page hook suffix.
	 */
	public function enqueue_email_builder_assets( string $hook ): void {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$page = sanitize_key( wp_unslash( $_GET['page'] ?? '' ) );
		if ( 'kramar-email-builder' !== $page ) {
			return;
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$email_id = sanitize_key( wp_unslash( $_GET['email_id'] ?? '' ) );

		wp_localize_script(
			'kramar-email-builder',
			'kramarInvAttach',
			[
				'ajaxUrl' => admin_url( 'admin-ajax.php' ),
				'nonce'   => wp_create_nonce( 'kramar_invoice_email_attachments' ),
				'emailId' => $email_id,
				'i18n'    => [
					'saving' => __( 'Saving…', 'kramar' ),
					'saved'  => __( 'Saved', 'kramar' ),
					'error'  => __( 'Error', 'kramar' ),
				],
			]
		);

		wp_add_inline_script(
			'kramar-email-builder',
			'( function () {
	var cfg = kramarInvAttach;
	var wrap = document.querySelector( \'.kramar-eb-canvas__attachments\' );
	if ( ! wrap ) { return; }
	var status = wrap.querySelector( \'.kramar-inv-attach-status\' );
	wrap.querySelectorAll( \'.kramar-inv-attach\' ).forEach( function ( cb ) {
		cb.addEventListener( \'change\', function () {
			var sel = Array.prototype.map.call( wrap.querySelectorAll( \'.kramar-inv-attach:checked\' ), function ( c ) { return c.value; } );
			var body = new FormData();
			body.append( \'action\', \'kramar_invoice_save_email_attachments\' );
			body.append( \'_ajax_nonce\', cfg.nonce );
			body.append( \'email_id\', cfg.emailId );
			sel.forEach( function ( v ) { body.append( \'attachments[]\', v ); } );
			status.textContent = cfg.i18n.saving;
			fetch( cfg.ajaxUrl, { method: \'POST\', body: body } )
				.then( function ( r ) { return r.json(); } )
				.then( function ( r ) { status.textContent = ( r && r.success ) ? cfg.i18n.saved : cfg.i18n.error; setTimeout( function () { status.textContent = \'\'; }, 1500 ); } )
				.catch( function () { status.textContent = cfg.i18n.error; } );
		} );
	} );
} )();',
			'after'
		);
	}

	/**
	 * Find a representative order to render previews with.
	 *
	 * Prefers a recent order that actually has line items so the template
	 * shows realistic data.
	 *
	 * @since 1.0.0
	 */
	private function get_preview_order(): ?\WC_Order {
		$orders = wc_get_orders( [ 'limit' => 20, 'orderby' => 'date', 'order' => 'DESC' ] );
		foreach ( $orders as $o ) {
			if ( $o instanceof \WC_Order && $o->get_item_count() > 0 ) {
				return $o;
			}
		}
		return ! empty( $orders ) && $orders[0] instanceof \WC_Order ? $orders[0] : null;
	}

	/**
	 * Build a document for the given doc type using the preview order.
	 *
	 * @since 1.0.0
	 */
	private function build_preview_document( string $doc_type, ?\WC_Order $order = null ): ?Document {
		$order = $order ?? $this->get_preview_order();
		if ( ! $order ) {
			return null;
		}

		$settings = $this->get_all_settings();

		$doc = match ( $doc_type ) {
			'packing-slip' => new Packing_Slip( $order, $settings ),
			'credit-note'  => ( static function () use ( $order, $settings ): ?Document {
				// No refund → no credit note to preview. Return null so the builder
				// shows a hint instead of a positive INVOICE mislabelled as a credit note.
				$refunds = $order->get_refunds();
				return ! empty( $refunds ) ? new Credit_Note( $order, $refunds[0], $settings ) : null;
			} )(),
			default        => new Invoice( $order, $settings ),
		};

		if ( $doc ) {
			$doc->preview = true;
		}

		return $doc;
	}

	/**
	 * Why a preview couldn't be built — drives the builder's hint message.
	 *
	 * @since 1.0.0
	 * @return string 'no_order' or 'no_refund'.
	 */
	private function preview_error_reason(): string {
		if ( ! $this->get_preview_order() ) {
			return 'no_order';
		}
		// A preview order exists, so build_preview_document() can only have
		// returned null for a credit note with no refunds (its one order-present
		// null path). Report 'no_refund' — never 'no_order', which would wrongly
		// tell an admin who already has an order open to go create one.
		return 'no_refund';
	}

	/**
	 * Decode + sanitize blocks posted from the builder, then persist them as
	 * the live template so the renderer reflects unsaved edits.
	 *
	 * @since 1.0.0
	 */
	private function stash_posted_blocks( string $doc_type ): void {
		// phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- caller verifies the nonce; decoded JSON is sanitized per-field below.
		$raw_blocks = isset( $_POST['blocks'] ) ? json_decode( wp_unslash( $_POST['blocks'] ), true ) : null;
		if ( ! is_array( $raw_blocks ) ) {
			return;
		}

		$clean = [];
		foreach ( $raw_blocks as $block ) {
			$clean[] = [
				'type'     => sanitize_key( $block['type'] ?? '' ),
				'settings' => self::sanitize_block_settings( (array) ( $block['settings'] ?? [] ) ),
			];
		}

		$templates = get_option( 'kramar_invoice_template', [] );
		if ( ! is_array( $templates ) ) {
			$templates = [];
		}
		$templates[ $doc_type ] = $clean;
		update_option( 'kramar_invoice_template', $templates );
	}

	public function render_builder_page(): void {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$requested = isset( $_GET['doc_type'] ) ? sanitize_key( wp_unslash( $_GET['doc_type'] ) ) : '';

		// No document type → show the overview of the three PDF templates.
		if ( ! in_array( $requested, [ 'invoice', 'packing-slip', 'credit-note' ], true ) ) {
			$this->render_overview();
			return;
		}

		$doc_type = $requested;

		$doc_types = [
			'invoice'      => __( 'Invoice', 'kramar' ),
			'packing-slip' => __( 'Packing Slip', 'kramar' ),
			'credit-note'  => __( 'Credit Note', 'kramar' ),
		];
		?>
		<div class="wrap kramar-eb-wrap">
			<div class="kramar-eb-toolbar">
				<a href="<?php echo esc_url( admin_url( 'admin.php?page=' . self::BUILDER_SLUG ) ); ?>" class="kramar-eb-toolbar__back" title="<?php esc_attr_e( 'Back to Invoices', 'kramar' ); ?>">
					<span class="dashicons dashicons-arrow-left-alt2"></span>
				</a>
				<span class="kramar-eb-toolbar__title">
					<?php echo esc_html( $doc_types[ $doc_type ] ?? 'Invoice' ); ?> Template
					<?php foreach ( $doc_types as $type => $label ) : ?>
						<?php if ( $type !== $doc_type ) : ?>
							<a href="<?php echo esc_url( admin_url( 'admin.php?page=' . self::BUILDER_SLUG . '&doc_type=' . $type ) ); ?>"
							   style="font-size:13px;font-weight:400;margin-left:12px;text-decoration:none;color:#2271b1;">
								<?php echo esc_html( $label ); ?>
							</a>
						<?php endif; ?>
					<?php endforeach; ?>
				</span>
				<div class="kramar-eb-toolbar__actions">
					<button type="button" class="button kramar-ib-toolbar__preview">
						<span class="dashicons dashicons-visibility"></span>
						<?php esc_html_e( 'Preview', 'kramar' ); ?>
					</button>
					<button type="button" class="button button-primary kramar-eb-toolbar__save">
						<span class="dashicons dashicons-saved"></span>
						<?php esc_html_e( 'Save', 'kramar' ); ?>
					</button>
				</div>
			</div>

			<div class="kramar-ib-builder">
				<!-- Left: Block Palette -->
				<div class="kramar-ib-palette" id="kramar-ib-palette">
					<div class="kramar-ib-palette__header">
						<h3><?php esc_html_e( 'Add Block', 'kramar' ); ?></h3>
					</div>
					<div class="kramar-ib-palette__body">
						<?php $this->render_invoice_palette(); ?>
					</div>
				</div>

				<!-- Center: Canvas -->
				<div class="kramar-ib-canvas">
					<div class="kramar-ib-canvas__document">
						<div id="kramar-ib-canvas-container">
							<!-- JS renders block cards here -->
						</div>
						<div class="kramar-ib-canvas__empty" id="kramar-ib-canvas-empty">
							<span class="dashicons dashicons-media-document"></span>
							<p><?php esc_html_e( 'Add blocks from the left panel to build your invoice template.', 'kramar' ); ?></p>
						</div>
					</div>
				</div>

				<!-- Right: Block Settings -->
				<div class="kramar-ib-settings">
					<div class="kramar-ib-settings__header">
						<h3 id="kramar-ib-settings-title"><?php esc_html_e( 'Block Settings', 'kramar' ); ?></h3>
					</div>
					<div class="kramar-ib-settings__body" id="kramar-ib-settings-body">
						<div class="kramar-ib-settings__empty">
							<span class="dashicons dashicons-admin-generic" style="font-size:32px;width:32px;height:32px;color:#d1d5db;"></span>
							<p style="color:#9ca3af;margin:8px 0 0;"><?php esc_html_e( 'Select a block to edit its settings.', 'kramar' ); ?></p>
						</div>
					</div>
				</div>
			</div>
		</div>
		<?php
	}

	/**
	 * Landing page: the three PDF document templates as cards.
	 *
	 * @since 1.0.0
	 */
	private function render_overview(): void {
		$saved = get_option( 'kramar_invoice_template', [] );
		$saved = is_array( $saved ) ? $saved : [];

		$docs = [
			'invoice'      => [
				'icon' => 'dashicons-media-text',
				'name' => __( 'Invoice', 'kramar' ),
				'desc' => __( 'The EU-compliant sales invoice (factuur). Auto-generated when an order reaches the configured status and attached to customer emails.', 'kramar' ),
			],
			'packing-slip' => [
				'icon' => 'dashicons-clipboard',
				'name' => __( 'Packing Slip', 'kramar' ),
				'desc' => __( 'A fulfillment slip for picking and shipping — line items without prices or VAT. Generated on demand.', 'kramar' ),
			],
			'credit-note'  => [
				'icon' => 'dashicons-image-rotate',
				'name' => __( 'Credit Note', 'kramar' ),
				'desc' => __( 'The creditnota issued when you refund an order, referencing the original invoice with negative amounts.', 'kramar' ),
			],
		];
		?>
		<div class="wrap kramar-settings">
			<h1 style="margin:0 0 6px;"><?php esc_html_e( 'Invoices', 'kramar' ); ?></h1>
			<p class="description" style="margin:0 0 20px;max-width:680px;">
				<?php esc_html_e( 'Design the PDF templates for each document type. Each opens the drag-and-drop builder with a live, true-to-print preview.', 'kramar' ); ?>
			</p>

			<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px;max-width:1000px;">
				<?php
				foreach ( $docs as $type => $d ) :
					$url        = admin_url( 'admin.php?page=' . self::BUILDER_SLUG . '&doc_type=' . $type );
					$customized = ! empty( $saved[ $type ] );
					?>
					<a href="<?php echo esc_url( $url ); ?>" class="kramar-ib-doc-card" style="display:flex;flex-direction:column;text-decoration:none;background:#fff;border:1px solid #e0e0e0;border-radius:8px;padding:20px;color:inherit;transition:box-shadow .15s,border-color .15s;">
						<span class="dashicons <?php echo esc_attr( $d['icon'] ); ?>" style="font-size:28px;width:28px;height:28px;color:#2271b1;"></span>
						<h2 style="margin:12px 0 4px;font-size:16px;color:#1d2327;"><?php echo esc_html( $d['name'] ); ?></h2>
						<p style="margin:0 0 16px;color:#646970;font-size:13px;line-height:1.5;flex:1;"><?php echo esc_html( $d['desc'] ); ?></p>
						<span style="display:flex;align-items:center;">
							<span style="font-size:11px;text-transform:uppercase;letter-spacing:.04em;font-weight:600;color:<?php echo $customized ? '#008a20' : '#646970'; ?>;">
								<?php echo $customized ? esc_html__( 'Customized', 'kramar' ) : esc_html__( 'Default', 'kramar' ); ?>
							</span>
							<span style="margin-left:auto;color:#2271b1;font-weight:600;font-size:13px;"><?php esc_html_e( 'Edit template', 'kramar' ); ?> &rarr;</span>
						</span>
					</a>
				<?php endforeach; ?>
			</div>
		</div>
		<?php
	}

	private function render_invoice_palette(): void {
		$groups = [
			'document' => [
				'label'  => __( 'Document', 'kramar' ),
				'blocks' => [
					'header'     => [ 'dashicons-cover-image', __( 'Header (Logo)', 'kramar' ) ],
					'title'      => [ 'dashicons-heading', __( 'Document Title', 'kramar' ) ],
					'addresses'  => [ 'dashicons-id-alt', __( 'Addresses & Data', 'kramar' ) ],
					'line-items' => [ 'dashicons-editor-table', __( 'Line Items', 'kramar' ) ],
					'totals'     => [ 'dashicons-calculator', __( 'Totals', 'kramar' ) ],
				],
			],
			'extra'    => [
				'label'  => __( 'Extra', 'kramar' ),
				'blocks' => [
					'notes'   => [ 'dashicons-edit', __( 'Customer Notes', 'kramar' ) ],
					'text'    => [ 'dashicons-editor-paragraph', __( 'Text', 'kramar' ) ],
					'divider' => [ 'dashicons-minus', __( 'Divider', 'kramar' ) ],
				],
			],
		];

		foreach ( $groups as $group_key => $group ) {
			printf( '<div class="kramar-ib-palette-group" data-group="%s">', esc_attr( $group_key ) );
			printf( '<h4 class="kramar-ib-palette-group__label">%s</h4>', esc_html( $group['label'] ) );

			foreach ( $group['blocks'] as $type => $block_info ) {
				printf(
					'<div class="kramar-ib-palette-item" data-block-type="%s" draggable="true">' .
					'<span class="dashicons %s kramar-ib-palette-icon"></span>' .
					'<span class="kramar-ib-palette-label">%s</span>' .
					'</div>',
					esc_attr( $type ),
					esc_attr( $block_info[0] ),
					esc_html( $block_info[1] )
				);
			}

			echo '</div>';
		}
	}

	private static function sanitize_block_settings( array $raw ): array {
		$clean = [];
		foreach ( $raw as $key => $value ) {
			$key = sanitize_key( $key );
			if ( is_bool( $value ) ) {
				$clean[ $key ] = $value;
			} elseif ( 'true' === $value ) {
				$clean[ $key ] = true;
			} elseif ( 'false' === $value ) {
				$clean[ $key ] = false;
			} else {
				$clean[ $key ] = sanitize_text_field( (string) $value );
			}
		}
		return $clean;
	}

	private function get_available_block_types(): array {
		return [
			'header'     => [ 'label' => __( 'Header (Logo)', 'kramar' ), 'icon' => 'dashicons-cover-image', 'defaults' => [ 'show_logo' => true ] ],
			'title'      => [ 'label' => __( 'Document Title', 'kramar' ), 'icon' => 'dashicons-heading', 'defaults' => [] ],
			'addresses'  => [ 'label' => __( 'Addresses & Data', 'kramar' ), 'icon' => 'dashicons-id-alt', 'defaults' => [ 'show_shipping' => true, 'show_phone' => true, 'show_vat_id' => true ] ],
			'line-items' => [ 'label' => __( 'Line Items', 'kramar' ), 'icon' => 'dashicons-editor-table', 'defaults' => [ 'show_sku' => true, 'show_vat' => true ] ],
			'totals'     => [ 'label' => __( 'Totals', 'kramar' ), 'icon' => 'dashicons-calculator', 'defaults' => [ 'show_shipping' => true, 'show_discount' => true ] ],
			'notes'      => [ 'label' => __( 'Customer Notes', 'kramar' ), 'icon' => 'dashicons-edit', 'defaults' => [ 'show_customer_note' => true ] ],
			'divider'    => [ 'label' => __( 'Divider', 'kramar' ), 'icon' => 'dashicons-minus', 'defaults' => [] ],
			'text'       => [ 'label' => __( 'Text', 'kramar' ), 'icon' => 'dashicons-editor-paragraph', 'defaults' => [ 'content' => '' ] ],
		];
	}

	// -------------------------------------------------------------------------
	// AJAX: Save Template, Preview, Regenerate All
	// -------------------------------------------------------------------------

	public function ajax_save_template(): void {
		check_ajax_referer( 'kramar_invoice_builder' );

		if ( ! current_user_can( 'manage_woocommerce' ) ) {
			wp_send_json_error( 'Unauthorized' );
		}

		$doc_type = sanitize_key( wp_unslash( $_POST['doc_type'] ?? 'invoice' ) );
		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- decoded JSON sanitized per-field below
		$blocks   = json_decode( wp_unslash( $_POST['blocks'] ?? '[]' ), true );

		if ( ! is_array( $blocks ) ) {
			wp_send_json_error( 'Invalid blocks data' );
		}

		$clean = [];
		foreach ( $blocks as $block ) {
			$clean[] = [
				'type'     => sanitize_key( $block['type'] ?? '' ),
				'settings' => self::sanitize_block_settings( (array) ( $block['settings'] ?? [] ) ),
			];
		}

		$templates = get_option( 'kramar_invoice_template', [] );
		if ( ! is_array( $templates ) ) {
			$templates = [];
		}
		$templates[ $doc_type ] = $clean;
		update_option( 'kramar_invoice_template', $templates );

		wp_send_json_success();
	}

	/**
	 * Live canvas render — returns each posted block's real PDF HTML so the
	 * builder canvas is a faithful WYSIWYG of the final document.
	 *
	 * @since 1.0.0
	 */
	public function ajax_render(): void {
		check_ajax_referer( 'kramar_invoice_builder' );

		if ( ! current_user_can( 'manage_woocommerce' ) ) {
			wp_send_json_error( 'Unauthorized' );
		}

		$doc_type = sanitize_key( wp_unslash( $_POST['doc_type'] ?? 'invoice' ) );
		if ( ! in_array( $doc_type, [ 'invoice', 'packing-slip', 'credit-note' ], true ) ) {
			$doc_type = 'invoice';
		}

		$doc = $this->build_preview_document( $doc_type );
		if ( ! $doc ) {
			wp_send_json_error( [ 'reason' => $this->preview_error_reason() ] );
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Missing -- referer checked above.
		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- decoded JSON sanitized per-field below
		$raw_blocks = isset( $_POST['blocks'] ) ? json_decode( wp_unslash( $_POST['blocks'] ), true ) : [];
		if ( ! is_array( $raw_blocks ) ) {
			$raw_blocks = [];
		}

		$rendered = [];
		foreach ( $raw_blocks as $block ) {
			$id   = isset( $block['id'] ) ? sanitize_text_field( (string) $block['id'] ) : '';
			$type = sanitize_key( $block['type'] ?? '' );
			$norm = [
				'type'     => $type,
				'settings' => self::sanitize_block_settings( (array) ( $block['settings'] ?? [] ) ),
			];
			$rendered[] = [
				'id'   => $id,
				'html' => Block_Renderer::render_block_public( $norm, $doc ),
			];
		}

		wp_send_json_success( [
			'blocks' => $rendered,
			'footer' => Block_Renderer::render_footer_html( $doc ),
		] );
	}

	/**
	 * Full A4 preview — returns the complete document HTML in browser-preview
	 * mode (true A4 page, single source of truth via Block_Renderer).
	 *
	 * @since 1.0.0
	 */
	public function ajax_preview(): void {
		check_ajax_referer( 'kramar_invoice_builder' );

		if ( ! current_user_can( 'manage_woocommerce' ) ) {
			wp_send_json_error( 'Unauthorized' );
		}

		$doc_type = sanitize_key( wp_unslash( $_POST['doc_type'] ?? 'invoice' ) );
		if ( ! in_array( $doc_type, [ 'invoice', 'packing-slip', 'credit-note' ], true ) ) {
			$doc_type = 'invoice';
		}

		// Persist posted blocks so the renderer reflects unsaved edits.
		$this->stash_posted_blocks( $doc_type );

		$doc = $this->build_preview_document( $doc_type );
		if ( ! $doc ) {
			wp_send_json_error( [ 'reason' => $this->preview_error_reason() ] );
		}

		$html = Block_Renderer::render( $doc, 'preview' );
		wp_send_json_success( [ 'html' => $html ] );
	}

	public function ajax_regenerate_all(): void {
		check_ajax_referer( 'kramar_invoice_builder' );

		if ( ! current_user_can( 'manage_woocommerce' ) ) {
			wp_send_json_error( 'Unauthorized' );
		}

		$settings = $this->get_all_settings();
		$page     = absint( $_POST['page'] ?? 1 );
		$per_page = 20;

		$orders = wc_get_orders( [
			// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- intentional meta query
			'meta_key' => '_kramar_invoice_file',
			'limit'    => $per_page,
			'offset'   => ( $page - 1 ) * $per_page,
			'orderby'  => 'ID',
			'order'    => 'ASC',
		] );

		$regenerated = 0;
		foreach ( $orders as $order ) {
			$old_file = (string) $order->get_meta( '_kramar_invoice_file' );
			if ( '' === $old_file ) {
				continue;
			}

			Storage::delete( $old_file );
			$order->delete_meta_data( '_kramar_invoice_file' );
			$order->save_meta_data();

			$invoice = new Invoice( $order, $settings );
			$invoice->generate_and_store();
			$regenerated++;
		}

		$has_more = count( $orders ) === $per_page;

		wp_send_json_success( [
			'regenerated' => $regenerated,
			'page'        => $page,
			'has_more'    => $has_more,
		] );
	}

	// -------------------------------------------------------------------------
	// Bulk Actions
	// -------------------------------------------------------------------------

	public function register_bulk_actions( array $actions ): array {
		$actions['kramar_download_invoices'] = __( 'Download PDF Invoices', 'kramar' );
		$actions['kramar_download_packing']  = __( 'Download PDF Packing Slips', 'kramar' );
		return $actions;
	}

	public function handle_bulk_action( string $redirect_url, string $action, array $order_ids ): string {
		if ( ! in_array( $action, [ 'kramar_download_invoices', 'kramar_download_packing' ], true ) ) {
			return $redirect_url;
		}

		if ( empty( $order_ids ) ) {
			return $redirect_url;
		}

		$settings = $this->get_all_settings();
		$pdfs     = [];

		foreach ( $order_ids as $order_id ) {
			$order = wc_get_order( $order_id );
			if ( ! $order ) {
				continue;
			}

			if ( 'kramar_download_invoices' === $action ) {
				$rel_path = (string) $order->get_meta( '_kramar_invoice_file' );
				if ( '' === $rel_path ) {
					$invoice  = new Invoice( $order, $settings );
					$rel_path = $invoice->generate_and_store();
				}
				$pdf = Storage::retrieve( $rel_path );
				if ( $pdf ) {
					$inv = new Invoice( $order, $settings );
					$pdfs[] = [ 'content' => $pdf, 'name' => $inv->get_filename() ];
				}
			} else {
				$slip = new Packing_Slip( $order, $settings );
				$pdfs[] = [ 'content' => $slip->generate(), 'name' => $slip->get_filename() ];
			}
		}

		if ( empty( $pdfs ) ) {
			return $redirect_url;
		}

		// Single PDF — serve directly.
		if ( 1 === count( $pdfs ) ) {
			nocache_headers();
			header( 'Content-Type: application/pdf' );
			header( 'Content-Disposition: attachment; filename="' . sanitize_file_name( $pdfs[0]['name'] ) . '"' );
			header( 'Content-Length: ' . strlen( $pdfs[0]['content'] ) );
			// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
			echo $pdfs[0]['content'];
			exit;
		}

		// Multiple PDFs — ZIP.
		$zip_file = wp_tempnam( 'kramar-invoices.zip' );
		$zip      = new \ZipArchive();
		if ( true !== $zip->open( $zip_file, \ZipArchive::CREATE | \ZipArchive::OVERWRITE ) ) {
			return $redirect_url;
		}

		foreach ( $pdfs as $pdf ) {
			$zip->addFromString( $pdf['name'], $pdf['content'] );
		}
		$zip->close();

		$zip_content = file_get_contents( $zip_file );
		wp_delete_file( $zip_file );

		$type_label = 'kramar_download_invoices' === $action ? 'invoices' : 'packing-slips';

		nocache_headers();
		header( 'Content-Type: application/zip' );
		header( 'Content-Disposition: attachment; filename="' . sanitize_file_name( $type_label . '-' . wp_date( 'Y-m-d' ) . '.zip' ) . '"' );
		header( 'Content-Length: ' . strlen( $zip_content ) );
		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		echo $zip_content;
		exit;
	}

	// -------------------------------------------------------------------------
	// Admin Styles
	// -------------------------------------------------------------------------

	/**
	 * Enqueue PDF button styles on order and Kramar admin screens.
	 *
	 * @since 1.0.0
	 */
	public function enqueue_pdf_btn_styles(): void {
		$screen = get_current_screen();
		if ( ! $screen ) {
			return;
		}
		if (
			! in_array( $screen->id, [ 'edit-shop_order', 'woocommerce_page_wc-orders', 'shop_order' ], true ) &&
			! str_contains( $screen->id, 'kramar' )
		) {
			return;
		}

		wp_enqueue_style( 'kramar-admin', KRAMAR_URL . 'assets/css/admin.css', [], KRAMAR_VERSION );
		wp_add_inline_style(
			'kramar-admin',
			'.kramar-pdf-btn{display:inline-block;position:relative;width:28px;height:28px;line-height:28px;text-align:center;border-radius:3px;color:#787c82;text-decoration:none;margin-left:2px;}' .
			'.kramar-pdf-btn:hover{background:#f0f0f1;color:#2271b1;}' .
			'.kramar-pdf-btn--exists{color:#2563eb;}' .
			'.kramar-pdf-btn--exists:hover{color:#1d4ed8;}' .
			'.kramar-pdf-check{position:absolute;bottom:-2px;right:-2px;font-size:9px;color:#16a34a;font-weight:bold;}' .
			'.kramar-pdf-btn .dashicons{font-size:18px;width:18px;height:18px;vertical-align:middle;}'
		);
	}

	// -------------------------------------------------------------------------
	// Settings UI
	// -------------------------------------------------------------------------

	/** {@inheritDoc} */
	public function render_extra_settings( string $option_name ): void {
		$slug = $this->get_slug();
		$cat  = $this->get_category();

		$prefix  = (string) $this->get_setting( 'number_prefix' );
		$suffix  = (string) $this->get_setting( 'number_suffix' );
		$padding = (int) $this->get_setting( 'number_padding' );
		$paper   = (string) $this->get_setting( 'paper_size' );
		$accent  = (string) $this->get_setting( 'accent_color' );
		$footer  = (string) $this->get_setting( 'footer_text' );
		$status  = (string) $this->get_setting( 'auto_generate_status' );

		$show_sku      = (bool) $this->get_setting( 'show_sku' );
		$show_image    = (bool) $this->get_setting( 'show_product_image' );
		$show_note     = (bool) $this->get_setting( 'show_customer_note' );
		$show_shipping = (bool) $this->get_setting( 'show_shipping_address' );
		$show_email    = (bool) $this->get_setting( 'show_email' );
		$show_phone    = (bool) $this->get_setting( 'show_phone' );

		$field = static function ( string $key ) use ( $option_name, $cat, $slug ): string {
			return sprintf( '%s[%s][%s][%s]', $option_name, $cat, $slug, $key );
		};

		$statuses = wc_get_order_statuses();
		$wc_emails = \WC()->mailer()->get_emails();
		$email_options = [];
		foreach ( $wc_emails as $email ) {
			if ( ! empty( $email->id ) ) {
				$title  = $email->get_title();
				$recipient_label = $email->is_customer_email() ? __( '(Customer)', 'kramar' ) : __( '(Admin)', 'kramar' );
				$email_options[ $email->id ] = $title . ' ' . $recipient_label;
			}
		}

		$attach_invoice = (array) $this->get_setting( 'attach_invoice_to' );
		$attach_packing = (array) $this->get_setting( 'attach_packing_to' );
		$attach_credit  = (array) $this->get_setting( 'attach_credit_to' );
		?>
		<div class="kramar-module__extra" style="flex-direction:column;align-items:flex-start;">
			<div class="kramar-settings-stack" style="max-width:720px;">

				<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px 20px;">
					<div>
						<label><strong><?php esc_html_e( 'Auto-generate on status', 'kramar' ); ?></strong></label><br>
						<select name="<?php echo esc_attr( $field( 'auto_generate_status' ) ); ?>" style="width:100%;">
							<?php foreach ( $statuses as $s => $label ) : $s = str_replace( 'wc-', '', $s ); ?>
								<option value="<?php echo esc_attr( $s ); ?>" <?php selected( $status, $s ); ?>><?php echo esc_html( $label ); ?></option>
							<?php endforeach; ?>
						</select>
					</div>
					<div>
						<label><strong><?php esc_html_e( 'Paper size', 'kramar' ); ?></strong></label><br>
						<select name="<?php echo esc_attr( $field( 'paper_size' ) ); ?>" style="width:100%;">
							<option value="a4" <?php selected( $paper, 'a4' ); ?>>A4</option>
							<option value="letter" <?php selected( $paper, 'letter' ); ?>>Letter</option>
						</select>
					</div>
					<div>
						<label><strong><?php esc_html_e( 'Accent color', 'kramar' ); ?></strong></label><br>
						<input type="color" name="<?php echo esc_attr( $field( 'accent_color' ) ); ?>" value="<?php echo esc_attr( $accent ); ?>" style="width:100%;height:32px;padding:2px;">
					</div>
				</div>

				<fieldset style="border:1px solid #e4e4e7;border-radius:4px;padding:12px 16px;">
					<legend style="font-weight:600;padding:0 6px;"><?php esc_html_e( 'Invoice number format', 'kramar' ); ?></legend>
					<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;">
						<div>
							<label><?php esc_html_e( 'Prefix', 'kramar' ); ?></label><br>
							<input type="text" name="<?php echo esc_attr( $field( 'number_prefix' ) ); ?>" value="<?php echo esc_attr( $prefix ); ?>" style="width:100%;" placeholder="INV-">
						</div>
						<div>
							<label><?php esc_html_e( 'Suffix', 'kramar' ); ?></label><br>
							<input type="text" name="<?php echo esc_attr( $field( 'number_suffix' ) ); ?>" value="<?php echo esc_attr( $suffix ); ?>" style="width:100%;">
						</div>
						<div>
							<label><?php esc_html_e( 'Zero padding', 'kramar' ); ?></label><br>
							<input type="number" name="<?php echo esc_attr( $field( 'number_padding' ) ); ?>" value="<?php echo esc_attr( (string) $padding ); ?>" min="1" max="10" style="width:100%;">
						</div>
					</div>
					<p class="description" style="margin-top:6px;">
						<?php
						echo esc_html(
							sprintf(
								/* translators: %s is a formatted invoice number preview, e.g. "INV-0042" */
								__( 'Preview: %s', 'kramar' ),
								Number_Store::format_number( 42, $prefix, $suffix, $padding )
							)
						);
						?>
						&nbsp;·&nbsp;<?php esc_html_e( 'Use {year}, {month}, {day} tokens in prefix/suffix.', 'kramar' ); ?>
					</p>
				</fieldset>

				<fieldset style="border:1px solid #e4e4e7;border-radius:4px;padding:12px 16px;">
					<legend style="font-weight:600;padding:0 6px;"><?php esc_html_e( 'Display options', 'kramar' ); ?></legend>
					<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px;">
						<label><input type="checkbox" name="<?php echo esc_attr( $field( 'show_sku' ) ); ?>" value="1" <?php checked( $show_sku ); ?>> <?php esc_html_e( 'Show SKU', 'kramar' ); ?></label>
						<label><input type="checkbox" name="<?php echo esc_attr( $field( 'show_product_image' ) ); ?>" value="1" <?php checked( $show_image ); ?>> <?php esc_html_e( 'Product image', 'kramar' ); ?></label>
						<label><input type="checkbox" name="<?php echo esc_attr( $field( 'show_customer_note' ) ); ?>" value="1" <?php checked( $show_note ); ?>> <?php esc_html_e( 'Customer notes', 'kramar' ); ?></label>
						<label><input type="checkbox" name="<?php echo esc_attr( $field( 'show_shipping_address' ) ); ?>" value="1" <?php checked( $show_shipping ); ?>> <?php esc_html_e( 'Shipping address', 'kramar' ); ?></label>
						<label><input type="checkbox" name="<?php echo esc_attr( $field( 'show_email' ) ); ?>" value="1" <?php checked( $show_email ); ?>> <?php esc_html_e( 'Customer email', 'kramar' ); ?></label>
						<label><input type="checkbox" name="<?php echo esc_attr( $field( 'show_phone' ) ); ?>" value="1" <?php checked( $show_phone ); ?>> <?php esc_html_e( 'Customer phone', 'kramar' ); ?></label>
					</div>
				</fieldset>

				<div>
					<label><strong><?php esc_html_e( 'Footer text', 'kramar' ); ?></strong></label><br>
					<input type="text" name="<?php echo esc_attr( $field( 'footer_text' ) ); ?>" value="<?php echo esc_attr( $footer ); ?>" style="width:100%;" placeholder="<?php esc_attr_e( 'Thank you for your business!', 'kramar' ); ?>">
				</div>

				<fieldset style="border:1px solid #e4e4e7;border-radius:4px;padding:12px 16px;">
					<legend style="font-weight:600;padding:0 6px;"><?php esc_html_e( 'Email attachments', 'kramar' ); ?></legend>
					<p class="description" style="margin:0 0 8px;"><?php esc_html_e( 'Select which emails should include PDF attachments.', 'kramar' ); ?></p>
					<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;">
						<div>
							<strong><?php esc_html_e( 'Invoice', 'kramar' ); ?></strong>
							<?php foreach ( $email_options as $eid => $elabel ) : ?>
								<br><label><input type="checkbox" name="<?php echo esc_attr( $field( 'attach_invoice_to' ) ); ?>[]" value="<?php echo esc_attr( $eid ); ?>" <?php checked( in_array( $eid, $attach_invoice, true ) ); ?>> <?php echo esc_html( $elabel ); ?></label>
							<?php endforeach; ?>
						</div>
						<div>
							<strong><?php esc_html_e( 'Packing Slip', 'kramar' ); ?></strong>
							<?php foreach ( $email_options as $eid => $elabel ) : ?>
								<br><label><input type="checkbox" name="<?php echo esc_attr( $field( 'attach_packing_to' ) ); ?>[]" value="<?php echo esc_attr( $eid ); ?>" <?php checked( in_array( $eid, $attach_packing, true ) ); ?>> <?php echo esc_html( $elabel ); ?></label>
							<?php endforeach; ?>
						</div>
						<div>
							<strong><?php esc_html_e( 'Credit Note', 'kramar' ); ?></strong>
							<?php foreach ( $email_options as $eid => $elabel ) : ?>
								<br><label><input type="checkbox" name="<?php echo esc_attr( $field( 'attach_credit_to' ) ); ?>[]" value="<?php echo esc_attr( $eid ); ?>" <?php checked( in_array( $eid, $attach_credit, true ) ); ?>> <?php echo esc_html( $elabel ); ?></label>
							<?php endforeach; ?>
						</div>
					</div>
				</fieldset>

				<div style="padding-top:12px;border-top:1px solid #e4e4e7;">
					<div style="padding:12px 14px;margin-bottom:12px;background:#fef2f2;border:1px solid #fecaca;border-left:4px solid #dc2626;border-radius:4px;color:#7f1d1d;font-size:13px;line-height:1.5;">
						<strong style="display:block;margin-bottom:4px;">&#9888; <?php esc_html_e( 'Destructive action — back up first', 'kramar' ); ?></strong>
						<?php esc_html_e( 'This permanently deletes every stored invoice, packing slip, and credit-note PDF, then rebuilds them from the current template. The old PDFs cannot be recovered. Sequential invoice numbers are preserved, but any manual edits to existing documents are lost. Make a full backup of your site and the uploads folder before continuing.', 'kramar' ); ?>
					</div>
					<button type="button" id="kramar-regenerate-all" class="button"
							onclick="if(!confirm('<?php echo esc_js( __( 'WARNING: This permanently deletes and rebuilds ALL stored invoice, packing-slip, and credit-note PDFs. This cannot be undone. Have you made a full backup? Click OK only if you have a current backup.', 'kramar' ) ); ?>'))return;var b=this,s=b.nextElementSibling;b.disabled=true;s.textContent='…';(function run(p){fetch(ajaxurl,{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'action=<?php echo esc_js( self::REGENERATE_ACTION ); ?>&_ajax_nonce='+encodeURIComponent('<?php echo esc_js( wp_create_nonce( 'kramar_invoice_builder' ) ); ?>')+'&page='+p}).then(function(r){return r.json()}).then(function(r){if(r.success){s.textContent=r.data.regenerated+' regenerated (batch '+r.data.page+')…';if(r.data.has_more)run(r.data.page+1);else{s.textContent='Done!';b.disabled=false}}else{s.textContent='Error';b.disabled=false}}).catch(function(){s.textContent='Error';b.disabled=false})})(1);">
						<?php esc_html_e( 'Regenerate All Invoices', 'kramar' ); ?>
					</button>
					<span style="margin-left:8px;font-size:13px;color:#6b7280;"></span>
				</div>

			</div>
		</div>
		<?php
	}
}
