<?php
/**
 * Template style resolver.
 *
 * Reads the Template Builder settings (the `template_*` keys saved by
 * ZINV_REST_Settings) and turns them into concrete, sanitized values the
 * PDF templates can drop straight into markup and CSS. One source of truth
 * so clean-sheet, bold-accent and executive-pro all behave identically.
 *
 * Every getter returns a value already safe for its use site: hex colors are
 * validated, numbers clamped to renderable ranges, font names mapped to
 * families mPDF actually bundles. Templates never re-sanitize.
 *
 * @since 1.1.0
 */

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

class ZINV_Template_Style {

	/**
	 * Resolved style values, lazily built once per request.
	 *
	 * @var array<string,mixed>|null
	 */
	private static ?array $resolved = null;

	/**
	 * Template Builder font keys allowed on this install.
	 *
	 * Must stay in sync with PDF_FONTS in TemplateBuilderPage.tsx and the
	 * DejaVuSans*.ttf files kept after .distignore pruning in release builds.
	 *
	 * @var string[]
	 */
	public const PDF_FONT_KEYS = [ 'Inter', 'Helvetica', 'DejaVuSans' ];

	/**
	 * mPDF font-family values for each Template Builder key.
	 *
	 * @var array<string,string>
	 */
	private const FONT_STACKS = [
		'Inter'      => 'inter',
		'Helvetica'  => 'inter',
		'DejaVuSans' => 'dejavusans',
	];

	/**
	 * Build (once) and return the resolved style map.
	 *
	 * @return array<string,mixed>
	 */
	private static function resolve(): array {
		if ( null !== self::$resolved ) {
			return self::$resolved;
		}

		$accent    = self::hex( ZINV_Settings::get( 'template_accent_color', '#000000' ), '#000000' );
		$secondary = self::hex( ZINV_Settings::get( 'template_secondary_color', '#607CEC' ), '#607CEC' );
		$header_bg = self::hex( ZINV_Settings::get( 'template_table_header_bg', '#f2f4f6' ), '#f2f4f6' );

		// Core typography color: a two-option preset, not free hex.
		$text_preset = (string) ZINV_Settings::get( 'template_text_color', 'rich-ink' );
		$text_color  = 'cool-slate' === $text_preset ? '#475569' : '#191c1e';

		self::$resolved = [
			'accent'        => $accent,
			'secondary'     => $secondary,
			'text_color'    => $text_color,
			'header_bg'     => $header_bg,
			'heading_font'  => self::font( ZINV_Settings::get( 'template_font_family', 'Helvetica' ) ),
			'body_font'     => self::font( ZINV_Settings::get( 'template_body_font', 'Helvetica' ) ),
			'heading_size'  => self::clamp_int( ZINV_Settings::get( 'template_heading_size', 36 ), 12, 72 ),
			'body_size'     => self::clamp_int( ZINV_Settings::get( 'template_body_size', 14 ), 8, 24 ),
			'line_height'   => self::clamp_float( ZINV_Settings::get( 'template_line_height', 1.6 ), 1.0, 2.5 ),
			'row_padding'   => self::clamp_int( ZINV_Settings::get( 'template_row_padding', 20 ), 8, 40 ),
			'border_style'  => self::enum( ZINV_Settings::get( 'template_border_style', 'bottom' ), [ 'bottom', 'all', 'horizontal', 'none' ], 'bottom' ),
			'alt_rows'      => self::bool( ZINV_Settings::get( 'template_alt_row_shading', false ) ),
			'desc_align'    => self::enum( ZINV_Settings::get( 'template_desc_align', 'left' ), [ 'left', 'center', 'right' ], 'left' ),
			'amount_align'  => self::enum( ZINV_Settings::get( 'template_amount_align', 'right' ), [ 'left', 'center', 'right' ], 'right' ),
			'header_bold'   => self::bool( ZINV_Settings::get( 'template_header_bold', true ) ),
			'header_italic' => self::bool( ZINV_Settings::get( 'template_header_italic', false ) ),
			'header_caps'   => self::bool( ZINV_Settings::get( 'template_header_caps', true ) ),
			'show_logo'     => self::bool( ZINV_Settings::get( 'template_show_logo', true ) ),
			'logo_place'    => self::enum( ZINV_Settings::get( 'template_logo_placement', 'left' ), [ 'left', 'center', 'right' ], 'left' ),
			'show_footer'   => self::bool( ZINV_Settings::get( 'template_show_footer', true ) ),
			'footer_layout' => self::enum( ZINV_Settings::get( 'template_footer_layout', 'full' ), [ 'full', 'two-column' ], 'full' ),
			'show_sig'      => self::bool( ZINV_Settings::get( 'template_show_signature', false ) ),
			'sig_style'     => self::enum( ZINV_Settings::get( 'template_signature_style', 'typed' ), [ 'typed', 'script', 'image' ], 'typed' ),
		];

		return self::$resolved;
	}

	/**
	 * Get one resolved style value.
	 *
	 * @param string $key     Resolved key (see resolve()).
	 * @param mixed  $default Returned when the key is unknown.
	 * @return mixed
	 */
	public static function get( string $key, $default = null ) {
		$all = self::resolve();
		return array_key_exists( $key, $all ) ? $all[ $key ] : $default;
	}

	/**
	 * Page margin in millimetres, derived from the px slider value.
	 *
	 * The builder slider is 24-80 CSS px. mPDF margins are mm and live in the
	 * constructor (a CSS `@page` rule is broken in mPDF). 1px ≈ 0.2646mm;
	 * result clamped to a printable 6-30mm band.
	 *
	 * @return int Margin in mm.
	 */
	public static function page_margin_mm(): int {
		$px = self::clamp_int( ZINV_Settings::get( 'template_page_margin', 48 ), 24, 80 );
		$mm = (int) round( $px * 0.2646 );
		return max( 6, min( 30, $mm ) );
	}

	/**
	 * CSS declarations for an items-table `th`, honoring the header style
	 * toggles (bold / italic / caps). No leading/trailing semicolon issues —
	 * each declaration ends with one.
	 *
	 * @return string CSS fragment, safe to inline inside a <style> rule.
	 */
	public static function table_header_css(): string {
		$s   = self::resolve();
		$css = 'font-weight:' . ( $s['header_bold'] ? '700' : '400' ) . ';';
		$css .= 'font-style:' . ( $s['header_italic'] ? 'italic' : 'normal' ) . ';';
		$css .= 'text-transform:' . ( $s['header_caps'] ? 'uppercase' : 'none' ) . ';';
		return $css;
	}

	/**
	 * Border CSS for an items-table cell, per the chosen border style.
	 *
	 * @return string CSS fragment for `td`/`th` cells.
	 */
	public static function table_cell_border_css(): string {
		switch ( self::get( 'border_style' ) ) {
			case 'all':
				return 'border:1px solid #e2e8f0;';
			case 'horizontal':
				return 'border-top:1px solid #e2e8f0;border-bottom:1px solid #e2e8f0;';
			case 'none':
				return 'border:none;';
			case 'bottom':
			default:
				return 'border-bottom:1px solid #e2e8f0;';
		}
	}

	// -------------------------------------------------------------------------
	// Sanitizers
	// -------------------------------------------------------------------------

	/**
	 * Whether a stored Template Builder font key is valid on this install.
	 *
	 * @param string $key Stored font key.
	 * @return bool
	 */
	public static function is_valid_pdf_font( string $key ): bool {
		return in_array( $key, self::PDF_FONT_KEYS, true );
	}

	/**
	 * Map a stored font name to an mPDF-safe CSS font stack.
	 *
	 * @param mixed $name Stored font key.
	 * @return string CSS font-family stack.
	 */
	private static function font( $name ): string {
		$name = is_string( $name ) ? $name : '';
		// Legacy settings: Times/Courier required fonts removed from release builds.
		if ( in_array( $name, [ 'Times', 'Courier' ], true ) ) {
			$name = 'Helvetica';
		}
		return self::FONT_STACKS[ $name ] ?? self::FONT_STACKS['Helvetica'];
	}

	/**
	 * Validate a 3- or 6-digit hex color, else return the fallback.
	 *
	 * @param mixed  $value    Candidate color.
	 * @param string $fallback Returned when invalid.
	 * @return string
	 */
	private static function hex( $value, string $fallback ): string {
		$value = is_string( $value ) ? trim( $value ) : '';
		return preg_match( '/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/', $value ) ? $value : $fallback;
	}

	private static function clamp_int( $value, int $min, int $max ): int {
		$n = is_numeric( $value ) ? (int) $value : $min;
		return max( $min, min( $max, $n ) );
	}

	private static function clamp_float( $value, float $min, float $max ): float {
		$n = is_numeric( $value ) ? (float) $value : $min;
		return max( $min, min( $max, $n ) );
	}

	/**
	 * @param mixed    $value   Candidate.
	 * @param string[] $allowed Allowed values.
	 * @param string   $default Fallback.
	 */
	private static function enum( $value, array $allowed, string $default ): string {
		$value = is_string( $value ) ? $value : '';
		return in_array( $value, $allowed, true ) ? $value : $default;
	}

	private static function bool( $value ): bool {
		return filter_var( $value, FILTER_VALIDATE_BOOLEAN );
	}

	/**
	 * Drop the resolved cache. Call after a settings write within the same
	 * request (mirrors ZINV_Settings::flush_cache()).
	 */
	public static function flush_cache(): void {
		self::$resolved = null;
	}
}
