<?php
/**
 * Vite Asset Manager
 *
 * @package RadiusBooking\Assets
 */

namespace RadiusTheme\RadiusBooking\Assets;

use Exception;
if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}
/**
 * Class Vite
 *
 * This class manages Vite assets for development and production environments.
 * It handles reading manifests, registering scripts and styles, and enqueuing them.
 */
class Vite {

	const DEV_CLIENT_HANDLE = 'vite-wp-react-assets-dev-client';

	/**
	 * Assets directory path
	 *
	 * @var string
	 */
	protected string $assets_path;

	/**
	 * Assets directory URL
	 *
	 * @var string
	 */
	protected string $assets_url;

	/**
	 * Configuration options
	 *
	 * @var array
	 */
	protected array $config;

	/**
	 * Development manifest
	 *
	 * @var ?array
	 */
	protected ?array $dev_manifest = null;

	/**
	 * Production manifest
	 *
	 * @var ?array
	 */
	protected ?array $prod_manifest = null;

	/**
	 * React refresh preamble printed ports
	 *
	 * @var array
	 */
	protected static $preamble_printed_ports = array();

	/**
	 * The registered assets.
	 *
	 * @var array
	 */
	private $registered_assets = array();

	/**
	 * ViteAssets constructor.
	 *
	 * @param string $assets_path Path to assets directory.
	 * @param string $assets_url  URL to assets directory.
	 * @param array  $config     Configuration options.
	 */
	public function __construct( string $assets_path, string $assets_url, array $config = array() ) {
		$this->assets_path = untrailingslashit( $assets_path );
		$this->assets_url  = untrailingslashit( $assets_url );
		$default_config    = array(
			'prod-manifest'      => 'manifest.json',
			'dev-manifest'       => 'dev-server.json',
			'preload-css-chunks' => true,
		);

		$this->config = wp_parse_args( $config, $default_config );

		$this->init();
	}

	/**
	 * Whether the manifest is for development.
	 *
	 * @return bool
	 */
	public function is_dev(): bool {
		return (bool) $this->dev_manifest;
	}

	/**
	 * Get the development manifest.
	 *
	 * @return null|array
	 */
	public function get_dev_manifest(): ?array {
		return $this->dev_manifest;
	}

	/**
	 * Initialize
	 *
	 * @return void
	 */
	protected function init(): void {
		try {
			$this->init_manifest();
		} catch ( Exception $e ) {
			$this->die_if_debug( esc_html( $e->getMessage() ) );
		}
	}

	/**
	 * Read JSON file in the assets directory.
	 *
	 * @param string $file_name File name.
	 *
	 * @return array|null
	 */
	protected function read_json_file( string $file_name ) {
		$path = "{$this->assets_path}/{$file_name}";
		if ( is_file( $path ) && is_readable( $path ) ) {
			return wp_json_file_decode( $path, array( 'associative' => true ) );
		}

		return null;
	}

	/**
	 * Initialize manifest
	 *
	 * @return void
	 * @throws Exception Exception is thrown when the file doesn't exist or is unreadble.
	 */
	protected function init_manifest(): void {

		$this->dev_manifest = $this->read_json_file( $this->config['dev-manifest'] );

		if ( ! $this->dev_manifest ) {
			$this->prod_manifest = $this->read_json_file( $this->config['prod-manifest'] );
		}

		if ( ! $this->dev_manifest && ! $this->prod_manifest ) {
			throw new Exception(
				esc_html(
					sprintf(
						'[ViteWPReactAssets] Manifest file not found or is not readable in %s.',
						$this->assets_path
					)
				)
			);
		}
	}

	/**
	 * Get the production manifest.
	 *
	 * @return null|array
	 */
	public function get_prod_manifest(): ?array {
		return $this->prod_manifest;
	}

	/**
	 * Die if debugging is enabled.
	 *
	 * @param mixed $message Message to display.
	 * @return void
	 */
	protected function die_if_debug( $message ): void {
		if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
			wp_die( esc_html( $message ) );
		}
	}

	/**
	 * Parse options and merge with defaults.
	 *
	 * @param array $options Array of options.
	 *
	 * @return array Array of options merged with defaults.
	 */
	protected static function parse_options( array $options ): array {
		$defaults = array(
			'handle'                 => '',
			'script-dependencies'    => array(),
			'script-args'            => false, // Can be [ 'in_footer' => false ].
			'style-dependencies'     => array(),
			'style-media'            => 'all',
			'skip-script'            => false,
			'skip-style'             => false,
			'inline-script-data'     => '',
			/**
			 * Should be an array with the following keys:
			 * - 'dev-dependency' (Optional): The script handle to use as a dependency in development.
			 * - 'data': The inline style data.
			 */
			'inline-style'           => array(),
			'inline-script-position' => 'after',
		);

		return wp_parse_args( $options, $defaults );
	}

	/**
	 * Get client dev script handle
	 *
	 * @return string
	 */
	protected function get_dev_client_handle() {

		$manifest = $this->get_dev_manifest();

		$port = $manifest['port'] ?? null;

		// Add port to the handle to allow multiple dev servers to run at the same time.
		return $port ? self::DEV_CLIENT_HANDLE . "-{$port}" : self::DEV_CLIENT_HANDLE;
	}

	/**
	 * Get development asset url
	 *
	 * @param string $entry Entry name.
	 *
	 * @return string
	 */
	protected function get_dev_asset_url( string $entry ): string {
		$manifest = $this->get_dev_manifest();

		// Ensure that we don't have double slashes in the path.
		$path = trim( preg_replace( '#[/]{2,}#', '/', "{$manifest['base']}/{$entry}" ), '/' );

		// Append path to the server origin.
		return sprintf( '%s/%s', untrailingslashit( $manifest['origin'] ), $path );
	}

	/**
	 * Ensures that a script is loaded as a module by adding `type="module"` to the script tag.
	 *
	 * @param string $handle Script handle.
	 *
	 * @return void
	 */
	private function load_script_as_module( string $handle ): void {
		add_filter(
			'wp_script_attributes',
			function ( array $attributes ) use ( $handle ): array {
				if ( isset( $attributes['id'] ) && "{$handle}-js" === $attributes['id'] ) {
					$attributes['type'] = 'module';
				}

				return $attributes;
			}
		);
	}

	/**
	 * Register vite client script
	 *
	 * @return void
	 */
	protected function register_dev_client(): void {
		$handle = $this->get_dev_client_handle();

		// Don't register the script if it's already registered.
		if ( wp_script_is( $handle, 'enqueued' ) ) {
			return;
		}

		$src = $this->get_dev_asset_url( '@vite/client' );

		wp_register_script( $handle, $src, array(), null, false ); //phpcs:ignore

		$this->load_script_as_module( $handle );
	}

	/**
	 * Inject react-refresh preamble script.
	 *
	 * @return void
	 */
	protected function add_react_refresh_preamble(): void {

		$manifest = $this->get_dev_manifest();

		$port = $manifest['port'] ?? null;

		// If we have already printed the preamble for this port, don't print it again.
		if ( ! $port || ! empty( self::$preamble_printed_ports[ $port ] ) ) {
			return;
		}

		self::$preamble_printed_ports[ $port ] = true;

		if ( ! in_array( 'vite:react-refresh', $manifest['plugins'], true ) ) {
			return;
		}

		$react_refresh_url = $this->get_dev_asset_url( '@react-refresh' );

		$data = implode(
			PHP_EOL,
			array(
				sprintf(
					'import RefreshRuntime from %s;',
					wp_json_encode( esc_url_raw( $react_refresh_url ) )
				),
				'RefreshRuntime.injectIntoGlobalHook(window);',
				'window.$RefreshReg$ = () => {};',
				'window.$RefreshSig$ = () => (type) => type;',
				'window.__vite_plugin_react_preamble_installed__ = true;',
			)
		);

		$handle = $this->get_dev_client_handle();

		wp_add_inline_script( esc_html( sanitize_text_field( wp_unslash( $handle ) ) ), $data );

		add_filter(
			'wp_inline_script_attributes',
			function ( array $attributes ) use ( $handle ): array {
				if ( isset( $attributes['id'] ) && "{$handle}-js-after" === $attributes['id'] ) {
					$attributes['type'] = 'module';
				}

				return $attributes;
			}
		);
	}

	/**
	 * Returns imports for a file listed in the manifest.
	 *
	 * Can be used to preload JS chunks.
	 *
	 * @param string $entry Entrypoint.
	 * @return array
	 */
	public function get_import_chunks( string $entry ): array {
		$manifest = $this->get_prod_manifest();

		if ( ! isset( $manifest[ $entry ]['imports'] ) || ! is_array( $manifest[ $entry ]['imports'] ) ) {
			return array();
		}

		$imports = array();

		foreach ( $manifest[ $entry ]['imports'] as $import ) {
			if ( isset( $manifest[ $import ]['file'] ) ) {
				$url = sprintf( '%s/%s', $this->assets_url, $manifest[ $import ]['file'] );

				$imports[] = array(
					'entry' => $import,
					'url'   => $url,
				);
			}
		}

		return $imports;
	}

	/**
	 * Register assets for development.
	 *
	 * @param string $entry   Entrypoint.
	 * @param array  $options Options.
	 *
	 * @return array|null Registered assets data.
	 */
	protected function register_for_dev( string $entry, array $options ): ?array {
		if ( ! $this->is_dev() ) {
			return null;
		}
		$this->register_dev_client();
		$this->add_react_refresh_preamble();

		// Add the dev client as a dependency to the script.
		$dependencies = array_merge( array( $this->get_dev_client_handle() ), $options['script-dependencies'] );

		$url = $this->get_dev_asset_url( $entry );

		$this->load_script_as_module( $options['handle'] );

		if ( ! wp_register_script( $options['handle'], $url, $dependencies, null, $options['script-args'] ) ) { //phpcs:ignore
			return null;
		}

		$data = array(
			'script' => $options['handle'],
			// Retain the style dependencies for development so that the styles are enqueued.
			'styles' => $options['style-dependencies'],
		);

		return apply_filters( 'rtrb_dev_entry_assets_data', $data, $entry, $options, $this );
	}

	/**
	 * Register assets for development.
	 *
	 * @param string $entry   Entrypoint.
	 * @param array  $options Options.
	 *
	 * @return array|null Registered assets data.
	 */
	protected function register_for_prod( string $entry, array $options ): ?array {
		$manifest = $this->get_prod_manifest();

		if ( ! isset( $manifest[ $entry ]['file'] ) ) {
			$this->die_if_debug( sprintf( '[ViteWPReactAssets] Entry "%s" does not exist.', $entry ) );

			return null;
		}

		$data = array(
			'script' => null,
			'styles' => array(),
		);

		$item = $manifest[ $entry ];

		if ( ! $options['skip-script'] ) {
			// $src = "{$this->assets_url}/{$item['file']}";

			$src = esc_url( "{$this->assets_url}/{$item['file']}" );

			$this->load_script_as_module( $options['handle'] );

			$result = wp_register_script(
				$options['handle'],
				$src,
				$options['script-dependencies'],
				!empty($options['version']) ? $options['version'] : null,
				$options['script-args']
			);

			if ( $result ) {
				$data['script'] = $options['handle'];
			}
		}

		if ( ! $options['skip-style'] ) {

			$css = $item['css'] ?? array();

			if ( $this->config['preload-css-chunks'] ) {
				// Actively load CSS from lazy loaded js chunks.
				foreach ( $this->get_import_chunks( $entry ) as $import ) {
					$css = array_merge( $css, $manifest[ $import['entry'] ]['css'] ?? array() );
				}
			}

			foreach ( $css as $index => $css_path ) {
				$handle = "{$options['handle']}-{$index}";

				$result = wp_register_style(
					$handle,
					// "{$this->assets_url}/{$css_path}",
					esc_url( "{$this->assets_url}/{$css_path}" ),
					$options['style-dependencies'],
					!empty($options['version']) ? $options['version'] : null,
					$options['style-media']
				);

				if ( $result ) {
					$data['styles'][] = $handle;
				}
			}
		}

		return apply_filters( 'rtrb_prod_entry_assets_data', $data, $entry, $options, $this );
	}

	/**
	 * Get the registered assets.
	 *
	 * @return array
	 */
	public function get_registered_assets(): array {
		return $this->registered_assets;
	}

	/**
	 * Get the registered script handle for a given entry.
	 *
	 * @param string $entry Entry name.
	 *
	 * @return string
	 */
	public function get_entry_script_handle( string $entry ): string {
		return $this->registered_assets[ $entry ]['script'] ?? '';
	}

	/**
	 * Get the registered style handles for a given entry.
	 *
	 * @param string $entry Entry name.
	 *
	 * @return array
	 */
	public function get_entry_style_handles( string $entry ): array {
		return $this->registered_assets[ $entry ]['styles'] ?? array();
	}

	/**
	 * Whether a handle is registered.
	 *
	 * @param string $entry Entry name.
	 * @param string $as    Asset type (script or style).
	 * @return bool
	 */
	public function is_registered( string $entry, string $as = 'script' ): bool { //phpcs:ignore
		$key = 'style' === $as ? 'styles' : 'script';
		return ! empty( $this->registered_assets[ $entry ][ $key ] );
	}

	/**
	 * Register asset
	 *
	 * @param string $entry   Entrypoint to register.
	 * @param array  $options Options.
	 *
	 * @return array
	 */
	public function register( string $entry, array $options ): ?array {

		$options = $this->parse_options( $options );

		$data = $this->is_dev() ? $this->register_for_dev( $entry, $options ) : $this->register_for_prod( $entry, $options );

		$this->registered_assets[ $entry ] = $data;
		return $data;
	}

	/**
	 * Enqueue asset
	 *
	 * @param string $entry   Entrypoint.
	 * @param array  $options Options.
	 *
	 * @return bool
	 */
	public function enqueue( string $entry, array $options = array() ): bool {
		if ( ! $this->is_registered( $entry ) ) {
			$this->register( $entry, $options );
		}

		// If the entry is still not registered, return false.
		if ( ! $this->is_registered( $entry ) ) {
			return false;
		}
		$script_handle = $this->get_entry_script_handle( $entry );

		$options = $this->parse_options( $options );

		if ( $script_handle && ! $options['skip-script'] ) {
			wp_enqueue_script( $script_handle );
		}

		if ( $options['skip-style'] ) {
			return true;
		}

		$style_handles = $this->get_entry_style_handles( $entry );

		foreach ( $style_handles as $style_handle ) {
			wp_enqueue_style( $style_handle );
		}

		if ( ! empty( $options['inline-style'] ) ) {
			$handle = $this->is_dev() && ! empty( $options['inline-style']['dev-dependency'] ) ? $options['inline-style']['dev-dependency'] : $style_handle ?? '';
			$data = $options['inline-style']['data'] ?? '';

			$data = wp_kses(
				$data,
				array(), // no HTML allowed
				array( 'style' => array() )
			);

			wp_add_inline_style( esc_html( sanitize_text_field( wp_unslash( $handle ) ) ), $data );
		}

		return true;
	}
}
