<?php
/**
 * Theme Options Manager
 *
 * Lightweight get/set API around a single wp_options row.
 *
 * @package Kutby
 * @since   1.0.0
 */

declare(strict_types=1);

namespace Kutby\ThemeOptions;

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

/**
 * Class Manager
 */
final class Manager {

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

	/**
	 * Initialize (warm cache).
	 *
	 * @return void
	 */
	public static function init(): void {
		self::all();
	}

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

		$stored = get_option( KUTBY_OPTION_KEY, array() );
		if ( ! is_array( $stored ) ) {
			$stored = array();
		}

		self::$cache = self::merge_recursive( Defaults::all(), $stored );

		return self::$cache;
	}

	/**
	 * Get a single option by dotted path (e.g. colors.primary).
	 *
	 * @param string $key     Dot-notated key.
	 * @param mixed  $default Fallback.
	 * @return mixed
	 */
	public static function get( string $key, mixed $default = null ): mixed {
		$parts  = explode( '.', $key );
		$cursor = self::all();

		foreach ( $parts as $part ) {
			if ( ! is_array( $cursor ) || ! array_key_exists( $part, $cursor ) ) {
				return $default;
			}
			$cursor = $cursor[ $part ];
		}

		return $cursor;
	}

	/**
	 * Get an entire option group.
	 *
	 * @param string               $group   Group key.
	 * @param array<string, mixed> $default Fallback.
	 * @return array<string, mixed>
	 */
	public static function get_group( string $group, array $default = array() ): array {
		$value = self::get( $group, $default );
		return is_array( $value ) ? $value : $default;
	}

	/**
	 * Persist options (replaces entire stored array after sanitize).
	 *
	 * Emits kutby_options_saved and Dispatcher options_saved once.
	 *
	 * @param array<string, mixed> $options Options to save.
	 * @param array<string, mixed> $context Optional event context (e.g. via).
	 * @return bool
	 */
	public static function save( array $options, array $context = array() ): bool {
		$sanitized   = Sanitizer::sanitize_all( $options );
		$updated     = update_option( KUTBY_OPTION_KEY, $sanitized, false );
		self::$cache = null;
		self::all();

		/**
		 * Fires after options are saved.
		 *
		 * @since 1.0.0
		 *
		 * @param array<string, mixed> $sanitized Saved options.
		 */
		do_action( 'kutby_options_saved', $sanitized );

		$payload = array_merge(
			array(
				'options' => $sanitized,
			),
			$context
		);

		if ( class_exists( \Kutby\Events\Dispatcher::class ) ) {
			\Kutby\Events\Dispatcher::emit( 'options_saved', $payload );
		}

		return (bool) $updated;
	}

	/**
	 * Flush in-memory cache.
	 *
	 * @return void
	 */
	public static function flush(): void {
		self::$cache = null;
	}

	/**
	 * Recursive merge preserving nested arrays.
	 *
	 * @param array<string, mixed> $base  Defaults.
	 * @param array<string, mixed> $over  Stored values.
	 * @return array<string, mixed>
	 */
	private static function merge_recursive( array $base, array $over ): array {
		foreach ( $over as $key => $value ) {
			if ( is_array( $value ) && isset( $base[ $key ] ) && is_array( $base[ $key ] ) ) {
				$base[ $key ] = self::merge_recursive( $base[ $key ], $value );
			} else {
				$base[ $key ] = $value;
			}
		}

		return $base;
	}
}
