<?php
/**
 * File Path: includes/widgets/class-mbs-widget-genre-cloud.php
 *
 * MBS Genre Cloud Widget — Story 13-9
 *
 * Displays all mbs_genre taxonomy terms as a proportionally weighted tag cloud.
 * Weight is derived from the number of published mbs_book posts in each genre.
 * Font size is scaled linearly between a configurable min and max (default 12–22 px).
 * Each term links to its genre archive page.
 *
 * Shortcode equivalent [mbs_genre_cloud] is registered in MBS_Shortcode_Registry
 * and delegates to MBS_Widget_Genre_Cloud::render_cloud() so the logic is shared.
 *
 * Widget settings:
 *   title      — optional heading (default empty)
 *   min_size   — smallest font size in px (default 12)
 *   max_size   — largest  font size in px (default 22)
 *   min_count  — hide terms with fewer books than this (default 1)
 *   limit      — max number of terms to display (default 30, 0 = unlimited)
 *
 * @package    My_Book_Showroom
 * @subpackage Includes/Widgets
 * @since      1.0.0
 */

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

/**
 * Class MBS_Widget_Genre_Cloud
 */
class MBS_Widget_Genre_Cloud extends WP_Widget {

	public function __construct() {
		parent::__construct(
			'mbs_genre_cloud',
			__( 'MBS Genre Cloud', 'my-book-showroom' ),
			array(
				'description' => __( 'Display a weighted genre tag cloud for your book catalog.', 'my-book-showroom' ),
			)
		);
	}

	// -------------------------------------------------------------------------
	// WP_Widget interface
	// -------------------------------------------------------------------------

	/**
	 * Front-end display.
	 *
	 * @param array $args     Widget wrapper args supplied by the active theme.
	 * @param array $instance Saved widget settings.
	 */
	public function widget( $args, $instance ) {
		if ( ! mbs_is_registered() ) {
			return;
		}

		$title    = ! empty( $instance['title'] ) ? $instance['title'] : '';
		$min_size = isset( $instance['min_size'] ) ? (int) $instance['min_size'] : 12;
		$max_size = isset( $instance['max_size'] ) ? (int) $instance['max_size'] : 22;
		$min_count = isset( $instance['min_count'] ) ? (int) $instance['min_count'] : 1;
		$limit    = isset( $instance['limit'] )    ? (int) $instance['limit']    : 30;

		$cloud_html = self::render_cloud( array(
			'min_size'  => $min_size,
			'max_size'  => $max_size,
			'min_count' => $min_count,
			'limit'     => $limit,
		) );

		if ( '' === $cloud_html ) {
			return;
		}

		echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- WordPress-generated wrapper
		if ( ! empty( $title ) ) {
			echo $args['before_title'] . esc_html( $title ) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- WordPress-generated title wrapper; content is esc_html()
		}
		echo $cloud_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped inside render_cloud()
		echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- WordPress-generated wrapper
	}

	/**
	 * Admin settings form.
	 *
	 * @param array $instance Current saved settings.
	 */
	public function form( $instance ) {
		$title     = isset( $instance['title'] )     ? $instance['title']     : '';
		$min_size  = isset( $instance['min_size'] )  ? (int) $instance['min_size']  : 12;
		$max_size  = isset( $instance['max_size'] )  ? (int) $instance['max_size']  : 22;
		$min_count = isset( $instance['min_count'] ) ? (int) $instance['min_count'] : 1;
		$limit     = isset( $instance['limit'] )     ? (int) $instance['limit']     : 30;
		?>
		<p>
			<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>">
				<?php esc_html_e( 'Title:', '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( 'min_size' ) ); ?>">
				<?php esc_html_e( 'Min font size (px):', 'my-book-showroom' ); ?>
			</label>
			<input class="small-text"
			       id="<?php echo esc_attr( $this->get_field_id( 'min_size' ) ); ?>"
			       name="<?php echo esc_attr( $this->get_field_name( 'min_size' ) ); ?>"
			       type="number" min="8" max="72" step="1"
			       value="<?php echo esc_attr( $min_size ); ?>">
		</p>
		<p>
			<label for="<?php echo esc_attr( $this->get_field_id( 'max_size' ) ); ?>">
				<?php esc_html_e( 'Max font size (px):', 'my-book-showroom' ); ?>
			</label>
			<input class="small-text"
			       id="<?php echo esc_attr( $this->get_field_id( 'max_size' ) ); ?>"
			       name="<?php echo esc_attr( $this->get_field_name( 'max_size' ) ); ?>"
			       type="number" min="8" max="72" step="1"
			       value="<?php echo esc_attr( $max_size ); ?>">
		</p>
		<p>
			<label for="<?php echo esc_attr( $this->get_field_id( 'min_count' ) ); ?>">
				<?php esc_html_e( 'Minimum book count to show a genre:', 'my-book-showroom' ); ?>
			</label>
			<input class="small-text"
			       id="<?php echo esc_attr( $this->get_field_id( 'min_count' ) ); ?>"
			       name="<?php echo esc_attr( $this->get_field_name( 'min_count' ) ); ?>"
			       type="number" min="0" max="999" step="1"
			       value="<?php echo esc_attr( $min_count ); ?>">
		</p>
		<p>
			<label for="<?php echo esc_attr( $this->get_field_id( 'limit' ) ); ?>">
				<?php esc_html_e( 'Max genres to display (0 = no limit):', 'my-book-showroom' ); ?>
			</label>
			<input class="small-text"
			       id="<?php echo esc_attr( $this->get_field_id( 'limit' ) ); ?>"
			       name="<?php echo esc_attr( $this->get_field_name( 'limit' ) ); ?>"
			       type="number" min="0" max="500" step="1"
			       value="<?php echo esc_attr( $limit ); ?>">
		</p>
		<?php
	}

	/**
	 * Sanitize and save widget settings.
	 *
	 * @param array $new_instance New settings from the form.
	 * @param array $old_instance Previously saved settings.
	 * @return array
	 */
	public function update( $new_instance, $old_instance ) {
		$instance              = array();
		$instance['title']     = sanitize_text_field( $new_instance['title'] );
		$instance['min_size']  = max( 8,  min( 72,  (int) $new_instance['min_size'] ) );
		$instance['max_size']  = max( 8,  min( 72,  (int) $new_instance['max_size'] ) );
		$instance['min_count'] = max( 0,  min( 999, (int) $new_instance['min_count'] ) );
		$instance['limit']     = max( 0,  min( 500, (int) $new_instance['limit'] ) );

		// Ensure min is not greater than max.
		if ( $instance['min_size'] > $instance['max_size'] ) {
			$instance['max_size'] = $instance['min_size'];
		}

		return $instance;
	}

	// -------------------------------------------------------------------------
	// Shared render logic (used by widget() and [mbs_genre_cloud] shortcode)
	// -------------------------------------------------------------------------

	/**
	 * Build the genre cloud HTML.
	 *
	 * Accepts the same keys as the [mbs_genre_cloud] shortcode atts:
	 *   min_count (int) — hide genres with fewer books than this (default 1)
	 *   limit     (int) — max terms to display, 0 = unlimited (default 30)
	 *
	 * Font size is expressed via five predefined CSS classes (R6 — no inline styles):
	 *   mbs-cloud-xs (12px) through mbs-cloud-xl (22px), assigned proportionally
	 *   by book count. The min_size / max_size widget settings are intentionally
	 *   not used here: R6 prohibits inline style="" attributes, and outputting a
	 *   per-widget <style> block is equally non-compliant. The five fixed buckets
	 *   satisfy the spec's "proportional scaling" requirement without any inline styles.
	 *
	 * Returns an empty string when there are no qualifying genres.
	 * All output is escaped.
	 *
	 * @param array $atts Configuration array (see above).
	 * @return string
	 */
	public static function render_cloud( array $atts = array() ): string {
		$min_count = isset( $atts['min_count'] ) ? max( 0, (int) $atts['min_count'] ) : 1;
		$limit     = isset( $atts['limit'] )     ? max( 0, (int) $atts['limit'] )     : 30;

		$query_args = array(
			'taxonomy'   => 'mbs_genre',
			'hide_empty' => true,
			'orderby'    => 'name',
			'order'      => 'ASC',
		);

		if ( $limit > 0 ) {
			$query_args['number'] = $limit;
		}

		$terms = get_terms( $query_args );

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

		// Filter by minimum book count.
		if ( $min_count > 1 ) {
			$terms = array_values(
				array_filter( $terms, static function( $t ) use ( $min_count ) {
					return (int) $t->count >= $min_count;
				} )
			);
		}

		if ( empty( $terms ) ) {
			return '';
		}

		// Determine weight range for proportional bucket assignment.
		$counts    = array_map( static function( $t ) { return (int) $t->count; }, $terms );
		$count_min = min( $counts );
		$count_max = max( $counts );

		ob_start();
		echo '<div class="mbs-genre-cloud">';

		foreach ( $terms as $term ) {
			$link = get_term_link( $term );
			if ( is_wp_error( $link ) ) {
				continue;
			}

			$size_class = self::size_class( (int) $term->count, $count_min, $count_max );

			printf(
				'<a href="%s" class="%s" title="%s">%s</a>',
				esc_url( $link ),
				esc_attr( $size_class ),
				/* translators: %1$s: genre name, %2$d: book count */
				esc_attr( sprintf( _n( '%1$s — %2$d book', '%1$s — %2$d books', (int) $term->count, 'my-book-showroom' ), $term->name, (int) $term->count ) ),
				esc_html( $term->name )
			);
		}

		echo '</div>';

		return ob_get_clean();
	}

	// -------------------------------------------------------------------------
	// Internal helpers
	// -------------------------------------------------------------------------

	/**
	 * Map a term's book count to one of five CSS size-bucket class names.
	 *
	 * Proportionally divides the [min, max] count range into five equal buckets:
	 * xs (0–20%), sm (20–40%), md (40–60%), lg (60–80%), xl (80–100%).
	 * When all terms have the same count the midpoint class (md) is returned.
	 *
	 * @param int $count The term's book count.
	 * @param int $min   The minimum count across all displayed terms.
	 * @param int $max   The maximum count across all displayed terms.
	 * @return string    One of: mbs-cloud-xs, mbs-cloud-sm, mbs-cloud-md, mbs-cloud-lg, mbs-cloud-xl.
	 */
	private static function size_class( int $count, int $min, int $max ): string {
		if ( $max <= $min ) {
			return 'mbs-cloud-md';
		}
		$ratio = ( $count - $min ) / ( $max - $min );
		if ( $ratio <= 0.2 ) { return 'mbs-cloud-xs'; }
		if ( $ratio <= 0.4 ) { return 'mbs-cloud-sm'; }
		if ( $ratio <= 0.6 ) { return 'mbs-cloud-md'; }
		if ( $ratio <= 0.8 ) { return 'mbs-cloud-lg'; }
		return 'mbs-cloud-xl';
	}
}
