<?php
/**
 * Settings Service
 *
 * Manages plugin settings including API keys, prompts, and configuration options.
 *
 * @package AIPT\Api\Services
 * @since 2.4.1
 */

namespace AIPT\Api\Services;

use AIPT\Api\OpenAI\OpenAIClient;
use AIPT\Api\Gemini\GeminiClient;
use AIPT\Api\OpenRouter\OpenRouterClient;
use AIPT\Api\Claude\ClaudeClient;
use AIPT\Core\Activator;
use AIPT\Utils\ApiKeyManager;
use AIPT\Utils\Credentials\CredentialResolver;

/**
 * Settings Service Implementation
 *
 * Handles all settings-related operations including retrieval, updates, and API key validation.
 *
 * @internal Used by RestEndpoints
 */
class SettingsService {

	/**
	 * Literal AI-prompt option fields.
	 *
	 * These hold instruction text sent verbatim to the AI provider — they are
	 * NOT HTML. The defaults legitimately contain XML-like tags
	 * (<product_data>, <task>), parentheses, quotes, apostrophes and multi-line
	 * structure, so they are routed through normalizePromptText() (UTF-8 +
	 * control-char + newline normalization only) on BOTH the write and read
	 * paths instead of the destructive sanitize_text_field() /
	 * wp_strip_all_tags() / wp_kses_post() chain. The names mirror
	 * RestEndpoints::$settings_fields and the options the per-type PromptFactory
	 * classes consume via get_option().
	 *
	 * @var string[]
	 */
	private const PROMPT_FIELDS = [
		'system_prompt',
		'user_prompt',
		'short_system_prompt',
		'short_user_prompt',
		'full_system_prompt',
		'full_user_prompt',
		'standard_short_system_prompt',
		'standard_short_user_prompt',
		'standard_full_system_prompt',
		'standard_full_user_prompt',
		'title_additional_prompt',
		'faq_additional_prompt',
		'seo_meta_additional_prompt',
		'taxonomy_additional_prompt',
		'image_alt_additional_prompt',
		'brand_context',
	];

	/**
	 * Maximum byte length accepted for a single prompt field.
	 *
	 * @var int
	 */
	private const MAX_PROMPT_BYTES = 65535;

	/**
	 * Boolean settings fields.
	 *
	 * These MUST be coerced via rest_sanitize_boolean() before update_option()
	 * so the stored value remains a canonical 0/1 — never an empty string.
	 *
	 * Background: sanitize_text_field((string) false) returns '', and an empty
	 * option_value fails WP REST's rest_is_boolean() check on the next save
	 * roundtrip (valid string list is only ['true','false','0','1']). That
	 * cycle surfaces to users as "rest_invalid_param: enable_tooltip" when
	 * saving any setting — a self-poisoning data-corruption loop triggered the
	 * first time a user toggles one of these booleans OFF.
	 *
	 * `metabox_hidden` is included even though it is not declared in the
	 * /settings REST schema args because it is in $this->settings_fields and
	 * hits the same sanitize_text_field branch — without coercion here it
	 * would still corrupt silently.
	 *
	 * @since 2.6.2
	 * @var array
	 */
	private const BOOLEAN_FIELDS = [
		'enable_tooltip',
		'metabox_hidden',
		'preserve_content_images',
	];

	/**
	 * Valid settings fields
	 *
	 * @var array
	 */
	private $settings_fields;

	/**
	 * Style descriptions
	 *
	 * @var array
	 */
	private $style_descriptions;

	/**
	 * AI Client Factory
	 *
	 * @var AIClientFactoryInterface
	 */
	private $ai_client_factory;

	/**
	 * Constructor
	 *
	 * @param array $settings_fields Valid settings field names
	 * @param array $style_descriptions Style descriptions mapping
	 * @param AIClientFactoryInterface $ai_client_factory AI client factory instance
	 */
	public function __construct( array $settings_fields, array $style_descriptions, AIClientFactoryInterface $ai_client_factory ) {
		$this->settings_fields    = $settings_fields;
		$this->style_descriptions = $style_descriptions;
		$this->ai_client_factory  = $ai_client_factory;
	}

	/**
	 * Get all settings
	 *
	 * @return array|\WP_Error Settings data or error
	 */
	public function getSettings() {
		if ( ! current_user_can( 'manage_options' ) ) {
			return new \WP_Error( 'rest_forbidden', 'Sorry, you are not allowed to do that.', array( 'status' => 401 ) );
		}

		$settings = array();
		foreach ( $this->settings_fields as $field ) {

			if ( $field === 'openai_api_key' ) {
				$settings[ $field ] = ApiKeyManager::get_api_key_for_display( 'openai' );
				continue;
			}

			if ( $field === 'gemini_api_key' ) {
				$settings[ $field ] = ApiKeyManager::get_api_key_for_display( 'gemini' );
				continue;
			}

			if ( $field === 'openrouter_api_key' ) {
				$settings[ $field ] = ApiKeyManager::get_api_key_for_display( 'openrouter' );
				continue;
			}

			if ( $field === 'claude_api_key' ) {
				$settings[ $field ] = ApiKeyManager::get_api_key_for_display( 'claude' );
				continue;
			}

			$option_value = get_option( 'aipt_' . $field, Activator::get_default_value( $field ) );

			$premium_fields = apply_filters( 'aipt_settings_premium_fields', array() );
			if ( in_array( $field, $premium_fields, true ) ) {
				$can_read = apply_filters( 'aipt_settings_can_read_field', false, $field );
				if ( ! $can_read ) {
					continue;
				}
			}

			if ( $field === 'model' ) {
				$provider_option  = get_option( 'aipt_api_provider', 'openai' );
				$normalized_model = $this->ai_client_factory->normalizeModel( $provider_option, $option_value );
				$settings[ $field ] = sanitize_text_field( $normalized_model );
				continue;
			}

		if ( in_array( $field, self::PROMPT_FIELDS, true ) ) {
			$settings[ $field ] = $this->normalizePromptText( $option_value );
			continue;
		}

			if ( $field === 'content_length_preset' ) {
				$valid = array( 'short', 'medium', 'long', 'very_long' );
				$settings[ $field ] = in_array( $option_value, $valid, true ) ? $option_value : 'medium';
				continue;
			}

			if ( $field === 'short_length_preset' ) {
				$valid = array( 'brief', 'standard', 'detailed' );
				$settings[ $field ] = in_array( $option_value, $valid, true ) ? $option_value : 'standard';
				continue;
			}

			if ( $field === 'allowed_html_tags' || $field === 'disallowed_html_tags' ) {

				if ( is_string( $option_value ) && ! empty( $option_value ) ) {
					$settings[ $field ] = array_filter( array_map( 'trim', explode( ',', $option_value ) ) );
				} elseif ( is_array( $option_value ) ) {
					$settings[ $field ] = array_map( 'sanitize_text_field', $option_value );
				} else {
					$settings[ $field ] = array();
				}
				continue;
			}

			if ( in_array( $field, self::BOOLEAN_FIELDS, true ) ) {
				$settings[ $field ] = rest_sanitize_boolean( $option_value );
				continue;
			}

			$settings[ $field ] = sanitize_text_field( $option_value );
		}
		return $settings;
	}

	/**
	 * Update settings
	 *
	 * @param array $request_params Request parameters containing settings to update
	 * @return array|\WP_Error Success response or error
	 */
	public function updateSettings( array $request_params ) {
		try {
			if ( ! current_user_can( 'manage_options' ) ) {
				return new \WP_Error( 'rest_forbidden', 'Sorry, you are not allowed to do that.', array( 'status' => 401 ) );
			}

			$api_provider = isset( $request_params['api_provider'] ) ? sanitize_text_field( $request_params['api_provider'] ) : 'openai';
			$model        = isset( $request_params['model'] ) ? sanitize_text_field( $request_params['model'] ) : '';

			$compatibility_error = $this->validateModelProviderCompatibility( $api_provider, $model );
			if ( is_wp_error( $compatibility_error ) ) {
				return $compatibility_error;
			}

			$api_key_error = $this->processApiKeys( $request_params );
			if ( is_wp_error( $api_key_error ) ) {
				return $api_key_error;
			}

foreach ( $this->settings_fields as $field ) {
				if ( isset( $request_params[ $field ] ) && ! in_array( $field, array( 'openai_api_key', 'gemini_api_key', 'openrouter_api_key', 'claude_api_key' ) ) ) {
					$value = $request_params[ $field ];

					$premium_fields = apply_filters( 'aipt_settings_premium_fields', array() );
					if ( in_array( $field, $premium_fields, true ) ) {
						$can_write = apply_filters( 'aipt_settings_can_write_field', false, $field );
						if ( ! $can_write ) {
							continue;
						}
					}

			if ( in_array( $field, self::PROMPT_FIELDS, true ) ) {

				if ( is_string( $value ) ) {
					$value = $this->normalizePromptText( $value );
				}
			} elseif ( in_array( $field, self::BOOLEAN_FIELDS, true ) ) {

				$value = (int) rest_sanitize_boolean( $value );
			} elseif ( $field === 'allowed_html_tags' || $field === 'disallowed_html_tags' ) {

					if ( is_array( $value ) ) {
						$value = array_map( 'sanitize_text_field', $value );
					} elseif ( is_string( $value ) ) {

						$value = array_filter( array_map( 'trim', explode( ',', $value ) ) );
						$value = array_map( 'sanitize_text_field', $value );
					} else {
						$value = array();
					}
				} else {
					$value = sanitize_text_field( $value );
				}

					if ( $field === 'model' ) {
						$value = $this->ai_client_factory->normalizeModel( $api_provider, $value );
					}

					$validation_error = $this->validateField( $field, $value );
					if ( is_wp_error( $validation_error ) ) {
						return $validation_error;
					}

					update_option( 'aipt_' . $field, $value );
				}
			}

			$api_key_fields  = array( 'openai_api_key', 'gemini_api_key', 'openrouter_api_key', 'claude_api_key' );
			$keys_in_request = (bool) array_intersect( $api_key_fields, array_keys( $request_params ) );

			$response = array(
				'success' => true,
				'message' => esc_html__( 'Settings updated successfully!', 'ai-product-tools' ),
			);

			if ( $keys_in_request ) {
				$response['connectors'] = $this->buildConnectorsSnapshot();
			}

			return $response;
		} catch ( \Exception $e ) {
			return new \WP_Error( 'update_failed', esc_html($e->getMessage()), array( 'status' => 500 ) );
		} finally {

			try {
				Activator::check_and_update_setup_status();
			} catch ( \Exception $e ) {
			}
		}
	}

	/**
	 * Validate API key
	 *
	 * @param string $api_key API key to validate
	 * @param string $api_provider Provider name (openai|gemini|openrouter)
	 * @return array|\WP_Error Validation result or error
	 */
	public function validateApiKey( string $api_key, string $api_provider = 'openai' ) {

		$api_key = preg_replace( '/\s+/', '', $api_key );

		if ( empty( $api_key ) ) {
			return new \WP_Error(
				'empty_api_key',
				'API key cannot be empty.',
				array(
					'status'        => 400,
					'error_details' => 'API key cannot be empty. Please enter a valid API key.',
				)
			);
		}

		try {
			if ( $api_provider === 'gemini' ) {
				GeminiClient::validate_api_key( $api_key );
			} elseif ( $api_provider === 'openrouter' ) {
				OpenRouterClient::validate_api_key( $api_key );
			} elseif ( $api_provider === 'claude' ) {
				ClaudeClient::validate_api_key( $api_key );
			} else {
				OpenAIClient::validate_api_key( $api_key );
			}

			return array(
				'success' => true,
				'message' => __( 'API key validated successfully', 'ai-product-tools' ),
				'data'    => array(
					'provider'     => $api_provider,
					'validated_at' => current_time( 'mysql' ),
				),
			);
		} catch ( \Exception $e ) {

			return new \WP_Error(
				'validation_error',
				__( 'An error occurred while validating the API key. Please try again.', 'ai-product-tools' ),
				array(
					'status' => 400,
					'error_details' => sanitize_text_field( $e->getMessage() ),
				)
			);
		}
	}

	/**
	 * Sanitize API key
	 *
	 * @param string $key API key to sanitize
	 * @return string Sanitized key
	 */
	public function sanitizeApiKey( string $key ): string {
		$key = trim( $key );
		$key = preg_replace( '/\s+/', '', $key );
		return sanitize_text_field( $key );
	}

	/**
	 * Normalize a prompt field for storage/display WITHOUT destroying meaning.
	 *
	 * Prompt options are literal instruction text handed to the AI provider —
	 * they are NOT HTML. The defaults intentionally contain XML-like tags
	 * (<product_data>, <task>), parentheses, quotes, apostrophes and multi-line
	 * structure. Running them through sanitize_text_field() /
	 * wp_strip_all_tags() / wp_kses_post() (as the old write and read paths
	 * did) silently corrupted that structure. These fields
	 * are admin-only (manage_options) and are rendered escaped in React
	 * textareas, so we only enforce valid UTF-8, strip dangerous control
	 * characters (keeping \n and \t) and normalize line endings.
	 *
	 * @param mixed $text Raw prompt text.
	 * @return string Normalized prompt text.
	 */
	private function normalizePromptText( $text ): string {
		if ( is_array( $text ) || is_object( $text ) ) {
			return '';
		}

		$text = wp_check_invalid_utf8( (string) $text, true );
		$text = str_replace( array( "\r\n", "\r" ), "\n", $text );

		$normalized = preg_replace( '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $text );

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

	/**
	 * Validate model/provider compatibility
	 *
	 * @param string $api_provider Provider name
	 * @param string $model Model identifier
	 * @return true|\WP_Error True if valid, WP_Error if invalid
	 */
	private function validateModelProviderCompatibility( string $api_provider, string $model ) {
		if ( $api_provider === 'openai' && strpos( $model, 'gemini-' ) === 0 ) {
			return new \WP_Error(
				'invalid_model',
				esc_html__( 'OpenAI API provider cannot be used with Gemini models.', 'ai-product-tools' ),
				array( 'status' => 400 )
			);
		}

		if ( $api_provider === 'gemini' && strpos( $model, 'gemini-' ) !== 0 ) {
			return new \WP_Error(
				'invalid_model',
				esc_html__( 'Gemini API provider cannot be used with non-Gemini models.', 'ai-product-tools' ),
				array( 'status' => 400 )
			);
		}

		if ( $api_provider === 'openrouter' && ( strpos( $model, 'gemini-' ) === 0 || strpos( $model, 'gpt-' ) === 0 ) ) {
			return new \WP_Error(
				'invalid_model',
				esc_html__( 'OpenRouter API provider cannot be used with OpenAI or Gemini direct models.', 'ai-product-tools' ),
				array( 'status' => 400 )
			);
		}

		if ( $api_provider === 'claude' && strpos( $model, 'claude-' ) !== 0 ) {
			return new \WP_Error(
				'invalid_model',
				esc_html__( 'Claude API provider cannot be used with non-Claude models.', 'ai-product-tools' ),
				array( 'status' => 400 )
			);
		}

		return true;
	}

	/**
	 * Process API key updates
	 *
	 * @param array $request_params Request parameters
	 * @return true|\WP_Error True if successful, WP_Error on failure
	 */
	private function processApiKeys( array $request_params ) {

		$openai_provided = array_key_exists( 'openai_api_key', $request_params );
		$api_key = (array_key_exists( 'openai_api_key', $request_params ) && is_string( $request_params['openai_api_key'] )) ? $this->sanitizeApiKey( $request_params['openai_api_key'] ) : null;
		$result = $this->processProviderKey(
			$api_key,
			[ OpenAIClient::class, 'validate_api_key' ],
			[ ApiKeyManager::class, 'get_openai_key' ],
			[ ApiKeyManager::class, 'set_openai_key' ],
			'OpenAI'
		);
		if ( is_wp_error( $result ) ) {
			return $result;
		}

		$gemini_provided = array_key_exists( 'gemini_api_key', $request_params );
		$gemini_api_key = (array_key_exists( 'gemini_api_key', $request_params ) && is_string( $request_params['gemini_api_key'] )) ? $this->sanitizeApiKey( $request_params['gemini_api_key'] ) : null;
		$result = $this->processProviderKey(
			$gemini_api_key,
			[ GeminiClient::class, 'validate_api_key' ],
			[ ApiKeyManager::class, 'get_gemini_key' ],
			[ ApiKeyManager::class, 'set_gemini_key' ],
			'Gemini'
		);
		if ( is_wp_error( $result ) ) {
			return $result;
		}

		$openrouter_provided = array_key_exists( 'openrouter_api_key', $request_params );
		$openrouter_api_key = (array_key_exists( 'openrouter_api_key', $request_params ) && is_string( $request_params['openrouter_api_key'] )) ? $this->sanitizeApiKey( $request_params['openrouter_api_key'] ) : null;
		$result = $this->processProviderKey(
			$openrouter_api_key,
			[ OpenRouterClient::class, 'validate_api_key' ],
			[ ApiKeyManager::class, 'get_openrouter_key' ],
			[ ApiKeyManager::class, 'set_openrouter_key' ],
			'OpenRouter'
		);
		if ( is_wp_error( $result ) ) {
			return $result;
		}

		$claude_api_key = (array_key_exists( 'claude_api_key', $request_params ) && is_string( $request_params['claude_api_key'] )) ? $this->sanitizeApiKey( $request_params['claude_api_key'] ) : null;
		$result = $this->processProviderKey(
			$claude_api_key,
			[ ClaudeClient::class, 'validate_api_key' ],
			[ ApiKeyManager::class, 'get_claude_key' ],
			[ ApiKeyManager::class, 'set_claude_key' ],
			'Claude'
		);
		if ( is_wp_error( $result ) ) {
			return $result;
		}

		return true;
	}

	/**
	 * Process a single provider API key
	 *
	 * @param string|null $api_key API key value (null = param not provided, '' = explicitly clear)
	 * @param callable $validator Validation function
	 * @param callable $getter Function to get current key
	 * @param callable $setter Function to set key
	 * @param string $provider_label Provider display name
	 * @return true|\WP_Error True if successful, WP_Error on failure
	 */
	private function processProviderKey( ?string $api_key, callable $validator, callable $getter, callable $setter, string $provider_label ) {

		if ( $api_key === null ) {
			return true;
		}

		$current_key = call_user_func( $getter );

		if ( $api_key === '' ) {
			if ( ! empty( $current_key ) ) {
				if ( ! call_user_func( $setter, '' ) ) {
					return new \WP_Error(
						'api_key_save_failed',
						sprintf(
							/* translators: %s: Provider name (e.g., OpenAI, Gemini) */
							__( 'Failed to save %s API key. Please try again.', 'ai-product-tools' ),
							$provider_label
						),
						array( 'status' => 500 )
					);
				}
			}
			return true;
		}

		if ( $api_key === $current_key ) {
			return true;
		}

		try {
			call_user_func( $validator, $api_key );
		} catch ( \Exception $e ) {
			return new \WP_Error(
				'invalid_api_key',
				sprintf(
					/* translators: %s: Provider name (e.g., OpenAI, Gemini) */
					__( 'Invalid %s API key', 'ai-product-tools' ),
					$provider_label
				),
				array( 'status' => 400 )
			);
		}

		if ( ! call_user_func( $setter, $api_key ) ) {
			return new \WP_Error(
				'api_key_save_failed',
				sprintf(
					/* translators: %s: Provider name (e.g., OpenAI, Gemini) */
					__( 'Failed to save %s API key. Please try again.', 'ai-product-tools' ),
					$provider_label
				),
				array( 'status' => 500 )
			);
		}

		return true;
	}

	/**
	 * Build a fresh connectors snapshot mirroring LocalizationDataBuilder's
	 * inner `connectors` sub-array.
	 *
	 * Returned shape (must match LocalizationDataBuilder.php:1130-1135 — same
	 * keys, same types — so the SPA can patch window.aiptData.connectors
	 * in place after a successful save):
	 *   - hasAnyConfigured        bool
	 *   - hasAnyUsableKey         bool
	 *   - hasAnyConnectorsManaged bool
	 *   - connectorsManagedCount  int
	 *   - providers               array<string, array{...}> (CredentialResolver::getStatus())
	 *
	 * Built from CredentialResolver directly (NOT via reflection on
	 * LocalizationDataBuilder) to keep coupling minimal and the change
	 * self-contained. The resolver returns fresh data here because
	 * ApiKeyManager setters invoked by processApiKeys() flushed the
	 * request-memo + ai-availability transient on every successful key write.
	 *
	 * @since 2.6.3
	 * @return array{hasAnyConfigured: bool, hasAnyUsableKey: bool, hasAnyConnectorsManaged: bool, connectorsManagedCount: int, providers: array<string, array<string, mixed>>}
	 */
	private function buildConnectorsSnapshot(): array {
		$resolver        = CredentialResolver::instance();
		$providers_state = $resolver->getStatus();

		$managed_count = count(
			array_filter(
				$providers_state,
				static function ( $provider_state ) {
					return ! empty( $provider_state['isConnectorsManaged'] );
				}
			)
		);

		return array(
			'hasAnyConfigured'        => $resolver->hasAnyConfiguredProvider(),
			'hasAnyUsableKey'         => $resolver->hasAnyUsableKey(),
			'hasAnyConnectorsManaged' => $managed_count > 0,
			'connectorsManagedCount'  => $managed_count,
			'providers'               => $providers_state,
		);
	}

	/**
	 * Validate individual field value
	 *
	 * @param string $field Field name
	 * @param mixed $value Field value
	 * @return true|\WP_Error True if valid, WP_Error if invalid
	 */
	private function validateField( string $field, $value ) {

		if ( in_array( $field, self::PROMPT_FIELDS, true ) ) {
			if ( ! is_string( $value ) ) {
				return new \WP_Error(
					'invalid_prompt_type',
					'Prompt field must be a string.',
					array( 'status' => 400 )
				);
			}
			if ( strlen( $value ) > self::MAX_PROMPT_BYTES ) {
				return new \WP_Error(
					'prompt_too_long',
					'Prompt field exceeds the maximum allowed length.',
					array( 'status' => 400 )
				);
			}
		}

		switch ( $field ) {
			case 'content_length_preset':
				$valid_presets = array( 'short', 'medium', 'long', 'very_long' );
				if ( ! in_array( $value, $valid_presets, true ) ) {
					return new \WP_Error(
						'invalid_content_length_preset',
						'Invalid content length preset',
						array( 'status' => 400 )
					);
				}
				break;
			case 'short_length_preset':
				$valid_presets = array( 'brief', 'standard', 'detailed' );
				if ( ! in_array( $value, $valid_presets, true ) ) {
					return new \WP_Error(
						'invalid_short_length_preset',
						'Invalid short description length preset',
						array( 'status' => 400 )
					);
				}
				break;
			case 'max_tags_count':
				if ( ! is_numeric( $value ) || intval( $value ) < 1 || intval( $value ) > 20 ) {
					return new \WP_Error(
						'invalid_max_tags_count',
						'Maximum tag count must be a positive number between 1 and 20',
						array( 'status' => 400 )
					);
				}
				break;
			case 'language':
				$valid_languages = array( 'Afrikaans', 'Arabic', 'Bulgarian', 'Bengali', 'Czech', 'Danish', 'German', 'Greek', 'English', 'English (UK)', 'Spanish', 'Estonian', 'Persian', 'Finnish', 'French', 'Hebrew', 'Hindi', 'Croatian', 'Hungarian', 'Indonesian', 'Italian', 'Japanese', 'Korean', 'Lithuanian', 'Latvian', 'Malay', 'Dutch', 'Norwegian', 'Polish', 'Portuguese', 'Romanian', 'Russian', 'Serbian', 'Slovak', 'Slovenian', 'Swedish', 'Thai', 'Turkish', 'Ukrainian', 'Urdu', 'Vietnamese', 'Chinese' );
				if ( ! in_array( $value, $valid_languages ) ) {
					return new \WP_Error( 'invalid_language', 'Invalid language selection', array( 'status' => 400 ) );
				}
				break;
			case 'writing_style':
				if ( ! array_key_exists( $value, $this->style_descriptions ) ) {
					return new \WP_Error( 'invalid_style', 'Invalid writing style selection', array( 'status' => 400 ) );
				}
				break;
		case 'api_provider':
				if ( ! in_array( $value, array( 'openai', 'gemini', 'openrouter', 'claude' ) ) ) {
					return new \WP_Error( 'invalid_provider', 'Invalid API provider selection', array( 'status' => 400 ) );
				}
				break;
		}

		return true;
	}
}
