<?php
/**
 * OptionsSanitizer
 *
 * Loop-driven sanitizer powered by the Options schema.
 * Replaces the 360-line procedural sanitize_settings() from v1.9.x.
 * Adding a new option = one line in Options::defaults(). Nothing else changes here.
 *
 * @package Relatedly\Core
 * @since   2.0.0
 */

namespace Relatedly\Core;

use Relatedly\Premium\FeatureGate;

defined( 'ABSPATH' ) || exit;

/**
 * Sanitizes settings form input against the option schema.
 *
 * @since 2.0.0
 */
class OptionsSanitizer {

	/**
	 * Plugin settings repository.
	 *
	 * @since 2.0.0
	 * @var Options
	 */
	private Options $options;

	/**
	 * Create the sanitizer.
	 *
	 * @since 2.0.0
	 * @param Options $options Plugin settings repository.
	 */
	public function __construct( Options $options ) {
		$this->options = $options;
	}

	/**
	 * Sanitize raw form input against the schema and return a clean array.
	 *
	 * @since 2.0.0
	 * @since 2.1.1 Added $active_tab to preserve settings from other tabs.
	 *
	 * @param array<string,mixed> $input      Raw $_POST data (already wp_unslash'd by caller).
	 * @param string              $active_tab Tab being saved, e.g. 'general' or 'inline'.
	 * @return array<string,mixed>
	 */
	public function sanitize( array $input, string $active_tab = '' ): array {
		$defaults = $this->options->defaults();
		$current  = $this->options->all();
		$out      = [];

		foreach ( $defaults as $key => $default ) {
			$raw   = $input[ $key ] ?? null;
			$saved = $current[ $key ] ?? $default;

			// When saving a specific tab, preserve values that belong to other tabs.
			// This prevents each tab's form from resetting settings in other tabs.
			if ( '' !== $active_tab && $this->get_field_tab( $key ) !== $active_tab ) {
				$out[ $key ] = $saved;
				continue;
			}

			$out[ $key ] = $this->sanitize_field( $key, $raw, $default );
		}

		return $this->enforce_tier_limits( $out );
	}

	/**
	 * Enforce free/premium limits on saved values.
	 *
	 * @since 2.1.0
	 * @param array<string,mixed> $options Sanitized options.
	 * @return array<string,mixed>
	 */
	private function enforce_tier_limits( array $options ): array {
		$options['inline_max_posts']          = min(
			(int) ( $options['inline_max_posts'] ?? 1 ),
			FeatureGate::max_inline_posts()
		);
		$options['end_content_max_posts']     = min(
			(int) ( $options['end_content_max_posts'] ?? 5 ),
			FeatureGate::max_end_content_posts()
		);
		$options['rss_end_content_max_posts'] = min(
			(int) ( $options['rss_end_content_max_posts'] ?? 3 ),
			FeatureGate::max_rss_posts()
		);

		if ( isset( $options['post_types'] ) && is_array( $options['post_types'] ) ) {
			$options['post_types'] = FeatureGate::allowed_post_types( $options['post_types'] );
		}

		// Premium features cannot be enabled on the free tier.
		if ( ! FeatureGate::is_premium() ) {
			$options['floating_sidebar_enabled'] = false;
			$options['email_reports_enabled']    = false;
		}

		return $options;
	}


	// -------------------------------------------------------------------------
	// Field router
	// -------------------------------------------------------------------------

	/**
	 * Route a single field to the correct sanitizer based on its default type.
	 *
	 * @param string $key     Option key.
	 * @param mixed  $raw      Raw input (null when field absent from POST - e.g. unchecked checkbox).
	 * @param mixed  $fallback Schema default value.
	 * @return mixed
	 */
	private function sanitize_field( string $key, $raw, $fallback ) {

		// Boolean: absent from POST = unchecked checkbox = false. Never fall back to default.
		if ( is_bool( $fallback ) ) {
			return $this->sanitize_bool( $raw );
		}

		// Integer.
		if ( is_int( $fallback ) ) {
			return $this->sanitize_int( $raw, $fallback );
		}

		// Array (multi-select, checkboxes).
		if ( is_array( $fallback ) ) {
			return $this->sanitize_array( $raw );
		}

		// Colour fields - keys ending in _color, _bg, _start, _end, _line_color.
		if ( $this->is_color_key( $key ) ) {
			return $this->sanitize_color( $raw, (string) $fallback );
		}

		// CSS size / shorthand - keys ending in _size, _padding, _radius, _width.
		if ( $this->is_size_key( $key ) ) {
			return $this->sanitize_css_value( $raw, (string) $fallback );
		}

		// Generic string.
		return sanitize_text_field( (string) ( $raw ?? $fallback ) );
	}


	// -------------------------------------------------------------------------
	// Type sanitizers
	// -------------------------------------------------------------------------

	/**
	 * Sanitize a checkbox or toggle value.
	 *
	 * @param mixed $raw Raw input value.
	 * @return bool
	 */
	private function sanitize_bool( $raw ): bool {
		return ! empty( $raw ) && in_array( $raw, [ '1', 'true', 'on', true, 1 ], true );
	}

	/**
	 * Sanitize a non-negative integer.
	 *
	 * @param mixed $raw      Raw input value.
	 * @param int   $fallback Schema default value.
	 * @return int
	 */
	private function sanitize_int( $raw, int $fallback ): int {
		return ( null !== $raw ) ? max( 0, (int) $raw ) : $fallback;
	}

	/**
	 * Sanitize a list of text values.
	 *
	 * @param mixed $raw Raw input value.
	 * @return array<int,string>
	 */
	private function sanitize_array( $raw ): array {
		if ( ! is_array( $raw ) ) {
			return [];
		}
		return array_map( 'sanitize_text_field', $raw );
	}

	/**
	 * Sanitize color-like values used by theme controls.
	 *
	 * @param mixed  $raw      Raw input value.
	 * @param string $fallback Schema default value.
	 * @return string
	 */
	private function sanitize_color( $raw, string $fallback ): string {
		if ( null === $raw || '' === trim( (string) $raw ) ) {
			return $fallback;
		}
		$val = sanitize_text_field( (string) $raw );

		// Allow keyword values.
		if ( 'none' === strtolower( $val ) || 'transparent' === strtolower( $val ) ) {
			return 'transparent';
		}

		// Allow rgba() / rgb().
		if ( preg_match( '/^rgba?\s*\(/i', $val ) ) {
			return $val;
		}

		// Normalise hex: 3-digit to 6-digit.
		$hex = ltrim( $val, '#' );
		if ( strlen( $hex ) === 3 ) {
			$hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
		}
		if ( preg_match( '/^[0-9a-fA-F]{6}$/', $hex ) ) {
			return '#' . strtolower( $hex );
		}

		return $fallback;
	}

	/**
	 * Sanitize a constrained CSS size or spacing value.
	 *
	 * @param mixed  $raw      Raw input value.
	 * @param string $fallback Schema default value.
	 * @return string
	 */
	private function sanitize_css_value( $raw, string $fallback ): string {
		if ( null === $raw || '' === trim( (string) $raw ) ) {
			return $fallback;
		}
		$val = sanitize_text_field( (string) $raw );
		// Allow digits, spaces, dots, and common CSS units / shorthand.
		if ( preg_match( '/^[\d\s.]+(%|px|em|rem|vh|vw)?(\s+[\d.]+(%|px|em|rem|vh|vw)?)*$/', trim( $val ) ) ) {
			return $val;
		}
		return $fallback;
	}


	// -------------------------------------------------------------------------
	// Key classifiers
	// -------------------------------------------------------------------------

	/**
	 * Determine whether an option key stores a color value.
	 *
	 * @param string $key Option key.
	 * @return bool
	 */
	private function is_color_key( string $key ): bool {
		foreach ( [ '_color', '_bg', '_start', '_end', '_line_color' ] as $suffix ) {
			if ( substr( $key, -strlen( $suffix ) ) === $suffix ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Determine whether an option key stores a CSS size value.
	 *
	 * @param string $key Option key.
	 * @return bool
	 */
	private function is_size_key( string $key ): bool {
		foreach ( [ '_size', '_padding', '_radius', '_width' ] as $suffix ) {
			if ( substr( $key, -strlen( $suffix ) ) === $suffix ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Map an option key to its settings tab.
	 *
	 * Unmapped keys are preserved when saving from any tab.
	 *
	 * @since 2.1.1
	 * @param string $key Option key.
	 * @return string|null
	 */
	private function get_field_tab( string $key ): ?string {
		$tab_map = [
			// General.
			'enabled'                               => 'general',
			'post_types'                            => 'general',
			'related_method'                        => 'general',
			'link_target'                           => 'general',
			'link_rel'                              => 'general',
			'enable_cache'                          => 'general',
			'cache_duration'                        => 'general',
			'debug_mode'                            => 'general',
			'exclude_current_post'                  => 'general',
			'exclude_categories'                    => 'general',
			'exclude_tags'                          => 'general',
			'enable_click_tracking'                 => 'general',
			'data_retention_days'                   => 'general',

			// Inline.
			'inline_enabled'                        => 'inline',
			'inline_paragraph_interval'             => 'inline',
			'inline_theme'                          => 'inline',
			'inline_max_posts'                      => 'inline',

			// End content.
			'end_content_enabled'                   => 'end-content',
			'end_content_theme'                     => 'end-content',
			'end_content_max_posts'                 => 'end-content',
			'end_content_columns'                   => 'end-content',

			// Floating sidebar.
			'floating_sidebar_enabled'              => 'floating-sidebar',
			'floating_sidebar_variant'              => 'floating-sidebar',
			'floating_sidebar_position'             => 'floating-sidebar',
			'floating_sidebar_width'                => 'floating-sidebar',
			'floating_sidebar_num_posts'            => 'floating-sidebar',
			'floating_sidebar_display_trigger'      => 'floating-sidebar',
			'floating_sidebar_scroll_percentage'    => 'floating-sidebar',
			'floating_sidebar_dismiss_behavior'     => 'floating-sidebar',
			'floating_sidebar_auto_refresh_enabled' => 'floating-sidebar',
			'floating_sidebar_auto_refresh_seconds' => 'floating-sidebar',
			'floating_sidebar_z_index'              => 'floating-sidebar',

			// RSS.
			'rss_enabled'                           => 'rss',
			'rss_paragraph_interval'                => 'rss',
			'rss_end_content_enabled'               => 'rss',
			'rss_end_content_max_posts'             => 'rss',
			'utm_source'                            => 'rss',
			'utm_medium'                            => 'rss',
			'utm_campaign'                          => 'rss',
			'rss_utm_content_inline'                => 'rss',
			'rss_utm_content_end'                   => 'rss',

			// Email reports.
			'email_reports_enabled'                 => 'email',
			'email_reports_frequency'               => 'email',
			'email_reports_recipients'              => 'email',
			'email_reports_day'                     => 'email',
			'email_reports_time'                    => 'email',
			'email_reports_include_summary'         => 'email',
			'email_reports_include_top_posts'       => 'email',
			'email_reports_include_comparison'      => 'email',
			'email_reports_top_posts_count'         => 'email',
		];

		if ( isset( $tab_map[ $key ] ) ) {
			return $tab_map[ $key ];
		}

		// Theme colour fields are grouped by prefix.
		if ( strpos( $key, 'inline_theme' ) === 0 ) {
			return 'inline';
		}
		if ( strpos( $key, 'end_theme' ) === 0 ) {
			return 'end-content';
		}
		if ( strpos( $key, 'floating_sidebar_variant' ) === 0 ) {
			return 'floating-sidebar';
		}

		return null;
	}
}
