<?php
/**
 * Custom Cursor extension — site-wide cursor styles with scope rules.
 *
 * @package DiviSupreme
 */

declare(strict_types=1);

namespace DiviSupreme\Extensions;

use DiviSupreme\Includes\DsmFrontendAssetRegistry;

if ( ! defined( 'ABSPATH' ) ) {
	die( 'Direct access forbidden.' );
}

/**
 * Custom Cursor extension bootstrap.
 */
class CustomCursor {

	/**
	 * wp_options key for global cursor settings.
	 */
	public const OPTION_KEY = 'dsm_custom_cursor_settings';

	/**
	 * Post meta key for per-page enable flag.
	 */
	public const PAGE_META_KEY = 'dsm-custom-cursor-page-enabled';

	/**
	 * @var array<string, mixed>
	 */
	private array $default_settings = array(
		'is_enabled'              => true,
		'cursor_style'            => 'filled_circle',
		'primary_color'           => '#4161d4',
		'secondary_color'         => '#ffffff',
		'cursor_size'             => '40px',
		'trail_size'              => '72px',
		'animation_speed'         => '0.2',
		'opacity'                 => '1',
		'blend_mode'              => 'normal',
		'hover_scale'             => '2',
		'hover_color'             => '',
		'hover_target_selector'   => 'a, button, [role="button"], #submit',
		'hide_default_cursor'     => 'on',
		'cursor_scope'              => 'entire_site',
		'cursor_scope_include_ids'  => '',
		'cursor_scope_exclude_ids'  => '',
		'cursor_scope_page_ids'     => '',
	);

	/**
	 * Constructor.
	 */
	public function __construct() {
		$GLOBALS['dsm_custom_cursor_extension'] = $this;

		add_action( 'wp_enqueue_scripts', array( $this, 'maybe_enqueue_frontend_assets' ), 20 );
		add_action(
			'divi_visual_builder_rest_save_post',
			array( $this, 'save_page_settings_from_rest' ),
			10,
			2
		);
	}

	/**
	 * Default settings shape.
	 *
	 * @return array<string, mixed>
	 */
	public function get_default_settings(): array {
		return $this->default_settings;
	}

	/**
	 * @return array<string, mixed>
	 */
	public function get_settings(): array {
		$saved = get_option( self::OPTION_KEY, array() );

		if ( ! is_array( $saved ) ) {
			$saved = array();
		}

		$settings = wp_parse_args( $saved, $this->default_settings );

		return $this->normalize_scope_list_settings( $settings );
	}

	/**
	 * @param array<string, mixed> $settings Raw settings from REST/modal.
	 * @return bool
	 */
	public function save_settings( array $settings ): bool {
		$existing = get_option( self::OPTION_KEY, array() );

		if ( ! is_array( $existing ) ) {
			$existing = array();
		}

		$merged = wp_parse_args( $settings, wp_parse_args( $existing, $this->default_settings ) );

		$merged['is_enabled']            = $this->normalize_bool_flag( $merged['is_enabled'] ?? false );
		$merged['cursor_style']          = $this->sanitize_style( (string) $merged['cursor_style'] );
		$merged['primary_color']         = sanitize_text_field( (string) $merged['primary_color'] );
		$merged['secondary_color']       = sanitize_text_field( (string) $merged['secondary_color'] );
		$merged['cursor_size']           = sanitize_text_field( (string) $merged['cursor_size'] );
		$merged['trail_size']            = sanitize_text_field( (string) $merged['trail_size'] );
		$merged['animation_speed']       = sanitize_text_field( (string) $merged['animation_speed'] );
		$merged['opacity']               = sanitize_text_field( (string) $merged['opacity'] );
		$merged['blend_mode']            = $this->sanitize_blend_mode( (string) $merged['blend_mode'] );
		$merged['hover_scale']           = sanitize_text_field( (string) $merged['hover_scale'] );
		$merged['hover_color']           = sanitize_text_field( (string) $merged['hover_color'] );
		$merged['hover_target_selector'] = sanitize_text_field( (string) $merged['hover_target_selector'] );
		$merged['hide_default_cursor']   = 'on' === $merged['hide_default_cursor'] ? 'on' : 'off';
		$merged['cursor_scope'] = $this->sanitize_scope( (string) $merged['cursor_scope'] );

		$merged = $this->normalize_scope_list_settings( $merged );

		$merged['cursor_scope_include_ids'] = $this->sanitize_page_ids(
			(string) ( $merged['cursor_scope_include_ids'] ?? '' )
		);
		$merged['cursor_scope_exclude_ids'] = $this->sanitize_page_ids(
			(string) ( $merged['cursor_scope_exclude_ids'] ?? '' )
		);
		$merged['cursor_scope_page_ids']    = '';

		update_option( self::OPTION_KEY, $merged );

		return true;
	}

	/**
	 * Normalize on/off, bool, and numeric flags from REST/modal payloads.
	 *
	 * @param mixed $value Raw value.
	 * @return bool
	 */
	private function normalize_bool_flag( $value ): bool {
		if ( is_bool( $value ) ) {
			return $value;
		}

		if ( is_int( $value ) ) {
			return 1 === $value;
		}

		if ( is_string( $value ) ) {
			$normalized = strtolower( trim( $value ) );

			if ( in_array( $normalized, array( '1', 'on', 'true', 'yes' ), true ) ) {
				return true;
			}

			if ( in_array( $normalized, array( '0', 'off', 'false', 'no', '' ), true ) ) {
				return false;
			}
		}

		return ! empty( $value );
	}

	/**
	 * Whether saved settings should activate the cursor on the frontend.
	 *
	 * @param array<string, mixed> $settings Merged settings.
	 * @return bool
	 */
	private function is_cursor_active_for_frontend( array $settings ): bool {
		return $this->normalize_bool_flag( $settings['is_enabled'] ?? false );
	}

	/**
	 * @param string $style Cursor style slug.
	 * @return string
	 */
	private function sanitize_style( string $style ): string {
		$allowed = array(
			'filter_glow_6',
			'filter_glow_5',
			'filter_glow_4',
			'filter_glow_3',
			'filter_glow_2',
			'filter_glow_1',
			'filled_circle',
			'two_circles',
			'svg_emitter',
		);
		$style = sanitize_key( $style );

		return in_array( $style, $allowed, true ) ? $style : 'filled_circle';
	}

	/**
	 * @param string $blend CSS mix-blend-mode value.
	 * @return string
	 */
	private function sanitize_blend_mode( string $blend ): string {
		$allowed = array(
			'normal',
			'multiply',
			'screen',
			'overlay',
			'darken',
			'lighten',
			'color-dodge',
			'color-burn',
			'hard-light',
			'soft-light',
			'difference',
			'exclusion',
			'hue',
			'saturation',
			'color',
			'luminosity',
		);
		$blend = sanitize_key( $blend );

		return in_array( $blend, $allowed, true ) ? $blend : 'normal';
	}

	/**
	 * @param string $scope Scope slug.
	 * @return string
	 */
	private function sanitize_scope( string $scope ): string {
		$allowed = array( 'entire_site', 'include_pages', 'exclude_pages', 'per_page' );
		$scope   = sanitize_key( $scope );

		return in_array( $scope, $allowed, true ) ? $scope : 'entire_site';
	}

	/**
	 * @param string $raw Comma-separated post IDs.
	 * @return string
	 */
	private function sanitize_page_ids( string $raw ): string {
		$parts = array_filter(
			array_map(
				static function ( $id ) {
					$id = absint( trim( (string) $id ) );
					return $id > 0 ? (string) $id : '';
				},
				explode( ',', $raw )
			)
		);

		return implode( ',', array_unique( $parts ) );
	}

	/**
	 * Migrate legacy single list into separate include/exclude keys when needed.
	 *
	 * @param array<string, mixed> $settings Settings array.
	 * @return array<string, mixed>
	 */
	private function normalize_scope_list_settings( array $settings ): array {
		$include = (string) ( $settings['cursor_scope_include_ids'] ?? '' );
		$exclude = (string) ( $settings['cursor_scope_exclude_ids'] ?? '' );
		$legacy  = (string) ( $settings['cursor_scope_page_ids'] ?? '' );

		if ( '' === trim( $include ) && '' === trim( $exclude ) && '' !== trim( $legacy ) ) {
			$scope = $this->sanitize_scope( (string) ( $settings['cursor_scope'] ?? 'entire_site' ) );

			if ( 'exclude_pages' === $scope ) {
				$exclude = $legacy;
			} elseif ( 'include_pages' === $scope ) {
				$include = $legacy;
			}
		}

		$settings['cursor_scope_include_ids'] = $this->sanitize_page_ids( $include );
		$settings['cursor_scope_exclude_ids'] = $this->sanitize_page_ids( $exclude );

		return $settings;
	}

	/**
	 * Parse comma-separated post IDs into int list.
	 *
	 * @param string $raw Comma-separated IDs.
	 * @return int[]
	 */
	private function parse_page_ids_string( string $raw ): array {
		if ( '' === trim( $raw ) ) {
			return array();
		}

		return array_values(
			array_filter(
				array_map(
					'absint',
					explode( ',', $raw )
				)
			)
		);
	}

	/**
	 * Scope IDs for the active display mode.
	 *
	 * @param array<string, mixed> $settings Settings array.
	 * @return int[]
	 */
	private function get_scope_ids_for_active_mode( array $settings ): array {
		$scope = $this->sanitize_scope( (string) ( $settings['cursor_scope'] ?? 'entire_site' ) );

		if ( 'include_pages' === $scope ) {
			return $this->parse_page_ids_string(
				(string) ( $settings['cursor_scope_include_ids'] ?? '' )
			);
		}

		if ( 'exclude_pages' === $scope ) {
			return $this->parse_page_ids_string(
				(string) ( $settings['cursor_scope_exclude_ids'] ?? '' )
			);
		}

		return array();
	}

	/**
	 * Whether the cursor should load on the current frontend request.
	 *
	 * @return bool
	 */
	public function should_load_on_current_request(): bool {
		if ( ! function_exists( 'dsm_is_custom_cursor_extension_enabled' )
			|| ! dsm_is_custom_cursor_extension_enabled() ) {
			return false;
		}

		if ( is_admin() ) {
			return false;
		}

		// Visual Builder canvas iframe: preview lives only in the modal pane (app window).
		if ( function_exists( 'et_core_is_fb_enabled' ) && et_core_is_fb_enabled() ) {
			return false;
		}

		$settings = $this->get_settings();

		if ( ! $this->is_cursor_active_for_frontend( $settings ) ) {
			return false;
		}

		$post_id = (int) get_queried_object_id();
		$scope   = $this->sanitize_scope( (string) ( $settings['cursor_scope'] ?? 'entire_site' ) );
		$ids     = $this->get_scope_ids_for_active_mode( $settings );

		switch ( $scope ) {
			case 'include_pages':
				return $post_id > 0 && in_array( $post_id, $ids, true );

			case 'exclude_pages':
				return $post_id <= 0 || ! in_array( $post_id, $ids, true );

			case 'per_page':
				if ( $post_id <= 0 ) {
					return false;
				}
				$page_flag = get_post_meta( $post_id, self::PAGE_META_KEY, true );
				return 'on' === $page_flag;

			case 'entire_site':
			default:
				return true;
		}
	}

	/**
	 * Persist per-page enable flag from Visual Builder save payload.
	 *
	 * @param int                  $post_id Post ID.
	 * @param array<string, mixed> $data    REST save payload.
	 * @return void
	 */
	public function save_page_settings_from_rest( int $post_id, array $data ): void {
		if ( $post_id <= 0 ) {
			return;
		}

		$page_settings = $data['pageSettings'] ?? array();
		if ( ! is_array( $page_settings ) ) {
			return;
		}

		if ( ! array_key_exists( 'dsm_custom_cursor_page_enabled', $page_settings ) ) {
			return;
		}

		$value = 'on' === $page_settings['dsm_custom_cursor_page_enabled'] ? 'on' : 'off';
		update_post_meta( $post_id, self::PAGE_META_KEY, $value );
	}

	/**
	 * Per-page enable flag for scope `per_page`.
	 *
	 * @param int $post_id Post ID.
	 * @return string `on` or `off`.
	 */
	public function get_page_enabled( int $post_id ): string {
		if ( $post_id <= 0 ) {
			return 'off';
		}

		$flag = get_post_meta( $post_id, self::PAGE_META_KEY, true );

		return 'on' === $flag ? 'on' : 'off';
	}

	/**
	 * Frontend script config for wp_localize_script.
	 *
	 * @return array<string, mixed>
	 */
	public function get_frontend_config(): array {
		$settings = $this->get_settings();

		return array(
			'cursor_style'            => (string) $settings['cursor_style'],
			'style'                   => (string) $settings['cursor_style'],
			'primary_color'           => (string) $settings['primary_color'],
			'secondary_color'         => (string) $settings['secondary_color'],
			'cursor_size'             => (string) $settings['cursor_size'],
			'trail_size'              => (string) $settings['trail_size'],
			'animation_speed'         => (string) $settings['animation_speed'],
			'opacity'                 => (string) $settings['opacity'],
			'blend_mode'              => (string) $settings['blend_mode'],
			'hover_scale'             => (string) $settings['hover_scale'],
			'hover_color'             => (string) $settings['hover_color'],
			'hover_target_selector'   => (string) $settings['hover_target_selector'],
			'hide_default_cursor'     => (string) $settings['hide_default_cursor'],
		);
	}

	/**
	 * Enqueue frontend bundle when scope matches.
	 *
	 * @return void
	 */
	public function maybe_enqueue_frontend_assets(): void {
		if ( DsmFrontendAssetRegistry::should_use_divi_dynamic_assets_pipeline() ) {
			return;
		}
		if ( ! $this->should_load_on_current_request() ) {
			return;
		}

		if ( ! wp_script_is( 'dsm-custom-cursor-frontend', 'registered' ) ) {
			return;
		}

		wp_enqueue_style( 'dsm-custom-cursor-frontend' );
		wp_enqueue_script( 'dsm-custom-cursor-frontend' );

		wp_localize_script(
			'dsm-custom-cursor-frontend',
			'dsmCustomCursorConfig',
			$this->get_frontend_config()
		);
	}
}

new CustomCursor();
