<?php
/**
 * PK Core - boot orchestration for the winning bundle.
 *
 * This file is the single payload entry the frozen loader requires. It loads
 * the rest of the payload, runs additive schema migrations, registers the
 * deterministic classification provider (the permanent tier-10 floor), mounts
 * the admin surface for activated modules, and announces boot.
 *
 * Everything in here is VERSIONED payload behaviour - free to evolve across
 * core versions, unlike the loader.
 *
 * @package PublisherKits\Core
 * @since   0.1.0
 */

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

if ( ! class_exists( 'PKCORE_Core' ) ) {

	/**
	 * Class PKCORE_Core
	 */
	class PKCORE_Core {

		/**
		 * The booted core version.
		 *
		 * @var string
		 */
		private static $version = '';

		/**
		 * The booted bundle's root directory.
		 *
		 * @var string
		 */
		private static $dir = '';

		/**
		 * Modules activated this request (union across bundles).
		 *
		 * @var string[]
		 */
		private static $modules = array();

		/**
		 * Initialise the winning bundle. Called once by PKCORE_Loader::boot().
		 *
		 * @param string $version Winning core version.
		 * @param string $dir     Winning bundle's core root path.
		 * @param array  $modules Union of requested modules.
		 * @return void
		 */
		public static function init( $version, $dir, $modules ) {
			self::$version = (string) $version;
			self::$dir     = (string) $dir;
			self::$modules = array_values( array_intersect( (array) $modules, array( 'profile', 'classify', 'notify' ) ) );

			$inc = trailingslashit( $dir ) . 'includes/';

			require_once $inc . 'class-pkcore-schema.php';
			// Vocab BEFORE profile: the profile's sanitiser validates closed
			// vocabularies through it, and an unknown slug is dropped rather
			// than stored.
			require_once $inc . 'class-pkcore-vocab.php';
			require_once $inc . 'class-pkcore-profile.php';
			// The question set and the consumer matrix are DATA, loaded always:
			// the profile screen renders from them, and the new-install notice
			// needs the matrix outside the profile screen.
			require_once $inc . 'class-pkcore-questions.php';
			require_once $inc . 'class-pkcore-consumers.php';
			require_once $inc . 'class-pkcore-pack.php';
			require_once $inc . 'class-pkcore-engine.php';
			require_once $inc . 'class-pkcore-providers.php';
			require_once $inc . 'class-pkcore-classify.php';
			require_once $inc . 'class-pkcore-alerts.php';
			require_once $inc . 'class-pkcore-mirror.php';

			// The Abilities API surface. Loaded ALWAYS and NOT behind
			// is_admin(): abilities are consumed over REST and by AI clients,
			// where is_admin() is false. Gating these the way the admin screens
			// below are gated would produce a helper that works in wp-admin and
			// answers nothing to the callers it exists for.
			//
			// Safe on every WordPress version. Both classes only add_action()
			// on hooks that WP < 6.9 never fires, so on a site without the
			// Abilities API this is inert rather than fatal - which is what
			// lets the three WP.org bundlers ship without requiring 6.9.
			require_once $inc . 'class-pkcore-abilities.php';
			require_once $inc . 'class-pkcore-ability-profile.php';
			PKCORE_Abilities::init();
			PKCORE_Ability_Profile::init();

			// Projection onto the pre-core shared options. Registered whenever the
			// profile module is active and NOT gated on is_admin(): set_many() is
			// a public contract method callable from anywhere, and a write that
			// skipped the projection would reintroduce the divergence it fixes.
			if ( self::module_active( 'profile' ) ) {
				PKCORE_Mirror::init();
			}

			// The deterministic engine is the permanent classification floor.
			// It registers whether or not the classify module is activated,
			// so the facade is always answerable; a non-activated module just
			// has no UI and never writes.
			PKCORE_Providers::register_floor();

			if ( self::module_active( 'classify' ) ) {
				PKCORE_Schema::maybe_migrate();
			}

			if ( self::module_active( 'notify' ) ) {
				PKCORE_Schema::maybe_migrate_alerts();
				PKCORE_Alerts::init();
			}

			// Gated on profile/classify specifically, not on "any module":
			// a notify-only bundler has no profile or topics screen to render,
			// and must not drag in the wizard or the legacy-profile shim.
			if ( is_admin() && ( self::module_active( 'profile' ) || self::module_active( 'classify' ) ) ) {
				// Temporary single-estate migration shim (see its removal note).
				require_once $inc . 'admin/class-pkcore-legacy-profile.php';
				require_once $inc . 'admin/class-pkcore-fields.php';
				require_once $inc . 'admin/class-pkcore-profile-page.php';
				require_once $inc . 'admin/class-pkcore-setup.php';
				require_once $inc . 'admin/class-pkcore-notices.php';
				require_once $inc . 'admin/class-pkcore-admin.php';
				PKCORE_Admin::init();
				PKCORE_Notices::init();

				// PKCORE_Wizard is GONE as of core 0.9.3.
				//
				// It was a three-step wizard carrying its OWN hardcoded field list
				// and its OWN save handler, separate from the Site Profile page's -
				// two implementations of one form, which is precisely the shape of
				// problem this module exists to remove. Leaving it in place while
				// adding PKCORE_Setup would have meant shipping the disease as the
				// cure.
				//
				// PKCORE_Setup replaces it and renders the SAME question set
				// (PKCORE_Questions), through the same renderer (PKCORE_Fields),
				// saved by the same collector (PKCORE_Profile_Page::collect_all).
				// Its genuinely good behaviour is kept: prefill from the pre-core
				// options, and never write anything the operator has not seen and
				// saved. That still comes from PKCORE_Legacy_Profile.
			}

			if ( is_admin() && self::module_active( 'notify' ) ) {
				require_once $inc . 'admin/class-pkcore-alerts-admin.php';
				PKCORE_Alerts_Admin::init();
			}

			/**
			 * Fires once the PublisherKits Core has booted.
			 *
			 * Providers (e.g. the full Classifier) register on or after this.
			 *
			 * @since 0.1.0
			 * @param string   $version The booted core version.
			 * @param string[] $modules Activated modules.
			 */
			do_action( 'pkcore_booted', self::$version, self::$modules );
		}

		/**
		 * The booted core version.
		 *
		 * @return string
		 */
		public static function version() {
			return self::$version;
		}

		/**
		 * The booted bundle's root directory.
		 *
		 * @return string
		 */
		public static function dir() {
			return self::$dir;
		}

		/**
		 * Activated modules this request.
		 *
		 * @return string[]
		 */
		public static function active_modules() {
			return self::$modules;
		}

		/**
		 * Whether a module was requested by any active bundle.
		 *
		 * @param string $module 'profile', 'classify' or 'notify'.
		 * @return bool
		 */
		public static function module_active( $module ) {
			return in_array( (string) $module, self::$modules, true );
		}
	}
}
