<?php
/**
 * Centralized sanitization helpers. Every untrusted value MUST pass through here
 * before reaching the database or being rendered to the user.
 *
 * @package SuiteMkt3k
 */

namespace SuiteMkt3k\Utils;

defined( 'ABSPATH' ) || exit;

final class Sanitizer {

	private static function substr_limit( string $value, int $max_len ): string {
		if ( function_exists( 'mb_substr' ) ) {
			return mb_substr( $value, 0, $max_len );
		}
		return substr( $value, 0, $max_len );
	}

	public static function text( $value, int $max_len = 255 ): string {
		$value = is_scalar( $value ) ? (string) $value : '';
		$value = sanitize_text_field( wp_unslash( $value ) );
		return self::substr_limit( $value, $max_len );
	}

	public static function textarea( $value, int $max_len = 5000 ): string {
		$value = is_scalar( $value ) ? (string) $value : '';
		$value = sanitize_textarea_field( wp_unslash( $value ) );
		return self::substr_limit( $value, $max_len );
	}

	public static function email( $value ): string {
		$value = is_scalar( $value ) ? (string) $value : '';
		$value = sanitize_email( wp_unslash( $value ) );
		return is_email( $value ) ? strtolower( $value ) : '';
	}

	public static function url( $value ): string {
		$value = is_scalar( $value ) ? (string) $value : '';
		return esc_url_raw( wp_unslash( $value ), array( 'http', 'https' ) );
	}

	public static function int( $value, int $min = PHP_INT_MIN, int $max = PHP_INT_MAX ): int {
		$value = (int) $value;
		return max( $min, min( $max, $value ) );
	}

	public static function float( $value, float $min = -INF, float $max = INF ): float {
		$value = (float) $value;
		return max( $min, min( $max, $value ) );
	}

	public static function bool( $value ): bool {
		return filter_var( $value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE ) ?? false;
	}

	public static function hex_color( $value ): string {
		$value = is_scalar( $value ) ? (string) $value : '';
		$value = trim( wp_unslash( $value ) );
		if ( preg_match( '/^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$/', $value ) ) {
			return strtoupper( $value );
		}
		return '';
	}

	public static function slug( $value, int $max_len = 60 ): string {
		$value = is_scalar( $value ) ? (string) $value : '';
		$value = sanitize_key( wp_unslash( $value ) );
		return self::substr_limit( $value, $max_len );
	}

	public static function date( $value ): ?string {
		$value = is_scalar( $value ) ? (string) $value : '';
		$value = trim( wp_unslash( $value ) );
		if ( '' === $value ) {
			return null;
		}
		$ts = strtotime( $value );
		return false === $ts ? null : gmdate( 'Y-m-d', $ts );
	}

	public static function datetime( $value ): ?string {
		$value = is_scalar( $value ) ? (string) $value : '';
		$value = trim( wp_unslash( $value ) );
		if ( '' === $value ) {
			return null;
		}
		$ts = strtotime( $value );
		return false === $ts ? null : gmdate( 'Y-m-d H:i:s', $ts );
	}

	/**
	 * Sanitize HTML allowing only a tight whitelist suitable for email/preheader/marketing copy.
	 */
	public static function html( $value ): string {
		$value = is_scalar( $value ) ? (string) $value : '';
		$allowed = array(
			'a'      => array(
				'href'   => true,
				'title'  => true,
				'target' => true,
				'rel'    => true,
			),
			'b'      => array(),
			'br'     => array(),
			'em'     => array(),
			'i'      => array(),
			'p'      => array( 'style' => true ),
			'span'   => array( 'style' => true ),
			'strong' => array(),
			'u'      => array(),
			'ul'     => array(),
			'ol'     => array(),
			'li'     => array(),
			'h1'     => array( 'style' => true ),
			'h2'     => array( 'style' => true ),
			'h3'     => array( 'style' => true ),
			'h4'     => array( 'style' => true ),
		);
		return wp_kses( wp_unslash( $value ), $allowed );
	}

	/**
	 * Read a sanitized value from $_GET.
	 *
	 * @param string $key    Query parameter name.
	 * @param string $format text|key|email|int.
	 */
	public static function get( string $key, string $format = 'text' ): string {
		return self::read_input( INPUT_GET, $key, $format );
	}

	/**
	 * Read a sanitized value from $_POST.
	 *
	 * @param string $key    Field name.
	 * @param string $format text|key|email|int.
	 */
	public static function post( string $key, string $format = 'text' ): string {
		return self::read_input( INPUT_POST, $key, $format );
	}

	/**
	 * Sanitized associative array from $_POST (e.g. design[...] payloads).
	 *
	 * @return array<string, mixed>
	 */
	public static function post_array( string $key ): array {
		$raw = filter_input( INPUT_POST, $key, FILTER_DEFAULT, FILTER_REQUIRE_ARRAY );
		if ( ! is_array( $raw ) ) {
			return array();
		}
		return self::recursive_text( wp_unslash( $raw ) );
	}

	/**
	 * Build sanitized admin.php query args for the language-switch referer.
	 *
	 * @return array<string, int|string>
	 */
	public static function admin_screen_query_args( string $default_page ): array {
		$args = array();
		$raw  = filter_input_array( INPUT_GET, FILTER_DEFAULT );
		if ( ! is_array( $raw ) ) {
			$args['page'] = sanitize_key( $default_page );
			return $args;
		}
		foreach ( wp_unslash( $raw ) as $key => $value ) {
			if ( ! is_string( $key ) || is_array( $value ) ) {
				continue;
			}
			$key = sanitize_key( $key );
			if ( in_array( $key, array( 'action', '_wpnonce', '_wp_http_referer' ), true ) ) {
				continue;
			}
			if ( 'page' === $key ) {
				$args['page'] = sanitize_key( (string) $value );
			} elseif ( in_array( $key, array( 'id', 'form_id', 'entity_id' ), true ) ) {
				$args[ $key ] = (int) $value;
			} elseif ( 's' === $key ) {
				$args['s'] = sanitize_text_field( (string) $value );
			} else {
				$args[ $key ] = sanitize_key( (string) $value );
			}
		}
		if ( empty( $args['page'] ) ) {
			$args['page'] = sanitize_key( $default_page );
		}
		return $args;
	}

	/**
	 * Read a sanitized value from $_SERVER.
	 *
	 * @param string $key    Server key.
	 * @param string $format text|key|email|int.
	 */
	public static function server( string $key, string $format = 'text' ): string {
		return self::read_input( INPUT_SERVER, $key, $format );
	}

	/**
	 * True when a GET/POST flag param is present and equals "1".
	 */
	public static function flag( int $source, string $key ): bool {
		$value = self::read_input( $source, $key, 'key' );
		return '1' === $value || 'true' === $value || 'yes' === $value;
	}

	/**
	 * Decode a JSON request field and recursively sanitize scalar values.
	 *
	 * @return array<string, mixed>
	 */
	public static function json_array( $raw ): array {
		if ( ! is_string( $raw ) || '' === $raw ) {
			return array();
		}
		$decoded = json_decode( wp_unslash( $raw ), true );
		if ( ! is_array( $decoded ) ) {
			return array();
		}
		return self::recursive_text( $decoded );
	}

	/**
	 * Decode JSON from a POST field name.
	 *
	 * @return array<string, mixed>
	 */
	public static function json_post( string $key ): array {
		$raw = filter_input( INPUT_POST, $key, FILTER_DEFAULT );
		return self::json_array( is_string( $raw ) ? $raw : '' );
	}

	/**
	 * @param int    $type   INPUT_GET|INPUT_POST|INPUT_SERVER.
	 * @param string $key    Parameter name.
	 * @param string $format text|key|email|int.
	 */
	private static function read_input( int $type, string $key, string $format ): string {
		$value = filter_input( $type, $key, FILTER_DEFAULT );
		if ( null === $value || false === $value ) {
			return '';
		}
		if ( ! is_scalar( $value ) ) {
			return '';
		}
		$value = (string) wp_unslash( $value );
		switch ( $format ) {
			case 'key':
				return sanitize_key( $value );
			case 'email':
				$email = sanitize_email( $value );
				return is_email( $email ) ? strtolower( $email ) : '';
			case 'int':
				return (string) absint( $value );
			default:
				return sanitize_text_field( $value );
		}
	}

	/**
	 * Recursively sanitize an arbitrary array (used for design_json payloads).
	 */
	public static function recursive_text( array $data ): array {
		$out = array();
		foreach ( $data as $key => $value ) {
			$clean_key = sanitize_key( (string) $key );
			if ( is_array( $value ) ) {
				$out[ $clean_key ] = self::recursive_text( $value );
			} elseif ( is_bool( $value ) ) {
				$out[ $clean_key ] = (bool) $value;
			} elseif ( is_int( $value ) ) {
				$out[ $clean_key ] = (int) $value;
			} elseif ( is_float( $value ) ) {
				$out[ $clean_key ] = (float) $value;
			} else {
				$out[ $clean_key ] = self::text( $value, 5000 );
			}
		}
		return $out;
	}
}
