<?php
/**
 * File Path: includes/class-mbs-plugin.php
 *
 * Main Plugin Class — My Book Showroom
 *
 * Orchestrates initialization, hooks, and core services.
 *
 * @package MyBookShowroom
 * @since   1.0.0
 */

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

final class MBS_Plugin {

	private static ?self $instance = null;

	/** @var bool True once load_metaboxes() has run for this request. */
	private bool $metaboxes_loaded = false;

	public static function get_instance(): self {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	private function __construct() {
		$this->init();
	}

	/**
	 * Bootstrap the plugin: register hooks and instantiate services.
	 * Each private load_*() method handles one logical layer of initialization.
	 * All classes are resolved via the autoloader — no explicit require_once needed
	 * in most load_*() methods. See WRITE-PROTOCOL.md §B for KEEP rationale.
	 *
	 * @return void
	 * @since 1.0.0
	 */
	private function init(): void {
		// 165-B: init() is a hook-registration stub. Each subsystem fires at
		// the earliest WordPress hook it genuinely requires.
		//
		// MBS_Plugin::get_instance() is called from plugins_loaded (p10), so
		// plugins_loaded p<=10 is past. Loaders may register on 'init' (all
		// priorities) or 'admin_init' -- both are still future at this point.
		//
		// Synchronous (must precede WP 'init'):
		//   load_display_helpers       -- class-mbs-geo-redirect.php registers
		//                                a file-scope add_filter() that must be
		//                                in place before any link filter fires.
		//   load_shortcodes_and_blocks -- block class files must exist when
		//                                'init' fires so register_block_type()
		//                                can reference them (fires at p10).
		$this->load_display_helpers();
		$this->load_shortcodes_and_blocks();

		// 'init' p0 -- CPT + taxonomy registration. Inner add_action('init')
		// calls inside load_post_types_and_taxonomies() target p10/20, which
		// are still future when this closure runs.
		add_action( 'init', function (): void { $this->load_post_types_and_taxonomies(); }, 0 );

		// 'init' p5 -- DAO activity hooks (save_post_*, mbs_export_complete,
		// mbs_import_complete, mbs_arc_send_complete). All target hooks fire
		// after 'init'.
		add_action( 'init', function (): void { $this->load_data_layer(); }, 5 );

		// 'init' p10 -- core, API, features, AJAX, broadcast, payment services.
		// All registered hook targets fire at init p20+ or later. MBS_PA_API_
		// Sync_Scheduler's inner add_action('init') uses p20, still future here.
		add_action( 'init', function (): void { $this->load_services(); }, 10 );

		// SW-2: The setup wizard registers its admin_menu page from its constructor
		// via add_action('admin_menu',...). In wp-admin/admin.php, admin_menu fires
		// at line 163 and admin_init fires at line 180 -- admin_menu is first. The
		// wizard must therefore be instantiated at 'init' (p20), which precedes both,
		// so its register_page() callback lands on admin_menu in time. AJAX handlers
		// registered in the constructor also work on admin-ajax.php (is_admin()=true,
		// init fires there too). The duplicate new MBS_Setup_Wizard() has been removed
		// from load_admin().
		add_action( 'init', function (): void {
			if ( is_admin() ) {
				new MBS_Setup_Wizard();
			}
		}, 20 );

		// 'admin_init' -- admin class chain (metaboxes, dashboard, bulk edit,
		// importers, banners). Fires only in wp-admin/* and admin-ajax.php,
		// after 'init', before wp_ajax_* dispatch. Replaces the is_admin()
		// guard previously in this method.
		add_action( 'admin_init', function (): void { $this->load_admin(); } );

		// SEC-3: Local file integrity check at admin_init (rate-limited to once/hour).
		MBS_Tamper_Detector::register_hooks();
	}

	// -------------------------------------------------------------------------
	// Private loader groups
	// -------------------------------------------------------------------------

	/**
	 * Register the book post type, all taxonomies, taxonomy seeders,
	 * and sort hooks.
	 *
	 * @return void
	 * @since 1.0.0
	 */
	private function load_post_types_and_taxonomies(): void {
		// Post types & taxonomies
		add_action( 'init', array( 'MBS_Book_Post_Type', 'register' ) );
		add_action( 'init', array( 'MBS_Taxonomy_Author',           'register' ) );
		add_action( 'init', array( 'MBS_Taxonomy_Series',           'register' ) );
		add_action( 'init', array( 'MBS_Taxonomy_Genre',            'register' ) );
		add_action( 'init', array( 'MBS_Taxonomy_Tag',              'register' ) );
		add_action( 'init', array( 'MBS_Taxonomy_Collection',       'register' ) ); // F-016
		add_action( 'init', array( 'MBS_Taxonomy_Universe',         'register' ) ); // F-017
		add_action( 'init', array( 'MBS_Taxonomy_Theme',            'register' ) ); // F-019
		add_action( 'init', array( 'MBS_Taxonomy_Content_Warning',  'register' ) ); // F-019
		add_action( 'init', array( 'MBS_Taxonomy_Publisher',        'register' ) ); // F-020
		add_action( 'init', array( 'MBS_Taxonomy_Imprint',          'register' ) ); // F-020
		add_action( 'init', array( 'MBS_Taxonomy_Contributor',      'register' ) ); // F-020

		// F-SORT-1: sort keys — runs after all taxonomies registered (priority 20).
		MBS_Sort_Hooks::register_hooks();

		// Taxonomy seeders — run once after taxonomies are registered (priority 20).
		MBS_Genre_Seeder::register_hooks();
		MBS_Collection_Seeder::register_hooks(); // F-016
		MBS_Theme_Seeder::register_hooks(); // F-019
		MBS_Content_Warning_Seeder::register_hooks(); // F-019
	}

	/**
	 * Instantiate all admin-only classes: metaboxes, dashboard,
	 * bulk edit, importer stubs, setup wizard, and nudge banners.
	 *
	 * Called via the 'admin_init' hook (registered in init()). Fires only
	 * in wp-admin/* and admin-ajax.php contexts. No inner is_admin() guard
	 * needed — the hook itself provides the admin context guarantee.
	 *
	 * @return void
	 * @since 1.0.0
	 */
	private function load_admin(): void {
		new MBS_Admin_Ajax();

		// Sprint CLASSIFICATION: DAO (shared) + AJAX handler for Layer 2 typeahead.
		new MBS_Classification_Ajax();

		// Story 10-7: PRO bulk edit for books list screen (MBS: Edit Selected action).
		// REF-013: save handler moved to MBS_Admin_Bulk_Edit_Save.
		new MBS_Admin_Bulk_Edit();
		new MBS_Admin_Bulk_Edit_Save();

		// Native WP bulk/quick edit panel overrides.
		new MBS_Admin_Native_Bulk_Edit();

		// Books list: Series Order column + sortable columns.
		new MBS_Admin_Book_List_Columns();

		// GC3-1: Dashboard singleton — instantiated before MBS_Admin so the
		// render callback exists when admin_menu fires. Autoloaded on this call.
		MBS_Admin_Dashboard::init();

		// Daily quote widget — dashboard card + settings-tab footer (Option C hook).
		MBS_Quote_Widget::init();

		// D-5 fix: wp_insert_post_data fires BEFORE save_post, so sync_title_to_post
		// must be registered here (admin_init time) rather than inside load_metaboxes()
		// which is triggered by save_post_mbs_book and would always be too late.
		MBS_Book_Header_Core_Save::register_hooks();

		// 165-A: Defer metabox instantiation to the first of add_meta_boxes or
		// save_post_mbs_book that fires, so metabox classes are never loaded on
		// admin pages that don't show the mbs_book edit screen.
		$load_once = function () use ( &$load_once ): void {
			if ( $this->metaboxes_loaded ) {
				return;
			}
			$this->metaboxes_loaded = true;
			remove_action( 'add_meta_boxes', $load_once, 0 );
			remove_action( 'save_post_' . MBS_POST_TYPE, $load_once, 0 );
			$this->load_metaboxes();
		};
		add_action( 'add_meta_boxes', $load_once, 0 );
		add_action( 'save_post_' . MBS_POST_TYPE, $load_once, 0 );

		// S15-3: WooCommerce bridge (non-metabox) — loaded now so product-sync
		// hooks and POS REST routes register at the normal admin init time.
		// MBS_Book_Header_Woocommerce (metabox child) is deferred to load_metaboxes().
		if ( class_exists( 'WooCommerce' ) ) {
			new MBS_WooCommerce_Bridge();
			// POS REST endpoints (Sprint POS-1) — registered on rest_api_init.
			MBS_POS_REST_Controller::register_hooks();
		}

		// S13-1: Guided setup wizard — instantiated at 'init' p20 (SW-2), not here,
		// because admin_menu fires before admin_init in wp-admin/admin.php (lines 163/180).
		// S13-2: Onboarding checklist dashboard widget.
		new MBS_Onboarding_Widget();
		// S13-8: Search shortcode AJAX handlers (wp_ajax_ fires via admin-ajax.php
		// which always has is_admin() = true, so this guard is correct).
		new MBS_Shortcode_Search();

		// GC2-2: FREE-user tenure-based upgrade nudges at 30/90/180/270/365 days.
		new MBS_Upgrade_Nudge();

		// GC2-2: WordPress.org review request nudges at 15/45/75/105/285 days.
		new MBS_Review_Nudge();

		// Book Bible XLSX export AJAX handler (PRO/PUB gated). Must be registered
		// here — not in load_metaboxes() — so the wp_ajax_mbs_export_book_bible
		// hook is active for admin-ajax.php requests, which never fire add_meta_boxes.
		MBS_Book_Bible_Exporter::get_instance();

		// Sprint 171: DOCX Look Inside extraction AJAX handler (PRO gated).
		// Registered here so wp_ajax_mbs_extract_docx_look_inside is active for
		// admin-ajax.php requests on the book edit screen.
		new MBS_Look_Inside_Ajax();

		// Session 177: EPUB Look Inside extraction AJAX handler (PRO gated).
		// Registered alongside the DOCX handler; both share mbs_look_inside_nonce.
		new MBS_Look_Inside_Epub_Ajax();
	}

	/**
	 * Instantiate all metabox classes for the mbs_book edit screen.
	 *
	 * Called at most once per request via the deferred closure registered
	 * in load_admin(). Triggered by add_meta_boxes (edit screen render) or
	 * save_post_mbs_book (save handler), whichever fires first.
	 *
	 * @return void
	 * @since 1.0.0
	 */
	private function load_metaboxes(): void {
		new MBS_Metabox_Book_Header();
		// C2-004: Child sections must be explicitly instantiated so the autoloader
		// loads their files and their self-registering new ClassName() calls fire.
		// Without these, the hooks for mbs_book_header_render_* never exist and the
		// Book Header metabox body renders empty.
		new MBS_Book_Header_Core();
		new MBS_Book_Header_Authors();
		new MBS_Book_Header_Publication();
		new MBS_Book_Header_Cover();
		new MBS_Book_Header_Links();          // S13-10 / S13-7

		// ── Sprint 14: Admin-only classes ──────────────────────────────────

		// S14-7: Layout preview iframe on Formatting tab.
		new MBS_Layout_Preview();

		new MBS_Book_Header_Extended_Meta();  // S13-11
		new MBS_Book_Header_Notes();

		// S169: Book Editor shell — single tabbed metabox replacing all individual
		// handler metaboxes. Handler instances are registered via MBS_Editor_Registry
		// inside MBS_Book_Editor::register_domain_config().
		new MBS_Book_Editor();

		// Sprint ARC: ARC reviewer tracking metabox (FREE-gated; PRO/PUB full access).
		new MBS_Metabox_Arc();
		// Export Book Bible side metabox — completeness badge, tier-aware button (all tiers visible).
		// MBS_Book_Bible_Exporter is instantiated in load_admin() so its wp_ajax_
		// hook is always registered at admin_init, not just on edit-screen loads.
		new MBS_Book_Bible_Metabox();

		// S15-3: WooCommerce book-header child panel — only when WooCommerce is active.
		// MBS_WooCommerce_Bridge and POS REST are registered earlier in load_admin().
		if ( class_exists( 'WooCommerce' ) ) {
			new MBS_Book_Header_Woocommerce();
		}
	}

	/**
	 * Load all data-access objects (DAOs) and register the book-save
	 * activity log hook.
	 *
	 * @return void
	 * @since 1.0.0
	 */
	private function load_data_layer(): void {
		// REFACTOR-PLAN-ARCH-001 Batch F: activity log hooks moved to MBS_Activity_Log_Admin::register_hooks().
		MBS_Activity_Log_Admin::register_hooks();
	}

	/**
	 * Load formatting helpers, cover renderer, buy-link renderer, breadcrumbs,
	 * section order, star rating, review handler, and color vars.
	 *
	 * All helper classes in this group are autoloaded by the PSR-4 autoloader on
	 * first reference — no explicit require_once is needed except for
	 * class-mbs-geo-redirect.php, which registers an add_filter() at file scope
	 * and must be required explicitly before any link filter fires.
	 *
	 * @return void
	 * @since 1.0.0
	 */
	private function load_display_helpers(): void {
		// S14-3: Geo-redirect link localization (static helper — no instantiation;
		// add_filter() is registered at the bottom of the file itself).
		require_once MBS_INCLUDES_DIR . 'helpers/class-mbs-geo-redirect.php'; // KEEP — autoloader cannot fire without instantiation
		MBS_Geo_Redirect::register();
	}

	/**
	 * Load the layout registry, block classes, and blocks registry.
	 *
	 * Shortcode classes are autoloaded on demand when a shortcode is invoked.
	 * Block classes and the layout registry must be available before the
	 * WordPress 'init' hook fires so register_block_type() can reference them.
	 *
	 * @return void
	 * @since 1.0.0
	 */
	private function load_shortcodes_and_blocks(): void {
		// KEEP — layout registry is referenced during block type registration on 'init'.
		require_once MBS_LAYOUTS_DIR . 'class-mbs-layout-registry.php'; // KEEP — needed before 'init' fires for block layout resolution
		// KEEP — block classes must be available when 'init' fires (priority 10)
		// so register_block_type() can reference the class by name.
		require_once MBS_INCLUDES_DIR . 'blocks/class-mbs-block-bestseller.php'; // KEEP — block registration fires on 'init'
		require_once MBS_INCLUDES_DIR . 'blocks/class-mbs-block-timeline.php';   // KEEP — block registration fires on 'init'
		// A-008: Explicit require guards against autoloader restructuring.
		// The autoloader would also resolve this file, but load order is intentional.
		require_once MBS_INCLUDES_DIR . 'blocks/class-mbs-blocks-registry.php'; // KEEP — A-008 intentional load-order guarantee
	}

	/**
	 * Orchestrate all service-load stages: core, API, feature, AJAX, broadcast,
	 * and payment. Each stage has its own private method.
	 *
	 * @return void
	 * @since 1.0.0
	 */
	private function load_services(): void {
		$this->load_core_services();
		$this->load_api_services();
		$this->load_feature_services();
		$this->load_ajax_handlers();
		$this->load_broadcast_and_esp();
		$this->load_payment_hooks();
	}

	/**
	 * Instantiate core services and wire asset-enqueue, DB-migration,
	 * capability, and attachment hooks.
	 *
	 * All classes in this group are autoloaded on instantiation or static-method
	 * call — no explicit require_once is needed beyond what load_core() pre-loads.
	 *
	 * @return void
	 * @since 1.0.0
	 */
	private function load_core_services(): void {
		// Register core components.
		new MBS_Admin();
		MBS_Settings::get_instance();

		new MBS_Compat_Loader();
		new MBS_Book_Display();

		// Shortcodes, blocks, and review handler.
		new MBS_Shortcode_Registry();
		new MBS_Blocks_Registry();
		new MBS_Review_Handler();

		// Color variables output — REF-02: now via MBS_Color_Vars class.
		MBS_Color_Vars::register();

		// Asset enqueuing.
		( new MBS_Front_Assets() )->register();

		// RF-007: Dark mode head-injection and analytics are now separate classes.
		new MBS_Dark_Mode_Manager();
		new MBS_Analytics_Manager();

		// Book creation defaults — pre-assigns default author on new book auto-drafts.
		new MBS_Book_Defaults();

		// Media cleanup + book deletion cleanup moved to MBS_Book_Post_Type::register_hooks() (Batch J1).
		MBS_Book_Post_Type::register_hooks();

		// Database migration.
		MBS_DB_Installer::maybe_migrate();

		// Sprint CLASSIFICATION: seed classification tables when plugin version advances.
		MBS_Classification_Seeder::maybe_seed();
		MBS_Classification_Seeder::register_hooks();

		// Capability sync for upgrade installs that don't re-trigger the activation hook.
		MBS_Capabilities::maybe_grant();

		// Admin notices — split to dedicated classes (Batch J2/J3).
		MBS_License_Notices::register_hooks();       // License not-activated warning.
		MBS_Tamper_Detector::register_notice_hooks(); // Tamper flag notice.
		MBS_Press_Kit_Generator::register_notice_hooks(); // Press Kit ratio error.
		MBS_DB_Installer::register_hooks();          // Section order warning (D-007).
	}

	/**
	 * Instantiate telemetry, PA-API sync, series sync, and update-checker
	 * services. Wire the daily license-ping cron callback.
	 *
	 * @return void
	 * @since 1.0.0
	 */
	private function load_api_services(): void {
		// S14-9: Telemetry consent tracking + cron scheduling + admin notice.
		// MBS_Telemetry_Client is static-only; MBS_Telemetry wires the cron hook.
		new MBS_Telemetry();

		// S15-1: PA-API price/rating sync — daily cron, PRO only.
		// MBS_PA_API_Sync_Scheduler::register() wires the cron callback at 'init'.
		new MBS_PA_API_Sync_Scheduler();

		// Round 3: Series reading-order sync. Registers set_object_terms +
		// trashed_post + deleted_post hooks on construction. Fires in all
		// contexts (not just admin).
		new MBS_Series_Sync();

		// WP.org builds exclude class-mbs-update-checker.php via .distignore; direct-download builds include it.
		if ( class_exists( 'MBS_Update_Checker' ) ) {
			new MBS_Update_Checker();
		}

		// Batch G: mbs_daily_license_ping stays here -- MBS_License_Client is namespaced (DMRS\MBS\Api),
		// so WordPress cannot resolve a static ::register_hooks() reference without FQCN.
		add_action( 'mbs_daily_license_ping', function(): void {
			$client = new \DMRS\MBS\Api\MBS_License_Client();
			$client->validate();
		} );

		// V7-D-005 / Batch H: import temp-dir cleanup moved to MBS_Catalog_Archive_Reader::register_hooks().
		MBS_Catalog_Archive_Reader::register_hooks();
	}

	/**
	 * Instantiate feature services: the buy-link router.
	 *
	 * All feature service classes are autoloaded on instantiation — no explicit
	 * require_once is needed.
	 *
	 * @return void
	 * @since 1.0.0
	 */
	private function load_feature_services(): void {
		new MBS_Buy_Link_Router(); // INSIGHTS-1 — autoloaded; constructor registers the 302-redirect handler
	}

	/**
	 * Register AJAX handlers for analytics reporting, dashboard widget prefs,
	 * activity log admin, and the Press Kit generator.
	 *
	 * All referenced classes are autoloaded via their ::register() calls.
	 * No explicit require_once is needed.
	 *
	 * @return void
	 * @since 1.0.0
	 */
	private function load_ajax_handlers(): void {
		MBS_Buy_Link_Analytics::register();    // INSIGHTS-1
		MBS_Dashboard_Widget_Prefs::register(); // DASH-2A
		MBS_Activity_Log_Admin::register();    // INSIGHTS-2
		MBS_Tutorial_Admin_Page::register();   // Tutorial installer / uninstaller page
		// INSIGHTS-3: Press Kit AJAX moved to MBS_Press_Kit_Generator::register_hooks() (Batch I).
		MBS_Press_Kit_Generator::register_hooks();
	}

	/**
	 * Register ESP subscriber/inject hooks, the ARC reviewer sync cron callback,
	 * and the new-release broadcast queue with its admin AJAX handlers.
	 *
	 * @return void
	 * @since 1.0.0
	 */
	private function load_broadcast_and_esp(): void {
		// F-026-5: Reader signup widget -- hooks self-register (SRP — R1-001).
		MBS_ESP_Widget::register_hooks();

		// F-026-6: ARC reviewer ESP sync cron callback moved to MBS_ESP_Arc_Sync::register_hooks() (Batch I).
		MBS_ESP_Arc_Sync::register_hooks();

		// F-026-7: New release broadcast queue -- hooks self-register in __construct() (SRP — R1-001).
		new MBS_ESP_Broadcast();
	}

	/**
	 * Lazy-load payment adapters (Stripe, PayPal) and the ARC self-signup REST
	 * endpoint. All three are deferred to rest_api_init so adapter classes are
	 * not required on non-REST page loads.
	 *
	 * @return void
	 * @since 1.0.0
	 */
	private function load_payment_hooks(): void {
		// REF-03: Payment adapters are lazy-loaded -- the interface and adapter
		// classes are required only when a REST API request arrives for the
		// matching webhook endpoint, not on every page load.
		if ( class_exists( 'MBS_Stripe_Adapter' ) ) {
			MBS_Stripe_Adapter::register_hooks();
		}
		if ( class_exists( 'MBS_PayPal_Adapter' ) ) {
			MBS_PayPal_Adapter::register_hooks();
		}

		// F-027: ARC self-signup REST endpoint.
		MBS_ESP_Arc_Signup::register_hooks();
	}













}
