<?php
/**
 * Generic sanitization for module settings blobs stored under wp_powersuite_settings.
 *
 * @package WP_PowerSuite\Core
 * @since 1.0.0
 */

namespace WP_PowerSuite\Core;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Sanitizes module settings keys/values using per-module allowlists or REST-style key patterns.
 */
final class ModuleSettingsSanitizer {

	/**
	 * Top-level wp_powersuite_settings keys that are not module blobs.
	 *
	 * @var list<string>
	 */
	public const RESERVED_ROOT_KEYS = [
		'social_oauth_debug',
		'enabled_modules',
		'favorites',
		'favorites_customized',
		'legacy_favorites_customized_migration',
		'module_order',
		'quick_launcher_shortcut',
		'enable_performance_monitor',
		'interface_color_mode',
		'custom_brand_color',
		'ui_preferences',
		'white_label',
	];

	/**
	 * Sanitize module blobs inside a full settings document (import / bulk save).
	 *
	 * @param array<string, mixed> $settings Raw settings document.
	 * @param string               $context  `save` or `import`.
	 * @return array<string, mixed>
	 */
	public static function sanitizeSettingsDocument( array $settings, string $context = 'save' ): array {
		foreach ( $settings as $key => $value ) {
			if ( ! self::isModuleSettingsKey( (string) $key ) || ! is_array( $value ) ) {
				continue;
			}
			$settings[ $key ] = self::sanitizeModuleSettings( (string) $key, $value, $context );
		}

		return $settings;
	}

	/**
	 * Whether a top-level settings key holds a module configuration blob.
	 */
	public static function isModuleSettingsKey( string $key ): bool {
		$key = sanitize_key( $key );

		return $key !== '' && ! in_array( $key, self::RESERVED_ROOT_KEYS, true );
	}

	/**
	 * Sanitize one module's settings array.
	 *
	 * Modules may restrict keys via {@see 'wp_powersuite_module_settings_allowed_keys'} or replace
	 * sanitization via {@see 'wp_powersuite_sanitize_module_settings'}.
	 *
	 * @param string               $module_id Module ID (settings key).
	 * @param array<string, mixed> $settings  Raw module settings.
	 * @param string               $context   `save` or `import`.
	 * @return array<string, mixed>
	 */
	public static function sanitizeModuleSettings( string $module_id, array $settings, string $context = 'save' ): array {
		$module_id = sanitize_key( $module_id );
		if ( $module_id === '' ) {
			return [];
		}

		/**
		 * Short-circuit or pre-process module settings before generic sanitization.
		 *
		 * @param array<string, mixed> $settings  Module settings.
		 * @param string               $module_id Module ID.
		 * @param string               $context   `save` or `import`.
		 */
		$settings = apply_filters( 'wp_powersuite_sanitize_module_settings', $settings, $module_id, $context );
		if ( ! is_array( $settings ) ) {
			return [];
		}

		/** @var list<string>|null $allowed */
		$allowed = apply_filters( 'wp_powersuite_module_settings_allowed_keys', null, $module_id, $context );
		$allowed = is_array( $allowed ) ? array_values( array_filter( array_map( 'sanitize_key', $allowed ) ) ) : null;

		$out = [];
		foreach ( $settings as $key => $value ) {
			$key = sanitize_key( (string) $key );
			if ( $key === '' ) {
				continue;
			}

			if ( is_array( $allowed ) && $allowed !== [] ) {
				if ( ! in_array( $key, $allowed, true ) ) {
					continue;
				}
			} elseif ( ! self::isAllowedGenericKey( $key ) ) {
				continue;
			}

			if ( $value === null ) {
				continue;
			}

			$out[ $key ] = self::sanitizeValue( $key, $value, $module_id, $context );
		}

		return $out;
	}

	/**
	 * @param string $key Setting key (already sanitize_key'd).
	 */
	private static function isAllowedGenericKey( string $key ): bool {
		return (bool) preg_match( '/^[a-z0-9_-]+$/', $key );
	}

	/**
	 * @param string $key Setting key.
	 * @param mixed  $value Raw value.
	 */
	private static function sanitizeValue( string $key, mixed $value, string $module_id, string $context ): mixed {
		if ( is_array( $value ) ) {
			if ( self::isListArray( $value ) ) {
				$sanitized = [];
				foreach ( $value as $item ) {
					if ( $item === null ) {
						continue;
					}
					if ( is_array( $item ) ) {
						$sanitized[] = self::sanitizeModuleSettings( $module_id, $item, $context );
						continue;
					}
					$sanitized[] = self::sanitizeScalar( $key, $item, $context );
				}
				return $sanitized;
			}

			$nested = [];
			foreach ( $value as $sub_key => $sub_value ) {
				$sub_key = is_int( $sub_key ) ? $sub_key : sanitize_key( (string) $sub_key );
				if ( $sub_key === '' || $sub_value === null ) {
					continue;
				}
				if ( is_int( $sub_key ) ) {
					$nested[ $sub_key ] = is_array( $sub_value )
						? self::sanitizeValue( $key, $sub_value, $module_id, $context )
						: self::sanitizeScalar( $key, $sub_value, $context );
					continue;
				}
				if ( ! self::isAllowedGenericKey( (string) $sub_key ) ) {
					continue;
				}
				$nested[ $sub_key ] = is_array( $sub_value )
					? self::sanitizeValue( $sub_key, $sub_value, $module_id, $context )
					: self::sanitizeScalar( $sub_key, $sub_value, $context );
			}

			return $nested;
		}

		return self::sanitizeScalar( $key, $value, $context );
	}

	/**
	 * @param string $key Setting key.
	 * @param mixed  $value Scalar value.
	 */
	private static function sanitizeScalar( string $key, mixed $value, string $context ): mixed {
		if ( is_bool( $value ) || is_int( $value ) || is_float( $value ) ) {
			return $value;
		}

		if ( ! is_string( $value ) ) {
			return $value;
		}

		if ( $value === '1' || strcasecmp( $value, 'true' ) === 0 ) {
			return true;
		}
		if ( $value === '0' || strcasecmp( $value, 'false' ) === 0 ) {
			return false;
		}

		$lk = strtolower( $key );

		// OAuth client / app identifiers must keep dots and other provider-specific characters.
		if ( in_array( $lk, [ 'client_id', 'app_id' ], true ) ) {
			return sanitize_text_field( $value );
		}

		if ( str_ends_with( $lk, '_id' ) ) {
			return ctype_digit( $value ) ? max( 0, (int) $value ) : sanitize_key( $value );
		}

		if ( preg_match( '/(?:_url|_link|_uri|redirect|webhook)$/', $lk ) ) {
			return esc_url_raw( $value );
		}

		if ( str_ends_with( $lk, '_email' ) ) {
			return sanitize_email( $value );
		}

		if ( str_contains( $lk, 'color' ) ) {
			$hex = sanitize_hex_color( $value );
			return $hex ?? sanitize_text_field( $value );
		}

		if ( preg_match( '/(?:^content$|_html$|_content$|message$|template$|css$|javascript$|script$|code$)/', $lk ) ) {
			if ( current_user_can( 'unfiltered_html' ) ) {
				return $value;
			}
			return wp_kses_post( $value );
		}

		if ( preg_match( '/(?:password|secret|api_key|api-key|token|passphrase|private_key)/', $lk ) ) {
			return sanitize_text_field( $value );
		}

		return sanitize_text_field( $value );
	}

	/**
	 * @param array<mixed> $array
	 */
	private static function isListArray( array $array ): bool {
		if ( $array === [] ) {
			return true;
		}

		return array_keys( $array ) === range( 0, count( $array ) - 1 );
	}
}
