<?php
/**
 * Block-to-HTML renderer for PDF documents.
 *
 * @package Kramar
 * @since   1.0.0
 */

declare( strict_types=1 );

namespace Kramar\Modules\Invoices;

defined( 'ABSPATH' ) || exit;

/**
 * Converts document block templates into Dompdf-compatible HTML.
 *
 * Uses table layout throughout — Dompdf has no flexbox/grid support.
 * All styles are inline for maximum Dompdf compatibility.
 *
 * @since 1.0.0
 */
final class Block_Renderer {

	/**
	 * Render a full document.
	 *
	 * @since 1.0.0
	 *
	 * @param Document $doc  The document.
	 * @param string   $mode 'pdf' (Dompdf, fixed footer) or 'preview' (browser A4 page).
	 */
	public static function render( Document $doc, string $mode = 'pdf' ): string {
		$blocks = self::get_template_blocks( $doc->get_type() );
		$accent = esc_attr( $doc->settings['accent_color'] ?? '#2563eb' );
		$paper  = ( $doc->settings['paper_size'] ?? 'a4' ) === 'letter' ? 'letter' : 'a4';

		$body = '';
		foreach ( $blocks as $block ) {
			$body .= self::render_block( $block, $doc, $accent );
		}

		$footer_inner = self::footer_inner( $doc );

		$html  = '<!DOCTYPE html><html><head><meta charset="UTF-8">';
		// Inline <style> is required here: this HTML is rendered to a PDF by Dompdf
		// (and to a standalone preview iframe), not served through wp_head, so
		// wp_enqueue_style() cannot apply.
		$html .= '<style>' . self::get_base_styles( $paper, $mode ) . '</style>';
		$html .= '</head><body>';

		if ( 'preview' === $mode ) {
			$html .= '<div class="kramar-doc">';
			$html .= $body;
			if ( '' !== $footer_inner ) {
				$html .= '<div class="kramar-doc__footer">' . $footer_inner . '</div>';
			}
			$html .= '</div>';
		} else {
			$html .= $body;
			if ( '' !== $footer_inner ) {
				$html .= '<div id="footer">' . $footer_inner . '</div>';
			}
		}

		$html .= '</body></html>';

		return $html;
	}

	/**
	 * Render a single block's HTML (used by the live builder canvas).
	 *
	 * Produces the exact same markup the PDF/preview uses, so the canvas
	 * is a faithful WYSIWYG of the final document.
	 *
	 * @since 1.0.0
	 */
	public static function render_block_public( array $block, Document $doc ): string {
		$accent = esc_attr( $doc->settings['accent_color'] ?? '#2563eb' );
		return self::render_block( $block, $doc, $accent );
	}

	/**
	 * Render the footer inner HTML (used by the live builder canvas).
	 *
	 * @since 1.0.0
	 */
	public static function render_footer_html( Document $doc ): string {
		return self::footer_inner( $doc );
	}

	/** @since 1.0.0 */
	public static function get_template_blocks( string $type ): array {
		$saved = get_option( 'kramar_invoice_template', [] );
		if ( is_array( $saved ) && ! empty( $saved[ $type ] ) ) {
			return $saved[ $type ];
		}
		return self::get_default_blocks( $type );
	}

	/** @since 1.0.0 */
	public static function get_default_blocks( string $type ): array {
		$blocks = [
			[ 'type' => 'header', 'settings' => [] ],
			[ 'type' => 'title', 'settings' => [] ],
			[ 'type' => 'addresses', 'settings' => [ 'show_shipping' => true ] ],
			[ 'type' => 'line-items', 'settings' => [ 'show_sku' => false, 'show_vat' => false ] ],
			[ 'type' => 'totals', 'settings' => [] ],
		];

		if ( 'packing-slip' === $type ) {
			$blocks = array_filter( $blocks, static fn( array $b ): bool => 'totals' !== $b['type'] );
			$blocks = array_values( $blocks );
		}

		return $blocks;
	}

	private static function render_block( array $block, Document $doc, string $accent ): string {
		return match ( $block['type'] ?? '' ) {
			'header'     => self::render_header( $doc, $accent, $block['settings'] ?? [] ),
			'title'      => self::render_title( $doc ),
			'addresses'  => self::render_addresses( $doc, $block['settings'] ?? [] ),
			'line-items' => self::render_line_items( $doc, $block['settings'] ?? [] ),
			'totals'     => self::render_totals( $doc ),
			'notes'      => self::render_notes( $doc ),
			'divider'    => '<div style="border-top:0.5pt solid #d1d5db;margin:10pt 0;"></div>',
			'text'       => self::render_text( $block['settings'] ?? [] ),
			default      => '',
		};
	}

	/**
	 * Convert a local-upload image URL to a base64 data URI for Dompdf embedding.
	 *
	 * Dompdf runs with isRemoteEnabled = false (SSRF safety), so images must be
	 * embedded inline rather than fetched over HTTP. Returns '' when the URL can't
	 * be resolved to a readable local image file.
	 *
	 * @since 1.0.0
	 *
	 * @param string $url Image URL (expected to be a local media-library upload).
	 * @return string A `data:` URI, or '' if it can't be resolved locally.
	 */
	private static function image_to_data_uri( string $url ): string {
		if ( '' === $url ) {
			return '';
		}

		$uploads = wp_get_upload_dir();
		$path    = '';

		if ( ! empty( $uploads['baseurl'] ) && str_starts_with( $url, $uploads['baseurl'] ) ) {
			$path = $uploads['basedir'] . substr( $url, strlen( (string) $uploads['baseurl'] ) );
		} else {
			$attachment_id = attachment_url_to_postid( $url );
			if ( $attachment_id ) {
				$path = (string) get_attached_file( $attachment_id );
			}
		}

		if ( '' === $path || ! is_file( $path ) || ! is_readable( $path ) ) {
			return '';
		}

		$mime = (string) ( wp_check_filetype( $path )['type'] ?? '' );
		if ( ! str_starts_with( $mime, 'image/' ) ) {
			return '';
		}

		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- reading a local upload to embed inline; this is not an HTTP request.
		$data = file_get_contents( $path );
		if ( false === $data ) {
			return '';
		}

		// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- encoding image bytes for a data: URI, not obfuscation.
		return 'data:' . $mime . ';base64,' . base64_encode( $data );
	}

	// ─── Header ────────────────────────────────────────────────────────────

	private static function render_header( Document $doc, string $accent, array $bs = [] ): string {
		$biz  = $doc->get_business_details();
		$logo = $doc->get_logo_url();

		// Builder "Show logo" toggle — defaults to on when the block carries no setting.
		$show_logo = ! array_key_exists( 'show_logo', $bs ) || ! empty( $bs['show_logo'] );

		$html = '<table style="width:100%;margin-bottom:20pt;"><tr>';

		// Left: logo. Embedded as a data URI so Dompdf renders it without remote
		// fetching (isRemoteEnabled is off for SSRF safety).
		$html .= '<td style="width:40%;vertical-align:top;">';
		if ( $show_logo && '' !== $logo ) {
			$logo_src = self::image_to_data_uri( $logo );
			if ( '' !== $logo_src ) {
				$html .= '<img src="' . esc_attr( $logo_src ) . '" style="max-height:80pt;max-width:200pt;">';
			}
		}
		$html .= '</td>';

		// Right: company details.
		$html .= '<td style="width:60%;vertical-align:top;font-size:9pt;line-height:1.7;color:#333333;">';
		$html .= '<strong style="color:' . $accent . ';">' . esc_html( $biz['company_name'] ) . '</strong><br>';
		if ( '' !== $biz['company_address'] ) {
			$lines = array_filter( array_map( 'trim', explode( "\n", $biz['company_address'] ) ) );
			foreach ( $lines as $line ) {
				$html .= esc_html( $line ) . '<br>';
			}
		}
		$country = $biz['country'] ?? '';
		if ( '' === $country && function_exists( 'WC' ) ) {
			$country = WC()->countries->get_base_country();
			if ( $country ) {
				$countries = WC()->countries->get_countries();
				$country   = $countries[ $country ] ?? $country;
			}
		}
		if ( '' !== $country ) {
			$html .= esc_html( $country ) . '<br>';
		}
		if ( '' !== $biz['email'] ) {
			$html .= esc_html( $biz['email'] ) . '<br>';
		}
		if ( '' !== $biz['phone'] ) {
			$html .= esc_html( $biz['phone'] );
		}
		$html .= '</td>';

		$html .= '</tr></table>';

		return $html;
	}

	// ─── Title ─────────────────────────────────────────────────────────────

	private static function render_title( Document $doc ): string {
		return '<div style="margin:10pt 0 18pt;">'
			. '<span style="font-size:18pt;font-weight:bold;color:#111111;">'
			. esc_html( $doc->get_title_label() )
			. '</span>'
			. '</div>';
	}

	// ─── Addresses + Document Data ─────────────────────────────────────────

	private static function render_addresses( Document $doc, array $bs = [] ): string {
		$billing  = $doc->get_billing_data();
		$shipping = $doc->get_shipping_data();
		$data     = $doc->get_document_data();
		$s        = $doc->settings;

		// Block settings (from the builder) take precedence over module settings.
		$opt = static fn( string $block_key, string $module_key ): bool =>
			array_key_exists( $block_key, $bs ) ? ! empty( $bs[ $block_key ] ) : ! empty( $s[ $module_key ] );

		$show_ship  = $opt( 'show_shipping', 'show_shipping_address' );
		$show_phone = $opt( 'show_phone', 'show_phone' );
		$show_email = $opt( 'show_email', 'show_email' );
		$show_vat   = array_key_exists( 'show_vat_id', $bs ) ? ! empty( $bs['show_vat_id'] ) : true;

		$html = '<table style="width:100%;margin-bottom:20pt;"><tr>';

		// Billing.
		$html .= '<td style="width:45%;vertical-align:top;">';
		$html .= '<div style="font-size:9pt;line-height:1.6;color:#333333;">';
		$html .= $doc->format_address( $billing );
		if ( $show_phone && '' !== $billing['phone'] ) {
			$html .= '<br>' . esc_html( $billing['phone'] );
		}
		if ( $show_email && '' !== $billing['email'] ) {
			$html .= '<br>' . esc_html( $billing['email'] );
		}
		if ( $show_vat && '' !== $billing['vat_id'] ) {
			$html .= '<br>VAT: ' . esc_html( $billing['vat_id'] );
		}
		$html .= '</div>';

		// Only when there is a shipping address worth printing. This used to
		// depend on the setting alone, so an order with no separate shipping
		// details — 283 of the 300 most recent on the test site — printed a
		// "Ship to" heading with nothing underneath it. Document already knew
		// how to answer this; has_different_shipping() existed with no callers.
		if ( $show_ship && $doc->has_different_shipping() ) {
			$html .= '<div style="margin-top:10pt;font-size:9pt;line-height:1.6;color:#333333;">';
			$html .= '<div style="font-size:7.5pt;text-transform:uppercase;color:#888888;letter-spacing:0.5pt;margin-bottom:2pt;font-weight:600;">' . esc_html__( 'Ship to', 'kramar' ) . '</div>';
			$html .= $doc->format_address( $shipping );
			$html .= '</div>';
		}
		$html .= '</td>';

		// Document data.
		$html .= '<td style="width:55%;vertical-align:top;">';
		$html .= '<table style="width:100%;font-size:9pt;" cellpadding="0" cellspacing="0">';

		// Labels adapt to the document type — a credit note is not an "Invoice",
		// and a packing slip carries no invoice number/date.
		$type         = $doc->get_type();
		$number_label = match ( $type ) {
			'credit-note' => __( 'Credit Note Number:', 'kramar' ),
			default       => __( 'Invoice Number:', 'kramar' ),
		};
		$date_label   = match ( $type ) {
			'credit-note'  => __( 'Credit Note Date:', 'kramar' ),
			'packing-slip' => __( 'Date:', 'kramar' ),
			default        => __( 'Invoice Date:', 'kramar' ),
		};

		$rows = [];
		if ( '' !== $data['number'] ) {
			$rows[] = [ $number_label, $data['number'] ];
		}
		if ( '' !== $data['order_number'] ) {
			$rows[] = [ __( 'Order Number:', 'kramar' ), $data['order_number'] ];
		}
		if ( '' !== ( $data['date'] ?? '' ) ) {
			$rows[] = [ $date_label, $data['date'] ];
		} elseif ( '' !== $data['order_date'] ) {
			$rows[] = [ __( 'Order Date:', 'kramar' ), $data['order_date'] ];
		}
		// Date of supply — required by EU VAT Directive Art. 226(7).
		if ( '' !== ( $data['delivery_date'] ?? '' ) ) {
			$rows[] = [ __( 'Delivery Date:', 'kramar' ), $data['delivery_date'] ];
		}
		if ( '' !== $data['payment_method'] ) {
			$rows[] = [ __( 'Payment Method:', 'kramar' ), $data['payment_method'] ];
		}
		if ( isset( $data['original_invoice'] ) && '' !== $data['original_invoice'] ) {
			$rows[] = [ __( 'Ref. Invoice:', 'kramar' ), $data['original_invoice'] ];
		}

		foreach ( $rows as $row ) {
			$html .= '<tr>';
			$html .= '<td style="padding:1pt 10pt 1pt 0;color:#555555;">' . esc_html( $row[0] ) . '</td>';
			$html .= '<td style="padding:1pt 0;color:#111111;">' . esc_html( $row[1] ) . '</td>';
			$html .= '</tr>';
		}

		$html .= '</table></td>';
		$html .= '</tr></table>';

		return $html;
	}

	// ─── Line Items ────────────────────────────────────────────────────────

	private static function render_line_items( Document $doc, array $block_settings = [] ): string {
		$show_sku    = ! empty( $doc->settings['show_sku'] ) || ! empty( $block_settings['show_sku'] );
		$show_vat    = ! empty( $block_settings['show_vat'] );
		$show_image  = ! empty( $doc->settings['show_product_image'] );
		$show_prices = $doc->shows_prices();
		$items       = ( $doc instanceof Credit_Note ) ? $doc->get_refund_items() : $doc->get_line_items();

		$html = '<table style="width:100%;border-collapse:collapse;margin-bottom:6pt;">';

		// Header — dark background.
		$html .= '<thead><tr style="background:#1a1a1a;">';
		$th = 'padding:5pt 8pt;font-size:8pt;font-weight:bold;color:#ffffff;';
		$html .= '<th style="' . $th . 'text-align:left;">' . esc_html__( 'Product', 'kramar' ) . '</th>';
		$html .= '<th style="' . $th . 'text-align:left;width:40pt;">' . esc_html__( 'Quantity', 'kramar' ) . '</th>';

		if ( $show_prices ) {
			// Unit price excl. VAT — required by EU VAT Directive Art. 226(8).
			$html .= '<th style="' . $th . 'text-align:right;width:60pt;">' . esc_html__( 'Unit Price', 'kramar' ) . '</th>';
			if ( $show_vat ) {
				$html .= '<th style="' . $th . 'text-align:right;width:40pt;">' . esc_html__( 'VAT', 'kramar' ) . '</th>';
			}
			$html .= '<th style="' . $th . 'text-align:right;width:60pt;">' . esc_html__( 'Amount', 'kramar' ) . '</th>';
		}

		$html .= '</tr></thead><tbody>';

		foreach ( $items as $item ) {
			$html .= '<tr>';
			$html .= '<td style="padding:5pt 8pt;font-size:9pt;border-bottom:0.5pt solid #dddddd;color:#333333;">';
			if ( $show_image && ! empty( $item['image_url'] ) ) {
				$img_uri = self::image_to_data_uri( (string) $item['image_url'] );
				if ( '' !== $img_uri ) {
					$html .= '<img src="' . esc_attr( $img_uri ) . '" style="width:28pt;height:28pt;vertical-align:middle;margin-right:6pt;" />';
				}
			}
			$html .= esc_html( $item['name'] );
			if ( $show_sku && '' !== $item['sku'] ) {
				$html .= ' <span style="color:#999999;font-size:7.5pt;">' . esc_html( $item['sku'] ) . '</span>';
			}
			if ( '' !== $item['meta'] ) {
				$html .= '<br><span style="font-size:7.5pt;color:#777777;">' . esc_html( $item['meta'] ) . '</span>';
			}
			$html .= '</td>';

			$html .= '<td style="padding:5pt 8pt;font-size:9pt;border-bottom:0.5pt solid #dddddd;color:#333333;">' . esc_html( (string) $item['quantity'] ) . '</td>';

			if ( $show_prices ) {
				$html .= '<td style="padding:5pt 8pt;text-align:right;font-size:9pt;border-bottom:0.5pt solid #dddddd;color:#333333;">' . wp_strip_all_tags( wc_price( $item['unit_price'] ) ) . '</td>';
				if ( $show_vat ) {
					$html .= '<td style="padding:5pt 8pt;text-align:right;font-size:9pt;border-bottom:0.5pt solid #dddddd;color:#777777;">' . esc_html( number_format( $item['tax_rate'], 0 ) . '%' ) . '</td>';
				}
				$html .= '<td style="padding:5pt 8pt;text-align:right;font-size:9pt;border-bottom:0.5pt solid #dddddd;color:#333333;">' . wp_strip_all_tags( wc_price( $item['total'] ) ) . '</td>';
			}

			$html .= '</tr>';
		}

		$html .= '</tbody></table>';

		return $html;
	}

	// ─── Totals ────────────────────────────────────────────────────────────

	private static function render_totals( Document $doc ): string {
		if ( ! $doc->shows_prices() ) {
			return '';
		}

		$is_credit = $doc instanceof Credit_Note;
		$totals    = $is_credit ? $doc->get_refund_totals() : $doc->get_grand_totals();
		// Only suppress the VAT breakdown when the order genuinely carries no
		// VAT. A reverse-charge flag on an order that WooCommerce still taxed
		// would otherwise print the notice, hide the VAT rows, and leave the
		// gap between subtotal and total unexplained.
		$reverse_charge = $doc->is_reverse_charge() && $doc->has_zero_tax();

		// Credit_Note::get_totals_by_vat_rate() already breaks down the refunded
		// VAT rather than the parent order's, summing the refund's own lines the
		// same way an invoice does. This used to be a separate local routine that
		// reverse-calculated the base from the tax, and drifted from the subtotal
		// printed above it.
		$vat_rates = $reverse_charge ? [] : $doc->get_totals_by_vat_rate();

		$html = '<table style="width:100%;margin-bottom:14pt;"><tr>';

		// Left: reverse charge or empty.
		$html .= '<td style="width:50%;vertical-align:top;">';
		if ( $reverse_charge ) {
			$html .= '<div style="font-size:8pt;padding:6pt 8pt;background:#fff8e1;border:0.5pt solid #ffcc02;border-radius:2pt;margin-top:4pt;">';
			$html .= '<strong>' . esc_html__( 'VAT reverse-charged — btw verlegd', 'kramar' ) . '</strong><br>';
			$html .= '<span style="font-size:7.5pt;">' . esc_html__( 'Reverse charge — VAT to be accounted for by the recipient (Directive 2006/112/EC, art. 138/196).', 'kramar' ) . '</span>';
			$html .= '</div>';
		}
		$html .= '</td>';

		// Right: totals.
		$html .= '<td style="width:50%;vertical-align:top;">';
		$html .= '<table style="width:100%;font-size:9pt;border-collapse:collapse;">';

		$td_l = 'padding:3pt 8pt 3pt 0;color:#555555;';
		$td_r = 'padding:3pt 0 3pt 8pt;text-align:right;color:#333333;';

		// Subtotal.
		$html .= '<tr><td style="' . $td_l . 'border-top:1pt solid #cccccc;"><strong>' . esc_html__( 'Subtotal', 'kramar' ) . '</strong></td>';
		$html .= '<td style="' . $td_r . 'border-top:1pt solid #cccccc;">' . wp_strip_all_tags( wc_price( $totals['subtotal'] ) ) . '</td></tr>';

		// VAT per rate, with the taxable base — required by Art. 226(8)–(10).
		foreach ( $vat_rates as $rate ) {
			$pct   = number_format( $rate['rate'], 0 );
			$base  = wp_strip_all_tags( wc_price( $rate['subtotal'] ) );
			$label = sprintf(
				/* translators: 1: VAT rate percentage, 2: taxable base amount */
				__( 'VAT %1$s%% (over %2$s)', 'kramar' ),
				$pct,
				$base
			);
			$html .= '<tr><td style="' . $td_l . '">' . esc_html( $label ) . '</td>';
			$html .= '<td style="' . $td_r . '">' . wp_strip_all_tags( wc_price( $rate['tax'] ) ) . '</td></tr>';
		}

		if ( abs( $totals['shipping'] ) > 0 ) {
			$html .= '<tr><td style="' . $td_l . '">' . esc_html__( 'Shipping', 'kramar' ) . '</td>';
			$html .= '<td style="' . $td_r . '">' . wp_strip_all_tags( wc_price( $totals['shipping'] ) ) . '</td></tr>';
		}

		// Fees, one labelled row each. Their net is already inside the VAT base
		// above; without these rows that base sat above the subtotal with nothing
		// on the document accounting for the difference.
		foreach ( $doc->get_fee_lines() as $fee ) {
			$html .= '<tr><td style="' . $td_l . '">' . esc_html( $fee['name'] ) . '</td>';
			$html .= '<td style="' . $td_r . '">' . wp_strip_all_tags( wc_price( $fee['total'] ) ) . '</td></tr>';
		}

		if ( $totals['discount'] > 0 ) {
			$html .= '<tr><td style="' . $td_l . '">' . esc_html__( 'Discount', 'kramar' ) . '</td>';
			$html .= '<td style="' . $td_r . '">-' . wp_strip_all_tags( wc_price( $totals['discount'] ) ) . '</td></tr>';
		}

		// Total.
		$html .= '<tr><td style="padding:4pt 8pt 4pt 0;border-top:1pt solid #cccccc;"><strong>' . esc_html__( 'Total', 'kramar' ) . '</strong></td>';
		$html .= '<td style="padding:4pt 0 4pt 8pt;text-align:right;border-top:1pt solid #cccccc;"><strong>' . wp_strip_all_tags( wc_price( $totals['total'] ) ) . '</strong></td></tr>';

		$html .= '</table></td></tr></table>';

		return $html;
	}


	// ─── Notes ─────────────────────────────────────────────────────────────

	private static function render_notes( Document $doc ): string {
		$note = $doc->get_order()->get_customer_note();
		if ( empty( $doc->settings['show_customer_note'] ) || '' === $note ) {
			return '';
		}

		return '<div style="margin:10pt 0;font-size:8pt;line-height:1.5;color:#555555;">'
			. '<strong style="color:#333333;">' . esc_html__( 'Notes', 'kramar' ) . ':</strong> '
			. esc_html( $note )
			. '</div>';
	}

	// ─── Text ──────────────────────────────────────────────────────────────

	private static function render_text( array $settings ): string {
		$content = $settings['content'] ?? '';
		if ( '' === $content ) {
			return '';
		}
		return '<div style="font-size:8.5pt;line-height:1.5;color:#333333;margin:8pt 0;">' . esc_html( $content ) . '</div>';
	}

	// ─── Footer ────────────────────────────────────────────────────────────

	private static function footer_inner( Document $doc ): string {
		$biz   = $doc->get_business_details();
		$parts = [];

		if ( '' !== $biz['company_name'] ) {
			$company = esc_html( $biz['company_name'] );
			// Statutory name — show only when it differs from the trade name (Hrw Art. 27).
			$statutory = trim( (string) ( $biz['statutory_name'] ?? '' ) );
			if ( '' !== $statutory && 0 !== strcasecmp( $statutory, (string) $biz['company_name'] ) ) {
				$company .= ' (' . esc_html( $statutory ) . ')';
			}
			$parts[] = $company;
		}
		// Statutory seat — required for BV/NV (Hrw Art. 27).
		$seat = trim( (string) ( $biz['statutory_seat'] ?? '' ) );
		if ( '' !== $seat ) {
			$parts[] = esc_html__( 'Reg. office', 'kramar' ) . ': ' . esc_html( $seat );
		}
		if ( '' !== $biz['kvk_number'] ) {
			$parts[] = 'KVK: ' . esc_html( $biz['kvk_number'] );
		}
		if ( '' !== $biz['vat_number'] ) {
			$parts[] = 'BTW: ' . esc_html( $biz['vat_number'] );
		}
		if ( '' !== $biz['iban'] ) {
			$parts[] = 'IBAN: ' . esc_html( $biz['iban'] );
		}

		$line2 = [];
		if ( '' !== $biz['company_address'] ) {
			$line2[] = esc_html( str_replace( "\n", ', ', trim( $biz['company_address'] ) ) );
		}
		if ( '' !== $biz['email'] ) {
			$line2[] = esc_html( $biz['email'] );
		}
		if ( '' !== $biz['phone'] ) {
			$line2[] = esc_html( $biz['phone'] );
		}

		$custom = $doc->settings['footer_text'] ?? '';

		if ( empty( $parts ) && empty( $line2 ) && '' === $custom ) {
			return '';
		}

		$html = '';
		if ( '' !== $custom ) {
			$html .= esc_html( $custom ) . '<br>';
		}
		if ( ! empty( $parts ) ) {
			$html .= implode( ' &middot; ', $parts );
		}
		if ( ! empty( $line2 ) ) {
			$html .= '<br>' . implode( ' &middot; ', $line2 );
		}

		return $html;
	}

	// ─── Base Styles ───────────────────────────────────────────────────────

	private static function get_base_styles( string $paper, string $mode = 'pdf' ): string {
		$font = "'Helvetica', 'Arial', sans-serif";

		// Browser preview: a true A4/Letter page on a grey backdrop.
		if ( 'preview' === $mode ) {
			$width = 'a4' === $paper ? '210mm' : '216mm';
			$minh  = 'a4' === $paper ? '297mm' : '279mm';
			$pad   = 'a4' === $paper ? '15mm 18mm 20mm' : '0.6in 0.75in 0.85in';

			return
				'body {' . "\n" .
				'  margin: 0;' . "\n" .
				'  padding: 16px;' . "\n" .
				'  background: #f0f0f1;' . "\n" .
				'  font-family: ' . $font . ';' . "\n" .
				'  font-size: 9pt;' . "\n" .
				'  color: #333333;' . "\n" .
				'  line-height: 1.4;' . "\n" .
				'}' . "\n" .
				'.kramar-doc {' . "\n" .
				'  width: ' . $width . ';' . "\n" .
				'  min-height: ' . $minh . ';' . "\n" .
				'  box-sizing: border-box;' . "\n" .
				'  margin: 0 auto;' . "\n" .
				'  background: #ffffff;' . "\n" .
				'  padding: ' . $pad . ';' . "\n" .
				'  box-shadow: 0 1px 10px rgba(0,0,0,0.15);' . "\n" .
				'}' . "\n" .
				'.kramar-doc__footer {' . "\n" .
				'  margin-top: 28pt;' . "\n" .
				'  padding-top: 5pt;' . "\n" .
				'  border-top: 0.5pt solid #dddddd;' . "\n" .
				'  text-align: center;' . "\n" .
				'  font-size: 6.5pt;' . "\n" .
				'  color: #999999;' . "\n" .
				'  line-height: 1.6;' . "\n" .
				'}' . "\n" .
				'h1, h2, h3 { margin: 0; }' . "\n" .
				'table { border-collapse: collapse; border: 0; }' . "\n" .
				'th, td { vertical-align: top; text-align: left; }' . "\n";
		}

		// PDF output: page margins come solely from @page (no body padding —
		// stacking both produced an oversized top margin that overflowed A4).
		$margin = 'a4' === $paper ? '15mm 18mm 20mm 18mm' : '0.6in 0.75in 0.85in 0.75in';

		return
			'@page {' . "\n" .
			'  margin: ' . $margin . ';' . "\n" .
			'}' . "\n" .
			'body {' . "\n" .
			'  font-family: ' . $font . ';' . "\n" .
			'  font-size: 9pt;' . "\n" .
			'  color: #333333;' . "\n" .
			'  margin: 0;' . "\n" .
			'  padding: 0;' . "\n" .
			'  line-height: 1.4;' . "\n" .
			'}' . "\n" .
			'h1, h2, h3 { margin: 0; }' . "\n" .
			'table { border-collapse: collapse; border: 0; }' . "\n" .
			'th, td { vertical-align: top; text-align: left; }' . "\n" .
			'#footer {' . "\n" .
			'  position: fixed;' . "\n" .
			'  bottom: -14mm;' . "\n" .
			'  left: 0;' . "\n" .
			'  right: 0;' . "\n" .
			'  height: 14mm;' . "\n" .
			'  text-align: center;' . "\n" .
			'  font-size: 6.5pt;' . "\n" .
			'  color: #999999;' . "\n" .
			'  border-top: 0.5pt solid #dddddd;' . "\n" .
			'  padding-top: 4pt;' . "\n" .
			'  line-height: 1.6;' . "\n" .
			'}' . "\n";
	}
}
