<?php
/**
 * File Path: includes/admin/tabs/class-mbs-tab-formatting.php
 *
 * Formatting Tab
 *
 * Implements all formatting settings from Doc 1a Tab 2, including the global
 * share text, section display order, and layout thumbnail picker.
 *
 * Two cards wrap the field groups for visual parity with Phase 4b (General):
 *   · "Display & Formatting" — default layout + thumbnail grid, archive title,
 *     items per page, price format, publication date format, purchase link text
 *   · "Advanced & Sharing" — global share text (PRO) with merge-tag pills,
 *     section display order with drag + collapsed arrow fallback
 *
 * Form wrapper: this tab does NOT own a <form>. MBS_Settings::render_settings_page()
 * wraps non-AJAX tabs in <form action="options.php"> with settings_fields() and
 * the closing submit_button(). Formatting is not in AJAX_ONLY_TABS, so this
 * render() returns just the tab body and relies on the parent for save flow.
 *
 * Default layout thumbnails:
 *   Abstract CSS wireframes that convey the structural shape of each layout
 *   (not photo-realistic previews). 9 thumbnails total — 5 FREE + 4 PRO.
 *   SVG screenshot thumbnails logged as LOW backlog item per client 2026-04-19.
 *
 * Section Display Order:
 *   Drag-and-drop sortable (#mbs-section-sortable) is the primary widget;
 *   the keyboard arrow fallback (#mbs-section-order-table) is preserved for
 *   accessibility and no-JS users, rendered inside a collapsible <details>
 *   element so it doesn't visually dominate the card. Live JS targets in
 *   assets/js/mbs-book-editor.js — element IDs preserved exactly so the
 *   active JS continues to work without modification.
 *
 * Hero as default layout:
 *   Per client 2026-04-19, the default layout fallback is "hero" rather than
 *   "grid" in all three fallback consumers:
 *     · class-mbs-tab-formatting.php (this file)
 *     · class-mbs-book-query.php
 *     · class-mbs-shortcode-books.php
 *
 * Phase 4c (2026-04-19):
 *   · Subtitle added: "Control how your books appear on archive and single pages."
 *   · Structural wrapping: two .mbs-settings-card blocks replace the two
 *     <h3> + <table class="form-table"> groupings.
 *   · Default Layout: dropdown retained (preserves Settings API save flow),
 *     augmented with a clickable thumbnail grid below it. Bidirectional sync
 *     between the dropdown and thumbnails is handled by
 *     assets/js/adminjs/child-admin-formatting.js (new file).
 *   · Four mid-section fields (archive title, items per page, price format,
 *     publication date format) now rendered in two 2-column .mbs-settings-field-grid
 *     rows instead of stacked <tr> rows.
 *   · Purchase Link Text: full-width, standalone.
 *   · Global Share Text: PRO-gated (disabled textarea + PRO badge on FREE tier).
 *     Merge-tag pills render only on PRO tier; clicking a pill inserts the tag
 *     at the textarea cursor (handled by child-admin-formatting.js).
 *   · Section Display Order: max-width constrained via CSS so the sortable
 *     rows no longer span the full card width (client complaint 2026-04-19).
 *     Arrow-table fallback wrapped in a collapsed <details> element.
 *   · Hero default — three call sites updated as documented above.
 *
 * @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
// phpcs:disable WordPress.Security.ValidatedSanitizedInput -- input sanitized at point of use (sanitize_text_field, absint, wp_kses_post, etc.); false positives from PHPCS not tracing sanitization helpers called before this method
class MBS_Tab_Formatting {

	/**
	 * Register all Formatting-tab settings with the WordPress Settings API.
	 *
	 * Called by MBS_Settings::register_settings() via the delegate pattern used
	 * by all tab classes (REF-012, Session 136).
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public function register(): void {
		register_setting( 'mbs_formatting', 'mbs_setting_formatting_default_layout', array(
			'sanitize_callback' => 'sanitize_key',
		) );
		register_setting( 'mbs_formatting', 'mbs_setting_formatting_items_per_page', array(
			'sanitize_callback' => 'absint',
		) );
		// ST-015 fix: render_share_text_field() was added in the Format & Contributors
		// Expansion Sprint but the matching register_setting() call was never restored.
		// Without registration, options.php silently discards this field on save.
		register_setting( 'mbs_formatting', 'mbs_setting_formatting_share_text', array(
			'sanitize_callback' => 'sanitize_textarea_field',
		) );
		register_setting( 'mbs_formatting', 'mbs_setting_formatting_price_format', array(
			'sanitize_callback' => 'sanitize_text_field',
		) );
		register_setting( 'mbs_formatting', 'mbs_setting_formatting_purchase_link_text', array(
			'sanitize_callback' => 'sanitize_text_field',
		) );
		register_setting( 'mbs_formatting', 'mbs_setting_formatting_archive_title', array(
			'sanitize_callback' => 'sanitize_text_field',
		) );
		register_setting( 'mbs_formatting', 'mbs_setting_formatting_date_format', array(
			'sanitize_callback' => 'sanitize_text_field',
		) );
		register_setting( 'mbs_formatting', 'mbs_setting_formatting_section_order', array(
			'sanitize_callback' => array( 'MBS_Tab_Formatting', 'save_section_order' ),
		) );
		// F-023: Admin-configurable format type list.
		register_setting( 'mbs_formatting', 'mbs_setting_format_types', array(
			'sanitize_callback' => array( 'MBS_Tab_Formatting', 'save_format_types' ),
		) );
	}

	/**
	 * Sanitize callback for mbs_setting_formatting_section_order.
	 *
	 * The mbs_section_order table (accessed via MBS_Section_Order_DAO) is the
	 * authoritative storage for section order — both per-book overrides and
	 * the global default (post_id = 0). This callback bridges the Settings
	 * API form submission into that table, then schedules deletion of the
	 * wp_options row so the table stays the sole source of truth.
	 *
	 * @param mixed $raw_value Comma-separated string of D-1 canonical section keys.
	 * @return string Always empty — the option row is deleted on shutdown.
	 */
	public static function save_section_order( $raw_value ): string {
		if ( is_string( $raw_value ) ) {
			$sections = array_filter( array_map( 'sanitize_key', explode( ',', $raw_value ) ) );
			if ( ! empty( $sections ) ) {
				// Phase 2-A allowlist (UX-EDIT-002): strip keys not permitted for the
				// current tier. FREE users cannot save PRO section keys. This is the
				// sole tier gate; the render loop shows all 10 sections to all users.
				$canonical     = MBS_Section_Helper::get_canonical_keys();
				$section_data  = MBS_Section_Helper::get_canonical_section_data();
				$allowed_tiers = mbs_is_pro() ? array( 'free', 'all', 'pro' ) : array( 'free', 'all' );
				$allowed_keys  = array_filter(
					$canonical,
					static function( $key ) use ( $section_data, $allowed_tiers ) {
						return isset( $section_data[ $key ] )
							&& in_array( $section_data[ $key ]['tier'], $allowed_tiers, true );
					}
				);
				$sections = array_values( array_filter(
					$sections,
					static function( $key ) use ( $allowed_keys ) {
						return in_array( $key, $allowed_keys, true );
					}
				) );
				// Guard: adversarial POST of PRO-only keys empties $sections after
				// the filter; calling save_order(0, []) would DELETE all global-default
				// rows. Skip silently when nothing permitted remains.
				if ( ! empty( $sections ) ) {
					MBS_Section_Order_DAO::save_order( 0, $sections );

					// UX-NAV-002-001: persist global default visibility from checkboxes.
					$raw_visible       = isset( $_POST['mbs_section_global_visible'] )
						? (array) wp_unslash( $_POST['mbs_section_global_visible'] )
						: array();
					$safe_visible_keys = array_map( 'sanitize_key', array_keys( $raw_visible ) );
					$visibility_map    = array();
					foreach ( $sections as $section_key ) {
						$visibility_map[ $section_key ] = in_array( $section_key, $safe_visible_keys, true );
					}
					MBS_Section_Order_DAO::save_global_visibility( $visibility_map );
				}
			}
		}

		// Settings API will write our return value to wp_options; the shutdown
		// hook deletes that row so no stale duplicate of the DAO state lingers.
		add_action( 'shutdown', array( __CLASS__, 'delete_section_order_option' ), 99 );

		return '';
	}

	/**
	 * Shutdown handler that removes the vestigial wp_options row for the
	 * section order. Settings API insists on writing the sanitize callback's
	 * return value; this runs after that write to clean up.
	 */
	public static function delete_section_order_option(): void {
		delete_option( 'mbs_setting_formatting_section_order' );
	}

	/**
	 * Sanitize callback for mbs_setting_format_types (F-023).
	 *
	 * The textarea submits one "key|Label" entry per line. This callback
	 * parses, validates, and JSON-encodes the result for storage in wp_options.
	 * Falls back to the hardcoded defaults if the submitted value is empty or
	 * produces no valid pairs.
	 *
	 * @param mixed $raw_value Raw textarea value submitted by the Settings API.
	 * @return string JSON-encoded key→label map, or JSON of defaults on failure.
	 */
	public static function save_format_types( $raw_value ): string {
		$lines  = array_filter( array_map( 'trim', explode( "\n", (string) $raw_value ) ) );
		$result = array();

		foreach ( $lines as $line ) {
			$parts = explode( '|', $line, 2 );
			if ( 2 !== count( $parts ) ) {
				continue;
			}
			$key   = sanitize_key( trim( $parts[0] ) );
			$label = sanitize_text_field( trim( $parts[1] ) );
			if ( $key && $label ) {
				$result[ $key ] = $label;
			}
		}

		if ( empty( $result ) ) {
			return (string) wp_json_encode( MBS_Format_Options::get_default_format_options() );
		}

		return (string) wp_json_encode( $result );
	}

	/**
	 * Enqueue Formatting tab assets when this tab is active.
	 *
	 * @param string $current_tab The active tab slug.
	 * @return void
	 */
	public function enqueue( string $current_tab ): void {
		if ( 'formatting' !== $current_tab ) {
			return;
		}

		wp_enqueue_style(
			'mbs-admin-settings-formatting',
			plugins_url( 'assets/css/mbs-admin-settings-formatting.css', MBS_DIR . 'bookshowroom.php' ),
			array( 'mbs-admin-settings' ),
			MBS_VERSION
		);

		foreach ( array( 'order-controls', 'layout-thumbnails', 'content-panels' ) as $component ) {
			wp_enqueue_style(
				'mbs-formatting-' . $component,
				plugins_url( 'assets/css/admin/formatting/formatting-' . $component . '.css', MBS_DIR . 'bookshowroom.php' ),
				array( 'mbs-admin-settings-formatting' ),
				MBS_VERSION
			);
		}

		// Shared layout description panel module — used here and on term edit
		// screens (SHC-6). Registered once so dependent scripts can list it.
		wp_register_script(
			'mbs-admin-layout-desc',
			plugins_url( 'assets/js/adminjs/mbs-admin-layout-desc.js', MBS_DIR . 'bookshowroom.php' ),
			array( 'jquery' ),
			MBS_VERSION,
			true
		);

		// Phase 4c: layout thumbnail sync + merge-tag pill insert.
		// Depends on mbs-admin-layout-desc for the description panel.
		wp_enqueue_script(
			'mbs-admin-formatting',
			plugins_url( 'assets/js/adminjs/admin-formatting.js', MBS_DIR . 'bookshowroom.php' ),
			array( 'jquery', 'mbs-admin-layout-desc', 'wp-i18n' ),
			MBS_VERSION,
			true
		);
		wp_set_script_translations( 'mbs-admin-formatting', 'my-book-showroom', MBS_DIR . 'languages' );

		// Section Display Order — drag-and-drop + arrow-key fallback.
		// jquery-ui-sortable is bundled with WordPress; declaring it as a
		// dependency ensures it loads before this script runs.
		wp_enqueue_script(
			'mbs-admin-section-order',
			plugins_url( 'assets/js/adminjs/admin-section-order.js', MBS_DIR . 'bookshowroom.php' ),
			array( 'jquery', 'jquery-ui-sortable', 'wp-i18n' ),
			MBS_VERSION,
			true
		);
		wp_set_script_translations( 'mbs-admin-section-order', 'my-book-showroom', MBS_DIR . 'languages' );
		// UX-EDIT-002 Phase 0-D: pass canonical default order to JS so bindReset()
		// does not hardcode the key list. Single canonical source: helper class.
		wp_localize_script(
			'mbs-admin-section-order',
			'mbsSectionOrderData',
			array( 'defaultOrder' => MBS_Section_Helper::get_canonical_keys() )
		);
	}


	// ── Public render ─────────────────────────────────────────────────────────

	/**
	 * Render the Formatting settings tab.
	 *
	 * R-02: All HTML output delegated to MBS_Tab_Formatting_Render.
	 */
	public function render(): void {
		( new MBS_Tab_Formatting_Render() )->render();
	}
}
