<?php
/**
 * Assigns / unassigns products to smart categories while tracking which
 * relationships the plugin manages, so manual assignments are never touched.
 *
 * @package SmartCategories
 */

defined( 'ABSPATH' ) || exit;

/**
 * Class SCWC_Assigner
 */
class SCWC_Assigner {

	/**
	 * Resolve the taxonomy a smart term belongs to (category or tag).
	 *
	 * @param int $term_id Term ID.
	 * @return string
	 */
	private static function taxonomy_of( $term_id ) {
		$term = get_term( (int) $term_id );
		return ( $term && ! is_wp_error( $term ) ) ? $term->taxonomy : SCWC_Term_Rules::TAXONOMY;
	}

	/**
	 * Assign a product to a smart term (idempotent, ownership-aware).
	 *
	 * @param int $term_id    Term ID (category or tag).
	 * @param int $product_id Product ID.
	 * @return bool True when the relationship changed.
	 */
	public static function assign( $term_id, $product_id ) {
		$term_id    = (int) $term_id;
		$product_id = (int) $product_id;
		$taxonomy   = self::taxonomy_of( $term_id );

		if ( self::is_managed( $term_id, $product_id ) ) {
			// Already ours — make sure the term relationship still exists.
			if ( ! has_term( $term_id, $taxonomy, $product_id ) ) {
				wp_set_object_terms( $product_id, array( $term_id ), $taxonomy, true );
				return true;
			}
			return false;
		}

		// If the product already sits in this term, it was assigned manually.
		// Respect that: do not take ownership, do not record it as managed.
		if ( has_term( $term_id, $taxonomy, $product_id ) ) {
			return false;
		}

		wp_set_object_terms( $product_id, array( $term_id ), $taxonomy, true );
		self::record( $term_id, $product_id );

		/**
		 * Fires after a product is auto-assigned to a smart term.
		 *
		 * @param int $term_id    Term ID.
		 * @param int $product_id Product ID.
		 */
		do_action( 'scwc_product_assigned', $term_id, $product_id );
		return true;
	}

	/**
	 * Remove a product from a smart term — only if the plugin assigned it.
	 *
	 * @param int $term_id    Term ID (category or tag).
	 * @param int $product_id Product ID.
	 * @return bool True when the relationship changed.
	 */
	public static function unassign( $term_id, $product_id ) {
		$term_id    = (int) $term_id;
		$product_id = (int) $product_id;

		if ( ! self::is_managed( $term_id, $product_id ) ) {
			return false; // Manual or absent — leave it alone.
		}

		wp_remove_object_terms( $product_id, array( $term_id ), self::taxonomy_of( $term_id ) );
		self::forget( $term_id, $product_id );

		/**
		 * Fires after a product is auto-removed from a smart term.
		 *
		 * @param int $term_id    Term ID.
		 * @param int $product_id Product ID.
		 */
		do_action( 'scwc_product_unassigned', $term_id, $product_id );
		return true;
	}

	/**
	 * Re-evaluate a single product against every enabled smart term.
	 *
	 * Runs up to three stabilisation passes: rules may reference the membership
	 * produced by other rules (the hierarchy generator's children match on the
	 * parent category), so one pass is not always enough for a cascade.
	 *
	 * @param int $product_id Product ID.
	 * @return void
	 */
	public static function sync_product( $product_id ) {
		if ( apply_filters( 'scwc_skip_sync', false, $product_id ) ) {
			return;
		}
		$product = wc_get_product( $product_id );
		if ( ! $product ) {
			return;
		}

		$term_ids = SCWC_Term_Rules::get_smart_term_ids();

		for ( $pass = 0; $pass < 3; $pass++ ) {
			$changed = false;
			foreach ( $term_ids as $term_id ) {
				$tree = SCWC_Term_Rules::get_rules( $term_id );
				if ( SCWC_Rule_Engine::matches( $product, $tree ) ) {
					$changed = self::assign( $term_id, $product_id ) || $changed;
				} else {
					$changed = self::unassign( $term_id, $product_id ) || $changed;
				}
			}
			if ( ! $changed ) {
				break;
			}
		}
	}

	/**
	 * Remove all managed-assignment records for a deleted product.
	 *
	 * @param int $product_id Product ID.
	 * @return void
	 */
	public static function cleanup_product( $product_id ) {
		global $wpdb;
		$wpdb->delete( SCWC_Install::table_name(), array( 'product_id' => (int) $product_id ), array( '%d' ) ); // phpcs:ignore WordPress.DB
	}

	/**
	 * Remove all managed assignments for a term (e.g. when its rules are deleted),
	 * detaching the products the plugin had auto-added.
	 *
	 * @param int $term_id Term ID.
	 * @return void
	 */
	public static function cleanup_term( $term_id ) {
		global $wpdb;
		$term_id = (int) $term_id;
		$table   = SCWC_Install::table_name();

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- the plugin's own tracking table.
		$product_ids = $wpdb->get_col( $wpdb->prepare( 'SELECT product_id FROM %i WHERE term_id = %d', $table, $term_id ) );
		$taxonomy    = self::taxonomy_of( $term_id );
		foreach ( $product_ids as $pid ) {
			wp_remove_object_terms( (int) $pid, array( $term_id ), $taxonomy );
		}
		$wpdb->delete( $table, array( 'term_id' => $term_id ), array( '%d' ) ); // phpcs:ignore WordPress.DB
	}

	/**
	 * Count of products currently managed for a term.
	 *
	 * @param int $term_id Term ID.
	 * @return int
	 */
	public static function managed_count( $term_id ) {
		global $wpdb;
		$table = SCWC_Install::table_name();
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- count over the plugin's own tracking table.
		return (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE term_id = %d', $table, (int) $term_id ) );
	}

	/* ---------------------------------------------------------------------
	 * Tracking table helpers.
	 * ------------------------------------------------------------------- */

	/**
	 * Whether the plugin manages this term/product relationship.
	 *
	 * @param int $term_id    Term ID.
	 * @param int $product_id Product ID.
	 * @return bool
	 */
	public static function is_managed( $term_id, $product_id ) {
		global $wpdb;
		$table = SCWC_Install::table_name();
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- the plugin's own tracking table; no core API exists and per-row ownership checks must be authoritative (uncached).
		return (bool) $wpdb->get_var(
			$wpdb->prepare( 'SELECT id FROM %i WHERE term_id = %d AND product_id = %d', $table, (int) $term_id, (int) $product_id )
		);
	}

	/**
	 * Record a managed relationship.
	 *
	 * @param int $term_id    Term ID.
	 * @param int $product_id Product ID.
	 * @return void
	 */
	private static function record( $term_id, $product_id ) {
		global $wpdb;
		$table = SCWC_Install::table_name();
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- write to the plugin's own tracking table (idempotent INSERT IGNORE).
		$wpdb->query(
			$wpdb->prepare( 'INSERT IGNORE INTO %i (term_id, product_id) VALUES (%d, %d)', $table, (int) $term_id, (int) $product_id )
		);
	}

	/**
	 * Forget a managed relationship.
	 *
	 * @param int $term_id    Term ID.
	 * @param int $product_id Product ID.
	 * @return void
	 */
	private static function forget( $term_id, $product_id ) {
		global $wpdb;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- delete from the plugin's own tracking table.
		$wpdb->delete(
			SCWC_Install::table_name(),
			array(
				'term_id'    => (int) $term_id,
				'product_id' => (int) $product_id,
			),
			array( '%d', '%d' )
		);
	}
}
