<?php
/**
 * Manager Base Class.
 *
 * Abstract base class for manager classes that provides common functionality
 * for configuration management, singleton pattern, and plugin maintenance.
 *
 * @package   Agenistrator
 * @since     1.0.0
 * @author    Alex Ruban <alex@maximum.guru>
 * @author    Maximum Software Inc <contact@maximum.software>
 * @license   GPL-3.0-only
 * @link      https://agenistrator.com/
 *
 * SPDX-FileCopyrightText: 2025-2026 Alex Ruban <alex@maximum.guru>
 * SPDX-FileCopyrightText: 2025-2026 Maximum Software Inc <contact@maximum.software>
 * SPDX-License-Identifier: GPL-3.0-only
 */

namespace Agenistrator;

// Direct access prevention
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Manager class.
 */
abstract class Manager implements Maintenance {
	use Singleton;

	/**
	 * Option name for storing configuration
	 * @var string
	 * @since     1.0.0
	 */
	protected $option_name;

	/**
	 * Configuration data
	 * Null indicates configuration hasn't been loaded yet
	 * @var array|null
	 */
	private $config = null;

	/**
	 * Constructor
	 *
	 * Initializes the manager with option name.
	 * Configuration is lazy-loaded when needed.
	 * Child classes should call parent::__construct() with an option name suffix.
	 *
	 * @param string $option_suffix The suffix for the option name (without plugin prefix)
	 * @since     1.0.0
	 */
	protected function __construct( $option_suffix ) {
		// Set option name with plugin prefix
		$this->option_name = 'agenistrator_' . $option_suffix;
	}

	/**
	 * Register hooks
	 *
	 * Base implementation does nothing. Child classes should override
	 * this method to register their hooks.
	 *
	 * @return void
	 * @since     1.0.0
	 */
	public function register_hooks() {
		// Base implementation does nothing
	}

	/**
	 * Plugin activation tasks
	 *
	 * Ensures configuration exists with at least default values.
	 * Child classes should override this method if they need to perform
	 * additional activation tasks.
	 *
	 * @return void
	 * @since     1.0.0
	 */
	public function activate_plugin() {
		// Force configuration to be loaded and initialized with defaults if needed
		$this->load_config();
	}

	/**
	 * Plugin deactivation tasks
	 *
	 * By default, does nothing to preserve user settings.
	 * Child classes should override this method if they need to perform
	 * deactivation tasks.
	 *
	 * @return void
	 * @since     1.0.0
	 */
	public function deactivate_plugin() {
		// By default, do nothing to preserve user settings
	}

	/**
	 * Uninstall plugin
	 *
	 * Removes configuration data from the database.
	 * Child classes should override this method if they need to perform
	 * additional cleanup.
	 *
	 * @return void
	 * @since     1.0.0
	 */
	public function uninstall_plugin() {
		delete_option( $this->option_name );
	}

	/**
	 * Get default configuration
	 *
	 * Child classes must implement this method to return default configuration values.
	 *
	 * @return array Default configuration values
	 * @since     1.0.0
	 */
	abstract protected function get_default_config();

	/**
	 * Load configuration from database
	 *
	 * Loads JSON-encoded configuration from the database and decodes it.
	 * If no configuration exists or is invalid, initializes with default values.
	 * This method is called lazily when configuration is first needed.
	 * If configuration is already loaded, it returns immediately.
	 *
	 * @return bool True if configuration was loaded successfully, false otherwise
	 * @since     1.0.0
	 */
	protected function load_config() {
		// If already loaded, don't reload
		if ( null !== $this->config ) {
			return true;
		}

		// Default empty configuration
		$this->config = array();

		// Get configuration from database
		$json_config = get_option( $this->option_name, '' );

		// If no configuration exists, initialize with defaults and save them
		if ( empty( $json_config ) ) {
			$this->config = $this->get_default_config();
			$this->save_config(); // Save defaults to database
			return true;
		}

		// Decode JSON configuration
		$config = json_decode( $json_config, true );
		if ( is_array( $config ) ) {
			$this->config = $config;
			return true;
		}

		// If JSON is invalid, initialize with defaults and save them
		$this->config = $this->get_default_config();
		$this->save_config(); // Save defaults to database
		return false;
	}

	/**
	 * Save configuration to database
	 *
	 * Encodes configuration as JSON and saves it to the database.
	 * Ensures configuration is loaded before saving it.
	 *
	 * @return bool True if configuration was saved successfully, false otherwise
	 * @since     1.0.0
	 */
	protected function save_config() {
		// Ensure configuration is loaded
		$this->load_config();

		// Encode configuration as JSON
		// phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode -- Using json_encode() directly to avoid wp_json_encode_options filter interference from other plugins.
		$json_config = json_encode( $this->config, JSON_PRETTY_PRINT );

		// Save to database
		$result = update_option( $this->option_name, $json_config );

		// Return true if the update succeeded or if the value didn't change (which is also a success)
		return $result || get_option( $this->option_name ) === $json_config;
	}

	/**
	 * Get a reference to a configuration value by key path
	 *
	 * Traverses the configuration array using the provided key path.
	 * If $create_path is true, creates missing array elements along the path.
	 * Returns a reference to the final element in the path, or null if not found.
	 * If key_path is empty, returns a reference to the entire configuration array.
	 *
	 * @param string|array $key_path Configuration key or array of keys for nested access
	 * @param bool $create_path Whether to create missing array elements along the path
	 * @return mixed|null Reference to the configuration value, or null if not found and $create_path is false
	 */
	private function &get_config_ref( $key_path, $create_path = false ) {
		$this->load_config();

		// Convert string key to array for consistent handling
		if ( ! is_array( $key_path ) ) {
			$key_path = array( $key_path );
		}

		// Start with a reference to the config array
		$ref = &$this->config;

		// Traverse the config array using the keys
		foreach ( $key_path as $k ) {
			// Create missing array elements if requested
			if ( $create_path && ! isset( $ref[ $k ] ) ) {
				$ref[ $k ] = array();
			}

			// If key doesn't exist and we're not creating the path, return null
			if ( ! isset( $ref[ $k ] ) ) {
				$null = null;
				return $null;
			}

			// Move reference to the next level
			$ref = &$ref[ $k ];
		}

		return $ref;
	}

	/**
	 * Get configuration value
	 *
	 * Gets a value from the configuration by key or array of keys for nested values.
	 * Ensures configuration is loaded before accessing it.
	 *
	 * @param string|array $key Configuration key or array of keys for nested access
	 * @param mixed $default_value Default value if key doesn't exist
	 * @return mixed Configuration value or default
	 * @since     1.0.0
	 */
	public function get_config( $key, $default_value = null ) {
		$ref = &$this->get_config_ref( $key );

		// If reference is null, return the default value
		if ( null === $ref ) {
			return $default_value;
		}

		return $ref;
	}

	/**
	 * Set configuration value
	 *
	 * Sets a value in the configuration by key or array of keys for nested values.
	 * If key is an empty array, sets the entire configuration.
	 * Ensures configuration is loaded before modifying it.
	 * Call save_config() after making all desired changes to persist them.
	 *
	 * @param string|array $key Configuration key or array of keys for nested access
	 * @param mixed $value Configuration value
	 * @return void
	 * @since     1.0.0
	 */
	public function set_config( $key, $value ) {
		// Convert string key to array for consistent handling
		if ( ! is_array( $key ) ) {
			$key_path = array( $key );
		} else {
			$key_path = $key;
		}

		// Get reference to the config element and set its value
		$ref = &$this->get_config_ref( $key_path, true );
		$ref = $value;
	}

	/**
	 * Set configuration value and save
	 *
	 * Sets a value in the configuration by key or array of keys for nested values,
	 * and immediately saves the configuration.
	 * Ensures configuration is loaded before modifying it.
	 * Use set_config() instead if you're making multiple changes to avoid unnecessary saves.
	 *
	 * @param string|array $key Configuration key or array of keys for nested access
	 * @param mixed $value Configuration value
	 * @return bool True if configuration was saved successfully, false otherwise
	 * @since     1.0.0
	 */
	public function set_config_and_save( $key, $value ) {
		$this->set_config( $key, $value );
		return $this->save_config();
	}
}
