<?php
/**
 * REST API Controller for Plugin Settings.
 *
 * @package OpusPriceBuilder\API
 */

declare(strict_types=1);

namespace OpusPriceBuilder\API;

use WP_REST_Controller;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;
use WP_Error;

/**
 * REST API controller for managing plugin settings.
 */
class SettingsController extends WP_REST_Controller {

	/**
	 * Option name for storing settings.
	 *
	 * @var string
	 */
	const OPTION_NAME = 'oppb_settings';

	/**
	 * Namespace for the REST routes.
	 *
	 * @var string
	 */
	protected $namespace = 'opus-price-builder/v1';

	/**
	 * Resource name.
	 *
	 * @var string
	 */
	protected $rest_base = 'settings';

	/**
	 * Default settings.
	 *
	 * @var array<string, mixed>
	 */
	private array $defaults = array(
		// Google Fonts settings
		'google_fonts_local'    => true, // Serve downloaded fonts from the site (privacy-friendly default).
		'google_fonts_cdn'      => false, // Opt-in: load fonts from Google CDN for visitors.
		'google_fonts_disabled' => false, // Disable all font loading (user loads manually)
		'google_fonts_cache'    => array(), // Cached font files

		// AI Provider settings
		'ai_provider'           => 'none', // 'openai', 'gemini', 'anthropic'
		'ai_api_key'            => '',
		'ai_model'              => '', // gpt-4o, gemini-pro, claude-3-sonnet, etc.

		// Other settings
		'default_scenario'      => 'simple',
	);

	/**
	 * Register the routes for settings.
	 *
	 * @return void
	 */
	public function register_routes(): void {
		// GET /settings - Get all settings
		// PUT /settings - Update settings
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_settings' ),
					'permission_callback' => array( $this, 'settings_permissions_check' ),
				),
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update_settings' ),
					'permission_callback' => array( $this, 'settings_permissions_check' ),
					'args'                => $this->get_setting_args(),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		// POST /settings/update-fonts - Download/update local Google fonts
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/update-fonts',
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'update_fonts' ),
				'permission_callback' => array( $this, 'settings_permissions_check' ),
				'args'                => array(
					'fonts'              => array(
						'description' => __( 'Array of Google font names to download.', 'opus-price-builder' ),
						'type'        => 'object',
						'items'       => array( 'type' => 'string' ),
						'required'    => true,
					),
					'google_fonts_local' => array(
						'description' => __( 'Whether local fonts loading is enabled (from UI state).', 'opus-price-builder' ),
						'type'        => 'boolean',
						'required'    => false,
					),
				),
			)
		);

	}

	/**
	 * Check if user has permission to manage settings.
	 *
	 * @param WP_REST_Request $request The request object.
	 * @return bool|WP_Error
	 */
	public function settings_permissions_check( WP_REST_Request $request ) {
		if ( ! current_user_can( 'manage_woocommerce' ) ) {
			return new WP_Error(
				'rest_forbidden',
				__( 'Sorry, you are not allowed to manage settings.', 'opus-price-builder' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}
		return true;
	}

	/**
	 * Get all settings.
	 *
	 * @param WP_REST_Request $request The request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function get_settings( WP_REST_Request $request ) {
		$settings = $this->get_all_settings();

		// Mask API key for security (show only last 4 chars)
		if ( ! empty( $settings['ai_api_key'] ) ) {
			$key                     = $settings['ai_api_key'];
			$settings['ai_api_key']  = str_repeat( '*', max( 0, strlen( $key ) - 4 ) ) . substr( $key, -4 );
			$settings['has_api_key'] = true;
		} else {
			$settings['has_api_key'] = false;
		}

		return rest_ensure_response( $settings );
	}

	/**
	 * Update settings.
	 *
	 * @param WP_REST_Request $request The request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function update_settings( WP_REST_Request $request ) {
		$current = $this->get_all_settings();
		$params  = $request->get_params();
		$updated = array();

		if ( ! \OpusPriceBuilder\oppb_can_use_premium_code() ) {
			unset( $params['ai_provider'], $params['ai_api_key'], $params['ai_model'], $params['clear_ai_api_key'] );
		}

		if ( ! empty( $params['clear_ai_api_key'] ) ) {
			$params['ai_api_key'] = '';
		}

		// Update only provided fields
		foreach ( array_keys( $this->defaults ) as $key ) {
			if ( isset( $params[ $key ] ) ) {
				// Special handling for API key - don't update if it's masked or empty
				if ( 'ai_api_key' === $key ) {
					if ( ! empty( $params['clear_ai_api_key'] ) ) {
						$updated[ $key ] = '';
						continue;
					}

					$api_key_value = trim( (string) $params[ $key ] );
					// Skip if empty, starts with asterisks (masked), or contains only asterisks
					if (
						'' === $api_key_value ||
						preg_match( '/^\*+/', $api_key_value ) ||
						preg_match( '/^\*+$/', $api_key_value )
					) {
						// Keep existing encrypted key
						$updated[ $key ] = $current[ $key ];
						continue;
					}
					// This is a new, unmasked API key - encrypt it
					$updated[ $key ] = $this->encrypt_api_key( $api_key_value );
					continue;
				}
				$updated[ $key ] = $params[ $key ];
			} else {
				$updated[ $key ] = $current[ $key ];
			}
		}

		// Sanitize values
		$updated['google_fonts_local']    = (bool) $updated['google_fonts_local'];
		$updated['google_fonts_cdn']      = (bool) $updated['google_fonts_cdn'];
		$updated['google_fonts_disabled'] = (bool) $updated['google_fonts_disabled'];
		$updated['ai_provider']           = sanitize_text_field( $updated['ai_provider'] );
		$updated['ai_model']              = sanitize_text_field( $updated['ai_model'] );
		$updated['default_scenario']      = sanitize_text_field( $updated['default_scenario'] );

		update_option( self::OPTION_NAME, $updated );

		// Return response with masked key
		$response                = $updated;
		$response['ai_api_key']  = ! empty( $updated['ai_api_key'] )
			? str_repeat( '*', 20 ) . substr( $this->decrypt_api_key( $updated['ai_api_key'] ), -4 )
			: '';
		$response['has_api_key'] = ! empty( $updated['ai_api_key'] );

		return rest_ensure_response( $response );
	}

	/**
	 * Update/download local Google fonts.
	 *
	 * @param WP_REST_Request $request The request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function update_fonts( WP_REST_Request $request ) {
		$fonts              = $request->get_param( 'fonts' );
		$google_fonts_local = $request->get_param( 'google_fonts_local' );
		$settings           = $this->get_all_settings();

		// Check if local fonts are enabled - use request param if provided, otherwise check DB
		$is_enabled = $google_fonts_local ?? $settings['google_fonts_local'];

		if ( ! $is_enabled ) {
			return new WP_Error(
				'local_fonts_disabled',
				__( 'Local fonts feature is disabled.', 'opus-price-builder' ),
				array( 'status' => 400 )
			);
		}

		// Fonts must be specified - we don't download all fonts anymore
		if ( empty( $fonts ) ) {
			return new WP_Error(
				'no_fonts_specified',
				__( 'No fonts specified to download.', 'opus-price-builder' ),
				array( 'status' => 400 )
			);
		}

		$downloaded = array();
		$skipped    = array();
		$errors     = array();

		// Determine if fonts is a simple array or detailed object with variants
		$is_detailed = $this->is_associative_array( $fonts );

		if ( $is_detailed ) {
			// Detailed format: { "Inter": { "weights": [400, 700], "styles": ["normal"] } }
			foreach ( $fonts as $font_name => $variants ) {
				$weights = isset( $variants['weights'] ) ? array_map( 'intval', $variants['weights'] ) : array( 400 );
				$styles  = isset( $variants['styles'] ) ? $variants['styles'] : array( 'normal' );

				$result = $this->download_google_font_variants( $font_name, $weights, $styles, $settings['google_fonts_cache'] );

				if ( is_wp_error( $result ) ) {
					$errors[ $font_name ] = $result->get_error_message();
				} else {
					if ( ! empty( $result['downloaded'] ) ) {
						$downloaded[ $font_name ] = $result['downloaded'];
					}
					if ( ! empty( $result['skipped'] ) ) {
						$skipped[ $font_name ] = $result['skipped'];
					}
				}
			}
		} else {
			// Simple array format: ["Inter", "Roboto"] - download default weights
			foreach ( $fonts as $font ) {
				$result = $this->download_google_font_variants( $font, array( 400, 500, 600, 700 ), array( 'normal' ), $settings['google_fonts_cache'] );
				if ( is_wp_error( $result ) ) {
					$errors[ $font ] = $result->get_error_message();
				} else {
					if ( ! empty( $result['downloaded'] ) ) {
						$downloaded[ $font ] = $result['downloaded'];
					}
					if ( ! empty( $result['skipped'] ) ) {
						$skipped[ $font ] = $result['skipped'];
					}
				}
			}
		}

		// Update cache with downloaded fonts
		$settings['google_fonts_cache'] = array_merge(
			$settings['google_fonts_cache'],
			$downloaded
		);

		// Auto-enable google_fonts_local if fonts were downloaded successfully
		if ( ! empty( $downloaded ) ) {
			$settings['google_fonts_local'] = true;
		}

		update_option( self::OPTION_NAME, $settings );

		return rest_ensure_response(
			array(
				'downloaded'         => $downloaded,
				'skipped'            => $skipped,
				'errors'             => $errors,
				'cache'              => $settings['google_fonts_cache'],
				'google_fonts_local' => $settings['google_fonts_local'],
			)
		);
	}

	/**
	 * Check if an array is associative (object-like) or sequential.
	 *
	 * @param array $arr The array to check.
	 * @return bool True if associative.
	 */
	private function is_associative_array( array $arr ): bool {
		if ( empty( $arr ) ) {
			return false;
		}
		return array_keys( $arr ) !== range( 0, count( $arr ) - 1 );
	}

	/**
	 * Get all settings with defaults.
	 *
	 * @return array<string, mixed>
	 */
	private function get_all_settings(): array {
		$stored   = get_option( self::OPTION_NAME, array() );
		$settings = wp_parse_args( $stored, $this->defaults );

		// Legacy installs used CDN whenever local hosting was off.
		if ( ! array_key_exists( 'google_fonts_cdn', $stored ) && empty( $settings['google_fonts_local'] ) ) {
			$settings['google_fonts_cdn'] = true;
		}

		return $settings;
	}

	/**
	 * Get setting arguments for REST API.
	 *
	 * @return array<string, array>
	 */
	private function get_setting_args(): array {
		return array(
			'google_fonts_local'    => array(
				'description' => __( 'Whether to load Google fonts locally.', 'opus-price-builder' ),
				'type'        => 'boolean',
				'required'    => false,
			),
			'google_fonts_cdn'      => array(
				'description' => __( 'Whether to load Google fonts from Google CDN for visitors (opt-in).', 'opus-price-builder' ),
				'type'        => 'boolean',
				'required'    => false,
			),
			'google_fonts_disabled' => array(
				'description' => __( 'Disable all font loading (user handles fonts manually).', 'opus-price-builder' ),
				'type'        => 'boolean',
				'required'    => false,
			),
			'ai_provider'           => array(
				'description' => __( 'AI provider for layout generation.', 'opus-price-builder' ),
				'type'        => 'string',
				'enum'        => array( 'none', 'openai', 'gemini', 'anthropic' ),
				'required'    => false,
			),
			'ai_api_key'            => array(
				'description' => __( 'API key for AI provider.', 'opus-price-builder' ),
				'type'        => 'string',
				'required'    => false,
			),
			'clear_ai_api_key'      => array(
				'description' => __( 'Remove the stored AI API key.', 'opus-price-builder' ),
				'type'        => 'boolean',
				'required'    => false,
			),
			'ai_model'              => array(
				'description' => __( 'AI model to use.', 'opus-price-builder' ),
				'type'        => 'string',
				'required'    => false,
			),
			'default_scenario'      => array(
				'description' => __( 'Default price scenario.', 'opus-price-builder' ),
				'type'        => 'string',
				'enum'        => array( 'simple', 'sale', 'variable_simple', 'variable_sale' ),
				'required'    => false,
			),
		);
	}

	/**
	 * Encrypt API key for storage.
	 *
	 * @param string $key The API key.
	 * @return string
	 */
	private function encrypt_api_key( string $key ): string {
		if ( empty( $key ) ) {
			return '';
		}

		$salt = wp_salt( 'auth' );
		return base64_encode( openssl_encrypt( $key, 'AES-256-CBC', $salt, 0, substr( md5( $salt ), 0, 16 ) ) );
	}

	/**
	 * Decrypt API key from storage.
	 *
	 * @param string $encrypted The encrypted key.
	 * @return string
	 */
	private function decrypt_api_key( string $encrypted ): string {
		if ( empty( $encrypted ) ) {
			return '';
		}

		$salt      = wp_salt( 'auth' );
		$decrypted = openssl_decrypt( base64_decode( $encrypted ), 'AES-256-CBC', $salt, 0, substr( md5( $salt ), 0, 16 ) );

		return $decrypted !== false ? $decrypted : '';
	}


	/**
	 * Download specific variants of a Google font.
	 *
	 * @param string $font_name   Font name.
	 * @param array  $weights     Array of font weights to download (e.g., [400, 700]).
	 * @param array  $styles      Array of font styles ('normal', 'italic').
	 * @param array  $fonts_cache Current fonts cache to check for existing files.
	 * @return array|WP_Error Font file info with downloaded and skipped files, or error.
	 */
	private function download_google_font_variants( string $font_name, array $weights, array $styles, array $fonts_cache ) {
		$upload_dir = wp_upload_dir();
		$fonts_dir  = $upload_dir['basedir'] . '/oppb-fonts';

		// Create fonts directory if not exists
		if ( ! file_exists( $fonts_dir ) ) {
			wp_mkdir_p( $fonts_dir );
		}

		$font_slug = sanitize_title( $font_name );
		$font_dir  = $fonts_dir . '/' . $font_slug;

		if ( ! file_exists( $font_dir ) ) {
			wp_mkdir_p( $font_dir );
		}

		// Check which variants already exist in cache
		$existing_variants = array();
		if ( isset( $fonts_cache[ $font_name ]['files'] ) ) {
			foreach ( $fonts_cache[ $font_name ]['files'] as $file_info ) {
				$key                       = $file_info['weight'] . '-' . ( $file_info['style'] ?? 'normal' );
				$existing_variants[ $key ] = $file_info;
			}
		}

		// Determine which variants we need to download
		$needed_variants  = array();
		$skipped_files    = array();
		$existing_on_disk = array(); // Files that exist on disk (with proper naming)

		foreach ( $weights as $weight ) {
			foreach ( $styles as $style ) {
				$key       = $weight . '-' . $style;
				$file_name = $font_slug . '-' . $weight . ( 'italic' === $style ? '-italic' : '' ) . '.woff2';
				$file_path = $font_dir . '/' . $file_name;

				// Skip if file already exists on disk
				if ( file_exists( $file_path ) ) {
					$skipped_files[]    = $file_name;
					$existing_on_disk[] = array(
						'file'   => $file_name,
						'weight' => (string) $weight,
						'style'  => $style,
					);
					continue;
				}

				$needed_variants[] = array(
					'weight' => $weight,
					'style'  => $style,
				);
			}
		}

		// If all variants exist on disk, return with file info to update cache
		if ( empty( $needed_variants ) ) {
			return array(
				'downloaded' => array(
					'name'  => $font_name,
					'slug'  => $font_slug,
					'files' => $existing_on_disk,
				),
				'skipped'    => $skipped_files,
			);
		}

		$css_result = $this->request_google_font_css( $font_name, $needed_variants );
		if ( is_wp_error( $css_result ) ) {
			return $css_result;
		}

		$css           = $css_result['css'];
		$used_fallback = $css_result['used_fallback'];
		$font_faces    = $this->parse_latin_font_faces_from_css( $css );

		if ( empty( $font_faces ) ) {
			return new WP_Error(
				'font_parse_error',
				sprintf(
					/* translators: %s: font name */
					__( 'Could not parse font CSS for %s. No woff2 URLs found.', 'opus-price-builder' ),
					$font_name
				)
			);
		}

		$downloaded_files = array();
		$base_file_path   = null;

		foreach ( $font_faces as $face ) {
			$url       = $face['url'];
			$style     = $face['style'];
			$file_name = $this->get_font_file_name( $font_slug, $face['weight'], $style );
			$file_path = $font_dir . '/' . $file_name;

			// Skip if file already exists
			if ( file_exists( $file_path ) ) {
				$skipped_files[]    = $file_name;
				$downloaded_files[] = array(
					'file'   => $file_name,
					'weight' => (string) $face['weight'],
					'style'  => $style,
				);
				if ( null === $base_file_path ) {
					$base_file_path = $file_path;
				}
				continue;
			}

			// Download font file
			$font_response = wp_remote_get( $url );

			if ( is_wp_error( $font_response ) ) {
				continue;
			}

			$font_content = wp_remote_retrieve_body( $font_response );

			if ( empty( $font_content ) ) {
				continue;
			}

			// Save font file
			// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
			file_put_contents( $file_path, $font_content );

			$downloaded_files[] = array(
				'file'   => $file_name,
				'weight' => (string) $face['weight'],
				'style'  => $style,
			);

			if ( null === $base_file_path ) {
				$base_file_path = $file_path;
			}
		}

		if ( $used_fallback && null !== $base_file_path && file_exists( $base_file_path ) ) {
			$downloaded_files = $this->synthesize_variant_files_from_base(
				$font_dir,
				$font_slug,
				$base_file_path,
				$needed_variants,
				$downloaded_files,
				$skipped_files
			);
		}

		// Merge newly downloaded files with variants that already existed on disk.
		$all_files = $existing_on_disk;
		foreach ( $downloaded_files as $file_info ) {
			$all_files[] = $file_info;
		}

		return array(
			'downloaded' => array(
				'name'  => $font_name,
				'slug'  => $font_slug,
				'files' => $all_files,
			),
			'skipped'    => $skipped_files,
		);
	}

	/**
	 * Build a Google Fonts CSS2 API URL for specific variants.
	 *
	 * @param string                             $font_name Font name.
	 * @param array<int, array<string, mixed>>   $variants  Variants with weight and style keys.
	 * @return string
	 */
	private function build_google_font_css_url( string $font_name, array $variants ): string {
		$font_family = str_replace( ' ', '+', $font_name );
		$has_italic  = false;

		foreach ( $variants as $variant ) {
			if ( 'italic' === ( $variant['style'] ?? 'normal' ) ) {
				$has_italic = true;
				break;
			}
		}

		if ( $has_italic ) {
			$axis_values = array();
			foreach ( $variants as $variant ) {
				$ital          = 'italic' === ( $variant['style'] ?? 'normal' ) ? '1' : '0';
				$axis_values[] = $ital . ',' . (int) ( $variant['weight'] ?? 400 );
			}
			$axis_values = array_values( array_unique( $axis_values ) );
			sort( $axis_values );

			return "https://fonts.googleapis.com/css2?family={$font_family}:ital,wght@" . implode( ';', $axis_values ) . '&display=swap';
		}

		$unique_weights = array_values( array_unique( array_map( 'intval', array_column( $variants, 'weight' ) ) ) );
		sort( $unique_weights );

		return "https://fonts.googleapis.com/css2?family={$font_family}:wght@" . implode( ';', $unique_weights ) . '&display=swap';
	}

	/**
	 * Perform an HTTP request to the Google Fonts CSS API.
	 *
	 * @param string $url Request URL.
	 * @return array|\WP_Error
	 */
	private function perform_google_font_request( string $url ) {
		return wp_remote_get(
			$url,
			array(
				'headers' => array(
					// Use modern user agent to get woff2
					'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
				),
			)
		);
	}

	/**
	 * Request Google Fonts CSS, with fallback for single-variant families.
	 *
	 * Some fonts (e.g. Fjalla One, Bebas Neue) only ship weight 400. Requesting
	 * unavailable weights or italic returns HTTP 400 from Google.
	 *
	 * @param string                           $font_name Font name.
	 * @param array<int, array<string, mixed>> $variants  Requested variants.
	 * @return array{css: string, used_fallback: bool}|\WP_Error
	 */
	private function request_google_font_css( string $font_name, array $variants ) {
		$response = $this->perform_google_font_request(
			$this->build_google_font_css_url( $font_name, $variants )
		);

		if ( is_wp_error( $response ) ) {
			return $response;
		}

		$status_code = (int) wp_remote_retrieve_response_code( $response );
		$css         = wp_remote_retrieve_body( $response );

		if ( 200 === $status_code && ! empty( $css ) ) {
			return array(
				'css'           => $css,
				'used_fallback' => false,
			);
		}

		// Retry with the base regular weight for single-variant font families.
		$fallback_variants = array(
			array(
				'weight' => 400,
				'style'  => 'normal',
			),
		);

		$fallback_response = $this->perform_google_font_request(
			$this->build_google_font_css_url( $font_name, $fallback_variants )
		);

		if ( is_wp_error( $fallback_response ) ) {
			return $fallback_response;
		}

		$fallback_status = (int) wp_remote_retrieve_response_code( $fallback_response );
		$fallback_css    = wp_remote_retrieve_body( $fallback_response );

		if ( 200 !== $fallback_status || empty( $fallback_css ) ) {
			return new WP_Error(
				'font_fetch_error',
				sprintf(
					/* translators: 1: font name, 2: HTTP status code */
					__( 'Could not fetch font %1$s (HTTP %2$d).', 'opus-price-builder' ),
					$font_name,
					$fallback_status > 0 ? $fallback_status : $status_code
				)
			);
		}

		return array(
			'css'           => $fallback_css,
			'used_fallback' => true,
		);
	}

	/**
	 * Parse latin @font-face blocks from a Google Fonts CSS response.
	 *
	 * @param string $css Google Fonts CSS.
	 * @return array<int, array{url: string, weight: string, style: string}>
	 */
	private function parse_latin_font_faces_from_css( string $css ): array {
		$font_faces = array();

		preg_match_all( '/\/\*\s*latin\s*\*\/\s*(@font-face\s*\{[^}]+\})/is', $css, $latin_blocks );

		if ( empty( $latin_blocks[1] ) ) {
			return $font_faces;
		}

		foreach ( $latin_blocks[1] as $block ) {
			$weight = '400';
			if ( preg_match( '/font-weight\s*:\s*(\d+)/i', $block, $w ) ) {
				$weight = $w[1];
			}

			$style = 'normal';
			if ( preg_match( '/font-style\s*:\s*(\w+)/i', $block, $s ) ) {
				$style = $s[1];
			}

			$url = null;
			if ( preg_match( '/url\s*\(\s*([^\s\)]+)\s*\)\s*format\s*\(\s*[\'"]?woff2[\'"]?\s*\)/i', $block, $u ) ) {
				$url = trim( $u[1], '\'" ' );
			} elseif ( preg_match( '/url\s*\(\s*([^\s\)]+\.woff2[^\s\)]*)\s*\)/i', $block, $u ) ) {
				$url = trim( $u[1], '\'" ' );
			}

			if ( $url ) {
				$font_faces[] = array(
					'url'    => $url,
					'weight' => $weight,
					'style'  => $style,
				);
			}
		}

		return $font_faces;
	}

	/**
	 * Build a local font filename for a variant.
	 *
	 * @param string       $font_slug Font slug.
	 * @param int|string   $weight    Font weight.
	 * @param string       $style     Font style.
	 * @return string
	 */
	private function get_font_file_name( string $font_slug, $weight, string $style ): string {
		return $font_slug . '-' . $weight . ( 'italic' === $style ? '-italic' : '' ) . '.woff2';
	}

	/**
	 * Create local files for requested variants using a downloaded base file.
	 *
	 * Used when Google only provides a single weight/style for a font family.
	 *
	 * @param string                           $font_dir          Font directory.
	 * @param string                           $font_slug         Font slug.
	 * @param string                           $base_file_path    Path to downloaded base file.
	 * @param array<int, array<string, mixed>> $needed_variants   Requested variants.
	 * @param array<int, array<string, string>> $downloaded_files Existing downloaded file metadata.
	 * @param array<int, string>               $skipped_files     Skipped filenames (by reference append).
	 * @return array<int, array<string, string>>
	 */
	private function synthesize_variant_files_from_base(
		string $font_dir,
		string $font_slug,
		string $base_file_path,
		array $needed_variants,
		array $downloaded_files,
		array &$skipped_files
	): array {
		$indexed_files = array();

		foreach ( $downloaded_files as $file_info ) {
			$key                   = ( $file_info['weight'] ?? '400' ) . '-' . ( $file_info['style'] ?? 'normal' );
			$indexed_files[ $key ] = $file_info;
		}

		foreach ( $needed_variants as $variant ) {
			$weight    = (string) (int) ( $variant['weight'] ?? 400 );
			$style     = (string) ( $variant['style'] ?? 'normal' );
			$key       = $weight . '-' . $style;
			$file_name = $this->get_font_file_name( $font_slug, $weight, $style );
			$file_path = $font_dir . '/' . $file_name;

			if ( isset( $indexed_files[ $key ] ) ) {
				continue;
			}

			if ( ! file_exists( $file_path ) ) {
				// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_copy
				copy( $base_file_path, $file_path );
			} else {
				$skipped_files[] = $file_name;
			}

			$file_info = array(
				'file'   => $file_name,
				'weight' => $weight,
				'style'  => $style,
			);

			$indexed_files[ $key ] = $file_info;
		}

		return array_values( $indexed_files );
	}

	/**
	 * Download a Google font and save locally (legacy method).
	 *
	 * @param string $font_name Font name.
	 * @return array|WP_Error Font file info or error.
	 * @deprecated Use download_google_font_variants instead.
	 */
	private function download_google_font( string $font_name ) {
		$result = $this->download_google_font_variants( $font_name, array( 400, 500, 600, 700 ), array( 'normal' ), array() );

		if ( is_wp_error( $result ) ) {
			return $result;
		}

		return $result['downloaded'] ?? array();
	}

	/**
	 * Get item schema.
	 *
	 * @return array
	 */
	public function get_public_item_schema(): array {
		return array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'settings',
			'type'       => 'object',
			'properties' => array(
				'google_fonts_local' => array(
					'description' => __( 'Whether to load Google fonts locally.', 'opus-price-builder' ),
					'type'        => 'boolean',
				),
				'ai_provider'        => array(
					'description' => __( 'AI provider for layout generation.', 'opus-price-builder' ),
					'type'        => 'string',
				),
				'ai_model'           => array(
					'description' => __( 'AI model to use.', 'opus-price-builder' ),
					'type'        => 'string',
				),
				'has_api_key'        => array(
					'description' => __( 'Whether API key is configured.', 'opus-price-builder' ),
					'type'        => 'boolean',
				),
				'default_scenario'   => array(
					'description' => __( 'Default price scenario.', 'opus-price-builder' ),
					'type'        => 'string',
				),
			),
		);
	}
}
