<?php
/**
 * Support Copilot — version-negotiation loader.
 *
 * A deliberately tiny, FROZEN registry shared by every plugin that vendors the
 * Support Copilot module. Each vendored copy registers its version and directory
 * via its bundle entry (support-copilot.php); hosts queue their widget config;
 * the loader requires the HIGHEST registered version's class file once and boots
 * every queued instance from it — no fatal, no double-load, no stale copy
 * answering. Because this class is guarded by class_exists, the FIRST-loaded
 * copy of THIS file is the one that runs even when a newer copy is present — so
 * this class must stay forward-compatible forever. Versioned behaviour lives in
 * the payload it loads, never here.
 *
 * THE FROZEN SURFACE (the complete list, ever):
 *   register(), queue(), boot(), instance(), is_booted(), active_version()
 *
 * boot() deliberately requires only ONE payload file from the winning copy
 * (class-copilot.php).
 *
 * 2.0.0 moved this class out of the global namespace, where it had carried a
 * short prefix, into BlackWhiteDigital\SupportCopilot. That is a HARD break by
 * design:
 * a 1.x copy and a 2.x copy cannot see each other's registry, so both would
 * boot. Every bundling plugin must therefore move to 2.x together, and a host
 * that can detect an active 1.x sibling must suppress its own widget until that
 * sibling updates. No BWD_* alias is provided, deliberately — an alias would
 * reintroduce the short global prefix this rename exists to remove.
 *
 * @package BlackWhiteDigital\SupportCopilot
 * @since   2.0.0
 */

namespace BlackWhiteDigital\SupportCopilot;

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

if ( ! class_exists( __NAMESPACE__ . '\\Loader' ) ) {

	/**
	 * Registry + newest-wins boot for the vendored Support Copilot module.
	 *
	 * @since 2.0.0
	 */
	class Loader {

		/**
		 * Registered vendored copies: version string => absolute module dir.
		 *
		 * @var array<string,string>
		 */
		private static $bundles = array();

		/**
		 * Host configs queued before boot.
		 *
		 * @var array<int,array>
		 */
		private static $queued = array();

		/**
		 * Booted widget instances, keyed by config id.
		 *
		 * @var array<string,object>
		 */
		private static $instances = array();

		/**
		 * Whether the module has been booted this request.
		 *
		 * @var bool
		 */
		private static $booted = false;

		/**
		 * The version that won negotiation and was booted.
		 *
		 * @var string|null
		 */
		private static $active_version = null;

		/**
		 * Register a vendored module copy.
		 *
		 * @since 2.0.0
		 * @param string $version Module version of this copy ($bwdsc_this_version).
		 * @param string $dir     Absolute path to this copy's module directory
		 *                        (the directory containing class-copilot.php).
		 * @return void
		 */
		public static function register( $version, $dir ) {
			$version = (string) $version;
			// If two copies ship the same version, first registration wins;
			// the payload is identical so the path choice is immaterial.
			if ( '' !== $version && ! isset( self::$bundles[ $version ] ) ) {
				self::$bundles[ $version ] = (string) $dir;
			}
		}

		/**
		 * Queue a host's widget config for boot. Called after boot (a very late
		 * adopter), the instance boots immediately instead.
		 *
		 * @since 2.0.0
		 * @param array $config Copilot constructor config.
		 * @return void
		 */
		public static function queue( array $config ) {
			if ( self::$booted ) {
				self::boot_instance( $config );
				return;
			}
			self::$queued[] = $config;
		}

		/**
		 * Boot the highest registered module version and every queued host
		 * instance. Idempotent.
		 *
		 * @since 2.0.0
		 * @return void
		 */
		public static function boot() {
			if ( self::$booted || empty( self::$bundles ) ) {
				return;
			}

			// Highest version wins.
			$versions = array_keys( self::$bundles );
			usort( $versions, 'version_compare' );
			$winner = end( $versions );
			$dir    = self::$bundles[ $winner ];

			// The ONLY payload file the loader ever names.
			require_once $dir . '/class-copilot.php';

			// Mark booted before instance boot so a re-entrant boot() is a no-op.
			self::$booted         = true;
			self::$active_version = $winner;

			foreach ( self::$queued as $config ) {
				self::boot_instance( $config );
			}
			self::$queued = array();
		}

		/**
		 * Construct + init one widget instance from a host config.
		 *
		 * @since 2.0.0
		 * @param array $config Copilot constructor config.
		 * @return void
		 */
		private static function boot_instance( array $config ) {
			if ( ! class_exists( __NAMESPACE__ . '\\Copilot' ) ) {
				return;
			}
			$id = isset( $config['id'] ) ? (string) $config['id'] : 'plugin';
			// One instance per id: AJAX action names derive from the id, so a
			// duplicate would register duplicate handlers. First queue wins.
			if ( isset( self::$instances[ $id ] ) ) {
				return;
			}
			$instance = new Copilot( $config );
			$instance->init();
			self::$instances[ $id ] = $instance;
		}

		/**
		 * The booted widget instance for a host id, or null before boot.
		 *
		 * @since 2.0.0
		 * @param string $id The config id the host queued with.
		 * @return object|null Copilot instance or null.
		 */
		public static function instance( $id ) {
			return isset( self::$instances[ (string) $id ] ) ? self::$instances[ (string) $id ] : null;
		}

		/**
		 * Whether the module has booted this request.
		 *
		 * @since 2.0.0
		 * @return bool
		 */
		public static function is_booted() {
			return self::$booted;
		}

		/**
		 * The booted (winning) module version, or null before boot.
		 *
		 * @since 2.0.0
		 * @return string|null
		 */
		public static function active_version() {
			return self::$active_version;
		}
	}
}
