<?php
/**
 * Sanitization Utilities
 *
 * @package SEOvaultSEO
 */

namespace SEOvaultSEO\Utils;

/**
 * Provides sanitization functions for SEO data
 */
class Sanitizer {

	/**
	 * Sanitize SEO title
	 *
	 * @param mixed $value Input value
	 * @return string
	 */
	public static function sanitize_title( $value ) {
		return sanitize_text_field( $value );
	}

	/**
	 * Sanitize SEO description
	 *
	 * @param mixed $value Input value
	 * @return string
	 */
	public static function sanitize_description( $value ) {
		return sanitize_textarea_field( $value );
	}

	/**
	 * Sanitize focus keyword
	 *
	 * @param mixed $value Input value
	 * @return string
	 */
	public static function sanitize_keyword( $value ) {
		return sanitize_text_field( $value );
	}

	/**
	 * Sanitize secondary keywords (comma-separated)
	 *
	 * @param mixed $value Input value
	 * @return string
	 */
	public static function sanitize_secondary_keywords( $value ) {
		return sanitize_text_field( $value );
	}

	/**
	 * Sanitize canonical URL
	 *
	 * @param mixed $value Input value
	 * @return string
	 */
	public static function sanitize_canonical_url( $value ) {
		return esc_url_raw( $value );
	}

	/**
	 * Sanitize OG title
	 *
	 * @param mixed $value Input value
	 * @return string
	 */
	public static function sanitize_og_title( $value ) {
		return sanitize_text_field( $value );
	}

	/**
	 * Sanitize OG description
	 *
	 * @param mixed $value Input value
	 * @return string
	 */
	public static function sanitize_og_description( $value ) {
		return sanitize_textarea_field( $value );
	}

	/**
	 * Sanitize OG image URL
	 *
	 * @param mixed $value Input value
	 * @return string
	 */
	public static function sanitize_og_image( $value ) {
		return esc_url_raw( $value );
	}

	/**
	 * Sanitize schema JSON.
	 *
	 * Valid input is normalized. Callers that intentionally retain malformed
	 * editor drafts must handle the empty-string return separately.
	 *
	 * @param mixed $value Input value
	 * @return string
	 */
	public static function sanitize_schema_json( $value ) {
		if ( empty( $value ) ) {
			return '';
		}

		// If it's already a string, validate and decode it.
		if ( is_string( $value ) ) {
			$decoded = json_decode( $value, true );
			if ( JSON_ERROR_NONE !== json_last_error() || ! is_array( $decoded ) ) {
				return '';
			}

			$value = $decoded;
		}

		if ( ! is_array( $value ) ) {
			return '';
		}

		$encoded = wp_json_encode(
			self::sanitize_schema_data( $value ),
			JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
		);

		return is_string( $encoded ) ? $encoded : '';
	}

	/**
	 * Recursively sanitize decoded JSON-LD without changing JSON scalar types.
	 *
	 * Schema data may legitimately contain numbers, booleans, and null values.
	 * Applying sanitize_text_field() indiscriminately would convert those values
	 * to strings and change the schema's meaning. String values are therefore
	 * stripped of markup while valid JSON scalar types are retained.
	 *
	 * @param mixed $value Decoded JSON-LD value.
	 * @return mixed Sanitized JSON-compatible value.
	 */
	public static function sanitize_schema_data( $value ) {
		if ( is_array( $value ) ) {
			$sanitized = array();

			foreach ( $value as $key => $item ) {
				$safe_key = is_int( $key ) ? $key : sanitize_text_field( (string) $key );
				if ( ! is_int( $safe_key ) && '' === $safe_key ) {
					continue;
				}

				$sanitized[ $safe_key ] = self::sanitize_schema_data( $item );
			}

			return $sanitized;
		}

		if ( is_string( $value ) ) {
			return wp_strip_all_tags( wp_check_invalid_utf8( $value ), true );
		}

		if ( is_int( $value ) || is_bool( $value ) || null === $value ) {
			return $value;
		}

		if ( is_float( $value ) ) {
			return is_finite( $value ) ? $value : null;
		}

		return null;
	}

	/**
	 * Sanitize robots policy
	 *
	 * @param mixed $value Input value
	 * @return string
	 */
	public static function sanitize_robots_policy( $value ) {
		$allowed = array( '', 'balanced', 'maximum_visibility', 'strict', 'custom' );
		$value   = sanitize_text_field( $value );
		return in_array( $value, $allowed, true ) ? $value : '';
	}

	/**
	 * Sanitize custom robots.txt content (plain directives only; no HTML/PHP).
	 *
	 * @param mixed $value Input value.
	 * @return string
	 */
	public static function sanitize_robots_custom_content( $value ) {
		if ( ! is_string( $value ) ) {
			return '';
		}
		// Strip tags while preserving newlines used by robots.txt directives.
		return sanitize_textarea_field( $value );
	}

	/**
	 * Validate/sanitize SEO import provider slug.
	 *
	 * @param mixed $value Input value.
	 * @return string|null Sanitized provider slug, or null when empty (auto-detect).
	 */
	public static function sanitize_import_provider( $value ) {
		if ( null === $value || '' === $value ) {
			return null;
		}
		$value   = sanitize_text_field( (string) $value );
		$allowed = array(
			\SEOvaultSEO\Compatibility\Detector::PROVIDER_YOAST,
			\SEOvaultSEO\Compatibility\Detector::PROVIDER_RANKMATH,
			\SEOvaultSEO\Compatibility\Detector::PROVIDER_AIOSEO,
			\SEOvaultSEO\Compatibility\Detector::PROVIDER_SLIMSEO,
			\SEOvaultSEO\Compatibility\Detector::PROVIDER_THESEOFRAMEWORK,
		);
		return in_array( $value, $allowed, true ) ? $value : '';
	}

	/**
	 * Sanitize output mode
	 *
	 * @param mixed $value Input value
	 * @return string
	 */
	public static function sanitize_output_mode( $value ) {
		$allowed = array( 'auto', 'manual', 'disabled' );
		$value   = sanitize_text_field( $value );
		return in_array( $value, $allowed, true ) ? $value : 'auto';
	}

	/**
	 * Sanitize boolean yes/no
	 *
	 * @param mixed $value Input value
	 * @return string
	 */
	public static function sanitize_yes_no( $value ) {
		$value = sanitize_text_field( $value );
		return in_array( $value, array( 'yes', 'no' ), true ) ? $value : 'no';
	}

	/**
	 * Sanitize integer ID
	 *
	 * @param mixed $value Input value
	 * @return int
	 */
	public static function sanitize_id( $value ) {
		return absint( $value );
	}

	/**
	 * Sanitize llms.txt content
	 *
	 * @param mixed $value Input value
	 * @return string
	 */
	public static function sanitize_llms_content( $value ) {
		// Allow plain text content for llms files
		if ( is_string( $value ) ) {
			// Remove any potentially harmful content but preserve markdown
			return wp_kses_post( $value );
		}
		return '';
	}

	/**
	 * Sanitize frontend score position.
	 *
	 * @param mixed $value Input value.
	 * @return string
	 */
	public static function sanitize_frontend_score_position( $value ) {
		$allowed = array( 'top', 'bottom', 'both', 'custom' );
		$value   = sanitize_text_field( $value );
		return in_array( $value, $allowed, true ) ? $value : 'bottom';
	}

	/**
	 * Sanitize frontend score post types array.
	 *
	 * @param mixed $value Input value.
	 * @return array
	 */
	public static function sanitize_frontend_score_post_types( $value ) {
		if ( ! is_array( $value ) ) {
			return array();
		}

		$public_types = get_post_types( array( 'public' => true ), 'names' );
		$clean        = array();

		foreach ( $value as $type ) {
			$type = sanitize_text_field( $type );
			if ( in_array( $type, $public_types, true ) ) {
				$clean[] = $type;
			}
		}

		return $clean;
	}

	/**
	 * Sanitize GA tracking method.
	 *
	 * Only the programmatically-generated gtag method is supported; the legacy
	 * arbitrary-JavaScript "custom" method was removed for review compliance.
	 *
	 * @param mixed $value Input value.
	 * @return string
	 */
	public static function sanitize_ga_tracking_method( $value ) {
		$allowed = array( 'gtag' );
		$value   = sanitize_text_field( $value );
		return in_array( $value, $allowed, true ) ? $value : 'gtag';
	}

	/**
	 * Sanitize GA4 Measurement ID (e.g. G-XXXXXXXXXX).
	 *
	 * @param mixed $value Input value.
	 * @return string
	 */
	public static function sanitize_ga_measurement_id( $value ) {
		$value = sanitize_text_field( $value );
		if ( '' === $value ) {
			return '';
		}
		if ( preg_match( '/^G-[A-Z0-9]{6,}$/', $value ) ) {
			return $value;
		}
		if ( preg_match( '/^UA-[0-9]+-[0-9]+$/', $value ) ) {
			return $value;
		}
		if ( preg_match( '/^AW-[A-Z0-9_-]+$/', $value ) ) {
			return $value;
		}
		return '';
	}

	/**
	 * Sanitize IndexNow post types array.
	 *
	 * Only allows public post types.
	 *
	 * @param mixed $value Input value.
	 * @return array
	 */
	public static function sanitize_indexnow_post_types( $value ) {
		if ( ! is_array( $value ) ) {
			return array();
		}

		$public_types = get_post_types( array( 'public' => true ), 'names' );
		$clean        = array();

		foreach ( $value as $type ) {
			$type = sanitize_text_field( $type );
			if ( in_array( $type, $public_types, true ) ) {
				$clean[] = $type;
			}
		}

		return $clean;
	}

	/**
	 * Sanitize post types selected for default noindex.
	 *
	 * @param mixed $value Input value.
	 * @return array
	 */
	public static function sanitize_noindex_post_types( $value ) {
		$clean = self::sanitize_indexnow_post_types( $value );
		return array_values( array_diff( $clean, array( 'attachment' ) ) );
	}

	/**
	 * Sanitize IndexNow URLs for manual submission.
	 *
	 * Accepts a newline-separated string or an array of URLs.
	 * Returns an array of valid URLs.
	 *
	 * @param mixed $value Input value.
	 * @return array
	 */
	public static function sanitize_indexnow_urls( $value ) {
		if ( is_string( $value ) ) {
			$value = array_filter( array_map( 'trim', explode( "\n", $value ) ) );
		}

		if ( ! is_array( $value ) ) {
			return array();
		}

		$clean = array();
		foreach ( $value as $url ) {
			$url = esc_url_raw( trim( $url ) );
			if ( '' !== $url ) {
				$clean[] = $url;
			}
		}

		return $clean;
	}

	/**
	 * Sanitize boolean yes/no with default 'yes'.
	 *
	 * Used for autopilot options that should default to enabled.
	 *
	 * @param mixed $value Input value.
	 * @return string 'yes' or 'no'
	 */
	public static function sanitize_yes_no_default_yes( $value ) {
		$value = sanitize_text_field( $value );
		return in_array( $value, array( 'yes', 'no' ), true ) ? $value : 'yes';
	}

	/**
	 * Sanitize robots max-image-preview value.
	 *
	 * @param mixed $value Input value.
	 * @return string 'none', 'standard', or 'large'
	 */
	public static function sanitize_image_preview_size( $value ) {
		$allowed = array( 'none', 'standard', 'large' );
		$value   = sanitize_text_field( $value );
		return in_array( $value, $allowed, true ) ? $value : 'large';
	}

	/**
	 * Sanitize robots max-snippet length.
	 *
	 * -1 = unlimited, 0 = disabled, positive integer = character limit.
	 *
	 * @param mixed $value Input value.
	 * @return int
	 */
	public static function sanitize_snippet_length( $value ) {
		$value = (int) $value;
		if ( $value < -1 ) {
			return -1;
		}
		return $value;
	}

	/**
	 * Sanitize robots max-video-preview length.
	 *
	 * -1 = unlimited, 0 = disabled, positive integer = seconds limit.
	 *
	 * @param mixed $value Input value.
	 * @return int
	 */
	public static function sanitize_video_preview_length( $value ) {
		$value = (int) $value;
		if ( $value < -1 ) {
			return -1;
		}
		return $value;
	}

	/**
	 * Sanitize feed excerpt length.
	 *
	 * Normalizes to a minimum of 1. Non-numeric or zero values default to 400.
	 *
	 * @param mixed $value Input value.
	 * @return int
	 */
	public static function sanitize_feed_excerpt_length( $value ) {
		$value = (int) $value;
		if ( $value < 1 ) {
			return 400;
		}
		return $value;
	}

	/**
	 * Sanitize feed boolean option (1/0).
	 *
	 * @param mixed $value Input value.
	 * @return string '1' or '0'
	 */
	public static function sanitize_feed_bool( $value ) {
		return $value ? '1' : '0';
	}

	/**
	 * Sanitize Twitter card type.
	 *
	 * Whitelist sanitizer — only 'summary_large_image' and 'summary' are valid.
	 * Any other value returns the default 'summary_large_image'.
	 *
	 * @param mixed $value Input value.
	 * @return string 'summary_large_image' or 'summary'
	 */
	public static function sanitize_twitter_card_type( $value ) {
		$value = sanitize_text_field( $value );
		if ( 'summary_large_image' === $value || 'summary' === $value ) {
			return $value;
		}
		return 'summary_large_image';
	}

	/**
	 * Sanitize social profiles array.
	 *
	 * Accepts an array of URL strings. Each URL is trimmed and passed through
	 * esc_url_raw(). Empty results are filtered out. Duplicate URLs are removed.
	 *
	 * @param mixed $value Input value.
	 * @return array Clean array of unique URLs.
	 */
	public static function sanitize_social_profiles( $value ) {
		if ( ! is_array( $value ) ) {
			return array();
		}

		$clean = array();
		foreach ( $value as $url ) {
			$url = trim( $url );
			if ( '' === $url || ! filter_var( $url, FILTER_VALIDATE_URL ) ) {
				continue;
			}
			$url = esc_url_raw( $url );
			if ( '' !== $url && ! in_array( $url, $clean, true ) ) {
				$clean[] = $url;
			}
		}

		return $clean;
	}
}
