<?php
/**
 * Front-end display: a server-rendered block and a [scwc_products] shortcode
 * that list the products of a smart (or any) product category.
 *
 * Because membership is materialised onto real product categories, native
 * WooCommerce blocks (e.g. "Products by Category") also work out of the box.
 *
 * @package SmartCategories
 */

defined( 'ABSPATH' ) || exit;

/**
 * Class SCWC_Block
 */
class SCWC_Block {

	/**
	 * Register the block and shortcode.
	 */
	public static function init() {
		add_action( 'init', array( __CLASS__, 'register' ) );
		// Shortcode tags share one global registry across every plugin, so the
		// tag carries the plugin prefix (this is also the tag the docs name).
		add_shortcode( 'scwc_products', array( __CLASS__, 'shortcode' ) );
	}

	/**
	 * Register the dynamic block.
	 */
	public static function register() {
		if ( ! function_exists( 'register_block_type' ) ) {
			return;
		}

		$asset_path = SCWC_DIR . 'build/block.asset.php';
		$asset      = file_exists( $asset_path )
			? require $asset_path
			: array(
				'dependencies' => array( 'wp-blocks', 'wp-element', 'wp-block-editor', 'wp-components', 'wp-server-side-render', 'wp-api-fetch', 'wp-i18n' ),
				'version'      => SCWC_VERSION,
			);

		if ( file_exists( SCWC_DIR . 'build/block.js' ) ) {
			wp_register_script(
				'scwc-block-editor',
				SCWC_URL . 'build/block.js',
				$asset['dependencies'],
				$asset['version'],
				true
			);
			wp_set_script_translations( 'scwc-block-editor', 'smart-categories-for-woocommerce', SCWC_DIR . 'languages' );
		}

		// Register from block.json so the block's title, description, icon and
		// attributes have ONE source of truth that both the editor and the
		// server (and the wp.org directory's block scanner) read. The dynamic
		// render callback stays in PHP.
		$scwc_metadata = SCWC_DIR . 'block.json';
		if ( file_exists( $scwc_metadata ) ) {
			register_block_type(
				$scwc_metadata,
				array( 'render_callback' => array( __CLASS__, 'render_block' ) )
			);
			return;
		}

		// Fallback for a tree without block.json.
		register_block_type(
			'scwc/products',
			array(
				'api_version'     => 3,
				'title'           => __( 'Smart Category Products', 'smart-categories-for-woocommerce' ),
				'description'     => __( 'Display the products of a smart category as a grid — anywhere on your site.', 'smart-categories-for-woocommerce' ),
				'category'        => 'woocommerce',
				'icon'            => 'grid-view',
				'editor_script'   => 'scwc-block-editor',
				'render_callback' => array( __CLASS__, 'render_block' ),
				'attributes'      => array(
					'categoryId' => array(
						'type'    => 'number',
						'default' => 0,
					),
					'columns'    => array(
						'type'    => 'number',
						'default' => 4,
					),
					'limit'      => array(
						'type'    => 'number',
						'default' => 12,
					),
					'orderby'    => array(
						'type'    => 'string',
						'default' => 'date',
					),
				),
			)
		);
	}

	/**
	 * Render callback for the block.
	 *
	 * @param array $attributes Block attributes.
	 * @return string
	 */
	public static function render_block( $attributes ) {
		return self::render_products(
			(int) ( $attributes['categoryId'] ?? 0 ),
			(int) ( $attributes['limit'] ?? 12 ),
			(int) ( $attributes['columns'] ?? 4 ),
			(string) ( $attributes['orderby'] ?? 'date' )
		);
	}

	/**
	 * The [scwc_products] shortcode.
	 *
	 * @param array $atts Shortcode attributes.
	 * @return string
	 */
	public static function shortcode( $atts ) {
		$atts = shortcode_atts(
			array(
				'id'      => 0,
				'limit'   => 12,
				'columns' => 4,
				'orderby' => 'date',
			),
			$atts,
			'scwc_products'
		);
		return self::render_products( (int) $atts['id'], (int) $atts['limit'], (int) $atts['columns'], (string) $atts['orderby'] );
	}

	/**
	 * Build the product grid markup using WooCommerce's product loop.
	 *
	 * @param int    $category_id Category term ID.
	 * @param int    $limit       Max products.
	 * @param int    $columns     Grid columns.
	 * @param string $orderby     Order key.
	 * @return string
	 */
	private static function render_products( $category_id, $limit, $columns, $orderby ) {
		if ( $category_id <= 0 ) {
			return '';
		}
		$term = get_term( $category_id, 'product_cat' );
		if ( ! $term || is_wp_error( $term ) ) {
			return '';
		}

		// Products hidden from the catalog must not take up slots: WooCommerce's
		// own loop template skips them, so querying them would silently return
		// fewer (or zero) cards than the requested limit.
		$tax_query = array(
			'relation' => 'AND',
			array(
				'taxonomy' => 'product_cat',
				'field'    => 'term_id',
				'terms'    => $category_id,
			),
		);
		if ( taxonomy_exists( 'product_visibility' ) ) {
			$tax_query[] = array(
				'taxonomy' => 'product_visibility',
				'field'    => 'name',
				'terms'    => array( 'exclude-from-catalog' ),
				'operator' => 'NOT IN',
			);
		}

		$args = array(
			'post_type'      => 'product',
			'post_status'    => 'publish',
			'posts_per_page' => max( 1, $limit ),
			// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- a single-term category filter bounded by posts_per_page; this IS the block's purpose.
			'tax_query'      => $tax_query,
		);

		// Map friendly order keys to WP_Query args (meta-based sorts need a meta_key).
		switch ( $orderby ) {
			case 'price':
				$args['meta_key'] = '_price'; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- indexed lookup-table meta, bounded by posts_per_page.
				$args['orderby']  = 'meta_value_num';
				$args['order']    = 'ASC';
				break;
			case 'popularity':
				$args['meta_key'] = 'total_sales'; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- indexed lookup-table meta, bounded by posts_per_page.
				$args['orderby']  = 'meta_value_num';
				$args['order']    = 'DESC';
				break;
			case 'rating':
				$args['meta_key'] = '_wc_average_rating'; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- indexed lookup-table meta, bounded by posts_per_page.
				$args['orderby']  = 'meta_value_num';
				$args['order']    = 'DESC';
				break;
			case 'title':
				$args['orderby'] = 'title';
				$args['order']   = 'ASC';
				break;
			case 'menu_order':
				$args['orderby'] = 'menu_order';
				$args['order']   = 'ASC';
				break;
			default:
				$args['orderby'] = 'date';
				$args['order']   = 'DESC';
		}

		$query = new WP_Query( $args );

		if ( ! $query->have_posts() ) {
			wp_reset_postdata();
			return '<p class="scwc-empty">' . esc_html__( 'No products found in this category.', 'smart-categories-for-woocommerce' ) . '</p>';
		}

		$columns = max( 1, min( 6, $columns ) );

		// Tell WooCommerce the column count so the loop wrapper gets the right
		// `columns-N` class and the theme's grid CSS actually applies.
		if ( function_exists( 'wc_set_loop_prop' ) ) {
			wc_set_loop_prop( 'columns', $columns );
		}

		ob_start();
		echo '<div class="scwc-products woocommerce columns-' . esc_attr( $columns ) . '">';
		woocommerce_product_loop_start();
		while ( $query->have_posts() ) {
			$query->the_post();
			wc_get_template_part( 'content', 'product' );
		}
		woocommerce_product_loop_end();
		echo '</div>';
		wp_reset_postdata();

		if ( function_exists( 'wc_reset_loop' ) ) {
			wc_reset_loop();
		}

		return ob_get_clean();
	}
}
