<?php
/**
 * File Path: includes/admin/class-mbs-settings.php
 *
 * Main Settings Page Controller
 *
 * Registers the plugin settings page, enqueues admin assets, and renders
 * the tabbed interface that delegates to dedicated tab classes.
 *
 * @package MyBookShowroom
 * @since   1.0.0
 */

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

// phpcs:disable WordPress.Security.NonceVerification -- nonce verified in parent class verify_save() / check_admin_referer() before $_POST access; PHPCS cannot trace cross-method nonce verification

final class MBS_Settings {

	/** @var self|null */
	private static ?self $instance = null;

	/** @var string Currently active tab slug. */
	private string $active_tab = 'dashboard';

	/**
	 * Tabs that are entirely AJAX-driven or per-row-form-driven and must NOT
	 * be wrapped in the options.php form. The Settings API whitelist error is
	 * triggered when options.php receives fields that were never registered.
	 * Affected:
	 *   · license       — pure AJAX
	 *   · import-export — pure AJAX
	 *   · general       — custom admin_init save handler
	 *   · authors       — per-row forms, each with its own nonce + save handler
	 *   · series        — per-row forms, each with its own nonce + save handler
	 *
	 */
	private const AJAX_ONLY_TABS = array( 'license', 'import-export', 'catalog', 'promote', 'general', 'authors', 'series', 'collections', 'universes', 'shortcodes', 'advanced', 'email', 'onix', 'dashboard' );

	/**
	 * Maps entity sub-tab slugs to their Catalog parent for top-nav highlighting.
	 *
	 * When any entity sub-tab is active, the Catalog nav item is highlighted.
	 * Added UX-NAV-002. Keep in sync with MBS_Tab_Catalog constructor parameters
	 * and MBS_Tab_Catalog_Subnav::render() $items keys.
	 *
	 * OOP note: the correct long-term home for this slug list is a public constant
	 * MBS_Tab_Catalog::ENTITY_SLUGS. Deferred until Phase 2 constructor cleanup.
	 */
	private const ENTITY_PARENT_MAP = array(
		'authors'     => 'catalog',
		'series'      => 'catalog',
		'collections' => 'catalog',
		'universes'   => 'catalog',
	);

	// -------------------------------------------------------------------------
	// Singleton
	// -------------------------------------------------------------------------

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

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

	// -------------------------------------------------------------------------
	// Bootstrap
	// -------------------------------------------------------------------------

	public function register(): void {
		add_action( 'admin_init',            array( $this, 'register_settings' ) );
		add_action( 'admin_init',            array( $this, 'maybe_redirect_catalog_tab' ) );
		add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );

		// Boot-time instantiation — registers AJAX hooks on all tab classes.
		$this->get_tab_objects();
	}

	// -------------------------------------------------------------------------
	// Settings registration
	// -------------------------------------------------------------------------

	/**
	 * Register all plugin settings with the WordPress Settings API.
	 *
	 * Each tab that uses the Settings API owns its own register() method.
	 * This orchestrator delegates to each tab in turn (REF-012, Session 136).
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public function register_settings(): void {
		$tab_objects = $this->get_tab_objects();

		// Tab 2: Formatting -- delegated to MBS_Tab_Formatting::register().
		if ( isset( $tab_objects['formatting'] ) && method_exists( $tab_objects['formatting'], 'register' ) ) {
			$tab_objects['formatting']->register();
		}

		// Tab 3: Appearance -- delegated to MBS_Tab_Appearance::register().
		// That method owns the sanitize callbacks (mbs_sanitize_color for all color
		// tokens, wp_strip_all_tags for custom CSS) and the mbs_is_pro() gate for
		// the 16 PRO tokens. Registering them here without callbacks would silently
		// override those sanitizers via a second register_setting() call.
		if ( isset( $tab_objects['appearance'] ) && method_exists( $tab_objects['appearance'], 'register' ) ) {
			$tab_objects['appearance']->register();
		}

		// Tab: Earn More (affiliate) -- delegated to MBS_Tab_Affiliate::register().
		if ( isset( $tab_objects['affiliate'] ) && method_exists( $tab_objects['affiliate'], 'register' ) ) {
			$tab_objects['affiliate']->register();
		}

		// Tab 1: General -- custom save handler via admin_init; no Settings API fields.
		// Tab 4: License -- AJAX-only; no Settings API fields registered here.
		// Tab 5: Import / Export -- AJAX-only; no Settings API fields registered here.
		// Authors / Series -- per-row forms with their own nonces and save handlers.
	}

	// -------------------------------------------------------------------------
	// Asset enqueuing
	// -------------------------------------------------------------------------

	public function enqueue_assets( string $hook ): void {
		if ( strpos( $hook, 'my-book-showroom' ) === false ) {
			return;
		}

		// ── Always-on: design token stylesheet (--mbs-* custom properties) ─────
		// Scoped to #mbs-settings-wrap; must load before mbs-admin-settings.css
		// so var() tokens resolve on all Settings-page tabs including Catalog sub-nav.
		wp_enqueue_style(
			'mbs-admin',
			plugins_url( 'assets/css/mbs-admin.css', MBS_DIR . 'bookshowroom.php' ),
			array(),
			MBS_VERSION
		);

		// ── Always-on: shared base stylesheet ─────────────────────────────────
		// Per-tab CSS is loaded by each tab's own enqueue() method below.
		wp_enqueue_style(
			'mbs-admin-settings',
			plugins_url( 'assets/css/mbs-admin-settings.css', MBS_DIR . 'bookshowroom.php' ),
			array( 'mbs-admin' ),
			MBS_VERSION
		);

		// ── Always-on: shared admin JS utilities (escHtml, escAttr) ─────────────
		wp_enqueue_script(
			'mbs-admin-utils',
			plugins_url( 'assets/js/adminjs/mbs-admin-utils.js', MBS_DIR . 'bookshowroom.php' ),
			array(),
			MBS_VERSION,
			true
		);

		// ── Always-on: sortable-list factory (entity tabs — Authors, Series,
		// Collections, Universes). initReadingOrderSortable() and the social
		// platform picker both call MBS.SortableList.create() (BUG-SOC-001).
		wp_enqueue_script(
			'mbs-sortable-list',
			plugins_url( 'assets/js/adminjs/mbs-sortable-list.js', MBS_DIR . 'bookshowroom.php' ),
			array( 'jquery', 'jquery-ui-sortable', 'mbs-admin-utils' ),
			MBS_VERSION,
			true
		);

		// ── Always-on: media uploader (Authors tab photo picker) ──────────────
		wp_enqueue_script(
			'mbs-media-uploader',
			MBS_ASSETS_URL . 'js/mbs-media-uploader.js',
			array( 'jquery' ),
			MBS_VERSION,
			true
		);

		if ( class_exists( 'MBS_Metabox_Media_Uploader' ) ) {
			MBS_Metabox_Media_Uploader::enqueue_media();
		}

		// ── Per-tab: delegate to each tab class's enqueue() method ────────────
		// C-002: Read the tab parameter once, sanitized.
		$current_tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'dashboard'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended

		// ── Import / Export scripts — only on the import-export tab ─────────────
		// mbs-admin-csv-import is registered first because MBS_Settings is
		// localized onto it; the other two declare it as a dependency.
		if ( 'import-export' === $current_tab ) {
			wp_enqueue_script(
				'mbs-admin-csv-import',
				plugins_url( 'assets/js/adminjs/mbs-admin-csv-import.js', MBS_DIR . 'bookshowroom.php' ),
				array( 'jquery', 'wp-i18n' ),
				MBS_VERSION,
				true
			);
			wp_set_script_translations( 'mbs-admin-csv-import', 'my-book-showroom', MBS_DIR . 'languages' );

			wp_enqueue_script(
				'mbs-admin-csv-export',
				plugins_url( 'assets/js/adminjs/mbs-admin-csv-export.js', MBS_DIR . 'bookshowroom.php' ),
				array( 'jquery', 'mbs-admin-csv-import', 'wp-i18n' ),
				MBS_VERSION,
				true
			);
			wp_set_script_translations( 'mbs-admin-csv-export', 'my-book-showroom', MBS_DIR . 'languages' );

			wp_enqueue_script(
				'mbs-admin-settings-import',
				plugins_url( 'assets/js/adminjs/mbs-admin-settings-import.js', MBS_DIR . 'bookshowroom.php' ),
				array( 'jquery', 'mbs-admin-csv-import', 'wp-i18n' ),
				MBS_VERSION,
				true
			);
			wp_set_script_translations( 'mbs-admin-settings-import', 'my-book-showroom', MBS_DIR . 'languages' );

			// MBS_Settings.ajaxUrl is used by all three import/export scripts.
			// Localizing onto the first handle makes it available to the others
			// via their declared dependency.
			wp_localize_script( 'mbs-admin-csv-import', 'MBS_Settings', array(
				'ajaxUrl'         => admin_url( 'admin-ajax.php' ),
				// F-09: Removed dead mbs_settings nonce — no handler ever calls
				// check_ajax_referer('mbs_settings') and no JS sends it.
				'acceptedColumns' => MBS_Child_Importer_CSV_Helpers::ACCEPTED_COLUMNS,
			) );
		}

		foreach ( $this->get_tab_objects() as $tab_id => $tab_object ) {
			if ( method_exists( $tab_object, 'enqueue' ) ) {
				$tab_object->enqueue( $current_tab );
			}
		}
	}

	// -------------------------------------------------------------------------
	// Catalog redirect (admin_init -- fires before headers sent)
	// -------------------------------------------------------------------------

	/**
	 * Redirect tab=catalog to tab=authors before any output.
	 *
	 * The Catalog tab is a navigation hub (UX-NAV-002). Until Phase 2 adds
	 * entity-count cards or onboarding context, the hub page has no content;
	 * redirecting to the default entity sub-tab avoids a blank-looking page.
	 * Fires on admin_init, before WordPress sends any output.
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public function maybe_redirect_catalog_tab(): void {
		if (
			isset( $_GET['page'], $_GET['tab'] ) && // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			'my-book-showroom-settings' === $_GET['page'] &&
			'catalog' === sanitize_key( wp_unslash( $_GET['tab'] ) )
		) {
			wp_safe_redirect( add_query_arg( 'tab', 'authors', menu_page_url( 'my-book-showroom-settings', false ) ) );
			exit;
		}
	}

	// -------------------------------------------------------------------------
	// Page rendering
	// -------------------------------------------------------------------------

	/**
	 * Output the full settings page: H1, nav-tab bar, and active-tab content.
	 *
	 * Reads $_GET['tab'] to determine the active tab (lines 212-214). Entity
	 * sub-tab slugs (authors, series, collections, universes) map to 'catalog'
	 * for top-nav highlighting via ENTITY_PARENT_MAP (UX-NAV-002). All other
	 * uses of $this->active_tab use the raw slug for correct dispatch and hook
	 * arguments.
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public function render_settings_page(): void {
		$this->active_tab = isset( $_GET['tab'] )
			? sanitize_key( wp_unslash( $_GET['tab'] ) )
			: 'dashboard';


		// Entity sub-tab slugs resolve to their parent for top-nav highlighting (UX-NAV-002).
		$nav_active_tab = self::ENTITY_PARENT_MAP[ $this->active_tab ] ?? $this->active_tab;

		$tabs         = $this->get_tabs();
		$icon_map     = self::get_tab_icon_map();
		$is_ajax_only = in_array( $this->active_tab, self::AJAX_ONLY_TABS, true );
		?>
		<div class="wrap" id="mbs-settings-wrap">
			<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>

			<h2 class="nav-tab-wrapper">
				<?php foreach ( $tabs as $tab_id => $tab_label ) : ?>
					<?php
					$icon_html = isset( $icon_map[ $tab_id ] ) ? mbs_icon( $icon_map[ $tab_id ], [
						'size'       => '20px',
						'aria_label' => '', // decorative -- label text is adjacent
					] ) : '';
					?>
					<a href="<?php echo esc_url( add_query_arg( 'tab', $tab_id, menu_page_url( 'my-book-showroom-settings', false ) ) ); ?>"
					   class="nav-tab <?php echo esc_attr( $nav_active_tab === $tab_id ? 'nav-tab-active' : '' ); ?>">
						<?php echo $icon_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Pre-sanitized by MBS_Icon::render() ?>
						<?php echo esc_html( $tab_label ); ?>
					</a>
				<?php endforeach; ?>
			</h2>

			<?php if ( $is_ajax_only ) : ?>
				<?php $this->render_active_tab(); ?>
			<?php else : ?>
				<form method="post" action="options.php">
					<?php
					settings_fields( 'mbs_' . $this->active_tab );
					?>
					<?php /* UX-01: Top save button — same submit as bottom, floated right. */ ?>
					<div class="mbs-save-top"><?php submit_button( null, 'primary', 'submit', true ); ?></div>
					<?php
					$this->render_active_tab();
					submit_button();
					?>
				</form>
			<?php endif; ?>

		<?php
		/**
		 * Fires after the active settings tab content is rendered.
		 *
		 * Use this to append content below every tab without modifying
		 * individual tab classes (Option C — hook-driven injection).
		 *
		 * @param string $active_tab The slug of the currently active tab.
		 */
		do_action( 'mbs_tab_footer', $this->active_tab ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- mbs_/MBS_ prefix is correct; scanner not configured with plugin prefix
		?>

		</div>
		<?php
	}

	// -------------------------------------------------------------------------
	// Private helpers
	// -------------------------------------------------------------------------

	/**
	 * Tier-gated tab list.
	 *   FREE : 8 tabs  (dashboard, general, appearance, formatting, catalog, import-export, shortcodes, advanced)
	 *   PRO  : +1 tab  (promote)                     = 9 total
	 *   PUB  : +1 tab  (onix)                        = 10 total
	 *
	 * License functions live on the Dashboard tab. The license tab object is
	 * retained in get_tab_objects() for AJAX hook registration only.
	 * Legacy per-domain slugs (authors, series, collections, universes,
	 * affiliate, email) are retained in get_tab_objects() for AJAX compat.
	 */
	private function get_tabs(): array {
		// Tab order: 1-Dashboard 2-General 3-Appearance 4-Formatting
		//            5-Catalog 6-Import/Export 7-Shortcodes 8-Advanced
		//            9-Promote(PRO) 10-Publisher Tools(PUB)
		// WO-002: Dashboard is Tab 1 (default landing tab) so keyless/FREE
		// users see the registration nudge immediately on first Settings visit.
		$tabs = array(
			'dashboard'  => __( 'Dashboard',  'my-book-showroom' ),
			'general'    => __( 'General',    'my-book-showroom' ),
			'appearance' => __( 'Appearance', 'my-book-showroom' ),
			'formatting' => __( 'Formatting', 'my-book-showroom' ),
			'catalog'    => __( 'Catalog',    'my-book-showroom' ),
		);

		$tabs['import-export'] = __( 'Import / Export', 'my-book-showroom' );
		$tabs['shortcodes']    = __( 'Pages & Shortcodes', 'my-book-showroom' );

		if ( mbs_is_pro() ) {
			$tabs['promote'] = __( 'Promote', 'my-book-showroom' );
		}

		if ( mbs_is_pub() ) {
			$tabs['onix'] = __( 'Publisher Tools', 'my-book-showroom' );
		}

		$tabs['advanced'] = __( 'Advanced', 'my-book-showroom' );

		return $tabs;
	}
	/**
	 * Returns the tab_id => icon_slug mapping for settings tab icons.
	 * Kept separate from get_tabs() to preserve the string-label return type.
	 * Consumed by render_settings_page() to look up icon slugs per tab.
	 *
	 * @return array<string, string>
	 * @since 1.0.0
	 */
	private static function get_tab_icon_map(): array {
		return [
			'general'       => 'tab-general',
			'appearance'    => 'tab-appearance',
			'formatting'    => 'tab-formatting',
			'dashboard'     => 'tab-dashboard',
			'catalog'       => 'tab-catalog',
			'import-export' => 'tab-import-export',
			'shortcodes'    => 'tab-shortcodes',
			'promote'       => 'tab-promote',
			'onix'          => 'tab-publisher-tools', // non-obvious: tab slug != icon slug
			'advanced'      => 'tab-advanced',
		];
	}

	private function get_tab_objects(): array {
		static $tabs = null;

		if ( null === $tabs ) {
			// Child instances: instantiated once, references shared with aggregators.
			$general     = class_exists( 'MBS_Tab_General' )       ? new MBS_Tab_General()       : new class { public function render(): void { echo '<p>General tab not yet implemented.</p>'; }       public function enqueue( string $t ): void {} };
			$formatting  = class_exists( 'MBS_Tab_Formatting' )    ? new MBS_Tab_Formatting()    : new class { public function render(): void { echo '<p>Formatting tab not yet implemented.</p>'; }    public function enqueue( string $t ): void {} };
			$appearance  = class_exists( 'MBS_Tab_Appearance' )    ? new MBS_Tab_Appearance()    : new class { public function render(): void { echo '<p>Appearance tab not yet implemented.</p>'; }    public function enqueue( string $t ): void {} public function register(): void {} };
			$license     = class_exists( 'MBS_Tab_License' )       ? new MBS_Tab_License()       : new class { public function render(): void { echo '<p>License tab not yet implemented.</p>'; } };
			$authors     = class_exists( 'MBS_Tab_Authors' )       ? new MBS_Tab_Authors()       : new class { public function render(): void { echo '<p>Authors tab not yet implemented.</p>'; }       public function enqueue( string $t ): void {} };
			$series      = class_exists( 'MBS_Tab_Series' )        ? new MBS_Tab_Series()        : new class { public function render(): void { echo '<p>Series tab not yet implemented.</p>'; }        public function enqueue( string $t ): void {} };
			$collections = class_exists( 'MBS_Tab_Collections' )   ? new MBS_Tab_Collections()   : new class { public function render(): void { echo '<p>Collections tab not yet implemented.</p>'; }   public function enqueue( string $t ): void {} };
			$universes   = class_exists( 'MBS_Tab_Universes' )     ? new MBS_Tab_Universes()     : new class { public function render(): void { echo '<p>Universes tab not yet implemented.</p>'; }     public function enqueue( string $t ): void {} };
			$import_exp  = class_exists( 'MBS_Tab_Import_Export' ) ? new MBS_Tab_Import_Export() : new class { public function render(): void { echo '<p>Import / Export tab not yet implemented.</p>'; } };
			$affiliate   = class_exists( 'MBS_Tab_Affiliate' )     ? new MBS_Tab_Affiliate()     : new class { public function render(): void { echo '<p>Earn More tab not yet implemented.</p>'; }     public function enqueue( string $t ): void {} };
			$advanced    = class_exists( 'MBS_Tab_Advanced' )      ? new MBS_Tab_Advanced()      : new class { public function render(): void { echo '<p>Advanced / Developer tab not yet implemented.</p>'; } };
			$shortcodes  = class_exists( 'MBS_Tab_Shortcodes' )    ? new MBS_Tab_Shortcodes()    : new class { public function render(): void { echo '<p>Add to Pages tab not yet implemented.</p>'; } };
			$dashboard   = class_exists( 'MBS_Tab_Dashboard' )     ? new MBS_Tab_Dashboard()     : new class { public function render(): void { echo '<p>Dashboard settings not yet implemented.</p>'; } };
			$email       = class_exists( 'MBS_Tab_Email' )         ? new MBS_Tab_Email()         : new class { public function render(): void { echo '<p>Email tab not yet implemented.</p>'; }         public function enqueue( string $t ): void {} };
			$onix        = class_exists( 'MBS_Tab_Onix' )          ? new MBS_Tab_Onix()          : new class { public function render(): void { echo '<p>ONIX tab not yet implemented.</p>'; } };

			// Aggregators: receive references to the named child instances.
			$catalog = class_exists( 'MBS_Tab_Catalog' ) ? new MBS_Tab_Catalog( $authors, $series, $collections, $universes ) : new class { public function render(): void { echo '<p>Catalog tab not yet implemented.</p>'; } public function enqueue( string $t ): void {} };
			$promote = class_exists( 'MBS_Tab_Promote' ) ? new MBS_Tab_Promote( $affiliate, $email )                         : new class { public function render(): void { echo '<p>Promote tab not yet implemented.</p>'; } public function enqueue( string $t ): void {} };

			$tabs = array(
				'general'       => $general,
				'formatting'    => $formatting,
				'appearance'    => $appearance,
				'catalog'       => $catalog,
				'promote'       => $promote,
				'license'       => $license,
				'import-export' => $import_exp,
				'shortcodes'    => $shortcodes,
				'dashboard'     => $dashboard,
				'advanced'      => $advanced,
				'email'         => $email,
				'onix'          => $onix,
				// Legacy slugs retained for AJAX backward compatibility.
				'authors'       => $authors,
				'series'        => $series,
				'collections'   => $collections,
				'universes'     => $universes,
				'affiliate'     => $affiliate,
			);
		}

		return $tabs;
	}

	// -------------------------------------------------------------------------
	// Private helpers (continued)
	// -------------------------------------------------------------------------

	private function render_active_tab(): void {
		$tab_objects = $this->get_tab_objects();
		$active      = $this->active_tab;

		if ( isset( $tab_objects[ $active ] ) && method_exists( $tab_objects[ $active ], 'render' ) ) {
			$tab_objects[ $active ]->render();
		} else {
			echo '<p>' . esc_html__( 'Tab not found.', 'my-book-showroom' ) . '</p>';
		}
	}
}
