<?php
/**
 * AI Alt Text module settings.
 *
 * @package AIAltTextGenerator
 * @since 1.4.0
 */

namespace AIAltTextGenerator;

use WP_PowerSuite\Core\OptionStoreModuleSettingsRepository;

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

require_once __DIR__ . '/dependencies.php';

/**
 * Centralized settings access and normalization.
 *
 * @since 1.4.0
 */
class Settings {

	/** @var array<string, mixed>|null */
	private static ?array $cache = null;

	public const MODULE_ID          = 'ai-alt-text-generator';
	public const OPENAI_KEY_CONSTANT = 'WPPS_AI_ALT_TEXT_OPENAI_KEY';
	public const GEMINI_KEY_CONSTANT = 'WPPS_AI_ALT_TEXT_GEMINI_KEY';
	public const CLAUDE_KEY_CONSTANT = 'WPPS_AI_ALT_TEXT_CLAUDE_KEY';
	public const API_KEY_CONSTANT    = self::OPENAI_KEY_CONSTANT;
	public const USAGE_OPTION           = 'wpps_ai_alt_usage';
	public const QUEUE_OPTION           = 'wpps_ai_alt_queue';
	public const CUSTOM_PROMPT_MAX      = 2048;
	public const BATCH_SIZE_MIN         = 1;
	public const BATCH_SIZE_MAX         = 10;
	public const LOG_RETENTION_DEFAULT  = 7;
	public const LOG_RETENTION_MAX      = 365;

	/**
	 * Default settings.
	 *
	 * @return array<string, mixed>
	 */
	public static function defaults(): array {
		return [
			'provider'                   => 'openai',
			'model'                      => Models::DEFAULT,
			'max_chars'                  => 125,
			'auto_on_upload'             => false,
			'prompt_style'               => 'concise',
			'overwrite_existing'         => false,
			'language'                   => '',
			'custom_prompt'              => '',
			'alt_prefix'                 => '',
			'alt_suffix'                 => '',
			'copy_to_title'              => false,
			'copy_to_caption'            => false,
			'copy_to_description'        => false,
			'allowed_extensions'         => '',
			'excluded_post_types'        => '',
			'skip_missing_files'         => false,
			'use_woo_context'            => false,
			'use_seo_keyphrase'          => false,
			'use_post_title_keywords'    => false,
			'default_seo_keywords'      => '',
			'default_negative_keywords' => '',
			'rewrite_prompt'             => '',
			'refresh_external_images'    => false,
			'woo_use_image_title'        => false,
			'request_timeout'            => 90,
			'generate_missing_metadata'    => false,
			'lazy_load_src_attr'         => '',
			'admin_only_access'          => false,
			'auto_approve_suggestions'   => true,
			'monthly_limit'              => 0,
			'batch_size'                 => 5,
			'log_retention_days'         => self::LOG_RETENTION_DEFAULT,
			'queue_enabled'              => true,
			'sync_content_alt'           => true,
			'sync_overwrite_content_alt' => false,
			'scan_schedule'              => 'manual',
			'email_reports'              => false,
			'report_email'               => '',
			'role_caps'                  => Capabilities::defaultRoleCaps(),
			'client_safe_mode'           => false,
			'debug_logging'              => false,
		];
	}

	/**
	 * Get merged module settings.
	 *
	 * @return array<string, mixed>
	 */
	public static function get(): array {
		if ( self::$cache !== null ) {
			return self::$cache;
		}

		$saved  = OptionStoreModuleSettingsRepository::forModule( self::MODULE_ID )->all( [] );
		self::maybeMigrateLegacyKeys( is_array( $saved ) ? $saved : [] );
		$merged = is_array( $saved ) ? array_merge( self::defaults(), $saved ) : self::defaults();
		$merged['provider'] = self::sanitizeProvider( (string) ( $merged['provider'] ?? 'openai' ) );
		$merged['model']    = self::resolveModelForProvider( $merged );
		$merged['language'] = Languages::sanitize( (string) ( $merged['language'] ?? '' ) );

		self::$cache = FeatureGate::applyTierLimits( $merged );

		return self::$cache;
	}

	public static function flushCache(): void {
		self::$cache = null;
	}

	/**
	 * Safe settings snapshot for REST/clients (no secrets).
	 *
	 * @return array<string, mixed>
	 */
	public static function getPublic(): array {
		$settings = self::get();

		return [
			'provider'             => (string) ( $settings['provider'] ?? 'openai' ),
			'model'                => (string) ( $settings['model'] ?? Models::DEFAULT ),
			'has_api_key'          => self::hasApiKey(),
			'is_pro'               => FeatureGate::isPro(),
			'monthly_limit'        => FeatureGate::effectiveMonthlyLimit( $settings ),
			'auto_approve'         => ! empty( $settings['auto_approve_suggestions'] ),
			'queue_enabled'        => ! empty( $settings['queue_enabled'] ),
			'sync_content_alt'     => ! empty( $settings['sync_content_alt'] ),
			'client_safe_mode'     => ! empty( $settings['client_safe_mode'] ),
		];
	}

	public static function sanitizeProvider( string $provider ): string {
		return class_exists( \WP_PowerSuite\AI\ConnectionSettings::class )
			? \WP_PowerSuite\AI\ConnectionSettings::sanitizeProvider( $provider )
			: ( in_array( $provider, [ 'openai', 'gemini', 'claude' ], true ) ? $provider : 'openai' );
	}

	/**
	 * @param array<string, mixed> $settings
	 */
	public static function resolveModelForProvider( array $settings ): string {
		$provider = self::sanitizeProvider( (string) ( $settings['provider'] ?? 'openai' ) );
		$model    = (string) ( $settings['model'] ?? Models::DEFAULT );

		return ProviderModels::sanitize( $model, $provider, $settings );
	}

	public static function isApiKeyFromConfig( ?string $provider = null ): bool {
		return class_exists( \WP_PowerSuite\AI\ConnectionSettings::class )
			&& \WP_PowerSuite\AI\ConnectionSettings::isApiKeyFromConfig( $provider );
	}

	public static function isApiKeyFromNetwork( ?string $provider = null ): bool {
		return class_exists( \WP_PowerSuite\AI\ConnectionSettings::class )
			&& \WP_PowerSuite\AI\ConnectionSettings::isApiKeyFromNetwork( $provider );
	}

	public static function hasApiKey( ?string $provider = null ): bool {
		return self::getApiKey( null, $provider ) !== '';
	}

	/**
	 * Resolve the effective API key for a provider from global AI connections.
	 */
	public static function getApiKey( ?array $settings = null, ?string $provider = null ): string {
		$settings = $settings ?? self::get();
		$provider = self::sanitizeProvider( $provider ?? (string) ( $settings['provider'] ?? 'openai' ) );

		if ( class_exists( \WP_PowerSuite\AI\ConnectionSettings::class ) ) {
			return \WP_PowerSuite\AI\ConnectionSettings::getApiKey( null, $provider );
		}

		return '';
	}

	/** @return list<string> */
	public static function getConfiguredProviders(): array {
		if ( class_exists( \WP_PowerSuite\AI\ConnectionSettings::class ) ) {
			return \WP_PowerSuite\AI\ConnectionSettings::getConfiguredProviders();
		}

		return [];
	}

	private static function stripLegacyKeyFields( array $settings ): array {
		foreach ( [ 'api_key', 'gemini_api_key', 'claude_api_key', 'use_network_api_keys' ] as $field ) {
			unset( $settings[ $field ] );
		}

		return $settings;
	}

	/**
	 * Move legacy module-stored keys into global AI connections once.
	 *
	 * @param array<string, mixed> $saved Raw stored module settings.
	 */
	private static function maybeMigrateLegacyKeys( array $saved ): void {
		static $migrated = false;
		if ( $migrated || ! class_exists( \WP_PowerSuite\AI\ConnectionSettings::class ) ) {
			return;
		}
		$migrated = true;

		$payload = [];
		foreach ( [ 'api_key', 'gemini_api_key', 'claude_api_key' ] as $field ) {
			$legacy = trim( (string) ( $saved[ $field ] ?? '' ) );
			if ( $legacy === '' ) {
				continue;
			}
			$provider = match ( $field ) {
				'gemini_api_key' => 'gemini',
				'claude_api_key' => 'claude',
				default          => 'openai',
			};
			if ( \WP_PowerSuite\AI\ConnectionSettings::hasApiKey( $provider ) ) {
				continue;
			}
			$payload[ $field ] = $legacy;
		}

		if ( $payload === [] ) {
			return;
		}

		\WP_PowerSuite\AI\ConnectionSettings::persist(
			array_merge( \WP_PowerSuite\AI\ConnectionSettings::get(), $payload )
		);

		$repo = OptionStoreModuleSettingsRepository::forModule( self::MODULE_ID );
		$repo->save( self::stripLegacyKeyFields( array_merge( self::defaults(), $saved ) ) );
	}

	/**
	 * Normalize settings on save.
	 *
	 * @param array<string, mixed> $settings Raw settings.
	 * @param array<string, mixed> $existing Existing stored settings.
	 * @return array<string, mixed>
	 */
	public static function normalize( array $settings, array $existing = [] ): array {
		$normalized = array_merge( self::defaults(), $existing, $settings );

		$normalized['provider'] = self::sanitizeProvider( (string) ( $settings['provider'] ?? $normalized['provider'] ?? 'openai' ) );

		$normalized['model'] = self::resolveModelForProvider( $normalized );
		$normalized['prompt_style'] = PromptStyles::sanitize( (string) ( $settings['prompt_style'] ?? $normalized['prompt_style'] ) );
		$normalized['max_chars']     = max( 50, min( 500, (int) ( $settings['max_chars'] ?? $normalized['max_chars'] ) ) );
		$normalized['language']      = Languages::sanitize( (string) ( $settings['language'] ?? $normalized['language'] ) );
		$normalized['custom_prompt'] = self::sanitizeCustomPrompt( (string) ( $settings['custom_prompt'] ?? $normalized['custom_prompt'] ) );
		$normalized['rewrite_prompt'] = self::sanitizeRewritePrompt( (string) ( $settings['rewrite_prompt'] ?? $normalized['rewrite_prompt'] ) );
		$normalized['alt_prefix']    = sanitize_text_field( (string) ( $settings['alt_prefix'] ?? $normalized['alt_prefix'] ) );
		$normalized['alt_suffix']    = sanitize_text_field( (string) ( $settings['alt_suffix'] ?? $normalized['alt_suffix'] ) );
		$normalized['default_seo_keywords'] = sanitize_text_field( (string) ( $settings['default_seo_keywords'] ?? $normalized['default_seo_keywords'] ) );
		$normalized['default_negative_keywords'] = sanitize_text_field( (string) ( $settings['default_negative_keywords'] ?? $normalized['default_negative_keywords'] ) );
		$normalized['allowed_extensions']  = self::sanitizeAllowedExtensions( (string) ( $settings['allowed_extensions'] ?? $normalized['allowed_extensions'] ) );
		if ( array_key_exists( 'excluded_post_types_sent', $settings ) ) {
			$excluded_raw = $settings['excluded_post_types'] ?? [];
			$normalized['excluded_post_types'] = self::sanitizeExcludedPostTypes(
				is_array( $excluded_raw ) ? $excluded_raw : (string) $excluded_raw
			);
		} elseif ( isset( $settings['excluded_post_types'] ) ) {
			$excluded_raw = $settings['excluded_post_types'];
			$normalized['excluded_post_types'] = self::sanitizeExcludedPostTypes(
				is_array( $excluded_raw ) ? $excluded_raw : (string) $excluded_raw
			);
		}
		$normalized['batch_size']    = self::clampBatchSize( (int) ( $settings['batch_size'] ?? $normalized['batch_size'] ) );
		$normalized['log_retention_days'] = self::clampLogRetentionDays( (int) ( $settings['log_retention_days'] ?? $normalized['log_retention_days'] ) );
		$normalized['request_timeout'] = max( 10, min( 180, (int) ( $settings['request_timeout'] ?? $normalized['request_timeout'] ) ) );
		$normalized['lazy_load_src_attr'] = sanitize_key( (string) ( $settings['lazy_load_src_attr'] ?? $normalized['lazy_load_src_attr'] ) );
		$normalized['monthly_limit'] = max( 0, (int) ( $settings['monthly_limit'] ?? $normalized['monthly_limit'] ) );
		$normalized['scan_schedule'] = in_array( (string) ( $settings['scan_schedule'] ?? $normalized['scan_schedule'] ), [ 'manual', 'daily', 'weekly', 'monthly' ], true )
			? (string) ( $settings['scan_schedule'] ?? $normalized['scan_schedule'] )
			: 'manual';
		$normalized['report_email']  = sanitize_email( (string) ( $settings['report_email'] ?? $normalized['report_email'] ) );

		foreach ( [
			'auto_on_upload',
			'overwrite_existing',
			'use_woo_context',
			'use_seo_keyphrase',
			'use_post_title_keywords',
			'refresh_external_images',
			'woo_use_image_title',
			'generate_missing_metadata',
			'admin_only_access',
			'auto_approve_suggestions',
			'queue_enabled',
			'sync_content_alt',
			'sync_overwrite_content_alt',
			'email_reports',
			'client_safe_mode',
			'copy_to_title',
			'copy_to_caption',
			'copy_to_description',
			'skip_missing_files',
			'debug_logging',
		] as $flag ) {
			$normalized[ $flag ] = ! empty( $settings[ $flag ] );
		}

		$normalized['role_caps'] = Capabilities::normalizeRoleCaps(
			! empty( $normalized['admin_only_access'] )
				? [
					'generate' => [ 'administrator' ],
					'view'     => [ 'administrator' ],
					'manage'   => [ 'administrator' ],
				]
				: (
					array_key_exists( 'role_caps', $settings )
						? ( is_array( $settings['role_caps'] ) ? $settings['role_caps'] : [] )
						: ( is_array( $existing['role_caps'] ?? null ) ? $existing['role_caps'] : Capabilities::defaultRoleCaps() )
				)
		);

		$normalized = FeatureGate::preservePremiumSettingsOnSave( $normalized, $existing );
		$normalized = FeatureGate::applyTierLimits( $normalized );

		return self::stripLegacyKeyFields( $normalized );
	}

	/**
	 * Persist settings from a dashboard or request payload.
	 *
	 * @param array<string, mixed> $raw Raw settings from POST.
	 */
	public static function persist( array $raw ): bool {
		$repo     = OptionStoreModuleSettingsRepository::forModule( self::MODULE_ID );
		$existing = $repo->all( [] );
		if ( ! is_array( $existing ) ) {
			$existing = [];
		}

		if ( ! isset( $raw['role_caps'] ) || ! is_array( $raw['role_caps'] ) ) {
			$raw['role_caps'] = [];
		}
		foreach ( [ 'generate', 'view', 'manage' ] as $cap ) {
			if ( ! isset( $raw['role_caps'][ $cap ] ) || ! is_array( $raw['role_caps'][ $cap ] ) ) {
				$raw['role_caps'][ $cap ] = [];
			}
		}

		$normalized = self::normalize( $raw, $existing );
		$saved      = $repo->save( $normalized );
		if ( $saved ) {
			self::flushCache();
		}

		return $saved;
	}

	/**
	 * Persist bulk-tab keyword defaults without touching the rest of settings.
	 */
	public static function persistBulkKeywords( string $seo_keywords, string $negative_keywords ): void {
		$current = self::get();
		$current['default_seo_keywords']      = sanitize_text_field( $seo_keywords );
		$current['default_negative_keywords'] = sanitize_text_field( $negative_keywords );
		self::persist( $current );
	}

	/**
	 * Normalize comma-separated extension allowlist.
	 */
	public static function sanitizeAllowedExtensions( string $raw ): string {
		$parts = array_map(
			static function ( string $part ): string {
				return strtolower( ltrim( trim( $part ), '.' ) );
			},
			preg_split( '/[\s,]+/', strtolower( $raw ), -1, PREG_SPLIT_NO_EMPTY ) ?: []
		);

		$allowed = array_values(
			array_unique(
				array_filter(
					$parts,
					static fn( string $ext ): bool => $ext !== '' && preg_match( '/^[a-z0-9]+$/', $ext ) === 1
				)
			)
		);

		return implode( ',', AltTextGenerator::expandExtensionAliases( $allowed ) );
	}

	/**
	 * Normalize comma-separated or checkbox-list excluded post type slugs.
	 *
	 * @param string|list<string> $raw
	 */
	public static function sanitizeExcludedPostTypes( string|array $raw ): string {
		if ( is_array( $raw ) ) {
			$parts = array_map(
				static fn( mixed $part ): string => sanitize_key( (string) $part ),
				$raw
			);
		} else {
			$parts = array_map(
				static fn( string $part ): string => sanitize_key( trim( $part ) ),
				preg_split( '/[\s,]+/', strtolower( $raw ), -1, PREG_SPLIT_NO_EMPTY ) ?: []
			);
		}

		$types = array_values(
			array_unique(
				array_filter(
					$parts,
					static fn( string $type ): bool => $type !== '' && preg_match( '/^[a-z0-9_-]+$/', $type ) === 1
				)
			)
		);

		return implode( ',', $types );
	}

	public static function sanitizeRewritePrompt( string $raw ): string {
		$raw = sanitize_textarea_field( $raw );
		if ( $raw === '' ) {
			return '';
		}

		return mb_substr( $raw, 0, 512 );
	}

	public static function sanitizeCustomPrompt( string $raw ): string {
		$raw = sanitize_textarea_field( $raw );
		if ( $raw === '' ) {
			return '';
		}

		return mb_substr( $raw, 0, self::CUSTOM_PROMPT_MAX );
	}

	public static function clampBatchSize( int $batch_size ): int {
		return max( self::BATCH_SIZE_MIN, min( self::BATCH_SIZE_MAX, $batch_size ) );
	}

	public static function clampLogRetentionDays( int $days ): int {
		if ( $days <= 0 ) {
			return 0;
		}

		return max( 1, min( self::LOG_RETENTION_MAX, $days ) );
	}
}
