<?php
/**
 * File Path: includes/blocks/class-mbs-block-related-books.php
 *
 * Block: mbs/related-books (PRO)
 *
 * Renders a grid of books related to a given book via shared genre, series,
 * or collection taxonomy terms (OR across all three axes). Designed for
 * single-book templates and "You might also like" placements.
 *
 * Attributes:
 *   bookId (int, default 0) — the source book. When 0, falls back to
 *   get_the_ID() so the block works inside single-book templates and Query
 *   Loop contexts without requiring an explicit selection.
 *   limit  (int, default 6, 1-12) — maximum books to display.
 *
 * Query logic:
 *   Collects term IDs from MBS_TAX_GENRE, MBS_TAX_SERIES, MBS_TAX_COLLECTION,
 *   and MBS_TAX_AUTHOR for the source book, then runs a WP_Query with a
 *   tax_query relation OR across whichever axes have terms. The source book
 *   is excluded via post__not_in. Results are ordered randomly so repeated
 *   page loads surface different titles on larger catalogs.
 *
 *   post_status: In the block editor (REST_REQUEST + edit_posts cap) the query
 *   includes draft and private books so authors see a complete preview. On the
 *   public frontend only published books are returned.
 *
 * PRO gate: returns an HTML comment on non-PRO installs — no visible output.
 * GP-UI-1: returns empty string when no related books are found.
 *
 * Registered in class-mbs-blocks-registry.php via register_related_books_block().
 *
 * @package    My_Book_Showroom
 * @subpackage Includes/Blocks
 * @since      1.0.0
 */

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

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

	/**
	 * Maximum number of books a caller may request.
	 *
	 * @var int
	 */
	private const MAX_LIMIT = 12;

	/**
	 * Render callback for the mbs/related-books block.
	 *
	 * @param array $attributes Block attributes from the editor.
	 * @return string Rendered HTML or empty string / HTML comment.
	 */
	public static function render( array $attributes ): string {
		if ( ! mbs_is_pro() ) {
			return '<!-- MBS: related-books requires PRO license -->';
		}

		// Resolve post ID: explicit attribute takes precedence; fall back to loop context.
		$book_id = isset( $attributes['bookId'] ) && $attributes['bookId'] > 0
			? absint( $attributes['bookId'] )
			: (int) get_the_ID();

		if ( ! $book_id ) {
			return '';
		}

		$limit = min( self::MAX_LIMIT, max( 1, absint( $attributes['limit'] ?? 6 ) ) );

		// Collect term IDs across genre, series, collection, and author axes.
		$genre_ids   = wp_get_object_terms( $book_id, MBS_TAX_GENRE,      array( 'fields' => 'ids' ) );
		$series_ids  = wp_get_object_terms( $book_id, MBS_TAX_SERIES,     array( 'fields' => 'ids' ) );
		$coll_ids    = wp_get_object_terms( $book_id, MBS_TAX_COLLECTION, array( 'fields' => 'ids' ) );
		$author_ids  = wp_get_object_terms( $book_id, MBS_TAX_AUTHOR,     array( 'fields' => 'ids' ) );

		// Guard against WP_Error (taxonomy not registered, etc.).
		$genre_ids   = is_array( $genre_ids )   ? array_map( 'absint', $genre_ids )   : array();
		$series_ids  = is_array( $series_ids )  ? array_map( 'absint', $series_ids )  : array();
		$coll_ids    = is_array( $coll_ids )    ? array_map( 'absint', $coll_ids )    : array();
		$author_ids  = is_array( $author_ids )  ? array_map( 'absint', $author_ids )  : array();

		// Build tax_query OR clauses — only include axes that have at least one term.
		$tax_clauses = array();

		if ( ! empty( $genre_ids ) ) {
			$tax_clauses[] = array(
				'taxonomy' => MBS_TAX_GENRE,
				'field'    => 'term_id',
				'terms'    => $genre_ids,
				'operator' => 'IN',
			);
		}

		if ( ! empty( $series_ids ) ) {
			$tax_clauses[] = array(
				'taxonomy' => MBS_TAX_SERIES,
				'field'    => 'term_id',
				'terms'    => $series_ids,
				'operator' => 'IN',
			);
		}

		if ( ! empty( $coll_ids ) ) {
			$tax_clauses[] = array(
				'taxonomy' => MBS_TAX_COLLECTION,
				'field'    => 'term_id',
				'terms'    => $coll_ids,
				'operator' => 'IN',
			);
		}

		if ( ! empty( $author_ids ) ) {
			$tax_clauses[] = array(
				'taxonomy' => MBS_TAX_AUTHOR,
				'field'    => 'term_id',
				'terms'    => $author_ids,
				'operator' => 'IN',
			);
		}

		// No terms on any axis — nothing to relate to (GP-UI-1).
		if ( empty( $tax_clauses ) ) {
			return '';
		}

		// Prepend the relation key and merge clauses.
		$tax_query = array_merge( array( 'relation' => 'OR' ), $tax_clauses );

		// In the block editor (REST_REQUEST + edit_posts cap), expand post_status
		// so authors see a complete related-books preview even when sibling books
		// are still draft or private. On the public frontend: published only.
		$preview_mode     = defined( 'REST_REQUEST' ) && REST_REQUEST && current_user_can( 'edit_posts' );
		$allowed_statuses = $preview_mode
			? array( 'publish', 'private', 'draft' )
			: 'publish';

		$query = new WP_Query( array(
			'post_type'           => MBS_POST_TYPE,
			'post_status'         => $allowed_statuses,
			'posts_per_page'      => $limit,
			'post__not_in'        => array( $book_id ), // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- standard WP query pattern; VIP rule does not apply to this plugin
			'orderby'             => 'rand',
			'ignore_sticky_posts' => true,
			'no_found_rows'       => true,
			'tax_query'           => $tax_query, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- standard WP query pattern; VIP rule does not apply to this plugin
		) );

		if ( ! $query->have_posts() ) {
			wp_reset_postdata();
			return '';
		}

		// RLB-002: Use the block's own layout attribute (default 'grid') rather than the
		// global default_layout setting. The global setting targets catalog/archive pages;
		// a 'related books' widget should default to a compact grid regardless of that setting.
		$layout_slug = (string) ( $attributes['layout'] ?? 'grid' );
		$layout_obj  = MBS_Layout_Registry::get_layout( $layout_slug );
		$output      = $layout_obj->render( $query );
		wp_reset_postdata();

		return $output;
	}
}
