<?php
/**
 * File Path: includes/class-mbs-archive-reader.php
 *
 * Archive Reader
 *
 * Static utility class that renders a taxonomy archive in a given Display
 * Mode ('order' | 'reading_order' | 'both') and resolves the effective
 * display mode for a given term (term_meta override → global option → 'order').
 *
 * Depends on: MBS_Reading_Order_DAO_Base, MBS_Layout (interface), MBS_POST_TYPE.
 * Registered in the autoloader via the MBS_* naming convention:
 *   MBS_Archive_Reader → includes/class-mbs-archive-reader.php
 *
 * @package MyBookShowroom
 * @since   4.34.0
 */

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

class MBS_Archive_Reader { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound -- mbs_/MBS_ prefix is correct; scanner not configured with plugin prefix

	/**
	 * Render an archive in a given Display Mode.
	 *
	 * Builds one or two WP_Query instances (based on display_mode) and
	 * delegates HTML rendering to the supplied layout object. In 'both' mode,
	 * if the two ID lists are identical the second query is suppressed.
	 *
	 * PRO gate (§6.3): if the site is not PRO and display_mode is
	 * 'reading_order' or 'both', silently falls back to 'order'.
	 *
	 * @param string     $dao_class   Fully-qualified DAO class name (must extend
	 *                                MBS_Reading_Order_DAO_Base).
	 * @param int        $term_id     Taxonomy term ID.
	 * @param MBS_Layout $layout_obj  Layout instance to render each row.
	 * @param string     $layout_slug Layout slug (passed through to layout render options).
	 * @param array      $options     {
	 *     Optional.
	 *     @type string $display_mode  'order'|'reading_order'|'both'. Default 'order'.
	 *     @type bool   $merge_missing Append books in term but absent from stored order.
	 *                                 Default false (series); true for universe.
	 *     @type string $context       'series'|'universe'|'collection'. Default 'series'.
	 *                                 Controls row-heading labels in 'both' mode.
	 * }
	 * @return string HTML output, or empty string on failure.
	 */
	public static function render_ordered(
		string     $dao_class,
		int        $term_id,
		MBS_Layout $layout_obj,
		string     $layout_slug,
		array      $options = array()
	): string {

		// ── 1. Guard: DAO must extend MBS_Reading_Order_DAO_Base ─────────────
		if ( ! is_a( $dao_class, MBS_Reading_Order_DAO_Base::class, true ) ) {
			_doing_it_wrong(
				__METHOD__,
				esc_html( $dao_class ) . ' does not extend MBS_Reading_Order_DAO_Base.',
				'4.34.0'
			);
			return '';
		}

		// ── 2. Resolve options with defaults ─────────────────────────────────
		$display_mode  = isset( $options['display_mode'] ) ? (string) $options['display_mode'] : 'order';
		$merge_missing = isset( $options['merge_missing'] ) ? (bool) $options['merge_missing'] : false;
		$context       = isset( $options['context'] ) ? (string) $options['context'] : 'series';

		$valid_modes = array( 'order', 'reading_order', 'both' );
		if ( ! in_array( $display_mode, $valid_modes, true ) ) {
			$display_mode = 'order';
		}

		/**
		 * Filter: cap on posts_per_page for both rows in render_ordered().
		 *
		 * @param int $cap Maximum number of posts per row. Default 100.
		 */
		$max_cap = (int) apply_filters( 'mbs_archive_max_reading_order_posts', 100 ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- mbs_/MBS_ prefix is correct; scanner not configured with plugin prefix
		$max_cap = max( 1, $max_cap );

		// ── PRO gate: reading_order / both require PRO (§6.3) ────────────────
		if ( 'order' !== $display_mode && ! mbs_is_pro() ) {
			$display_mode = 'order';
		}

		// ── 3. Fetch IDs based on display_mode ───────────────────────────────
		$ord_ids = array();
		$ro_ids  = array();

		if ( 'order' === $display_mode ) {
			$ord_ids = $dao_class::get_order_sorted_ids( $term_id );

		} elseif ( 'reading_order' === $display_mode ) {
			$ro_ids = $dao_class::get_ordered_book_ids( $term_id );
			if ( $merge_missing ) {
				$ro_ids = self::merge_missing_ids( $ro_ids, $dao_class::get_books_in_term( $term_id ) );
			}
		} else {
			// 'both': fetch reading order (with optional merge) and series order.
			$ro_ids = $dao_class::get_ordered_book_ids( $term_id );
			if ( $merge_missing ) {
				$ro_ids = self::merge_missing_ids( $ro_ids, $dao_class::get_books_in_term( $term_id ) );
			}
			$ord_ids = $dao_class::get_order_sorted_ids( $term_id );
		}

		// ── 4. 'both': compare arrays before building queries ────────────────
		if ( 'both' === $display_mode && $ord_ids === $ro_ids ) {
			// Identical lists — suppress the duplicate row (treat as reading_order).
			$display_mode = 'reading_order';
		}

		// ── 5–7. Build queries, render rows, reset postdata ──────────────────
		if ( 'order' === $display_mode ) {
			if ( empty( $ord_ids ) ) {
				return '';
			}
			$query = self::build_query( $ord_ids, $max_cap );
			$html  = $layout_obj->render( $query, $options );
			wp_reset_postdata();
			return $html;
		}

		if ( 'reading_order' === $display_mode ) {
			if ( empty( $ro_ids ) ) {
				return '';
			}
			$query = self::build_query( $ro_ids, $max_cap );
			$html  = $layout_obj->render( $query, $options );
			wp_reset_postdata();
			return $html;
		}

		// 'both' with two different rows.
		$order_label = ( 'universe' === $context )
			? __( 'Universe Order', 'my-book-showroom' )
			: __( 'Series Order', 'my-book-showroom' );
		$ro_label    = __( 'Reading Order', 'my-book-showroom' );

		$html = '<div class="mbs-archive-ordered-rows">';

		if ( ! empty( $ord_ids ) ) {
			$ord_query = self::build_query( $ord_ids, $max_cap );
			$ord_html  = $layout_obj->render( $ord_query, $options );
			wp_reset_postdata();
			$html .= '<div class="mbs-archive-row mbs-archive-row--order">';
			$html .= '<h3 class="mbs-archive-row__label">' . esc_html( $order_label ) . '</h3>';
			$html .= $ord_html;
			$html .= '</div>';
		}

		if ( ! empty( $ro_ids ) ) {
			$ro_query = self::build_query( $ro_ids, $max_cap );
			$ro_html  = $layout_obj->render( $ro_query, $options );
			wp_reset_postdata();
			$html .= '<div class="mbs-archive-row mbs-archive-row--reading-order">';
			$html .= '<h3 class="mbs-archive-row__label">' . esc_html( $ro_label ) . '</h3>';
			$html .= $ro_html;
			$html .= '</div>';
		}

		$html .= '</div>';
		return $html;
	}

	/**
	 * Resolve the effective display mode for a taxonomy term.
	 *
	 * Resolution order: term_meta override → wp_options global → 'order'.
	 * After resolving, if the toggle option is '0' and the resolved mode is
	 * not 'order', the mode is overridden to 'order'.
	 *
	 * @param int    $term_id       WordPress term ID.
	 * @param string $term_meta_key Term meta key for the per-term override.
	 * @param string $option_key    wp_options key for the global display mode.
	 * @param string $toggle_key    wp_options key for the Reading Order enable toggle.
	 * @return string 'order' | 'reading_order' | 'both'
	 */
	public static function resolve_display_mode(
		int    $term_id,
		string $term_meta_key,
		string $option_key,
		string $toggle_key
	): string {
		$valid = array( 'order', 'reading_order', 'both' );

		// 1. Per-term override.
		$term_val = (string) get_term_meta( $term_id, $term_meta_key, true );
		if ( '' !== $term_val && in_array( $term_val, $valid, true ) ) {
			$mode = $term_val;
		} else {
			// 2. Global option fallback.
			$global = (string) get_option( $option_key, 'order' );
			$mode   = in_array( $global, $valid, true ) ? $global : 'order';
		}

		// 3. Apply toggle: if RO is globally disabled, force 'order'.
		if ( 'order' !== $mode && '0' === (string) get_option( $toggle_key, '0' ) ) {
			$mode = 'order';
		}

		return $mode;
	}

	// ── Private helpers ──────────────────────────────────────────────────────

	/**
	 * Build a WP_Query for a given ordered list of post IDs.
	 *
	 * @param int[] $ids     Post IDs in desired output order.
	 * @param int   $max_cap Maximum posts_per_page cap.
	 * @return WP_Query
	 */
	private static function build_query( array $ids, int $max_cap ): WP_Query {
		return new WP_Query( array(
			'post_type'              => MBS_POST_TYPE,
			'post_status'            => 'publish',
			'post__in'               => $ids,
			'orderby'                => 'post__in',
			'posts_per_page'         => min( count( $ids ), $max_cap ),
			'no_found_rows'          => true,
			'update_post_meta_cache' => true,
			'update_post_term_cache' => true,
		) );
	}

	/**
	 * Append IDs from $all_ids that are not already present in $existing_ids.
	 *
	 * O(n) hash-set merge (avoids nested loops for large ID lists).
	 *
	 * @param int[] $existing_ids IDs already in the stored order.
	 * @param int[] $all_ids      All book IDs for the term.
	 * @return int[] Merged array.
	 */
	private static function merge_missing_ids( array $existing_ids, array $all_ids ): array {
		$seen = array_flip( $existing_ids );
		foreach ( $all_ids as $book_id ) {
			if ( ! isset( $seen[ $book_id ] ) ) {
				$existing_ids[] = $book_id;
				$seen[ $book_id ] = true;
			}
		}
		return $existing_ids;
	}
}
