<?php

namespace AlpineJsEnqueue;

use WP_PowerSuite\Core\AbstractModule;
use WP_PowerSuite\Core\AdminAssetContext;
use WP_PowerSuite\Core\Contracts\DataLocationsInterface;

class Module extends AbstractModule implements DataLocationsInterface {

	private const MODULE_ID = 'alpine-js-enqueue';
	private const MODULE_VERSION = '1.1.0';
	private const SCRIPT_HANDLE = 'wpps-alpine-js';
	private const BUNDLED_VERSION = '3.13.5';
	private const INIT_CODE_MAX_LENGTH = 10000;

	/** @var list<string> */
	private const KNOWN_ALPINE_HANDLES = [
		'wpps-alpine-js',
		'alpine-js',
		'alpinejs',
		'alpine',
		'alpinejs-cdn',
	];

	/** @var array<string, string> */
	private const CDN_INTEGRITY = [
		'3.13.5' => 'sha384-BxpSbjbDhVKwnC1UfcjsNEuMuxg4af5IXOaSi1Iq5rASQ/9a7uslhEXbP9UI/fXo',
	];

	private ?array $settings_cache = null;

	public function getId(): string {
		return self::MODULE_ID;
	}

	public function getName(): string {
		return __( 'Alpine.js Enqueue', 'wp-powersuite' );
	}

	public function getVersion(): string {
		return self::MODULE_VERSION;
	}

	public function getDescription(): string {
		return __( 'Adds Alpine.js on the front end and/or admin for small reactive UI without a heavy framework.', 'wp-powersuite' );
	}

	public function getContext(): string|array {
		return 'both';
	}

	protected function registerHooks(): void {
		$settings      = $this->getSettings();
		$load_frontend = ! empty( $settings['load_frontend'] );
		$load_admin    = ! empty( $settings['load_admin'] );

		if ( ! $load_frontend && ! $load_admin ) {
			return;
		}

		$defer = ! empty( $settings['defer'] );
		$async = ! empty( $settings['async'] );
		if ( $defer || $async ) {
			add_filter( 'script_loader_tag', [ $this, 'addScriptAttributes' ], 10, 2 );
		}

		if ( $load_frontend ) {
			add_action( 'wp_enqueue_scripts', [ $this, 'enqueueFrontend' ], 20 );
		}

		if ( $load_admin ) {
			add_action( 'admin_enqueue_scripts', [ $this, 'enqueueAdmin' ], 20 );
		}
	}

	public function enqueueFrontend(): void {
		if ( ! $this->shouldLoadOnFrontend() ) {
			return;
		}

		$this->enqueueAlpine();
	}

	public function enqueueAdmin(): void {
		$hook = function_exists( 'get_current_screen' ) && get_current_screen()
			? (string) get_current_screen()->id
			: '';

		if ( ! AdminAssetContext::shouldLoadAssets( $hook ) ) {
			return;
		}

		if ( ! $this->shouldLoadOnAdmin( $hook ) ) {
			return;
		}

		$this->enqueueAlpine();
	}

	private function enqueueAlpine(): void {
		$settings = $this->getSettings();

		if ( ! empty( $settings['skip_if_detected'] ) && self::isAlpineAlreadyPresent() ) {
			return;
		}

		$resolved = self::resolveScriptSource( $settings );
		if ( $resolved === null ) {
			return;
		}

		wp_register_script( self::SCRIPT_HANDLE, $resolved['url'], [], $resolved['version'], true );

		if ( ! empty( $resolved['integrity'] ) ) {
			wp_script_add_data( self::SCRIPT_HANDLE, 'integrity', $resolved['integrity'] );
			wp_script_add_data( self::SCRIPT_HANDLE, 'crossorigin', 'anonymous' );
		}

		$init_code = self::prepareInitInlineScript( $settings['init_code'] ?? '' );
		if ( $init_code !== '' ) {
			wp_add_inline_script( self::SCRIPT_HANDLE, $init_code, 'after' );
		}

		wp_enqueue_script( self::SCRIPT_HANDLE );
	}

	public function addScriptAttributes( string $tag, string $handle ): string {
		if ( $handle !== self::SCRIPT_HANDLE ) {
			return $tag;
		}

		$settings = $this->getSettings();
		$defer    = ! empty( $settings['defer'] );
		$async    = ! empty( $settings['async'] );

		if ( $async && strpos( $tag, 'async' ) === false ) {
			$tag = str_replace( ' src', ' async src', $tag );
			return $tag;
		}

		if ( $defer && strpos( $tag, 'defer' ) === false ) {
			$tag = str_replace( ' src', ' defer src', $tag );
		}

		return $tag;
	}

	private function shouldLoadOnFrontend(): bool {
		$settings = $this->getSettings();

		if ( ! empty( $settings['exclude_feeds'] ) && function_exists( 'is_feed' ) && is_feed() ) {
			return false;
		}

		if ( ! empty( $settings['exclude_customizer'] ) && function_exists( 'is_customize_preview' ) && is_customize_preview() ) {
			return false;
		}

		if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
			return false;
		}

		if ( wp_doing_ajax() || wp_doing_cron() ) {
			return false;
		}

		$scope = $settings['frontend_scope'] ?? 'all';
		if ( $scope === 'singular' && ! is_singular() ) {
			return false;
		}

		if ( $scope === 'post_types' ) {
			if ( ! is_singular() ) {
				return false;
			}

			$post = get_post();
			if ( ! $post instanceof \WP_Post ) {
				return false;
			}

			$allowed = $settings['frontend_post_types'] ?? [];
			if ( ! is_array( $allowed ) || $allowed === [] ) {
				return false;
			}

			if ( ! in_array( $post->post_type, $allowed, true ) ) {
				return false;
			}
		}

		if ( ! empty( $settings['require_alpine_markup'] ) ) {
			if ( ! $this->currentFrontendContentHasAlpineMarkup() ) {
				return false;
			}
		}

		/**
		 * Filter whether Alpine.js should enqueue on the current frontend request.
		 *
		 * @param bool               $should_load Default decision from module settings.
		 * @param array<string,mixed> $settings   Normalized module settings.
		 */
		return (bool) apply_filters( 'wp_powersuite_alpine_js_should_enqueue_frontend', true, $settings );
	}

	private function shouldLoadOnAdmin( string $hook ): bool {
		$settings = $this->getSettings();
		$scope    = $settings['admin_scope'] ?? 'powersuite';

		if ( $scope === 'all' ) {
			$allowed = true;
		} elseif ( $scope === 'screens' ) {
			$screens = $settings['admin_screens'] ?? [];
			$allowed = is_array( $screens ) && $screens !== [] && in_array( $hook, $screens, true );
		} else {
			$allowed = self::isPowerSuiteAdminScreen( $hook );
		}

		/**
		 * Filter whether Alpine.js should enqueue on the current admin screen.
		 *
		 * @param bool               $should_load Default decision from module settings.
		 * @param string             $hook        Current admin screen hook suffix.
		 * @param array<string,mixed> $settings   Normalized module settings.
		 */
		return (bool) apply_filters( 'wp_powersuite_alpine_js_should_enqueue_admin', $allowed, $hook, $settings );
	}

	private function currentFrontendContentHasAlpineMarkup(): bool {
		if ( ! is_singular() ) {
			return false;
		}

		$post = get_post();
		if ( ! $post instanceof \WP_Post ) {
			return false;
		}

		return self::contentContainsAlpineMarkup( (string) $post->post_content );
	}

	public static function contentContainsAlpineMarkup( string $content ): bool {
		if ( $content === '' ) {
			return false;
		}

		return (bool) preg_match(
			'/(?:\sx-data|\sx-show|\sx-if|\sx-for|\sx-model|\sx-bind|\sx-cloak|@click|@submit|@input|x-on:|Alpine\.)/i',
			$content
		);
	}

	public static function isPowerSuiteAdminScreen( string $hook ): bool {
		if ( $hook === '' ) {
			return false;
		}

		return str_contains( $hook, 'powersuite' );
	}

	public static function isAlpineAlreadyPresent(): bool {
		foreach ( self::KNOWN_ALPINE_HANDLES as $handle ) {
			if ( $handle === self::SCRIPT_HANDLE ) {
				continue;
			}

			if ( wp_script_is( $handle, 'enqueued' ) || wp_script_is( $handle, 'done' ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * @param array<string, mixed> $settings
	 * @return array{url:string,version:string,integrity:string}|null
	 */
	public static function resolveScriptSource( array $settings ): ?array {
		$source  = $settings['source'] ?? 'bundled';
		$version = (string) ( $settings['version'] ?? self::BUNDLED_VERSION );

		if ( $source === 'cdn' ) {
			if ( ! isset( self::getAvailableVersions()[ $version ] ) ) {
				$version = self::BUNDLED_VERSION;
			}

			return [
				'url'       => sprintf( 'https://cdn.jsdelivr.net/npm/alpinejs@%s/dist/cdn.min.js', $version ),
				'version'   => $version,
				'integrity' => self::CDN_INTEGRITY[ $version ] ?? '',
			];
		}

		if ( $source === 'custom' ) {
			$url = self::resolveCustomScriptUrl( (string) ( $settings['custom_path'] ?? '' ) );
			if ( $url === '' ) {
				if ( ! empty( $settings['allow_cdn_fallback'] ) ) {
					return self::resolveScriptSource( array_merge( $settings, [ 'source' => 'cdn' ] ) );
				}

				return null;
			}

			return [
				'url'       => $url,
				'version'   => $version,
				'integrity' => '',
			];
		}

		$bundled = self::getBundledAssetPath();
		if ( ! is_readable( $bundled ) ) {
			if ( ! empty( $settings['allow_cdn_fallback'] ) ) {
				return self::resolveScriptSource( array_merge( $settings, [ 'source' => 'cdn' ] ) );
			}

			return null;
		}

		return [
			'url'       => self::getBundledAssetUrl(),
			'version'   => self::BUNDLED_VERSION,
			'integrity' => self::CDN_INTEGRITY[ self::BUNDLED_VERSION ] ?? '',
		];
	}

	public static function getBundledAssetPath(): string {
		return __DIR__ . '/assets/alpine.min.js';
	}

	public static function getBundledAssetUrl(): string {
		$base = defined( 'WP_POWERSUITE_PLUGIN_URL' ) ? WP_POWERSUITE_PLUGIN_URL : '';

		return $base . 'modules/alpine-js-enqueue/assets/alpine.min.js';
	}

	public static function resolveCustomScriptUrl( string $path ): string {
		$path = trim( $path );
		if ( $path === '' ) {
			return '';
		}

		$normalized = wp_normalize_path( $path );
		$real       = realpath( $normalized );
		if ( $real === false || ! is_readable( $real ) || ! is_file( $real ) ) {
			return '';
		}

		$real        = wp_normalize_path( $real );
		$content_dir = wp_normalize_path( WP_CONTENT_DIR );
		if ( ! str_starts_with( $real, $content_dir ) ) {
			return '';
		}

		if ( ! preg_match( '/\.js$/i', $real ) ) {
			return '';
		}

		return content_url( str_replace( $content_dir, '', $real ) );
	}

	public static function prepareInitInlineScript( string $init_code ): string {
		$init_code = self::sanitizeInitCode( $init_code );
		if ( $init_code === '' ) {
			return '';
		}

		return "document.addEventListener('alpine:init', function() {\n" . $init_code . "\n});";
	}

	public static function sanitizeInitCode( string $value ): string {
		$value = wp_unslash( $value );
		$value = str_replace( "\0", '', $value );
		$value = wp_strip_all_tags( $value );
		$value = preg_replace( '#</?script[^>]*>#i', '', $value ) ?? '';
		$value = trim( $value );

		if ( $value === '' ) {
			return '';
		}

		if ( ! current_user_can( 'unfiltered_html' ) ) {
			return '';
		}

		if ( strlen( $value ) > self::INIT_CODE_MAX_LENGTH ) {
			$value = substr( $value, 0, self::INIT_CODE_MAX_LENGTH );
		}

		return $value;
	}

	/**
	 * @return array<string, string>
	 */
	public static function getAvailableVersions(): array {
		return [
			'3.13.5' => '3.13.5',
			'3.13.4' => '3.13.4',
			'3.13.3' => '3.13.3',
			'3.13.2' => '3.13.2',
			'3.13.1' => '3.13.1',
			'3.13.0' => '3.13.0',
			'3.12.4' => '3.12.4',
			'3.12.3' => '3.12.3',
			'3.12.2' => '3.12.2',
			'3.12.1' => '3.12.1',
			'3.12.0' => '3.12.0',
			'3.11.1' => '3.11.1',
			'3.11.0' => '3.11.0',
			'3.10.5' => '3.10.5',
			'3.10.4' => '3.10.4',
			'3.10.3' => '3.10.3',
			'3.10.2' => '3.10.2',
			'3.10.1' => '3.10.1',
			'3.10.0' => '3.10.0',
			'3.9.6'  => '3.9.6',
			'3.9.5'  => '3.9.5',
			'3.9.4'  => '3.9.4',
			'3.9.3'  => '3.9.3',
			'3.9.2'  => '3.9.2',
			'3.9.1'  => '3.9.1',
			'3.9.0'  => '3.9.0',
			'3.8.3'  => '3.8.3',
			'3.8.2'  => '3.8.2',
			'3.8.1'  => '3.8.1',
			'3.8.0'  => '3.8.0',
		];
	}

	/**
	 * @param array<string, mixed> $raw
	 * @return array<string, mixed>
	 */
	public static function normalizeSettings( array $raw ): array {
		$defaults = ( new self() )->getDefaultSettings();
		$settings = array_merge( $defaults, $raw );

		$settings['load_frontend'] = ! empty( $settings['load_frontend'] );
		$settings['load_admin']    = ! empty( $settings['load_admin'] );
		$settings['defer']         = ! empty( $settings['defer'] );
		$settings['async']         = ! empty( $settings['async'] );
		$settings['exclude_feeds'] = ! empty( $settings['exclude_feeds'] );
		$settings['exclude_customizer'] = ! empty( $settings['exclude_customizer'] );
		$settings['require_alpine_markup'] = ! empty( $settings['require_alpine_markup'] );
		$settings['skip_if_detected'] = ! empty( $settings['skip_if_detected'] );
		$settings['allow_cdn_fallback'] = ! empty( $settings['allow_cdn_fallback'] );

		if ( $settings['async'] ) {
			$settings['defer'] = false;
		} elseif ( $settings['defer'] ) {
			$settings['async'] = false;
		} else {
			$settings['defer'] = true;
			$settings['async'] = false;
		}

		$source = sanitize_key( (string) ( $settings['source'] ?? 'bundled' ) );
		if ( $source === 'local' ) {
			$source = 'custom';
		}
		if ( ! in_array( $source, [ 'bundled', 'cdn', 'custom' ], true ) ) {
			$source = 'bundled';
		}
		$settings['source'] = $source;

		$versions = self::getAvailableVersions();
		$version  = (string) ( $settings['version'] ?? self::BUNDLED_VERSION );
		if ( ! isset( $versions[ $version ] ) ) {
			$version = self::BUNDLED_VERSION;
		}
		$settings['version'] = $version;

		$custom_path = trim( (string) ( $settings['custom_path'] ?? '' ) );
		if ( $custom_path === '' && ! empty( $settings['local_path'] ) ) {
			$custom_path = trim( (string) $settings['local_path'] );
		}
		$settings['custom_path'] = sanitize_text_field( $custom_path );
		unset( $settings['local_path'] );

		$frontend_scope = sanitize_key( (string) ( $settings['frontend_scope'] ?? 'all' ) );
		$settings['frontend_scope'] = in_array( $frontend_scope, [ 'all', 'singular', 'post_types' ], true )
			? $frontend_scope
			: 'all';

		$post_types = $settings['frontend_post_types'] ?? [];
		if ( is_string( $post_types ) ) {
			$post_types = preg_split( '/\r\n|\r|\n|,/', $post_types ) ?: [];
		}
		if ( ! is_array( $post_types ) ) {
			$post_types = [];
		}
		$settings['frontend_post_types'] = array_values(
			array_unique(
				array_filter(
					array_map( 'sanitize_key', array_map( 'strval', $post_types ) )
				)
			)
		);

		$admin_scope = sanitize_key( (string) ( $settings['admin_scope'] ?? 'powersuite' ) );
		$settings['admin_scope'] = in_array( $admin_scope, [ 'powersuite', 'all', 'screens' ], true )
			? $admin_scope
			: 'powersuite';

		$screens = $settings['admin_screens'] ?? [];
		if ( is_string( $screens ) ) {
			$screens = preg_split( '/\r\n|\r|\n|,/', $screens ) ?: [];
		}
		if ( ! is_array( $screens ) ) {
			$screens = [];
		}
		$settings['admin_screens'] = array_values(
			array_unique(
				array_filter(
					array_map(
						static fn( $screen ): string => sanitize_text_field( (string) $screen ),
						$screens
					)
				)
			)
		);

		if ( isset( $raw['init_code'] ) && is_string( $raw['init_code'] ) ) {
			$settings['init_code'] = self::sanitizeInitCode( $raw['init_code'] );
		} else {
			$settings['init_code'] = self::sanitizeInitCode( (string) ( $settings['init_code'] ?? '' ) );
		}

		return $settings;
	}

	private function getSettings(): array {
		if ( $this->settings_cache !== null ) {
			return $this->settings_cache;
		}

		$settings = $this->moduleSettings()->all( [] );
		$this->settings_cache = self::normalizeSettings( is_array( $settings ) ? $settings : [] );

		return $this->settings_cache;
	}

	public function getDefaultSettings(): array {
		return [
			'load_frontend'          => false,
			'load_admin'             => false,
			'source'                 => 'bundled',
			'version'                => self::BUNDLED_VERSION,
			'custom_path'            => '',
			'allow_cdn_fallback'     => false,
			'defer'                  => true,
			'async'                  => false,
			'init_code'              => '',
			'frontend_scope'         => 'all',
			'frontend_post_types'    => [],
			'exclude_feeds'          => true,
			'exclude_customizer'     => true,
			'require_alpine_markup'  => false,
			'admin_scope'            => 'powersuite',
			'admin_screens'          => [],
			'skip_if_detected'       => true,
		];
	}

	/**
	 * {@inheritdoc}
	 */
	public function getDataLocations(): array {
		return [
			'option_store_keys' => [ self::MODULE_ID ],
		];
	}
}
