<?php
/**
 * Customer Interest Rule Helper.
 *
 * @package PRECO
 */

namespace PRECO\Includes\Frontend\Engine\Filters\Query;

use PRECO\Includes\Frontend\UserEvents\UserEventsTable;

defined( 'ABSPATH' ) || exit;

/**
 * Helper for filtering products based on the current user's taxonomy interests
 * (categories, tags, attributes) derived from their view history.
 *
 * Strategy:
 *   1. Fetch the user's viewed products from preco_user_events (or guest transient).
 *   2. Apply average-threshold logic — only products at or above average view count
 *      qualify as "interest signals" (same approach as best-selling filter).
 *   3. Execute a single JOIN SQL to find other products sharing those taxonomy terms.
 *      No PHP intermediary looping — purely DB-level resolution.
 *
 * For purchased_before_* variants, step 1 uses wc_order_product_lookup instead.
 */
class CustomerInterestRuleHelper {

	/**
	 * Mode config map — keys match frontend option values.
	 *
	 * source:    'views' (preco_user_events) or 'orders' (wc_order_product_lookup)
	 * taxonomy:  WP taxonomy slug, or 'attribute' for all pa_* taxonomies
	 * order:     'count' (sort by view count) or 'updated_at' (sort by recency)
	 * min_count: optional minimum count gate (repeat_viewed uses 2)
	 *
	 * @var array
	 */
	private static $modes = array(
		'most_view_categories'         => array(
			'source'   => 'views',
			'taxonomy' => 'product_cat',
			'order'    => 'count',
		),
		'frequently_viewed_categories' => array(
			'source'   => 'views',
			'taxonomy' => 'product_cat',
			'order'    => 'count',
		),
		'recently_viewed_categories'   => array(
			'source'   => 'recently_viewed',
			'taxonomy' => 'product_cat',
		),
		'repeat_viewed_categories'     => array(
			'source'    => 'views',
			'taxonomy'  => 'product_cat',
			'order'     => 'count',
			'min_count' => 2,
		),
		'purchased_before_categories'  => array(
			'source'   => 'orders',
			'taxonomy' => 'product_cat',
		),
		'most_view_tags'               => array(
			'source'   => 'views',
			'taxonomy' => 'product_tag',
			'order'    => 'count',
		),
		'frequently_viewed_tags'       => array(
			'source'   => 'views',
			'taxonomy' => 'product_tag',
			'order'    => 'count',
		),
		'recently_viewed_tags'         => array(
			'source'   => 'recently_viewed',
			'taxonomy' => 'product_tag',
		),
		'repeat_viewed_tags'           => array(
			'source'    => 'views',
			'taxonomy'  => 'product_tag',
			'order'     => 'count',
			'min_count' => 2,
		),
		'purchased_before_tags'        => array(
			'source'   => 'orders',
			'taxonomy' => 'product_tag',
		),
		'most_view_attributes'         => array(
			'source'   => 'views',
			'taxonomy' => 'attribute',
			'order'    => 'count',
		),
		'frequently_viewed_attributes' => array(
			'source'   => 'views',
			'taxonomy' => 'attribute',
			'order'    => 'count',
		),
		'recently_viewed_attributes'   => array(
			'source'   => 'recently_viewed',
			'taxonomy' => 'attribute',
		),
		'repeat_viewed_attributes'     => array(
			'source'    => 'views',
			'taxonomy'  => 'attribute',
			'order'     => 'count',
			'min_count' => 2,
		),
		'purchased_before_attributes'  => array(
			'source'   => 'orders',
			'taxonomy' => 'attribute',
		),
	);

	/**
	 * Add customer interest categories rule.
	 *
	 * @param array|null &$include_ids   Include IDs array.
	 * @param bool       &$enforce_empty Enforce empty flag.
	 * @param string     $relation       Relation (in/not_in).
	 * @param mixed      $value          Mode string, e.g. 'most_view_categories'.
	 * @param int        $user_id        Current user ID (0 = guest).
	 * @return void
	 */
	public function add_customer_interest_categories_rule( &$include_ids, &$enforce_empty, $relation, $value, $user_id ) {
		$this->apply_rule( $include_ids, $enforce_empty, $relation, $value, $user_id );
	}

	/**
	 * Add customer interest tags rule.
	 *
	 * @param array|null &$include_ids   Include IDs array.
	 * @param bool       &$enforce_empty Enforce empty flag.
	 * @param string     $relation       Relation (in/not_in).
	 * @param mixed      $value          Mode string, e.g. 'most_view_tags'.
	 * @param int        $user_id        Current user ID (0 = guest).
	 * @return void
	 */
	public function add_customer_interest_tags_rule( &$include_ids, &$enforce_empty, $relation, $value, $user_id ) {
		$this->apply_rule( $include_ids, $enforce_empty, $relation, $value, $user_id );
	}

	/**
	 * Add customer interest attributes rule.
	 *
	 * @param array|null &$include_ids   Include IDs array.
	 * @param bool       &$enforce_empty Enforce empty flag.
	 * @param string     $relation       Relation (in/not_in).
	 * @param mixed      $value          Mode string, e.g. 'most_view_attributes'.
	 * @param int        $user_id        Current user ID (0 = guest).
	 * @return void
	 */
	public function add_customer_interest_attributes_rule( &$include_ids, &$enforce_empty, $relation, $value, $user_id ) {
		$this->apply_rule( $include_ids, $enforce_empty, $relation, $value, $user_id );
	}

	/**
	 * Core rule application shared by all three public methods.
	 *
	 * @param array|null &$include_ids   Include IDs array.
	 * @param bool       &$enforce_empty Enforce empty flag.
	 * @param string     $relation       Relation (in/not_in).
	 * @param mixed      $value          Mode string.
	 * @param int        $user_id        Current user ID (0 = guest).
	 * @return void
	 */
	private function apply_rule( &$include_ids, &$enforce_empty, $relation, $value, $user_id ) {
		$mode = is_array( $value ) ? ( isset( $value['mode'] ) ? sanitize_text_field( $value['mode'] ) : '' ) : sanitize_text_field( (string) $value );

		if ( ! isset( self::$modes[ $mode ] ) ) {
			return;
		}

		$config = self::$modes[ $mode ];

		if ( 'orders' === $config['source'] ) {
			// purchased_before_* — only meaningful for logged-in users with order history.
			if ( 0 === $user_id ) {
				if ( 'in' === $relation ) {
					$enforce_empty = true;
				}
				return;
			}
			$source_ids = $this->get_purchased_product_ids( $user_id );
		} elseif ( 'recently_viewed' === $config['source'] ) {
			// recently_viewed_* — DB-backed, no cookie dependency.
			// Logged-in: query preco_user_events for today's views.
			// Guest: read preco_guest_* transient (set by maybe_set_guest_cookie).
			if ( $user_id > 0 ) {
				$source_ids = $this->get_today_viewed_product_ids( $user_id );
			} else {
				$source_ids = $this->get_guest_recently_viewed_product_ids();
			}
		} else {
			$source_ids = $this->get_viewed_product_ids( $user_id, $config );
		}

		if ( empty( $source_ids ) ) {
			if ( 'in' === $relation ) {
				$enforce_empty = true;
			}
			return;
		}

		$product_ids = $this->get_products_with_shared_terms( $source_ids, $config['taxonomy'] );

		if ( 'not_in' === $relation ) {
			if ( null === $include_ids ) {
				return;
			}
			$include_ids = array_diff( $include_ids, $product_ids );
		} else {
			$include_ids = ( null === $include_ids ) ? $product_ids : array_intersect( $include_ids, $product_ids );
		}

		if ( empty( $include_ids ) && 'in' === $relation ) {
			$enforce_empty = true;
		}
	}

	/**
	 * Get viewed product IDs for a user using average-threshold logic.
	 *
	 * For 'recently_viewed' order, all viewed products qualify (recency signal).
	 * For count-based modes, only products at or above average view count qualify.
	 *
	 * @param int   $user_id Current user ID (0 = guest).
	 * @param array $config  Mode config entry.
	 * @return array Product IDs.
	 */
	private function get_viewed_product_ids( $user_id, $config ) {
		$min_count = isset( $config['min_count'] ) ? (int) $config['min_count'] : 1;

		if ( 0 === $user_id ) {
			return $this->get_guest_viewed_product_ids( $config, $min_count );
		}

		global $wpdb;
		$table = esc_sql( UserEventsTable::get_table_name() );

		// Use two explicit queries so no column name is ever interpolated from a variable.
		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		if ( 'updated_at' === $config['order'] ) {
			$results = $wpdb->get_results(
				$wpdb->prepare(
					"SELECT product_id, count
					FROM {$table}
					WHERE user_id = %d AND event_type = %d AND count >= %d
					ORDER BY updated_at DESC",
					$user_id,
					UserEventsTable::EVENT_VIEW,
					$min_count
				),
				ARRAY_A
			);
		} else {
			$results = $wpdb->get_results(
				$wpdb->prepare(
					"SELECT product_id, count
					FROM {$table}
					WHERE user_id = %d AND event_type = %d AND count >= %d
					ORDER BY count DESC",
					$user_id,
					UserEventsTable::EVENT_VIEW,
					$min_count
				),
				ARRAY_A
			);
		}
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared

		if ( empty( $results ) ) {
			return array();
		}

		// Recently-viewed: return all products regardless of count distribution.
		if ( 'updated_at' === $config['order'] ) {
			return array_map( 'intval', array_column( $results, 'product_id' ) );
		}

		// Count-based: average threshold — only above-average products drive recommendations.
		$counts  = array_column( $results, 'count' );
		$average = (int) floor( array_sum( $counts ) / count( $counts ) );

		return array_map(
			'intval',
			array_column(
				array_filter(
					$results,
					function ( $row ) use ( $average ) {
						return (int) $row['count'] >= $average;
					}
				),
				'product_id'
			)
		);
	}

	/**
	 * Get viewed product IDs for a guest user from their cookie transient.
	 *
	 * @param array $config    Mode config entry.
	 * @param int   $min_count Minimum count gate.
	 * @return array Product IDs.
	 */
	private function get_guest_viewed_product_ids( $config, $min_count ) {
		$cookie_name = 'preco_guest_id';

		if ( empty( $_COOKIE[ $cookie_name ] ) ) {
			return array();
		}

		$guest_id = sanitize_text_field( wp_unslash( $_COOKIE[ $cookie_name ] ) );
		$events   = get_transient( 'preco_guest_' . $guest_id );

		if ( empty( $events ) || ! is_array( $events ) ) {
			return array();
		}

		$view_events = array_values(
			array_filter(
				$events,
				function ( $e ) use ( $min_count ) {
					return (int) $e['event_type'] === UserEventsTable::EVENT_VIEW
						&& (int) $e['count'] >= $min_count;
				}
			)
		);

		if ( empty( $view_events ) ) {
			return array();
		}

		// Recently-viewed: no threshold needed.
		if ( 'updated_at' === $config['order'] ) {
			return array_map(
				function ( $e ) {
					return (int) $e['product_id'];
				},
				$view_events
			);
		}

		// Count-based average threshold.
		$counts  = array_column( $view_events, 'count' );
		$average = (int) floor( array_sum( $counts ) / count( $counts ) );

		return array_map(
			'intval',
			array_column(
				array_values(
					array_filter(
						$view_events,
						function ( $e ) use ( $average ) {
							return (int) $e['count'] >= $average;
						}
					)
				),
				'product_id'
			)
		);
	}

	/**
	 * Get product IDs viewed today by a logged-in user.
	 *
	 * Queries preco_user_events for EVENT_VIEW rows with updated_at >= today.
	 *
	 * @param int $user_id Logged-in user ID.
	 * @return array Product IDs ordered by most-recently-viewed first.
	 */
	private function get_today_viewed_product_ids( $user_id ) {
		global $wpdb;

		$table = esc_sql( UserEventsTable::get_table_name() );
		$today = gmdate( 'Y-m-d' ) . ' 00:00:00';

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$results = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT product_id
				FROM {$table}
				WHERE user_id = %d AND event_type = %d AND updated_at >= %s
				ORDER BY updated_at DESC",
				$user_id,
				UserEventsTable::EVENT_VIEW,
				$today
			)
		);
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared

		return $results ? array_map( 'intval', $results ) : array();
	}

	/**
	 * Get product IDs viewed by a guest user from their preco_guest_* transient.
	 *
	 * Guests receive a preco_guest_id cookie via maybe_set_guest_cookie().
	 * All view events are returned (no count threshold — recency-only signal).
	 *
	 * @return array Product IDs.
	 */
	private function get_guest_recently_viewed_product_ids() {
		if ( empty( $_COOKIE['preco_guest_id'] ) ) {
			return array();
		}

		$guest_id = sanitize_text_field( wp_unslash( $_COOKIE['preco_guest_id'] ) );
		$events   = get_transient( 'preco_guest_' . $guest_id );

		if ( empty( $events ) || ! is_array( $events ) ) {
			return array();
		}

		$view_events = array_values(
			array_filter(
				$events,
				function ( $e ) {
					return (int) $e['event_type'] === UserEventsTable::EVENT_VIEW;
				}
			)
		);

		return array_map(
			function ( $e ) {
				return (int) $e['product_id'];
			},
			$view_events
		);
	}

	/**
	 * Get purchased product IDs for a logged-in user via wc_order_product_lookup.
	 *
	 * @param int $user_id Logged-in user ID.
	 * @return array Product IDs.
	 */
	private function get_purchased_product_ids( $user_id ) {
		global $wpdb;

		// Guard: wc_order_stats must exist before querying.
		$stats_table  = $wpdb->prefix . 'wc_order_stats';
		$stats_exists = (bool) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $stats_table ) )
		);
		if ( ! $stats_exists ) {
			return array();
		}

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
		$results = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT DISTINCT opl.product_id
				FROM {$wpdb->prefix}wc_order_product_lookup AS opl
				INNER JOIN {$stats_table} AS os
					ON opl.order_id = os.order_id
					AND os.status IN ('wc-completed','wc-processing')
					AND os.customer_id = %d",
				$user_id
			)
		);
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter

		return $results ? array_map( 'intval', $results ) : array();
	}

	/**
	 * Find all products sharing taxonomy terms with the given source products.
	 *
	 * Single SQL JOIN — no PHP intermediary.
	 * Source products themselves are excluded from the result.
	 *
	 * @param int[]  $source_ids Product IDs representing the user's interest signal.
	 * @param string $taxonomy   Taxonomy slug, or 'attribute' for all pa_* taxonomies.
	 * @return array Recommended product IDs.
	 */
	private function get_products_with_shared_terms( $source_ids, $taxonomy ) {
		global $wpdb;

		if ( empty( $source_ids ) ) {
			return array();
		}

		$placeholders = implode( ',', array_fill( 0, count( $source_ids ), '%d' ) );

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
		if ( 'attribute' === $taxonomy ) {
			// Match all pa_* attribute taxonomies using a parameterized LIKE pattern.
			$attribute_like = $wpdb->esc_like( 'pa_' ) . '%';
			$sql            = $wpdb->prepare(
				"SELECT DISTINCT tr2.object_id
				FROM {$wpdb->term_relationships} AS tr1
				INNER JOIN {$wpdb->term_taxonomy} AS tt
					ON tt.term_taxonomy_id = tr1.term_taxonomy_id
					AND tt.taxonomy LIKE %s
				INNER JOIN {$wpdb->term_relationships} AS tr2
					ON tr2.term_taxonomy_id = tt.term_taxonomy_id
				WHERE tr1.object_id IN ({$placeholders})
				AND tr2.object_id NOT IN ({$placeholders})",
				array_merge( array( $attribute_like ), $source_ids, $source_ids )
			);
		} else {
			$sql = $wpdb->prepare(
				"SELECT DISTINCT tr2.object_id
				FROM {$wpdb->term_relationships} AS tr1
				INNER JOIN {$wpdb->term_taxonomy} AS tt
					ON tt.term_taxonomy_id = tr1.term_taxonomy_id
					AND tt.taxonomy = %s
				INNER JOIN {$wpdb->term_relationships} AS tr2
					ON tr2.term_taxonomy_id = tt.term_taxonomy_id
				WHERE tr1.object_id IN ({$placeholders})
				AND tr2.object_id NOT IN ({$placeholders})",
				array_merge( array( $taxonomy ), $source_ids, $source_ids )
			);
		}
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared
		$results = $wpdb->get_col( $sql );

		return $results ? array_map( 'intval', $results ) : array();
	}
}
