<?php
/**
 * File Path: includes/widgets/class-mbs-widget-related-books.php
 *
 * My Book Showroom — Related Books Widget (Story 13-5)
 *
 * FREE tier. Sidebar widget that shows up to N books related to the current
 * single-book page. Relation fallback order:
 *   1. Other books in the same series.
 *   2. Other books by the same author.
 *   3. Other books in the same genre.
 *
 * Widget degrades gracefully (renders nothing) when:
 *   - Not on a single mbs_book page.
 *   - No related books are found at any fallback level.
 *
 * Shortcode equivalent: [mbs_related_books limit="4"]
 *
 * @package MyBookShowroom
 * @since   1.0.0
 */

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

/**
 * Class MBS_Widget_Related_Books
 */
class MBS_Widget_Related_Books extends WP_Widget {

	/**
	 * Register widget with WordPress.
	 */
	public function __construct() {
		parent::__construct(
			'mbs_related_books',
			__( 'MBS Related Books', 'my-book-showroom' ),
			array(
				'description' => __( 'Displays books related to the current book page (series, author, or genre).', 'my-book-showroom' ),
			)
		);
	}

	/**
	 * Front-end display of the widget.
	 *
	 * @param array $args     Widget area arguments.
	 * @param array $instance Saved widget settings.
	 * @return void
	 */
	public function widget( $args, $instance ) {
		if ( ! mbs_is_registered() ) {
			return;
		}

		// Only meaningful on single book pages.
		if ( ! is_singular( 'mbs_book' ) ) {
			return;
		}

		$post_id      = get_the_ID();
		$limit        = isset( $instance['limit'] ) ? max( 1, min( 6, (int) $instance['limit'] ) ) : 4;
		$heading_text = ! empty( $instance['title'] )
			? $instance['title']
			: __( 'You may also like', 'my-book-showroom' );
		$strategy     = ! empty( $instance['strategy'] ) ? $instance['strategy'] : 'auto';

		$related_books = self::get_related( $post_id, $limit, $strategy );

		// GP-UI-1: nothing to render when no related books found.
		if ( empty( $related_books ) ) {
			return;
		}

		echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- WordPress-generated widget wrapper HTML

		mbs_get_template_part(
			'partials/section-related-books',
			array(
				'related_books' => $related_books,
				'heading_text'  => $heading_text,
			)
		);

		echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- WordPress-generated widget wrapper HTML
	}

	/**
	 * Widget settings form in the admin.
	 *
	 * @param array $instance Current settings.
	 * @return void
	 */
	public function form( $instance ) {
		$title    = ! empty( $instance['title'] )    ? $instance['title']    : '';
		$limit    = isset( $instance['limit'] )      ? (int) $instance['limit'] : 4;
		$strategy = ! empty( $instance['strategy'] ) ? $instance['strategy'] : 'auto';
		?>
		<p>
			<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>">
				<?php esc_html_e( 'Heading:', 'my-book-showroom' ); ?>
			</label>
			<input
				class="widefat"
				id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
				name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>"
				type="text"
				value="<?php echo esc_attr( $title ); ?>"
			>
		</p>
		<p>
			<label for="<?php echo esc_attr( $this->get_field_id( 'limit' ) ); ?>">
				<?php esc_html_e( 'Books to show (1–6):', 'my-book-showroom' ); ?>
			</label>
			<input
				class="tiny-text"
				id="<?php echo esc_attr( $this->get_field_id( 'limit' ) ); ?>"
				name="<?php echo esc_attr( $this->get_field_name( 'limit' ) ); ?>"
				type="number"
				min="1"
				max="6"
				value="<?php echo esc_attr( $limit ); ?>"
			>
		</p>
		<p>
			<label for="<?php echo esc_attr( $this->get_field_id( 'strategy' ) ); ?>">
				<?php esc_html_e( 'Relation strategy:', 'my-book-showroom' ); ?>
			</label>
			<select
				id="<?php echo esc_attr( $this->get_field_id( 'strategy' ) ); ?>"
				name="<?php echo esc_attr( $this->get_field_name( 'strategy' ) ); ?>"
				class="widefat"
			>
				<option value="auto"        <?php selected( $strategy, 'auto' ); ?>><?php esc_html_e( 'Auto (series → author → genre)', 'my-book-showroom' ); ?></option>
				<option value="series_only" <?php selected( $strategy, 'series_only' ); ?>><?php esc_html_e( 'Series only', 'my-book-showroom' ); ?></option>
				<option value="author_only" <?php selected( $strategy, 'author_only' ); ?>><?php esc_html_e( 'Author only', 'my-book-showroom' ); ?></option>
			</select>
		</p>
		<?php
	}

	/**
	 * Sanitize and save widget settings.
	 *
	 * @param array $new_instance New settings submitted by the user.
	 * @param array $old_instance Previously saved settings.
	 * @return array Sanitized settings.
	 */
	public function update( $new_instance, $old_instance ) {
		$allowed_strategies = array( 'auto', 'series_only', 'author_only' );
		$instance             = array();
		$instance['title']    = sanitize_text_field( $new_instance['title'] );
		$instance['limit']    = max( 1, min( 6, (int) $new_instance['limit'] ) );
		$instance['strategy'] = in_array( $new_instance['strategy'], $allowed_strategies, true )
			? $new_instance['strategy']
			: 'auto';
		return $instance;
	}

	// -------------------------------------------------------------------------
	// Relation query logic (also used by the shortcode handler)
	// -------------------------------------------------------------------------

	/**
	 * Find related books for a given post using the configured strategy.
	 *
	 * Fallback order for 'auto': series → author → genre.
	 * Returns an empty array when nothing is found at any level.
	 *
	 * @param int    $post_id  The current book post ID.
	 * @param int    $limit    Maximum number of books to return (1–6).
	 * @param string $strategy 'auto' | 'series_only' | 'author_only'.
	 * @return WP_Post[]
	 */
	public static function get_related( $post_id, $limit = 4, $strategy = 'auto' ) {
		$limit = max( 1, min( 6, (int) $limit ) );

		if ( 'series_only' === $strategy ) {
			return self::by_series( $post_id, $limit );
		}

		if ( 'author_only' === $strategy ) {
			return self::by_author( $post_id, $limit );
		}

		// auto: try series → author → genre.
		$results = self::by_series( $post_id, $limit );
		if ( ! empty( $results ) ) {
			return $results;
		}

		$results = self::by_author( $post_id, $limit );
		if ( ! empty( $results ) ) {
			return $results;
		}

		return self::by_genre( $post_id, $limit );
	}

	/**
	 * Query books in the same series (excluding current book).
	 *
	 * @param int $post_id Current book post ID.
	 * @param int $limit   Maximum results.
	 * @return WP_Post[]
	 */
	private static function by_series( $post_id, $limit ) {
		$terms = wp_get_post_terms( $post_id, 'mbs_series', array( 'fields' => 'ids' ) );
		if ( is_wp_error( $terms ) || empty( $terms ) ) {
			return array();
		}

		return get_posts( array(
			'post_type'      => 'mbs_book',
			'post_status'    => 'publish',
			'posts_per_page' => $limit,
			'post__not_in'   => array( $post_id ), // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- standard WP query pattern; VIP rule does not apply to this plugin
			'tax_query'      => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- standard WP query pattern; VIP rule does not apply to this plugin
				array(
					'taxonomy' => 'mbs_series',
					'field'    => 'term_id',
					'terms'    => $terms,
				),
			),
			'no_found_rows'  => true,
		) );
	}

	/**
	 * Query books by the same author (excluding current book).
	 *
	 * @param int $post_id Current book post ID.
	 * @param int $limit   Maximum results.
	 * @return WP_Post[]
	 */
	private static function by_author( $post_id, $limit ) {
		$terms = wp_get_post_terms( $post_id, 'mbs_author', array( 'fields' => 'ids' ) );
		if ( is_wp_error( $terms ) || empty( $terms ) ) {
			return array();
		}

		return get_posts( array(
			'post_type'      => 'mbs_book',
			'post_status'    => 'publish',
			'posts_per_page' => $limit,
			'post__not_in'   => array( $post_id ), // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- standard WP query pattern; VIP rule does not apply to this plugin
			'tax_query'      => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- standard WP query pattern; VIP rule does not apply to this plugin
				array(
					'taxonomy' => 'mbs_author',
					'field'    => 'term_id',
					'terms'    => $terms,
				),
			),
			'no_found_rows'  => true,
		) );
	}

	/**
	 * Query books in the same genre (excluding current book).
	 *
	 * @param int $post_id Current book post ID.
	 * @param int $limit   Maximum results.
	 * @return WP_Post[]
	 */
	private static function by_genre( $post_id, $limit ) {
		$terms = wp_get_post_terms( $post_id, 'mbs_genre', array( 'fields' => 'ids' ) );
		if ( is_wp_error( $terms ) || empty( $terms ) ) {
			return array();
		}

		return get_posts( array(
			'post_type'      => 'mbs_book',
			'post_status'    => 'publish',
			'posts_per_page' => $limit,
			'post__not_in'   => array( $post_id ), // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- standard WP query pattern; VIP rule does not apply to this plugin
			'tax_query'      => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- standard WP query pattern; VIP rule does not apply to this plugin
				array(
					'taxonomy' => 'mbs_genre',
					'field'    => 'term_id',
					'terms'    => $terms,
				),
			),
			'no_found_rows'  => true,
		) );
	}
}
