<?php
/**
 * Asset Loader Utility Class
 *
 * Provides static methods to enqueue scripts and styles with WordPress.
 * Automatically handles .asset.php metadata files generated by the plugin build step.
 *
 * Usage:
 *     Asset_Loader::enqueue_script( 'my-handle', 'my-script' );
 *     Asset_Loader::enqueue_style( 'my-style', 'my-stylesheet' );
 *
 * @package WordPress\AI
 */

declare( strict_types=1 );

namespace WordPress\AI;

// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;

/**
 * Class Asset_Loader
 *
 * A utility class for registering and enqueuing assets (scripts and styles)
 * with support for asset metadata files.
 *
 * @internal This class is intended for internal plugin use only and should not be used directly by external code.
 *           Breaking changes may be made without a major version bump or prior notice.
 *
 * @since 0.1.0
 */
final class Asset_Loader {
	/**
	 * The path to the directory containing the built assets.
	 */
	private const ASSET_DIR = WPAI_PLUGIN_DIR . 'build-scripts/';

	/**
	 * The URL to the directory containing the built assets.
	 */
	private const ASSET_URL = WPAI_PLUGIN_URL . 'build-scripts/';

	/**
	 * The prefix to use for all asset handles to avoid conflicts with other plugins or themes.
	 */
	private const HANDLE_PREFIX = 'ai_';

	/**
	 * Global data to be localized.
	 *
	 * @since 1.0.0
	 * @var array<string, array<string, mixed>>
	 */
	private static array $global_data = array();

	/**
	 * Registers data to be localized onto the first plugin script enqueued in the current request.
	 *
	 * Use this for plugin-wide data that any script may need but that should only be
	 * output when at least one plugin script is present on the page.
	 *
	 * @since 1.0.0
	 *
	 * @param string              $object_name The name of the JavaScript object (without the 'ai' prefix).
	 * @param array<string,mixed> $data        The data to localize.
	 */
	public static function add_global_data( string $object_name, array $data ): void {
		self::$global_data[ $object_name ] = $data;
	}

	/**
	 * Enqueue a script using a script path and its asset metadata.
	 *
	 * @since 0.1.0
	 *
	 * @param string $handle    The handle for the script.
	 * @param string $file_name The script file name without the .js extension.
	 * @param array{ include_core_abilities?: bool } $extra_args Additional arguments.
	 */
	public static function enqueue_script( string $handle, string $file_name, array $extra_args = array() ): void {
		$script_url = self::ASSET_URL . $file_name . '.js';
		$asset_data = self::get_asset_file_data( $file_name );

		// Bail if there's nothing to enqueue.
		if ( ! $asset_data ) {
			return;
		}

		$args = array(
			'in_footer' => true,
			'strategy'  => 'defer',
		);

		if ( $extra_args['include_core_abilities'] ?? false ) {
			$args['module_dependencies'] = array(
				'@wordpress/abilities',
				'@wordpress/core-abilities',
			);
		}

		wp_enqueue_script(
			self::HANDLE_PREFIX . $handle,
			$script_url,
			$asset_data['dependencies'],
			$asset_data['version'],
			$args
		);

		// Localize global data.
		foreach ( self::$global_data as $object_name => $data ) {
			wp_add_inline_script(
				self::HANDLE_PREFIX . $handle,
				sprintf( 'window.ai%s=%s;', $object_name, wp_json_encode( $data ) ),
				'before'
			);
		}
		self::$global_data = array();
		wp_set_script_translations( self::HANDLE_PREFIX . $handle, 'ai' );
	}

	/**
	 * Enqueue a style using a style path and its asset metadata.
	 *
	 * @since 0.1.0
	 *
	 * @param string                    $handle     The handle for the style.
	 * @param string                    $file_name  The script file name.
	 * @param string[]                  $dependencies Optional. An array of registered style handles this style depends on. Default empty array.
	 */
	public static function enqueue_style( string $handle, string $file_name, array $dependencies = array() ): void {
		$handle     = self::HANDLE_PREFIX . $handle;
		$style_path = self::ASSET_DIR . $file_name . '.css';
		$style_url  = self::ASSET_URL . $file_name . '.css';
		$asset_data = self::get_asset_file_data( $file_name );

		// Bail if there's nothing to enqueue.
		if ( ! $asset_data ) {
			return;
		}

		wp_enqueue_style(
			$handle,
			$style_url,
			$dependencies, // *.asset.php dependencies are only for scripts.
			$asset_data['version']
		);

		wp_style_add_data( $handle, 'path', $style_path );

		$rtl_style_path = str_replace( '.css', '-rtl.css', $style_path );
		if ( ! file_exists( $rtl_style_path ) ) {
			self::log_and_display_error(
				sprintf(
					/* translators: %1$s: The RTL style filename. */
					__( 'RTL stylesheet "%1$s" is missing and will not be available.', 'ai' ),
					basename( $rtl_style_path ),
				)
			);
			return;
		}

		wp_style_add_data( $handle, 'rtl', 'replace' );
		if ( ! is_rtl() ) {
			return;
		}

		wp_style_add_data( $handle, 'path', $rtl_style_path );
	}

	/**
	 * Localize data for an enqueued script.
	 *
	 * This method allows passing PHP data to JavaScript using `wp_localize_script()`.
	 * It must be called after the script has been enqueued using `enqueue_script()`.
	 *
	 * @since 0.1.0
	 *
	 * @param string $handle The script handle used in `enqueue_script()` (without prefix).
	 * @param string $object_name The name of the JavaScript object to contain the data.
	 * @param array<string, mixed> $data The data to localize.
	 */
	public static function localize_script( string $handle, string $object_name, array $data ): void {
		wp_localize_script(
			self::HANDLE_PREFIX . $handle,
			'ai' . $object_name,
			$data
		);
	}

	/**
	 * Get the asset file data for a given asset filename.
	 *
	 * @param string $filename Path of the asset relative to the assets directory, excluding the file extension.
	 *
	 * @return ?array{
	 *   version:string,
	 *   dependencies:array<string>,
	 *   ...
	 * } The asset file array, or null if the asset file does not exist or is invalid.
	 */
	private static function get_asset_file_data( string $filename ): ?array {
		$asset_file = self::ASSET_DIR . $filename . '.asset.php';

		// Bail if the asset file does not exist.
		if ( ! file_exists( $asset_file ) ) {
			self::log_and_display_error(
				sprintf(
					/* translators: %1$s: The asset filename. */
					__( 'Asset file for "%1$s" is missing and cannot be registered.', 'ai' ),
					$filename,
				),
			);
			return null;
		}

		// phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable -- The file is checked above.
		$asset = require $asset_file;

		if ( ! is_array( $asset ) ) {
			self::log_and_display_error(
				sprintf(
					/* translators: %1$s: The asset filename. */
					__( 'Asset file for "%1$s" is invalid and cannot be registered.', 'ai' ),
					$filename,
				),
			);
			return null;
		}

		// Fallback to filemtime if version is not set in the asset file.
		if ( ! isset( $asset['version'] ) ) {
			$asset['version'] = filemtime( $asset_file );
		}

		// Fallback to empty dependencies if not set in the asset file.
		if ( ! isset( $asset['dependencies'] ) ) {
			$asset['dependencies'] = array();
		}

		return $asset;
	}

	/**
	 * Logs a _doing_it_wrong() and displays an admin notice with the provided message.
	 *
	 * Messages are escaped with esc_html() before being logged or displayed.
	 *
	 * @param string $message The message to display in the admin notice.
	 */
	private static function log_and_display_error( string $message ): void {
		_doing_it_wrong( self::class, esc_html( $message ), '0.8.0' );

		$hooks = array(
			'admin_notices',
			'network_admin_notices',
		);

		foreach ( $hooks as $hook ) {
			add_action(
				$hook,
				static function () use ( $message ) {
					wp_admin_notice(
						esc_html( $message ),
						array(
							'type' => 'error',
						)
					);
				}
			);
		}
	}
}
