<?php
/**
 * Hooks into WooCommerce product queries to apply sort rules.
 *
 * IMPORTANT: This plugin ONLY sorts products. It NEVER filters or hides products.
 * ALL products must always show regardless of which sort rule is active.
 *
 * @package Smart_Product_Sort
 */

defined( 'ABSPATH' ) || exit;

/**
 * Class SPSORT_Query
 */
class SPSORT_Query {

	/**
	 * Single instance.
	 *
	 * @var SPSORT_Query|null
	 */
	private static $instance = null;

	/**
	 * Get or create instance.
	 *
	 * @return SPSORT_Query
	 */
	public static function instance() {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Constructor.
	 */
	private function __construct() {
		add_filter( 'woocommerce_get_catalog_ordering_args', array( $this, 'apply_sort_rule' ) );
		add_filter( 'woocommerce_catalog_orderby', array( $this, 'register_orderby_options' ) );
	}

	/**
	 * Apply the active sort rule to WooCommerce catalog query args.
	 *
	 * @param array $args Current ordering args.
	 * @return array Modified args.
	 */
	public function apply_sort_rule( array $args ) {
		// Respect user's manual selection from the dropdown.
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		if ( ! empty( $_GET['orderby'] ) ) {
			return $args;
		}

		$category_id = $this->get_current_category_id();
		$rule        = SPSORT_Sort_Rules::get_active_for_category( $category_id );

		if ( ! $rule ) {
			return $args;
		}

		// For Pro sort types that need custom SQL (stock_status, discount),
		// we use the posts_clauses filter to add a LEFT JOIN + ORDER BY.
		// This sorts WITHOUT filtering — all products always show.
		if ( in_array( $rule->sort_by, array( 'stock_status', 'discount' ), true ) ) {
			$sort_by    = $rule->sort_by;
			$sort_order = $rule->sort_order;

			add_filter(
				'posts_clauses',
				function ( $clauses, $query ) use ( $sort_by, $sort_order ) {
					return $this->add_custom_sort_clauses( $clauses, $query, $sort_by, $sort_order );
				},
				999,
				2
			);

			// Tell WooCommerce not to apply its own sorting.
			$args['orderby'] = 'ID';
			$args['order']   = $sort_order;
			return $args;
		}

		$new_args = $this->build_query_args( $rule->sort_by, $rule->sort_order );

		// Only override orderby and order — never add anything that filters products.
		$args['orderby'] = $new_args['orderby'];
		if ( isset( $new_args['order'] ) ) {
			$args['order'] = $new_args['order'];
		}

		return $args;
	}

	/**
	 * Add custom SQL clauses for Pro sort types that need LEFT JOIN.
	 * Uses LEFT JOIN so products without the meta still appear (never filtered).
	 *
	 * @param array    $clauses SQL clauses.
	 * @param WP_Query $query   The query.
	 * @param string   $sort_by Sort type.
	 * @param string   $sort_order ASC or DESC.
	 * @return array
	 */
	private function add_custom_sort_clauses( $clauses, $query, $sort_by, $sort_order ) {
		global $wpdb;

		// Only modify the main WooCommerce product query on frontend.
		if ( ! $query->is_main_query() || is_admin() ) {
			return $clauses;
		}

		$opposite = ( 'ASC' === $sort_order ) ? 'DESC' : 'ASC';

		if ( 'stock_status' === $sort_by ) {
			/*
			 * LEFT JOIN _stock_status meta — products without it still show.
			 * ASC: instock first (alphabetically i < o).
			 * DESC: outofstock first.
			 * Secondary sort: newest first within each group.
			 */
			$clauses['join']   .= " LEFT JOIN {$wpdb->postmeta} AS spsort_stock ON ({$wpdb->posts}.ID = spsort_stock.post_id AND spsort_stock.meta_key = '_stock_status')";
			$clauses['orderby'] = "spsort_stock.meta_value {$sort_order}, {$wpdb->posts}.post_date DESC";
		}

		if ( 'discount' === $sort_by ) {
			/*
			 * LEFT JOIN _sale_price meta — products without sale price still show.
			 * ASC: lowest sale price first, NULL (no sale) last.
			 * DESC: highest sale price first, NULL (no sale) last.
			 * We use COALESCE to push NULLs to the end regardless of ASC/DESC.
			 */
			$clauses['join'] .= " LEFT JOIN {$wpdb->postmeta} AS spsort_sale ON ({$wpdb->posts}.ID = spsort_sale.post_id AND spsort_sale.meta_key = '_sale_price' AND spsort_sale.meta_value != '')";

			if ( 'ASC' === $sort_order ) {
				// ASC: sale products with lowest price first, non-sale products last.
				$clauses['orderby'] = "CASE WHEN spsort_sale.meta_value IS NULL OR spsort_sale.meta_value = '' THEN 1 ELSE 0 END ASC, CAST(spsort_sale.meta_value AS DECIMAL(10,2)) ASC, {$wpdb->posts}.post_date DESC";
			} else {
				// DESC: sale products with highest price first, non-sale products last.
				$clauses['orderby'] = "CASE WHEN spsort_sale.meta_value IS NULL OR spsort_sale.meta_value = '' THEN 1 ELSE 0 END ASC, CAST(spsort_sale.meta_value AS DECIMAL(10,2)) DESC, {$wpdb->posts}.post_date DESC";
			}
		}

		return $clauses;
	}

	/**
	 * Add plugin-managed sort options to the catalog orderby dropdown.
	 *
	 * @param array $options Existing orderby options.
	 * @return array Modified options.
	 */
	public function register_orderby_options( array $options ) {
		$options['rand'] = esc_html__( 'Random', 'smart-product-sort' );
		return $options;
	}

	/**
	 * Detect the current product category term ID from query vars.
	 *
	 * @return int|null
	 */
	private function get_current_category_id() {
		if ( ! is_product_category() ) {
			return null;
		}

		$term = get_queried_object();

		return ( $term instanceof WP_Term ) ? (int) $term->term_id : null;
	}

	/**
	 * Convert a sort_by / sort_order pair into WooCommerce-compatible query args.
	 *
	 * Uses WooCommerce's built-in orderby values (price, popularity, rating)
	 * which WooCommerce handles internally without meta_key filtering.
	 *
	 * @param string $sort_by    Column / meta key to sort by.
	 * @param string $sort_order 'ASC' or 'DESC'.
	 * @return array
	 */
	private function build_query_args( $sort_by, $sort_order ) {
		switch ( $sort_by ) {

			case 'price':
				// WooCommerce handles 'price' internally — no meta_key needed.
				return array(
					'orderby' => 'price',
					'order'   => $sort_order,
				);

			case 'name':
				return array(
					'orderby' => 'title',
					'order'   => $sort_order,
				);

			case 'popularity':
				// WooCommerce handles 'popularity' internally.
				return array(
					'orderby' => 'popularity',
					'order'   => $sort_order,
				);

			case 'rating':
				// WooCommerce handles 'rating' internally.
				return array(
					'orderby' => 'rating',
					'order'   => $sort_order,
				);

			case 'rand':
				return array(
					'orderby' => 'rand',
				);

			case 'menu_order':
				return array(
					'orderby' => 'menu_order title',
					'order'   => $sort_order,
				);

			case 'review_count':
				return array(
					'orderby' => 'comment_count',
					'order'   => $sort_order,
				);

			case 'date':
			default:
				return array(
					'orderby' => 'date',
					'order'   => $sort_order,
				);
		}
	}
}
