<?php
/**
 * File Path: includes/shortcodes/class-mbs-shortcode-series-directory.php
 *
 * Series Directory Shortcode
 *
 * [mbs_series_directory] — PRO-gated. Renders an accordion listing every
 * non-empty series. Each series is an expandable <details>/<summary> header;
 * the expanded body lists its books in admin-configured reading order.
 *
 * Decisions locked 2026-04-22 (F-013):
 *   - Output shape: accordion (option A).
 *   - PRO-gated: returns HTML comment on FREE tier (GP-T-3).
 *   - Reading order sourced from MBS_Series_Reading_Order_DAO::get_ordered_book_ids().
 *   - Hierarchical display: child series rendered under their parent (F-018).
 *   - hide_empty => true: series with zero books are omitted.
 *
 * Attributes:
 *   sort  (str, default 'name') Sort series by 'name' (raw term name) or
 *         'name_sort' (article-stripped sort key via mbs_sort_key). F-SORT-2.
 *
 * @package    My_Book_Showroom
 * @subpackage Includes/Shortcodes
 * @since      1.0.0
 */

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

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

	/**
	 * Render [mbs_series_directory].
	 *
	 * @param array $atts Shortcode attributes (reserved for future use).
	 * @return string     HTML output.
	 */
	public static function render( array $atts = array() ): string {
		if ( ! mbs_is_registered() ) {
			return '';
		}

		if ( ! mbs_is_pro() ) {
			return '<!-- MBS: Series Directory requires PRO license -->';
		}

		// F-SORT-2: Validate sort attribute.
		$atts = shortcode_atts( array( 'sort' => 'name' ), $atts, 'mbs_series_directory' );
		$sort = in_array( $atts['sort'], array( 'name', 'name_sort' ), true )
			? $atts['sort']
			: 'name';

		// Fetch all series that have at least one book.
		$all_series = get_terms( array(
			'taxonomy'   => MBS_TAX_SERIES,
			'hide_empty' => true,
			'orderby'    => 'name',
			'order'      => 'ASC',
		) );

		if ( is_wp_error( $all_series ) || empty( $all_series ) ) {
			return '';
		}

		// F-SORT-2: When sort=name_sort, re-sort using mbs_sort_key.
		if ( 'name_sort' === $sort ) {
			usort( $all_series, static function ( $a, $b ) {
				$ka = (string) get_term_meta( $a->term_id, 'mbs_sort_key', true );
				$kb = (string) get_term_meta( $b->term_id, 'mbs_sort_key', true );
				if ( '' === $ka ) {
					$ka = function_exists( 'mbs_compute_sort_key' ) ? mbs_compute_sort_key( $a->name ) : strtolower( $a->name );
				}
				if ( '' === $kb ) {
					$kb = function_exists( 'mbs_compute_sort_key' ) ? mbs_compute_sort_key( $b->name ) : strtolower( $b->name );
				}
				// Sort by key; put children after their parent by preserving hierarchy.
				return strcmp( $ka, $kb );
			} );
		}

		// Build parent → children map for hierarchical display (F-018).
		$top_level = array();
		$children  = array();
		foreach ( $all_series as $series ) {
			if ( 0 === (int) $series->parent ) {
				$top_level[] = $series;
			} else {
				$children[ (int) $series->parent ][] = $series;
			}
		}

		$output  = '<div class="mbs-wrapper mbs-series-directory">';
		$output .= self::render_series_list( $top_level, $children );
		$output .= '</div>';

		return $output;
	}

	/**
	 * Render a list of series terms as accordion items, recursing into children.
	 *
	 * @param WP_Term[] $terms    Series terms at this level.
	 * @param array     $children Map of parent_id → WP_Term[] for one level of children.
	 * @return string
	 */
	private static function render_series_list( array $terms, array $children ): string {
		if ( empty( $terms ) ) {
			return '';
		}

		// Collect all book IDs across all series for cache priming.
		$all_book_ids = array();
		foreach ( $terms as $_s ) {
			$_ids = MBS_Series_Reading_Order_DAO::get_ordered_book_ids( (int) $_s->term_id );
			$all_book_ids = array_merge( $all_book_ids, $_ids );
		}
		if ( ! empty( $all_book_ids ) ) {
			_prime_post_caches( array_unique( $all_book_ids ), false, false );
		}

		$output = '';
		foreach ( $terms as $series ) {
			$term_id  = (int) $series->term_id;
			$book_ids = MBS_Series_Reading_Order_DAO::get_ordered_book_ids( $term_id );

			// Skip parent-only terms that have no books of their own (children may still render).
			$has_books    = ! empty( $book_ids );
			$has_children = isset( $children[ $term_id ] ) && ! empty( $children[ $term_id ] );

			if ( ! $has_books && ! $has_children ) {
				continue;
			}

			$series_url = get_term_link( $series );
			$book_count = count( $book_ids );

			$output .= '<details class="mbs-series-directory-item">';
			$output .= '<summary class="mbs-series-directory-summary">';

			if ( ! is_wp_error( $series_url ) ) {
				$output .= '<a href="' . esc_url( $series_url ) . '" class="mbs-series-directory-name">'
				         . esc_html( $series->name ) . '</a>';
			} else {
				$output .= '<span class="mbs-series-directory-name">' . esc_html( $series->name ) . '</span>';
			}

			if ( $has_books ) {
				$output .= ' <span class="mbs-series-directory-count">'
				         . esc_html( sprintf(
				             /* translators: %d: number of books */
				             _n( '(%d book)', '(%d books)', $book_count, 'my-book-showroom' ),
				             $book_count
				         ) )
				         . '</span>';
			}

			$output .= '</summary>';

			// Book list in reading order.
			if ( $has_books ) {
				$output .= '<ol class="mbs-series-directory-books">';
				foreach ( $book_ids as $book_id ) {
					$book_title = get_the_title( $book_id );
					$book_url   = get_permalink( $book_id );
					$output .= '<li><a href="' . esc_url( $book_url ) . '">'
					         . esc_html( $book_title ) . '</a></li>';
				}
				$output .= '</ol>';
			}

			// Recurse into child series (one level only per F-018 decision).
			if ( $has_children ) {
				$output .= '<div class="mbs-series-directory-children">';
				$output .= self::render_series_list( $children[ $term_id ], array() );
				$output .= '</div>';
			}

			$output .= '</details>';
		}

		return $output;
	}
}
