<?php
/**
 * Theme configuration REST controller.
 *
 * @package JOCBM
 *
 * Copyright (C) 2026 JayOnCode
 * License: GPL-2.0-or-later
 */

namespace JOCBM\Rest;

use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;

defined( 'ABSPATH' ) || exit;

/**
 * Class ThemeConfigController
 */
final class ThemeConfigController {

	use RestPermissions;

	public const OPTION = 'jocbm_theme_config';

	/**
	 * Register routes.
	 */
	public function register_routes(): void {
		register_rest_route(
			RestApi::NAMESPACE,
			'/theme-config',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'show' ),
					'permission_callback' => array( $this, 'can_manage_options' ),
				),
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update' ),
					'permission_callback' => array( $this, 'can_manage_options' ),
				),
			)
		);
	}

	/**
	 * Get theme config.
	 */
	public function show(): WP_REST_Response {
		return new WP_REST_Response( self::get_config_with_fonts(), 200 );
	}

	/**
	 * Update theme config.
	 *
	 * @param WP_REST_Request $request Request.
	 */
	public function update( WP_REST_Request $request ): WP_REST_Response {
		$current = self::get_config();

		$fonts = $this->json_param( $request, 'fonts', null );
		if ( null !== $fonts ) {
			$current['fonts'] = array_values(
				array_filter(
					array_map(
						static function ( $font ) {
							$family = sanitize_text_field( $font['family'] ?? '' );
							$source = esc_url_raw( (string) ( $font['source'] ?? '' ), array( 'http', 'https' ) );

							// Reject non-http(s) or invalid URLs; empty source = system/local family only.
							if ( $source && ! wp_http_validate_url( $source ) ) {
								$source = '';
							}

							if ( ! $family ) {
								return null;
							}

							return array(
								'family' => $family,
								'slug'   => sanitize_title( $font['slug'] ?? $family ),
								'source' => $source,
							);
						},
						(array) $fonts
					)
				)
			);
		}

		$colors = $this->json_param( $request, 'colors', null );
		if ( null !== $colors ) {
			$current['colors'] = array_values(
				array_map(
					static function ( $color ) {
						return array(
							'name'  => sanitize_text_field( $color['name'] ?? '' ),
							'slug'  => sanitize_title( $color['slug'] ?? ( $color['name'] ?? '' ) ),
							'color' => sanitize_hex_color( $color['color'] ?? '#000000' ) ?: '#000000',
						);
					},
					(array) $colors
				)
			);
		}

		$font_sizes = $this->json_param( $request, 'fontSizes', null );
		if ( null !== $font_sizes ) {
			$current['fontSizes'] = array_values(
				array_map(
					static function ( $size ) {
						$line_height = sanitize_text_field( $size['lineHeight'] ?? '' );
						return array(
							'name'       => sanitize_text_field( $size['name'] ?? '' ),
							'slug'       => sanitize_title( $size['slug'] ?? ( $size['name'] ?? '' ) ),
							'size'       => sanitize_text_field( $size['size'] ?? '1rem' ),
							'lineHeight' => $line_height,
						);
					},
					(array) $font_sizes
				)
			);
		}

		update_option( self::OPTION, self::enforce_locked_defaults( $current ) );

		// Drop cached theme.json so fontFamilies resolve against the new option immediately.
		if ( class_exists( '\WP_Theme_JSON_Resolver' ) && method_exists( '\WP_Theme_JSON_Resolver', 'clean_cached_data' ) ) {
			\WP_Theme_JSON_Resolver::clean_cached_data();
		}

		return new WP_REST_Response( self::get_config_with_fonts(), 200 );
	}

	/**
	 * Stored config plus resolved font family picker list.
	 *
	 * @return array<string, mixed>
	 */
	public static function get_config_with_fonts(): array {
		$config                      = self::get_config();
		$config['fontFamilies']      = \JOCBM\Theme\ThemeConfig::get_available_font_families();
		$config['themeFontFamilies'] = \JOCBM\Theme\ThemeConfig::get_theme_font_families_for_display();
		$config['themeColors']       = \JOCBM\Theme\ThemeConfig::get_theme_origin_colors();
		return $config;
	}

	/**
	 * Factory defaults for global theme settings.
	 *
	 * @return array{fonts: array<int, array<string, string>>, colors: array<int, array<string, string>>, fontSizes: array<int, array<string, string>>}
	 */
	public static function defaults(): array {
		// Inter is self-hosted under assets/fonts/inter/ (no remote CDN).
		$inter_source = defined( 'JOCBM_URL' )
			? JOCBM_URL . 'assets/fonts/inter/inter.css'
			: '';

		return array(
			'fonts'     => array(
				array(
					'family' => 'Inter',
					'slug'   => 'inter',
					'source' => $inter_source,
				),
				array(
					'family' => 'system-ui',
					'slug'   => 'system-ui',
					'source' => '',
				),
			),
			'colors'    => array(
				// Brand — main / container / surface.
				array(
					'name'  => 'Primary',
					'slug'  => 'primary',
					'color' => '#3525cd',
				),
				array(
					'name'  => 'Primary Container',
					'slug'  => 'primary-container',
					'color' => '#4f46e5',
				),
				array(
					'name'  => 'Primary Surface',
					'slug'  => 'primary-surface',
					'color' => '#eaedff',
				),
				array(
					'name'  => 'Secondary',
					'slug'  => 'secondary',
					'color' => '#4648d4',
				),
				array(
					'name'  => 'Secondary Container',
					'slug'  => 'secondary-container',
					'color' => '#6a6be7',
				),
				array(
					'name'  => 'Secondary Surface',
					'slug'  => 'secondary-surface',
					'color' => '#eef0ff',
				),
				// Semantic status.
				array(
					'name'  => 'Success',
					'slug'  => 'success',
					'color' => '#16a34a',
				),
				array(
					'name'  => 'Success Container',
					'slug'  => 'success-container',
					'color' => '#4ade80',
				),
				array(
					'name'  => 'Success Surface',
					'slug'  => 'success-surface',
					'color' => '#dcfce7',
				),
				array(
					'name'  => 'Warning',
					'slug'  => 'warning',
					'color' => '#d97706',
				),
				array(
					'name'  => 'Warning Container',
					'slug'  => 'warning-container',
					'color' => '#f59e0b',
				),
				array(
					'name'  => 'Warning Surface',
					'slug'  => 'warning-surface',
					'color' => '#fef3c7',
				),
				array(
					'name'  => 'Error',
					'slug'  => 'error',
					'color' => '#ba1a1a',
				),
				array(
					'name'  => 'Error Container',
					'slug'  => 'error-container',
					'color' => '#dc2626',
				),
				array(
					'name'  => 'Error Surface',
					'slug'  => 'error-surface',
					'color' => '#fee2e2',
				),
				// On colors.
				array(
					'name'  => 'On Primary',
					'slug'  => 'on-primary',
					'color' => '#ffffff',
				),
				// Neutrals.
				array(
					'name'  => 'Surface',
					'slug'  => 'surface',
					'color' => '#faf8ff',
				),
				array(
					'name'  => 'Surface Container',
					'slug'  => 'surface-container',
					'color' => '#eaedff',
				),
				array(
					'name'  => 'On Surface',
					'slug'  => 'on-surface',
					'color' => '#131b2e',
				),
				array(
					'name'  => 'On Surface Variant',
					'slug'  => 'on-surface-variant',
					'color' => '#464555',
				),
				array(
					'name'  => 'Outline',
					'slug'  => 'outline',
					'color' => '#777587',
				),
			),
			'fontSizes' => array(
				array(
					'name'       => 'X-Small',
					'slug'       => 'x-small',
					'size'       => '0.75rem',
					'lineHeight' => '1.4',
				),
				array(
					'name'       => 'Small',
					'slug'       => 'small',
					'size'       => '0.875rem',
					'lineHeight' => '1.4',
				),
				array(
					'name'       => 'Medium',
					'slug'       => 'medium',
					'size'       => '1rem',
					'lineHeight' => '1.5',
				),
				array(
					'name'       => 'Large',
					'slug'       => 'large',
					'size'       => '1.5rem',
					'lineHeight' => '1.3',
				),
				array(
					'name'       => 'X-Large',
					'slug'       => 'x-large',
					'size'       => '2.25rem',
					'lineHeight' => '1.2',
				),
				array(
					'name'       => 'XX-Large',
					'slug'       => 'xx-large',
					'size'       => '3rem',
					'lineHeight' => '1.15',
				),
				array(
					'name'       => 'XXX-Large',
					'slug'       => 'xxx-large',
					'size'       => '3.75rem',
					'lineHeight' => '1.1',
				),
			),
		);
	}

	/**
	 * Built-in font-size slugs that cannot be edited or removed.
	 *
	 * @return array<int, string>
	 */
	public static function locked_font_size_slugs(): array {
		return array( 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large', 'xxx-large' );
	}

	/**
	 * Whether a font row is the built-in Inter face.
	 *
	 * @param array<string, mixed> $font Font row.
	 */
	public static function is_locked_font( array $font ): bool {
		$slug   = strtolower( (string) ( $font['slug'] ?? '' ) );
		$family = strtolower( trim( (string) ( $font['family'] ?? '' ) ) );
		return 'inter' === $slug || 'inter' === $family;
	}

	/**
	 * Keep Inter + factory font sizes intact; append custom extras.
	 *
	 * @param array<string, mixed> $config Theme config.
	 * @return array<string, mixed>
	 */
	public static function enforce_locked_defaults( array $config ): array {
		$defaults = self::defaults();

		// Fonts: Inter always first with factory source; drop duplicate Inter rows.
		$inter   = $defaults['fonts'][0];
		$customs = array();
		foreach ( (array) ( $config['fonts'] ?? array() ) as $font ) {
			if ( ! is_array( $font ) || self::is_locked_font( $font ) ) {
				continue;
			}
			$customs[] = $font;
		}
		$config['fonts'] = array_values( array_merge( array( $inter ), $customs ) );

		// Font sizes: factory locked sizes in order, then custom (non-locked) rows.
		$locked_slugs = array_fill_keys( self::locked_font_size_slugs(), true );
		$locked_rows  = array();
		foreach ( $defaults['fontSizes'] as $def ) {
			$slug = (string) ( $def['slug'] ?? '' );
			if ( '' !== $slug && isset( $locked_slugs[ $slug ] ) ) {
				$locked_rows[] = $def;
			}
		}
		$custom_sizes = array();
		foreach ( (array) ( $config['fontSizes'] ?? array() ) as $size ) {
			if ( ! is_array( $size ) ) {
				continue;
			}
			$slug = strtolower( (string) ( $size['slug'] ?? '' ) );
			if ( '' === $slug || isset( $locked_slugs[ $slug ] ) ) {
				continue;
			}
			$custom_sizes[] = $size;
		}
		$config['fontSizes'] = array_values( array_merge( $locked_rows, $custom_sizes ) );

		if ( isset( $config['colors'] ) && is_array( $config['colors'] ) ) {
			$config['colors'] = self::prune_redundant_default_colors( $config['colors'] );
		}

		return $config;
	}

	/**
	 * Wipe stored global settings and restore factory defaults.
	 *
	 * @return array<string, mixed> Config with fontFamilies for the admin UI.
	 */
	public static function reset_to_defaults(): array {
		delete_option( self::OPTION );
		// Force a clean write so caches / partial merges cannot keep old brand settings.
		update_option( self::OPTION, self::defaults(), false );
		return self::get_config_with_fonts();
	}

	/**
	 * Default / stored config.
	 *
	 * @return array<string, mixed>
	 */
	public static function get_config(): array {
		$defaults = self::defaults();
		$stored   = get_option( self::OPTION, null );

		if ( ! is_array( $stored ) || array() === $stored ) {
			return $defaults;
		}

		// Replace list keys entirely when present so stale custom palettes cannot linger
		// via shallow wp_parse_args merges of nested arrays.
		$out = $defaults;
		foreach ( array( 'fonts', 'colors', 'fontSizes' ) as $key ) {
			if ( isset( $stored[ $key ] ) && is_array( $stored[ $key ] ) ) {
				$out[ $key ] = array_values( $stored[ $key ] );
			}
		}

		// Append any new factory brand tokens missing from older saved palettes.
		if ( isset( $out['colors'] ) && is_array( $out['colors'] ) ) {
			$out['colors'] = self::merge_missing_default_colors(
				$out['colors'],
				$defaults['colors']
			);
			$out['colors'] = self::prune_redundant_default_colors( $out['colors'] );
		}

		return self::enforce_locked_defaults( $out );
	}

	/**
	 * Drop obsolete factory swatches that duplicate On Primary (#fff).
	 *
	 * @param array<int, array<string, mixed>> $colors Palette.
	 * @return array<int, array<string, mixed>>
	 */
	private static function prune_redundant_default_colors( array $colors ): array {
		$drop = array(
			'on-secondary' => true,
			'on-success'   => true,
			'on-warning'   => true,
			'on-error'     => true,
		);
		$out  = array();
		foreach ( $colors as $color ) {
			$slug = isset( $color['slug'] ) ? (string) $color['slug'] : '';
			if ( '' !== $slug && isset( $drop[ $slug ] ) ) {
				continue;
			}
			$out[] = $color;
		}
		return array_values( $out );
	}

	/**
	 * Keep saved colors; append factory tokens whose slug is not yet present.
	 *
	 * @param array<int, array<string, mixed>> $stored   Saved palette.
	 * @param array<int, array<string, mixed>> $defaults Factory palette.
	 * @return array<int, array<string, mixed>>
	 */
	private static function merge_missing_default_colors( array $stored, array $defaults ): array {
		$have = array();
		foreach ( $stored as $color ) {
			$slug = isset( $color['slug'] ) ? (string) $color['slug'] : '';
			if ( '' !== $slug ) {
				$have[ $slug ] = true;
			}
		}

		$out = array_values( $stored );
		foreach ( $defaults as $def ) {
			$slug = isset( $def['slug'] ) ? (string) $def['slug'] : '';
			if ( '' === $slug || isset( $have[ $slug ] ) ) {
				continue;
			}
			$out[]         = $def;
			$have[ $slug ] = true;
		}

		return $out;
	}
}
