<?php
/**
 * File Path: layouts/class-mbs-layout-registry.php
 *
 * Layout Registry
 *
 * Registers and manages all available book display layouts.
 *
 * Each layout is registered with its class name, display label, and PRO flag.
 * get_layout_options() is derived from this registration data so there is
 * a single source of truth — adding a new layout to register() automatically
 * makes it available in the settings dropdown (LAYOUT-007 fix).
 *
 * TLA-001: get_free_layout_slugs() added. resolve_slug_for_term() extended to
 * honour FREE-tier term overrides (clamped to FREE layout slugs). resolve_for_term()
 * now delegates to resolve_slug_for_term() eliminating duplicated resolution logic.
 *
 * @package MyBookShowroom
 * @since   1.0.0
 */

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

class MBS_Layout_Registry {

	/**
	 * Registered layouts.
	 *
	 * Shape: array<string, array{ class: string, label: string, pro: bool }>
	 */
	private static array $layouts = array();

	/**
	 * Register a layout.
	 *
	 * @param string $slug       Layout slug (e.g. 'grid').
	 * @param string $class_name PHP class name (e.g. 'MBS_Layout_Grid').
	 * @param string $label      Translatable display label.
	 * @param bool   $pro        True when this layout requires a PRO license.
	 */
	public static function register( string $slug, string $class_name, string $label, bool $pro = false ): void {
		self::$layouts[ $slug ] = array(
			'class' => $class_name,
			'label' => $label,
			'pro'   => $pro,
		);
	}

	/**
	 * Return a layout instance by slug.
	 *
	 * Falls back to MBS_Layout_Grid when:
	 *   - The slug is not registered.
	 *   - The class does not exist (e.g. PRO layout requested on FREE tier).
	 */
	public static function get_layout( string $slug ): MBS_Layout {
		// Registry-level registration gate. Returning MBS_Layout_Grid
		// here means unregistered users get an empty string from Grid's own
		// mbs_is_registered() check — consistent with every individual layout's
		// existing fallback. Future layouts get this protection for free without
		// needing to remember to add it themselves.
		if ( ! mbs_is_registered() ) {
			return new MBS_Layout_Grid();
		}
		if ( isset( self::$layouts[ $slug ] ) && class_exists( self::$layouts[ $slug ]['class'] ) ) {
			$class = self::$layouts[ $slug ]['class'];
			return new $class();
		}
		return new MBS_Layout_Grid();
	}

	/**
	 * Resolve the correct layout object for a taxonomy term.
	 *
	 * Delegates to resolve_slug_for_term() for all resolution logic, then
	 * instantiates the layout. Call pattern (archive templates + shortcodes):
	 *   $layout = MBS_Layout_Registry::resolve_for_term( $term_id );
	 *   echo $layout->render( $query );
	 *
	 * @param int    $term_id       WordPress term ID.
	 * @param string $entity_default Layout slug to use when no per-term override is set
	 *                              (overrides the global default). Pass 'grid' for
	 *                              Series/Collection/Universe entity archives.
	 * @return MBS_Layout           Layout instance (always valid — falls back to Grid).
	 */
	public static function resolve_for_term( int $term_id, string $entity_default = '' ): MBS_Layout {
		return self::get_layout( self::resolve_slug_for_term( $term_id, $entity_default ) );
	}

	/**
	 * Resolve the layout slug for a taxonomy term (without instantiating a layout).
	 *
	 * Single source of truth for per-term slug resolution. Used by resolve_for_term()
	 * and directly by archive templates that need the slug for badge-compat gating.
	 *
	 * Resolution order (TLA-001):
	 *   1. Read mbs_term_layout term meta.
	 *   2. PRO/PUB: honour any registered slug stored in meta.
	 *   3. FREE: honour meta only when slug is a FREE layout; otherwise fall back
	 *      to global default. If global default is also PRO, fall back to 'grid'.
	 *   4. No meta set: use global default.
	 *   5. Clamp to registered layouts (orphaned slug safety net).
	 *
	 * @param int    $term_id        WordPress term ID.
	 * @param string $entity_default Layout slug to use when no per-term override is set.
	 *                               '' = fall back to global default (existing behavior).
	 *                               'grid' = used by Series/Collection/Universe archives.
	 * @return string                Layout slug (always a registered slug; falls back to 'grid').
	 */
	public static function resolve_slug_for_term( int $term_id, string $entity_default = '' ): string {
		$global_default = (string) get_option( 'mbs_setting_formatting_default_layout', 'grid' );
		$free_slugs     = self::get_free_layout_slugs();
		$is_elevated    = mbs_is_pro() || mbs_is_pub();

		if ( mbs_is_registered() && $term_id > 0 ) {
			$override = (string) get_term_meta( $term_id, 'mbs_term_layout', true );
			if ( '' !== $override ) {
				if ( $is_elevated || in_array( $override, $free_slugs, true ) ) {
					$slug = $override;
				} else {
					// FREE user has a PRO layout stored — fall back to global default;
					// if that is also PRO, fall back to 'grid'.
					$slug = in_array( $global_default, $free_slugs, true )
						? $global_default
						: 'grid';
				}
			} else {
				// No per-term override — use entity_default when supplied, otherwise global.
				$slug = '' !== $entity_default ? $entity_default : $global_default;
			}
		} else {
			$slug = '' !== $entity_default ? $entity_default : $global_default;
		}

		// Clamp to registered layouts — fall back to 'grid' if an orphaned slug exists.
		if ( ! isset( self::$layouts[ $slug ] ) ) {
			$slug = 'grid';
		}

		return $slug;
	}

	/**
	 * Return all registered layout slugs.
	 *
	 * @return string[]
	 */
	public static function get_all_layouts(): array {
		return array_keys( self::$layouts );
	}

	/**
	 * Return the slugs of all FREE-tier layouts (pro === false).
	 *
	 * Used by render and resolver logic to clamp FREE-user choices.
	 * FREE layouts are those registered without the $pro flag.
	 *
	 * @return string[]
	 * @since 1.0.0
	 */
	public static function get_free_layout_slugs(): array {
		return array_keys( array_filter( self::$layouts, fn( $m ) => ! $m['pro'] ) );
	}

	/**
	 * Return layout options suitable for a <select> element.
	 *
	 * Derived from the registration data — no separate maintenance required.
	 * Marks PRO layouts so the settings UI can show the PRO badge.
	 *
	 * @return array<string, array{label: string, pro: bool}>
	 */
	public static function get_layout_options(): array {
		$options = array();
		foreach ( self::$layouts as $slug => $meta ) {
			$options[ $slug ] = array(
				'label' => $meta['label'],
				'pro'   => $meta['pro'],
			);
		}
		return $options;
	}

	/**
	 * Return layout options valid for archive contexts (excludes Hero).
	 *
	 * Used by taxonomy layout-field dropdowns so Hero is never offered
	 * for entity archives (Series, Collection, Universe, Genre).
	 *
	 * @return array<string, array{label: string, pro: bool}>
	 * @since  4.34.0
	 */
	public static function get_archive_layout_options(): array {
		$options = array();
		foreach ( self::$layouts as $slug => $meta ) {
			if ( 'hero' === $slug ) {
				continue;
			}
			$options[ $slug ] = array(
				'label' => $meta['label'],
				'pro'   => $meta['pro'],
			);
		}
		return $options;
	}

	/**
	 * Return layout slugs valid for archive contexts (excludes Hero).
	 *
	 * @return string[]
	 * @since  4.34.0
	 */
	public static function get_archive_layout_slugs(): array {
		return array_keys( static::get_archive_layout_options() );
	}

	/**
	 * Resolve the layout slug for an archive term, excluding Hero.
	 *
	 * Falls back to 'grid' if the stored term_meta is 'hero' or not a
	 * valid archive layout slug, or if the term has no override.
	 *
	 * @param int    $term_id  Term ID.
	 * @param string $taxonomy Taxonomy string (reserved; unused).
	 * @return string Valid archive layout slug (never 'hero').
	 * @since  4.34.0
	 */
	public static function resolve_slug_for_archive( int $term_id, string $taxonomy ): string {
		$archive_slugs = static::get_archive_layout_slugs();
		$stored        = (string) get_term_meta( $term_id, 'mbs_term_layout', true );
		if ( '' !== $stored && in_array( $stored, $archive_slugs, true ) ) {
			return $stored;
		}
		return 'grid';
	}
}

// ============================================================================
// Layout registrations deferred to init p1 so __() fires after the textdomain
// has loaded (WP 6.7+).  FREE: Sprint 4.  PRO: Sprint 8.
// ============================================================================

add_action(
	'init',
	function (): void {
		// FREE layouts.
		MBS_Layout_Registry::register( 'hero',    'MBS_Layout_Hero',    __( 'Hero',    'my-book-showroom' ) );
		MBS_Layout_Registry::register( 'grid',    'MBS_Layout_Grid',    __( 'Grid',    'my-book-showroom' ) );
		MBS_Layout_Registry::register( 'card',    'MBS_Layout_Card',    __( 'Card',    'my-book-showroom' ) );
		MBS_Layout_Registry::register( 'masonry', 'MBS_Layout_Masonry', __( 'Masonry', 'my-book-showroom' ) );
		MBS_Layout_Registry::register( 'list',    'MBS_Layout_List',    __( 'List',    'my-book-showroom' ) );

		// PRO layouts.  Classes are always registered by name. get_layout() falls
		// back to Grid if the class is absent.  The classes themselves enforce
		// mbs_is_pro() internally so there is no leakage if a slug is somehow
		// requested without PRO.
		MBS_Layout_Registry::register( 'slider', 'MBS_Layout_Slider', __( 'Slider', 'my-book-showroom' ), true );
		MBS_Layout_Registry::register( 'flip',   'MBS_Layout_Flip',   __( 'Flip',   'my-book-showroom' ), true );
		MBS_Layout_Registry::register( 'filter', 'MBS_Layout_Filter', __( 'Filter', 'my-book-showroom' ), true );
		MBS_Layout_Registry::register( 'popup',  'MBS_Layout_Popup',  __( 'Popup',  'my-book-showroom' ), true );
	},
	1
);
