<?php
/**
 * Main plugin class.
 *
 * @package Kramar
 * @since   1.0.0
 */

declare( strict_types=1 );

namespace Kramar;

// Prevent direct access.
defined( 'ABSPATH' ) || exit;

/**
 * Plugin class (singleton).
 *
 * @since 1.0.0
 */
final class Plugin {

	/**
	 * Singleton instance.
	 *
	 * @since 1.0.0
	 */
	private static ?self $instance = null;

	/**
	 * Plugin settings merged with defaults.
	 *
	 * @since 1.0.0
	 * @var array<string, mixed>
	 */
	private array $settings = [];

	/**
	 * Registered modules keyed by "category/slug".
	 *
	 * @since 1.0.0
	 * @var array<string, Module>
	 */
	private array $modules = [];

	/**
	 * Get the singleton instance.
	 *
	 * @since 1.0.0
	 *
	 * @return self Plugin instance.
	 */
	public static function get_instance(): self {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}

		return self::$instance;
	}

	/**
	 * Constructor.
	 *
	 * Modules are instantiated immediately (no translation calls) so that the
	 * modules list is available at any point after plugins_loaded. Settings
	 * loading, the admin Settings page, and module hook registration are all
	 * deferred to the `init` action (priority 0) so that no __() / _e() call
	 * is evaluated before the text-domain JIT loader is ready (WP 6.7+).
	 *
	 * @since 1.0.0
	 */
	private function __construct() {
		$this->register_modules();

		add_action(
			'init',
			function (): void {
				$this->load_settings();

				if ( is_admin() ) {
					$settings_page = new Settings( $this );
					$settings_page->register_hooks();
				}

				$this->init_modules();
			},
			0
		);
	}

	/**
	 * Register all available modules.
	 *
	 * @since 1.0.0
	 */
	private function register_modules(): void {
		$module_classes = [
			Modules\Performance\Cart_Fragments::class,
			Modules\Performance\Dequeue_Assets::class,
			Modules\Performance\WC_Cleanup::class,
			Modules\Compliance\Privacy_Hardening::class,
			Modules\Compliance\Order_Button::class,
			Modules\Compliance\Legal_Checkboxes::class,
			Modules\Compliance\EU_Product_Compliance::class,

			Modules\Checkout\Address_Validation::class,
			Modules\Checkout\Field_Editor::class,
			Modules\Checkout\Vat_Id::class,
			Modules\Checkout\Order_Delivery_Date::class,
			Modules\Orders\Sequential_Numbers::class,
			Modules\Orders\Custom_Statuses::class,
			Modules\Orders\Refund_Return::class,
			Modules\Products\Min_Max_Quantities::class,
			Modules\Products\Extra_Options::class,
			Modules\Products\Swatches::class,
			Modules\Products\Omnibus::class,
			Modules\Products\Enhanced_Reviews::class,
			Modules\Email\Email_Manager::class,
			Modules\Marketing\Google_Shopping_Feed::class,
			Modules\Marketing\Back_In_Stock::class,
			Modules\Marketing\Abandoned_Cart::class,
			Modules\Marketing\Review_Reminder::class,
			Modules\Invoices\Pdf_Invoices::class,
			Modules\Account\Account_Flow::class,
			Modules\Shipping\Weight_Based_Shipping::class,
		];

		foreach ( $module_classes as $class ) {
			$module = new $class( $this );
			$key    = $module->get_category() . '/' . $module->get_slug();

			$this->modules[ $key ] = $module;
		}
	}

	/**
	 * Load settings from the database.
	 *
	 * @since 1.0.0
	 */
	private function load_settings(): void {
		$saved    = get_option( Settings::OPTION_NAME, [] );
		$defaults = $this->get_defaults();

		$this->settings = self::recursive_parse_args(
			is_array( $saved ) ? $saved : [],
			$defaults
		);
	}

	/**
	 * Initialize all enabled modules.
	 *
	 * @since 1.0.0
	 */
	private function init_modules(): void {
		foreach ( $this->modules as $module ) {
			$module->register();
		}
	}

	/**
	 * Build default settings from all registered modules.
	 *
	 * @since 1.0.0
	 *
	 * @return array<string, array<string, array<string, mixed>>> Nested defaults array.
	 */
	public function get_defaults(): array {
		$defaults = [];

		foreach ( $this->modules as $module ) {
			$defaults[ $module->get_category() ][ $module->get_slug() ] = $module->get_default_settings();
		}

		return $defaults;
	}

	/**
	 * Get a specific setting value.
	 *
	 * @since 1.0.0
	 *
	 * @param string      $category Module category.
	 * @param string      $slug     Module slug.
	 * @param string|null $key      Setting key; null returns all module settings.
	 * @return mixed Setting value, or null if not found.
	 */
	public function get_setting( string $category, string $slug, ?string $key = null ): mixed {
		if ( null === $key ) {
			return $this->settings[ $category ][ $slug ] ?? null;
		}

		return $this->settings[ $category ][ $slug ][ $key ] ?? null;
	}

	/**
	 * Get all plugin settings.
	 *
	 * @since 1.0.0
	 *
	 * @return array<string, mixed> Full settings array.
	 */
	public function get_settings(): array {
		return $this->settings;
	}

	/**
	 * Get all registered modules.
	 *
	 * @since 1.0.0
	 *
	 * @return array<string, Module> Modules keyed by "category/slug".
	 */
	public function get_modules(): array {
		return $this->modules;
	}

	/**
	 * Plugin activation callback.
	 *
	 * @since 1.0.0
	 */
	public static function activate(): void {
		$instance = self::get_instance();

		if ( false === get_option( Settings::OPTION_NAME ) ) {
			add_option( Settings::OPTION_NAME, $instance->get_defaults(), '', true );
		}
	}

	/**
	 * Recursively merge arguments with defaults.
	 *
	 * @since 1.0.0
	 *
	 * @param array<string, mixed> $args     Values to merge.
	 * @param array<string, mixed> $defaults Default values.
	 * @return array<string, mixed> Merged array.
	 */
	private static function recursive_parse_args( array $args, array $defaults ): array {
		foreach ( $defaults as $key => $value ) {
			if ( is_array( $value ) && isset( $args[ $key ] ) && is_array( $args[ $key ] ) ) {
				$args[ $key ] = self::recursive_parse_args( $args[ $key ], $value );
			} elseif ( ! isset( $args[ $key ] ) ) {
				$args[ $key ] = $value;
			}
		}

		return $args;
	}
}
