<?php
/**
 * This file is part of the WooCommerce Email Editor package.
 *
 * @package Automattic\WooCommerce\EmailEditor
 */

declare(strict_types = 1);
namespace Automattic\WooCommerce\EmailEditor\Engine;

use WP_Theme_JSON;
use WP_Theme_JSON_Resolver;
use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Styles_Helper;

/**
 * Site Style Sync Controller
 *
 * Manages the live synchronization of site styles to email templates.
 * Converts site theme styles to email-compatible formats while maintaining
 * visual consistency between the site and emails.
 */
class Site_Style_Sync_Controller {
	/**
	 * Current site theme data
	 *
	 * @var WP_Theme_JSON|null
	 */
	private ?WP_Theme_JSON $site_theme = null;

	/**
	 * Base theme data for fallback lookups
	 *
	 * @var array|null
	 */
	private ?array $base_theme_data = null;

	/**
	 * Email-safe fonts
	 *
	 * @var array
	 */
	private $email_safe_fonts = array();

	/**
	 * Constructor
	 */
	public function __construct() {
		add_action( 'init', array( $this, 'initialize' ), 20 );
	}

	/**
	 * Initialize the sync controller
	 *
	 * Hook into theme changes to trigger automatic sync
	 *
	 * @return void
	 */
	public function initialize(): void {
		add_action( 'switch_theme', array( $this, 'invalidate_site_theme_cache' ) );
		add_action( 'customize_save_after', array( $this, 'invalidate_site_theme_cache' ) );
	}

	/**
	 * Sync site styles to email theme format
	 *
	 * @param WP_Theme_JSON|null $base_theme Base theme for fallback values. If null, no fallbacks are used.
	 * @return array Email-compatible theme data.
	 */
	public function sync_site_styles( ?WP_Theme_JSON $base_theme = null ): array {
		// Store base theme data for fallback lookups.
		$this->base_theme_data = $base_theme ? $base_theme->get_data() : null;

		$site_theme = $this->get_site_theme();
		$site_data  = $site_theme->get_data();

		$synced_data = array(
			'version'  => 3,
			'settings' => $this->sync_settings_data( $site_data['settings'] ?? array() ),
			'styles'   => $this->sync_styles_data( $site_data['styles'] ?? array() ),
		);

		/**
		 * Filter the synced site style data before applying to email theme
		 *
		 * @param array $synced_data The converted email-compatible theme data.
		 * @param array $site_data The original site theme data.
		 */
		$synced_data = apply_filters( 'woocommerce_email_editor_synced_site_styles', $synced_data, $site_data );

		return $synced_data;
	}

	/**
	 * Getter for site theme.
	 *
	 * @param WP_Theme_JSON|null $base_theme Base theme for fallback values. If null, no fallbacks are used.
	 * @return ?WP_Theme_JSON Synced site theme.
	 */
	public function get_theme( ?WP_Theme_JSON $base_theme = null ): ?WP_Theme_JSON {
		if ( ! $this->is_sync_enabled() ) {
			return null;
		}

		$synced_data = $this->sync_site_styles( $base_theme );

		if ( empty( $synced_data ) || ! isset( $synced_data['version'] ) ) {
			return null;
		}

		return new WP_Theme_JSON( $synced_data, 'theme' );
	}

	/**
	 * Check if site style sync is enabled
	 *
	 * @return bool
	 */
	public function is_sync_enabled(): bool {
		/**
		 * Filter to enable/disable site style sync functionality
		 *
		 * @param bool $enabled Whether site style sync is enabled.
		 */
		return apply_filters( 'woocommerce_email_editor_site_style_sync_enabled', true );
	}

	/**
	 * Invalidate cached site theme data
	 *
	 * @return void
	 */
	public function invalidate_site_theme_cache(): void {
		if ( ! $this->is_sync_enabled() ) {
			return;
		}
		$this->site_theme = null;
	}

	/**
	 * Get site theme data
	 *
	 * @return WP_Theme_JSON
	 */
	private function get_site_theme(): WP_Theme_JSON {
		if ( null === $this->site_theme ) {
			// Get only the theme and user customizations (e.g. from site editor).
			$this->site_theme = new WP_Theme_JSON();
			$this->site_theme->merge( WP_Theme_JSON_Resolver::get_theme_data() );
			$this->site_theme->merge( WP_Theme_JSON_Resolver::get_user_data() );

			/**
			 * Filter the site theme data used for email style sync.
			 *
			 * This filter allows overriding the site theme used when syncing global styles
			 * for the email editor. Useful for environments where theme data is not directly
			 * accessible (e.g., wp.com Atomic and Jetpack sites).
			 *
			 * @since 2.3.0
			 *
			 * @param WP_Theme_JSON $site_theme The site theme data.
			 */
			$this->site_theme = apply_filters( 'woocommerce_email_editor_site_theme', $this->site_theme );

			if ( isset( $this->site_theme->get_raw_data()['styles'] ) ) {
				$this->site_theme = WP_Theme_JSON::resolve_variables( $this->site_theme );
			}
		}
		return $this->site_theme;
	}

	/**
	 * Sync settings data from site theme to email-compatible format
	 *
	 * @param array $site_settings Site theme settings.
	 * @return array Email-compatible settings.
	 */
	private function sync_settings_data( array $site_settings ): array {
		$email_settings = array();

		// Sync color palette.
		if ( isset( $site_settings['color']['palette'] ) ) {
			$email_settings['color']['palette'] = $site_settings['color']['palette'];
		}

		return $email_settings;
	}

	/**
	 * Sync styles data from site theme to email-compatible format
	 *
	 * @param array $site_styles Site theme styles.
	 * @return array Email-compatible styles.
	 */
	private function sync_styles_data( array $site_styles ): array {
		$email_styles = array();

		// Sync color styles.
		if ( ! empty( $site_styles['color'] ) ) {
			$email_styles['color'] = $this->convert_color_styles( $site_styles['color'] );
		}

		// Sync typography styles.
		if ( ! empty( $site_styles['typography'] ) ) {
			$email_styles['typography'] = $this->convert_typography_styles( $site_styles['typography'] );
		}

		// Sync spacing styles.
		if ( ! empty( $site_styles['spacing'] ) ) {
			$email_styles['spacing'] = $this->convert_spacing_styles( $site_styles['spacing'] );
		}

		// Sync element styles.
		if ( ! empty( $site_styles['elements'] ) ) {
			$email_styles['elements'] = $this->convert_element_styles( $site_styles['elements'] );
		}

		return $email_styles;
	}

	/**
	 * Get email-safe fonts
	 *
	 * @return array Email-safe fonts.
	 */
	public function get_email_safe_fonts(): array {
		if ( empty( $this->email_safe_fonts ) ) {
			/**
			 * Pull email-safe fonts from theme.json (src/Engine/theme.json).
			 *
			 * @var array{settings?: array{typography?: array{fontFamilies?: array<array{name: string, slug: string, fontFamily: string}>}}} $theme_data
			 */
			$theme_data    = (array) json_decode( (string) file_get_contents( __DIR__ . '/theme.json' ), true );
			$font_families = $theme_data['settings']['typography']['fontFamilies'] ?? array();

			if ( ! empty( $font_families ) ) {
				foreach ( $font_families as $font_family ) {
					$this->email_safe_fonts[ strtolower( $font_family['slug'] ) ] = $font_family['fontFamily'];
				}
			}
		}
		return $this->email_safe_fonts;
	}

	/**
	 * Convert site color styles to email format
	 *
	 * @param array $color_styles Site color styles.
	 * @return array Email-compatible color styles.
	 */
	private function convert_color_styles( array $color_styles ): array {
		$email_colors = array();

		$this->resolve_and_assign( $color_styles, 'background', $email_colors );
		$this->resolve_and_assign( $color_styles, 'text', $email_colors );

		return $email_colors;
	}

	/**
	 * Convert site typography styles to email format
	 *
	 * @param array  $typography_styles Site typography styles.
	 * @param string $element Optional element name for context-aware fallbacks.
	 * @return array Email-compatible typography styles.
	 */
	private function convert_typography_styles( array $typography_styles, string $element = '' ): array {
		$email_typography = array();

		// Handle special cases with processors.
		$this->resolve_and_assign( $typography_styles, 'fontFamily', $email_typography, array( $this, 'convert_to_email_safe_font' ) );
		$this->resolve_and_assign(
			$typography_styles,
			'fontSize',
			$email_typography,
			function ( $value ) use ( $element ) {
				// Try element-specific fallback first, then global fallback.
				$fallback = null;
				if ( $element ) {
					$fallback = $this->get_base_theme_value( array( 'styles', 'elements', $element, 'typography', 'fontSize' ) );
				}
				if ( ! $fallback ) {
					$fallback = $this->get_base_theme_value( array( 'styles', 'typography', 'fontSize' ) );
				}
				return $this->convert_to_px_size( $value, $fallback );
			}
		);

		// Handle compatible properties without processing.
		$compatible_props = array( 'fontWeight', 'fontStyle', 'lineHeight', 'letterSpacing', 'textTransform', 'textDecoration' );
		foreach ( $compatible_props as $prop ) {
			$this->resolve_and_assign( $typography_styles, $prop, $email_typography );
		}

		return $email_typography;
	}

	/**
	 * Convert site spacing styles to email format
	 *
	 * @param array $spacing_styles Site spacing styles.
	 * @return array Email-compatible spacing styles.
	 */
	private function convert_spacing_styles( array $spacing_styles ): array {
		$email_spacing = array();

		$this->resolve_and_assign(
			$spacing_styles,
			'padding',
			$email_spacing,
			function ( $value ) {
				return $this->convert_spacing_values( $value, array( 'styles', 'spacing', 'padding' ) );
			}
		);
		$this->resolve_and_assign(
			$spacing_styles,
			'blockGap',
			$email_spacing,
			function ( $value ) {
				$fallback = $this->get_base_theme_value( array( 'styles', 'spacing', 'blockGap' ) );
				return $this->convert_to_px_size( $value, $fallback );
			}
		);

		// Note: We intentionally skip margin as it's not supported in email renderer.

		return $email_spacing;
	}

	/**
	 * Convert site element styles to email format
	 *
	 * @param array $element_styles Site element styles.
	 * @return array Email-compatible element styles.
	 */
	private function convert_element_styles( array $element_styles ): array {
		$email_elements = array();

		// Process supported elements.
		$supported_elements = array( 'heading', 'button', 'link', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' );

		foreach ( $supported_elements as $element ) {
			if ( isset( $element_styles[ $element ] ) ) {
				$email_elements[ $element ] = $this->convert_element_style( $element_styles[ $element ], $element );
			}
		}

		return $email_elements;
	}

	/**
	 * Convert individual element style to email format
	 *
	 * @param array  $element_style Site element style.
	 * @param string $element_name Element name (e.g., 'h1', 'h2', 'button').
	 * @return array Email-compatible element style.
	 */
	private function convert_element_style( array $element_style, string $element_name = '' ): array {
		$email_element = array();

		// Convert typography if present.
		if ( isset( $element_style['typography'] ) ) {
			$email_element['typography'] = $this->convert_typography_styles( $element_style['typography'], $element_name );
		}

		// Convert color if present.
		if ( isset( $element_style['color'] ) ) {
			$email_element['color'] = $this->convert_color_styles( $element_style['color'] );
		}

		// Convert spacing if present.
		if ( isset( $element_style['spacing'] ) ) {
			$email_element['spacing'] = $this->convert_spacing_styles( $element_style['spacing'] );
		}

		return $email_element;
	}

	/**
	 * Resolve and assign a single style property
	 *
	 * @param array         $styles     The source styles array.
	 * @param string        $property   The property key to resolve.
	 * @param array         $target     The target array to assign the value to.
	 * @param callable|null $processor  Optional processor function for the resolved value.
	 * @return bool True if the property was resolved and assigned, false otherwise.
	 */
	private function resolve_and_assign( array $styles, string $property, array &$target, ?callable $processor = null ): bool {
		if ( ! isset( $styles[ $property ] ) ) {
			return false;
		}

		$resolved = $this->resolve_style_value( $styles[ $property ] );
		if ( ! $resolved ) {
			return false;
		}

		$target[ $property ] = $processor ? $processor( $resolved ) : $resolved;
		return true;
	}

	/**
	 * Styles may contain references to other styles.
	 * This function resolves the reference to the actual value.
	 * https://make.wordpress.org/core/2022/10/11/reference-styles-values-in-theme-json/
	 * It is not allowed to reference another reference so we don't need to check recursively.
	 *
	 * @param mixed $style_value Style value that might contain a reference.
	 * @return mixed Resolved style value or null when the reference is not found.
	 */
	private function resolve_style_value( $style_value ) {
		// Check if this is a reference array.
		if ( is_array( $style_value ) && isset( $style_value['ref'] ) ) {
			$ref = $style_value['ref'];
			if ( ! is_string( $ref ) || empty( $ref ) ) {
				return null;
			}
			$path = explode( '.', $ref );

			return _wp_array_get( $this->get_site_theme()->get_data(), $path, null );
		}

		return $style_value;
	}

	/**
	 * Convert font family to email-safe alternative
	 *
	 * @param string $font_family Original font family.
	 * @return string Email-safe font family.
	 */
	private function convert_to_email_safe_font( string $font_family ): string {
		// Get email-safe fonts.
		$email_safe_fonts = $this->get_email_safe_fonts();

		// Map common web fonts to email-safe alternatives.
		$font_map = array(
			'helvetica' => $email_safe_fonts['arial'], // Arial fallback.
			'times'     => $email_safe_fonts['georgia'], // Georgia fallback.
			'courier'   => $email_safe_fonts['courier-new'], // Courier New.
			'trebuchet' => $email_safe_fonts['trebuchet-ms'],
		);

		$email_safe_fonts = array_merge( $email_safe_fonts, $font_map );

		$get_font_family = function ( $font_name ) use ( $email_safe_fonts ) {
			$font_name_lower = strtolower( $font_name );

			// First check for match in the email-safe slug.
			if ( isset( $email_safe_fonts[ $font_name_lower ] ) ) {
				return $email_safe_fonts[ $font_name_lower ];
			}

			// If no match in the slug, check for match in the font family name.
			foreach ( $email_safe_fonts as $safe_font_slug => $safe_font ) {
				if ( stripos( $safe_font, $font_name_lower ) !== false ) {
					return $safe_font;
				}
			}
			return null;
		};

		// Check if it's already an email-safe font.
		$font_family_array = explode( ',', $font_family );
		$first_font        = trim( $font_family_array[0] );
		$first_font        = trim( $first_font, "\"'" );
		$safe_font_family  = $get_font_family( $first_font );
		if ( $safe_font_family ) {
			return $safe_font_family;
		}

		// Default to arial font if no match found.
		return $email_safe_fonts['arial'];
	}

	/**
	 * Convert size value to px format with optional fallback
	 *
	 * @param string      $size Original size value.
	 * @param string|null $fallback Fallback value to use if conversion fails.
	 * @return string Size in px format.
	 */
	private function convert_to_px_size( string $size, ?string $fallback = null ): string {
		$converted = null;
		// Replace clamp() with its minimum value. We use min because it's emails are most likely to be viewed on smaller screens.
		if ( stripos( $size, 'clamp(' ) !== false ) {
			$converted = Styles_Helper::clamp_to_static_px( $size, 'min' );
			// If clamp_to_static_px returns the original value, it failed to convert.
			if ( $converted === $size ) {
				$converted = null;
			}
		}

		// Try standard conversion.
		if ( is_null( $converted ) ) {
			$converted = Styles_Helper::convert_to_px( $size, false );
		}

		// If all conversions failed, use fallback if provided.
		if ( is_null( $converted ) && $fallback ) {
			return $fallback;
		}

		// Return converted value or original if conversion failed.
		return $converted ?? $size;
	}

	/**
	 * Convert spacing values to px format.
	 *
	 * @param string|array $spacing_values Original spacing values.
	 * @param array        $base_path Base path for fallback lookup (e.g., ['styles', 'spacing', 'padding']).
	 * @return string|array Spacing values in px format.
	 */
	private function convert_spacing_values( $spacing_values, array $base_path ) {
		if ( ! is_string( $spacing_values ) && ! is_array( $spacing_values ) ) {
			return $spacing_values;
		}

		if ( is_string( $spacing_values ) ) {
			$fallback = $this->get_base_theme_value( $base_path );
			return $this->convert_to_px_size( $spacing_values, $fallback );
		}

		$px_values = array();

		foreach ( $spacing_values as $side => $value ) {
			if ( is_string( $value ) ) {
				// Build path for side-specific fallback (e.g., ['styles', 'spacing', 'padding', 'top']).
				$side_path          = array_merge( $base_path, array( $side ) );
				$fallback           = $this->get_base_theme_value( $side_path );
				$px_values[ $side ] = $this->convert_to_px_size( $value, $fallback );
			} else {
				$px_values[ $side ] = $value;
			}
		}

		return $px_values;
	}

	/**
	 * Get value from base theme by path
	 *
	 * @param array $path Path array for _wp_array_get (e.g., ['styles', 'typography', 'fontSize']).
	 * @return string|null Value from base theme or null if not found.
	 */
	private function get_base_theme_value( array $path ): ?string {
		if ( ! $this->base_theme_data ) {
			return null;
		}

		$value = _wp_array_get( $this->base_theme_data, $path );
		return is_string( $value ) ? $value : null;
	}
}
