<?php
/**
 * Price Badge block — SSR renderer.
 *
 * Gutenberg block: crtoday/price-badge
 *
 * Inline badge showing a primary price that expands to reveal
 * converted prices in multiple target currencies.
 *
 * @package CRTodayCurrencyBlocks
 */

declare(strict_types=1);

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

final class CRToday_Price_Badge_Block {

	use CRToday_Color_Helpers;
	use CRToday_Decimal_Helpers;
	use CRToday_Rate_Helpers;

	private const HANDLE_FRONTEND        = 'crtoday-price-badge-frontend';
	private const SHORTCODE              = 'crtoday_price_badge';
	private const AJAX_PREVIEW_ACTION    = 'crtoday_price_badge_shortcode_preview';

	public const DEFAULT_ATTRIBUTES = [
		'sourceId'          => 'FEX',
		'deal'              => 'STANDARD',
		'primaryCurrency'   => 'USD',
		'primaryPrice'      => '100',
		'targetCurrencies'  => ['EUR', 'GBP', 'JPY'],
		'decimals'          => 2,
		'showFlags'         => true,
		'alwaysExpanded'    => false,
		'theme'             => 'light',
		'accentTheme'       => 'default',
		'showTimestamp'      => true,
		'adjustmentPercent'  => 0,
		'showCurrencySymbols' => true,
		'sourceLabel'        => '',
		'shadow'             => true,
		'seamless'           => false,
		'radius'             => 12,
		'accentMode'         => 'none',
		'accentAngle'        => 135,
		'compactMode'        => true,
		'attribution'        => true,
		'fullWidth'          => false,
	];

	/* ================================================================
	 * Registration
	 * ================================================================ */

	public static function register(): void {
		self::register_frontend_assets();

		register_block_type(
			CRTODAY_PLUGIN_DIR . 'blocks/price-badge',
			[
				'render_callback' => [self::class, 'render'],
			]
		);

		add_shortcode(self::SHORTCODE, [self::class, 'render_shortcode']);
		add_action('wp_ajax_' . self::AJAX_PREVIEW_ACTION, [self::class, 'handle_ajax_preview']);
	}

	/* ================================================================
	 * Render
	 * ================================================================ */

	public static function render(array $attributes): string {
		$settings = self::sanitize_attributes($attributes);
		$stateful = self::resolve_snapshot($settings);

		$rates = $stateful['viewModel']['rates'] ?? [];

		/* Seamless mode — strip chrome so widget blends into host */
		if ($settings['seamless']) {
			$settings['theme']      = 'light';
			$settings['shadow']     = false;
			$settings['radius']     = 0;
			$settings['accentMode'] = 'none';
		}

		$root_classes = array_filter([
			'crtoday-widget',
			'crtoday-price-badge',
			'crtoday-theme-' . $settings['theme'],
			$settings['shadow'] ? 'crtoday-has-shadow' : 'crtoday-no-shadow',
			$settings['seamless'] ? 'crtoday-seamless' : '',
			'crtoday-state-' . $stateful['state'],
			$settings['alwaysExpanded'] ? 'crtoday-price-badge--expanded' : '',
			$settings['fullWidth'] ? '' : 'crtoday-fit-width',
			$settings['showFlags'] ? '' : 'crtoday-no-flags',
			$settings['compactMode'] ? 'crtoday-compact' : '',
			'crtoday-accent-' . $settings['accentMode'],
		]);

		$inline_style = self::build_accent_style_vars($settings);

		if ($settings['radius'] > 0) {
			$inline_style .= '--crtoday-card-radius:' . (int) $settings['radius'] . 'px;';
		}

		/* Primary price display */
		$primary_code = $settings['primaryCurrency'];
		$primary_amt  = $settings['primaryPrice'];
		$decimals     = $settings['decimals'];
		$adj_pct      = (float) $settings['adjustmentPercent'];

		$show_symbols = (bool) $settings['showCurrencySymbols'];

		$primary_formatted = self::prefix_symbol(
			self::format_decimal($primary_amt, $decimals),
			$primary_code,
			$show_symbols
		);
		$primary_label     = self::format_currency_label($primary_code, $show_symbols);

		$flag_dim          = $settings['compactMode'] ? 20 : 28;
		$primary_flag_html = '';
		if ($settings['showFlags']) {
			$flag = CRToday_Plugin_Settings::get_currency_flag($primary_code);
			if ($flag !== null) {
				$primary_flag_html = sprintf(
					'<img class="crtoday-price-badge__flag" src="%s" alt="" loading="lazy" decoding="async" width="%d" height="%d" />',
					esc_url(CRTODAY_PLUGIN_URL . 'assets/flags/webp/128/' . $flag . '.webp'),
					$flag_dim,
					$flag_dim
				);
			}
		}

		/* Build converted rows */
		$rows_html = '';
		foreach ($rates as $rate) {
			$to_code = $rate['to'] ?? '';
			$mid_raw = self::normalize_to_string($rate['mid'] ?? null);
			if ($mid_raw === null) {
				continue;
			}

			$converted = self::adjust_value($mid_raw, $primary_amt, $adj_pct, $decimals);
			$converted_fmt = self::prefix_symbol(
				self::format_decimal($converted, $decimals),
				$to_code,
				$show_symbols
			);

			$flag_html = '';
			if ($settings['showFlags']) {
				$flag = CRToday_Plugin_Settings::get_currency_flag($to_code);
				if ($flag !== null) {
					$flag_html = sprintf(
						'<img class="crtoday-price-badge__flag" src="%s" alt="" loading="lazy" decoding="async" width="%d" height="%d" />',
						esc_url(CRTODAY_PLUGIN_URL . 'assets/flags/webp/128/' . $flag . '.webp'),
						$flag_dim,
						$flag_dim
					);
				}
			}

			$to_label = self::format_currency_label($to_code, $show_symbols);

			$rows_html .= sprintf(
				'<div class="crtoday-price-badge__row">'
				. '%s'
				. '<span class="crtoday-price-badge__code">%s</span>'
				. '<span class="crtoday-price-badge__value">%s</span>'
				. '</div>',
				$flag_html,
				esc_html($to_label),
				esc_html($converted_fmt)
			);
		}

		/* Source-centric footer: source label left, disclaimer + timestamp right */
		$source_info = CRToday_Widget_Template::resolve_source_label(
			$settings['sourceId'],
			$settings['deal'],
			$settings['sourceLabel']
		);
		$header_html = '';
		if (! $source_info['is_own_source']) {
			$header_html = CRToday_Widget_Template::build_header(
				'',
				$source_info['label'],
				$source_info['verified']
			);
		}
		$timestamp_str     = (string) ($stateful['viewModel']['timestamp'] ?? '');
		$footer_html       = CRToday_Widget_Template::build_source_footer(
			$source_info['label'],
			$source_info['verified'],
			$settings['showTimestamp'],
			$timestamp_str,
			(bool) $settings['compactMode'],
			$source_info['is_own_source']
		);
		$disclaimer_html   = CRToday_Widget_Template::build_disclaimer_dialog($settings['attribution'], $source_info['name'], $timestamp_str);
		$disclaimer_script = CRToday_Widget_Template::build_disclaimer_script();

		/* Expand section — uses <details>/<summary> for native a11y */
		$expand_open   = $settings['alwaysExpanded'] ? ' open' : '';
		$toggle_class  = $settings['alwaysExpanded'] ? ' crtoday-price-badge__toggle--always' : '';

		$html = sprintf(
			'<div class="%1$s" style="%2$s" data-crtoday-version="1" data-crtoday-state="%3$s">'
			. '<details class="crtoday-price-badge__details"%4$s>'
			. '<summary class="crtoday-price-badge__summary crtoday-card%5$s">'
			. '<span class="crtoday-accent"></span>'
			. '<span class="crtoday-price-badge__primary">'
			. '%6$s'
			. '<span class="crtoday-price-badge__code">%7$s</span>'
			. ' '
			. '<span class="crtoday-price-badge__value">%8$s</span>'
			. '</span>'
			. '<span class="crtoday-price-badge__chevron" aria-hidden="true"></span>'
			. '</summary>'
			. '<div class="crtoday-price-badge__panel">'
			. '<div class="crtoday-card">'
			. '<div class="crtoday-accent"></div>'
			. '%13$s'
			. '<div class="crtoday-price-badge__rows">'
			. '%9$s'
			. '</div>'
			. '%10$s'
			. '%11$s'
			. '</div>'
			. '</div>'
			. '</details>'
			. '</div>'
			. '%12$s',
			esc_attr(implode(' ', $root_classes)),
			esc_attr($inline_style),
			esc_attr($stateful['state']),
			$expand_open,
			esc_attr($toggle_class),
			$primary_flag_html,
			esc_html($primary_label),
			esc_html($primary_formatted),
			$rows_html,
			$footer_html,
			$disclaimer_html,
			$disclaimer_script,
			$header_html
		);

		return $html;
	}

	/* ================================================================
	 * Shortcode handler
	 * ================================================================ */

	/**
	 * [crtoday_price_badge primary="USD" targets="EUR,GBP,JPY" source="FEX:STANDARD" …]
	 *
	 * @param array<string,string>|string $atts
	 */
	public static function render_shortcode($atts): string {
		$atts = shortcode_atts(
			array_merge(self::DEFAULT_ATTRIBUTES, ['source' => '', 'targets' => '', 'primary' => '']),
			is_array($atts) ? $atts : [],
			self::SHORTCODE
		);

		/* Shortcode aliases */
		if (! empty($atts['primary'])) {
			$atts['primaryCurrency'] = $atts['primary'];
		}
		unset($atts['primary']);

		if (! empty($atts['targets']) && is_string($atts['targets'])) {
			$atts['targetCurrencies'] = array_filter(array_map('trim', explode(',', $atts['targets'])));
		}
		unset($atts['targets']);

		$source_param = sanitize_text_field((string) ($atts['source'] ?? ''));
		if ($source_param !== '') {
			$parsed = CRToday_Source_Selection::parse_compound_source($source_param);
			$atts['sourceId'] = $parsed['sourceId'];
			$atts['deal']     = $parsed['deal'];
		}
		unset($atts['source']);

		$alias_map = [
			'primary_currency'    => 'primaryCurrency',
			'primary_price'       => 'primaryPrice',
			'target_currencies'   => 'targetCurrencies',
			'adjustment_percent'  => 'adjustmentPercent',
			'adjustment'          => 'adjustmentPercent',
			'show_flags'          => 'showFlags',
			'always_expanded'     => 'alwaysExpanded',
			'full_width'          => 'fullWidth',
			'show_timestamp'      => 'showTimestamp',
			'accent_theme'        => 'accentTheme',
			'show_currency_symbols' => 'showCurrencySymbols',
			'source_label'        => 'sourceLabel',
			'accent_mode'         => 'accentMode',
			'accent_angle'        => 'accentAngle',
		];
		foreach ($alias_map as $snake => $camel) {
			if (isset($atts[$snake])) {
				$atts[$camel] = $atts[$snake];
				unset($atts[$snake]);
			}
		}

		/* Convert comma-separated targetCurrencies string */
		if (isset($atts['targetCurrencies']) && is_string($atts['targetCurrencies'])) {
			$atts['targetCurrencies'] = array_filter(array_map('trim', explode(',', $atts['targetCurrencies'])));
		}

		foreach (['decimals', 'radius', 'accentAngle'] as $num_key) {
			$atts[$num_key] = (int) $atts[$num_key];
		}
		if (isset($atts['adjustmentPercent'])) {
			$atts['adjustmentPercent'] = (float) $atts['adjustmentPercent'];
		}
		foreach (['showFlags', 'alwaysExpanded', 'showTimestamp', 'showCurrencySymbols', 'shadow', 'seamless', 'compactMode', 'attribution', 'fullWidth'] as $bool_key) {
			if (isset($atts[$bool_key])) {
				$atts[$bool_key] = filter_var($atts[$bool_key], FILTER_VALIDATE_BOOLEAN);
			}
		}

		if (wp_style_is('crtoday-block-style', 'registered')) {
			wp_enqueue_style('crtoday-block-style');
		}

		return self::render($atts);
	}

	/* ================================================================
	 * AJAX preview handler (admin shortcode generator)
	 * ================================================================ */

	public static function handle_ajax_preview(): void {
		if (! current_user_can('manage_options')) {
			wp_send_json_error('Unauthorized', 403);
		}

		check_ajax_referer(self::AJAX_PREVIEW_ACTION, 'nonce');

		$raw = isset($_POST['attributes']) && is_array($_POST['attributes'])
			? map_deep(wp_unslash($_POST['attributes']), 'sanitize_text_field')
			: [];
		if (! is_array($raw)) {
			wp_send_json_error('Invalid attributes', 400);
		}

		if (isset($raw['source']) && is_string($raw['source']) && $raw['source'] !== '') {
			$parsed = CRToday_Source_Selection::parse_compound_source((string) $raw['source']);
			$raw['sourceId'] = $parsed['sourceId'];
			$raw['deal']     = $parsed['deal'];
			unset($raw['source']);
		}

		/* Convert comma-separated targetCurrencies */
		if (isset($raw['targetCurrencies']) && is_string($raw['targetCurrencies'])) {
			$raw['targetCurrencies'] = array_filter(array_map('trim', explode(',', $raw['targetCurrencies'])));
		}

		foreach (['decimals', 'radius', 'accentAngle'] as $k) {
			if (isset($raw[$k])) {
				$raw[$k] = (int) $raw[$k];
			}
		}
		if (isset($raw['adjustmentPercent'])) {
			$raw['adjustmentPercent'] = (float) $raw['adjustmentPercent'];
		}
		foreach (['showFlags', 'alwaysExpanded', 'showTimestamp', 'showCurrencySymbols', 'shadow', 'seamless', 'compactMode', 'attribution', 'fullWidth'] as $k) {
			if (isset($raw[$k])) {
				$raw[$k] = filter_var($raw[$k], FILTER_VALIDATE_BOOLEAN);
			}
		}

		$merged = wp_parse_args($raw, self::DEFAULT_ATTRIBUTES);
		$html   = self::render($merged);

		wp_send_json_success(['html' => $html]);
	}

	/* ================================================================
	 * Sanitise attributes
	 * ================================================================ */

	private static function sanitize_attributes(array $attributes): array {
		$a = wp_parse_args($attributes, self::DEFAULT_ATTRIBUTES);

		$allowed_themes = ['auto', 'light', 'dark'];
		$theme = in_array($a['theme'], $allowed_themes, true) ? $a['theme'] : 'light';

		$decimals = max(0, min(8, (int) $a['decimals']));

		$primary = strtoupper((string) $a['primaryCurrency']);
		if (preg_match('/^[A-Z]{3}$/', $primary) !== 1) {
			$primary = 'USD';
		}

		/* Validate target currencies — array of 3-letter codes, deduplicated, no self-pair */
		$raw_targets = is_array($a['targetCurrencies']) ? $a['targetCurrencies'] : ['EUR', 'GBP', 'JPY'];
		$targets = [];
		$seen    = [];
		foreach ($raw_targets as $code) {
			$code = strtoupper((string) $code);
			if (preg_match('/^[A-Z]{3}$/', $code) !== 1) {
				continue;
			}
			if ($code === $primary || isset($seen[$code])) {
				continue;
			}
			$seen[$code] = true;
			$targets[]   = $code;
		}
		if ($targets === []) {
			$targets = $primary === 'EUR' ? ['USD', 'GBP', 'JPY'] : ['EUR', 'GBP', 'JPY'];
		}

		$source_id = strtoupper((string) $a['sourceId']);
		$deal      = strtoupper((string) $a['deal']);
		$resolved  = CRToday_Plugin_Settings::resolve_source($source_id, $deal);
		if ($resolved === null) {
			$resolved  = CRToday_Plugin_Settings::resolve_source('FEX', 'STANDARD');
			$source_id = 'FEX';
			$deal      = 'STANDARD';
		}

		/* Build pairs array: primary → each target */
		$pairs = [];
		foreach ($targets as $to) {
			$pairs[] = ['from' => $primary, 'to' => $to];
		}

		$allowed_accent_modes = ['color-border', 'stripe', 'none'];
		$accent_mode = in_array($a['accentMode'], $allowed_accent_modes, true)
			? $a['accentMode']
			: 'none';

		$accent_presets = CRToday_Plugin_Settings::get_accent_theme_presets();
		$accent_theme = strtolower((string) $a['accentTheme']);
		if (! isset($accent_presets[$accent_theme])) {
			$accent_theme = 'default';
		}

		$accent_angle = max(0, min(360, (int) $a['accentAngle']));

		return [
			'sourceId'          => $source_id,
			'deal'              => $deal,
			'sourceBase'        => strtoupper((string) ($resolved['base'] ?? 'USD')),
			'sourceType'        => $resolved['type'] ?? 'builtin',
			'primaryCurrency'   => $primary,
			'primaryPrice'      => self::sanitize_amount((string) $a['primaryPrice']),
			'targetCurrencies'  => $targets,
			'pairs'             => $pairs,
			'decimals'          => $decimals,
			'showFlags'         => (bool) $a['showFlags'],
			'alwaysExpanded'    => (bool) $a['alwaysExpanded'],
			'theme'             => $theme,
			'accentTheme'       => $accent_theme,
			'accentMode'        => $accent_mode,
			'accentAngle'       => $accent_angle,
			'shadow'            => (bool) $a['shadow'],
			'seamless'          => (bool) $a['seamless'],
			'radius'            => max(0, min(32, (int) $a['radius'])),
			'showTimestamp'      => (bool) $a['showTimestamp'],
			'adjustmentPercent'  => self::sanitize_adjustment_percent($a['adjustmentPercent']),
			'showCurrencySymbols' => (bool) $a['showCurrencySymbols'],
			'sourceLabel'        => mb_substr(sanitize_text_field((string) $a['sourceLabel']), 0, 20),
			'compactMode'        => (bool) $a['compactMode'],
			'attribution'        => (bool) $a['attribution'],
			'fullWidth'          => (bool) $a['fullWidth'],
		];
	}

	/* ================================================================
	 * Spread helpers (reused from rate-card pattern)
	 * ================================================================ */

	/**
	 * @param mixed $entry
	 * @return array{mid: ?float, bid: float, ask: float}
	 */
	private static function parse_spread_entry($entry): array {
		$mid = null;
		$bid = 0.0;
		$ask = 0.0;

		if (is_array($entry)) {
			$mid = isset($entry['mid']) && is_numeric($entry['mid']) ? (float) $entry['mid'] : null;
			$bid = isset($entry['bid']) && is_numeric($entry['bid']) ? (float) $entry['bid'] : 0.0;
			$ask = isset($entry['ask']) && is_numeric($entry['ask']) ? (float) $entry['ask'] : 0.0;
		} elseif (is_numeric($entry)) {
			$mid = (float) $entry;
		}

		if (($mid === null || $mid <= 0) && $bid > 0 && $ask > 0) {
			$mid = ($bid + $ask) / 2;
		}

		return ['mid' => $mid, 'bid' => $bid, 'ask' => $ask];
	}

	private static function build_spread_lookups(array $raw_rates, string $base): array {
		$mid_map = [$base => 1.0];
		$bid_map = [$base => 1.0];
		$ask_map = [$base => 1.0];

		foreach ($raw_rates as $code => $entry) {
			$code   = strtoupper((string) $code);
			$spread = self::parse_spread_entry($entry);

			if ($spread['mid'] !== null && $spread['mid'] > 0) {
				$mid_map[$code] = $spread['mid'];
			}
			if ($spread['bid'] > 0) {
				$bid_map[$code] = $spread['bid'];
			}
			if ($spread['ask'] > 0) {
				$ask_map[$code] = $spread['ask'];
			}
		}

		return [$mid_map, $bid_map, $ask_map];
	}

	private static function compute_cross_rates(array $pairs, array $mid_vs_base, array $bid_vs_base, array $ask_vs_base): array {
		$rates = [];

		foreach ($pairs as $pair) {
			$from = $pair['from'];
			$to   = $pair['to'];

			$from_mid = $mid_vs_base[$from] ?? null;
			$to_mid   = $mid_vs_base[$to] ?? null;

			if ($from_mid === null || $to_mid === null || $from_mid <= 0) {
				continue;
			}

			$cross_mid = $to_mid / $from_mid;
			if ($cross_mid <= 0) {
				continue;
			}

			$rates[] = [
				'from' => $from,
				'to'   => $to,
				'mid'  => $cross_mid,
			];
		}

		return ['rates' => $rates];
	}

	/* ================================================================
	 * Data pipeline
	 * ================================================================ */

	private static function resolve_snapshot(array $settings): array {
		if ($settings['sourceType'] === 'local') {
			$local_vm = self::resolve_local_view_model($settings);
			if ($local_vm !== null) {
				return [
					'state'     => self::view_model_state($local_vm),
					'viewModel' => $local_vm,
				];
			}
			return [
				'state'     => 'error',
				'viewModel' => self::empty_view_model($settings),
			];
		}

		/* Remote (built-in) source: shared per-source cache */
		$source = CRToday_Source_Cache::resolve(
			(string) $settings['sourceId'],
			(string) $settings['deal']
		);

		if ($source['state'] === 'error' || $source['payload'] === null) {
			return [
				'state'     => 'error',
				'viewModel' => self::empty_view_model($settings),
			];
		}

		$view_model = self::build_view_model_from_payload(
			$settings,
			$source['payload'],
			$source['fetch_ts'],
			$source['api_ts']
		);

		if ($view_model === null) {
			return [
				'state'     => 'error',
				'viewModel' => self::empty_view_model($settings),
			];
		}

		$state = $source['state'] === 'stale' ? 'stale' : self::view_model_state($view_model);
		return [
			'state'     => $state,
			'viewModel' => $view_model,
		];
	}

	private static function build_view_model_from_payload(array $settings, array $payload, int $fetch_ts_unix, int $api_ts_unix): ?array {
		$api_ts = $api_ts_unix > 0
			? gmdate('c', $api_ts_unix)
			: gmdate('c', $fetch_ts_unix);

		$api_base = strtoupper((string) ($payload['base'] ?? 'USD'));

		[$mid_vs_base, $bid_vs_base, $ask_vs_base] = self::build_spread_lookups($payload['rates'], $api_base);
		$cross = self::compute_cross_rates($settings['pairs'], $mid_vs_base, $bid_vs_base, $ask_vs_base);

		return [
			'timestamp' => $api_ts,
			'source'    => $settings['sourceId'],
			'rates'     => $cross['rates'],
		];
	}

	private static function resolve_local_view_model(array $settings): ?array {
		$resolved = CRToday_Plugin_Settings::resolve_source(
			(string) $settings['sourceId'],
			'STANDARD',
		);
		if ($resolved === null || $resolved['type'] !== 'local') {
			return null;
		}

		$local_rates = $resolved['rates'] ?? [];
		$source_base = strtoupper((string) ($resolved['base'] ?? 'USD'));
		$updated_at  = (int) ($resolved['updated_at'] ?? 0);
		$api_ts      = $updated_at > 0 ? gmdate('c', $updated_at) : gmdate('c');

		[$mid_vs_base, $bid_vs_base, $ask_vs_base] = self::build_spread_lookups($local_rates, $source_base);
		$cross = self::compute_cross_rates($settings['pairs'], $mid_vs_base, $bid_vs_base, $ask_vs_base);

		return [
			'timestamp' => $api_ts,
			'source'    => $settings['sourceId'],
			'rates'     => $cross['rates'],
		];
	}

	/* ================================================================
	 * Helpers
	 * ================================================================ */

	private static function empty_view_model(array $settings): array {
		return [
			'timestamp' => gmdate('c'),
			'source'    => $settings['sourceId'],
			'rates'     => [],
		];
	}

	private static function format_currency_label(string $code, bool $show_symbols): string {
		return $code;
	}

	/**
	 * Prefix a formatted value with the currency symbol when enabled.
	 */
	private static function prefix_symbol(string $formatted, string $code, bool $show_symbols): string {
		if (! $show_symbols) {
			return $formatted;
		}
		return CRToday_Plugin_Settings::format_with_symbol($formatted, $code);
	}

	/* ================================================================
	 * Frontend asset registration
	 * ================================================================ */

	private static function register_frontend_assets(): void {
		$asset_path = CRTODAY_PLUGIN_DIR . 'build/price-badge-frontend.asset.php';
		$asset      = file_exists($asset_path)
			? require $asset_path
			: [
				'dependencies' => [],
				'version'      => CRTODAY_PLUGIN_VERSION,
			];

		wp_register_script(
			self::HANDLE_FRONTEND,
			CRTODAY_PLUGIN_URL . 'build/price-badge-frontend.js',
			$asset['dependencies'],
			$asset['version'],
			true
		);
	}
}
