<?php
/**
 * FoodOrd Product Modal Upsells
 *
 * @package FoodOrd
 * @since 1.3.36
 */

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

/**
 * Builds the upsell product collection shown in the FoodOrd product modal.
 */
class Foodord_Product_Modal_Upsells {

	/**
	 * Default maximum number of products to render.
	 */
	private const DEFAULT_LIMIT = 4;

	/**
	 * Get modal upsell products for a WooCommerce product.
	 *
	 * @param WC_Product $product Base product.
	 * @param int[]      $excluded_product_ids Product IDs to exclude.
	 * @param int        $limit_override Optional limit override.
	 * @return WC_Product[]
	 */
	public function get_products_for_modal( $product, array $excluded_product_ids = array(), int $limit_override = 0 ) {
		if ( ! $product instanceof WC_Product ) {
			return array();
		}

		$limit                = $this->resolve_modal_limit( $product, $limit_override );
		$excluded_product_ids = $this->normalize_product_ids( $excluded_product_ids );

		$upsell_ids = $product->get_upsell_ids();
		$products   = $this->build_products_from_ids( is_array( $upsell_ids ) ? $upsell_ids : array(), $product, $limit, $excluded_product_ids );

		if ( empty( $products ) ) {
			$products = $this->build_fallback_products( $product, $limit, $excluded_product_ids );
		}

		return apply_filters( 'foodord_modal_upsells_products', $products, $product );
	}

	/**
	 * Get upsell products for the current cart contents.
	 *
	 * @param array $cart_contents WooCommerce cart contents.
	 * @return WC_Product[]
	 */
	public function get_products_for_cart( array $cart_contents ): array {
		if ( empty( $cart_contents ) ) {
			return array();
		}

		$limit            = $this->resolve_card_limit( $cart_contents );
		$base_products    = array();
		$cart_product_ids = array();

		foreach ( array_reverse( $cart_contents, true ) as $cart_item ) {
			if ( ! is_array( $cart_item ) ) {
				continue;
			}

			$product_id = isset( $cart_item['product_id'] ) ? absint( $cart_item['product_id'] ) : 0;
			if ( $product_id < 1 ) {
				continue;
			}

			$base_product = wc_get_product( $product_id );
			if ( ! $base_product instanceof WC_Product ) {
				continue;
			}

			$cart_product_ids[ $product_id ] = $product_id;
			$base_products[ $product_id ]    = $base_product;
		}

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

		if ( $this->cart_has_multiple_categories( $base_products ) ) {
			return apply_filters(
				'foodord_card_upsells_products',
				$this->build_randomized_cart_products( $base_products, array_values( $cart_product_ids ), $limit ),
				$base_products,
				$cart_contents
			);
		}

		$products             = array();
		$selected_product_ids = array();

		foreach ( $base_products as $base_product_id => $base_product ) {
			$remaining = $limit - count( $products );
			if ( $remaining < 1 ) {
				break;
			}

			$excluded_product_ids = array_merge(
				array_values( $cart_product_ids ),
				array_values( $selected_product_ids ),
				array( (int) $base_product_id )
			);

			$candidates = $this->get_products_for_modal( $base_product, $excluded_product_ids, $remaining );

			foreach ( $candidates as $candidate ) {
				if ( ! $candidate instanceof WC_Product ) {
					continue;
				}

				$candidate_id = $candidate->get_id();

				if ( isset( $selected_product_ids[ $candidate_id ] ) ) {
					continue;
				}

				$selected_product_ids[ $candidate_id ] = $candidate_id;
				$products[]                            = $candidate;

				if ( count( $products ) >= $limit ) {
					break 2;
				}
			}
		}

		return apply_filters( 'foodord_card_upsells_products', $products, $base_products, $cart_contents );
	}

	/**
	 * Build cart upsells by rotating across randomized base products.
	 *
	 * @param WC_Product[] $base_products Base cart products.
	 * @param int[]        $cart_product_ids Product IDs already in cart.
	 * @param int          $limit Max products.
	 * @return WC_Product[]
	 */
	private function build_randomized_cart_products( array $base_products, array $cart_product_ids, int $limit ): array {
		$base_products    = $this->randomize_base_products( $base_products );
		$cart_product_ids = $this->normalize_product_ids( $cart_product_ids );
		$candidate_pools  = array();

		foreach ( $base_products as $base_product_id => $base_product ) {
			if ( ! $base_product instanceof WC_Product ) {
				continue;
			}

			$excluded_product_ids = array_merge(
				$cart_product_ids,
				array( (int) $base_product_id )
			);

			$candidate_pools[ $base_product_id ] = $this->get_products_for_modal( $base_product, $excluded_product_ids, $limit );
		}

		$products             = array();
		$selected_product_ids = array();
		$added_in_pass        = true;

		while ( count( $products ) < $limit && $added_in_pass ) {
			$added_in_pass = false;

			foreach ( $base_products as $base_product_id => $base_product ) {
				if ( empty( $candidate_pools[ $base_product_id ] ) || ! is_array( $candidate_pools[ $base_product_id ] ) ) {
					continue;
				}

				while ( ! empty( $candidate_pools[ $base_product_id ] ) ) {
					$candidate = array_shift( $candidate_pools[ $base_product_id ] );

					if ( ! $candidate instanceof WC_Product ) {
						continue;
					}

					$candidate_id = $candidate->get_id();

					if ( isset( $selected_product_ids[ $candidate_id ] ) ) {
						continue;
					}

					$selected_product_ids[ $candidate_id ] = $candidate_id;
					$products[]                            = $candidate;
					$added_in_pass                         = true;
					break;
				}

				if ( count( $products ) >= $limit ) {
					break;
				}
			}
		}

		return $products;
	}

	/**
	 * Determine whether cart products span multiple categories.
	 *
	 * @param WC_Product[] $base_products Base cart products.
	 * @return bool
	 */
	private function cart_has_multiple_categories( array $base_products ): bool {
		$category_ids = array();

		foreach ( $base_products as $base_product ) {
			if ( ! $base_product instanceof WC_Product ) {
				continue;
			}

			foreach ( (array) $base_product->get_category_ids() as $category_id ) {
				$category_id = absint( $category_id );

				if ( $category_id > 0 ) {
					$category_ids[ $category_id ] = $category_id;
				}
			}

			if ( count( $category_ids ) > 1 ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Randomize base product order while preserving product IDs as keys.
	 *
	 * @param WC_Product[] $base_products Base cart products.
	 * @return WC_Product[]
	 */
	private function randomize_base_products( array $base_products ): array {
		$product_ids = array_keys( $base_products );

		if ( count( $product_ids ) < 2 ) {
			return $base_products;
		}

		shuffle( $product_ids );

		$randomized_products = array();

		foreach ( $product_ids as $product_id ) {
			if ( isset( $base_products[ $product_id ] ) ) {
				$randomized_products[ $product_id ] = $base_products[ $product_id ];
			}
		}

		return $randomized_products;
	}

	/**
	 * Determine whether an upsell product can be added directly without opening modal.
	 *
	 * @param WC_Product $product Product instance.
	 * @return bool
	 */
	public function supports_direct_add( $product ): bool {
		if ( ! $product instanceof WC_Product ) {
			return false;
		}

		if ( ! $product->is_type( 'simple' ) ) {
			return false;
		}

		return ! $this->product_has_toppings( $product->get_id() );
	}

	/**
	 * Build upsell product objects from a list of WooCommerce product IDs.
	 *
	 * @param int[]      $product_ids Candidate product IDs.
	 * @param WC_Product $current_product Current product.
	 * @param int        $limit Max products.
	 * @param int[]      $excluded_product_ids Product IDs to exclude.
	 * @return WC_Product[]
	 */
	private function build_products_from_ids( array $product_ids, WC_Product $current_product, int $limit, array $excluded_product_ids ): array {
		$products = array();

		foreach ( $product_ids as $product_id ) {
			$product_id = absint( $product_id );

			if ( $product_id < 1 || $product_id === $current_product->get_id() || in_array( $product_id, $excluded_product_ids, true ) ) {
				continue;
			}

			$product = wc_get_product( $product_id );

			$ineligible_reason = $this->get_ineligible_reason( $product );

			if ( null !== $ineligible_reason ) {
				continue;
			}

			$products[] = $product;

			if ( count( $products ) >= $limit ) {
				break;
			}
		}

		return $products;
	}

	/**
	 * Build fallback upsell products from shared categories.
	 *
	 * @param WC_Product $product Base product.
	 * @param int        $limit Max products.
	 * @param int[]      $excluded_product_ids Product IDs to exclude.
	 * @return WC_Product[]
	 */
		private function build_fallback_products( WC_Product $product, int $limit, array $excluded_product_ids ): array {
			$category_ids = array_map( 'absint', (array) $product->get_category_ids() );
			$category_ids = array_filter( $category_ids );

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

			$excluded_lookup = array_values(
				array_unique(
					array_merge(
						array( $product->get_id() ),
						$this->normalize_product_ids( $excluded_product_ids )
					)
				)
			);

			$query_args = array(
				'status'              => 'publish',
				'limit'               => $limit * 3,
				'orderby'             => 'rand',
				'return'              => 'objects',
				'product_category_id' => $category_ids,
		);

		$fallback_products = wc_get_products( $query_args );

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

			$products = array();

			foreach ( $fallback_products as $fallback_product ) {
				$fallback_product_id = $fallback_product instanceof WC_Product ? absint( $fallback_product->get_id() ) : 0;

				if ( $fallback_product_id < 1 || in_array( $fallback_product_id, $excluded_lookup, true ) ) {
					continue;
				}

				$ineligible_reason = $this->get_ineligible_reason( $fallback_product );

				if ( null !== $ineligible_reason ) {
				continue;
			}

			$products[] = $fallback_product;

			if ( count( $products ) >= $limit ) {
				break;
			}
		}

		return $products;
	}

	/**
	 * Resolve modal upsell limit from filter or override.
	 *
	 * @param mixed $context Context passed to filter.
	 * @param int   $limit_override Optional limit override.
	 * @return int
	 */
	private function resolve_modal_limit( $context, int $limit_override ): int {
		$limit = $limit_override > 0 ? $limit_override : self::DEFAULT_LIMIT;
		$limit = (int) apply_filters( 'foodord_modal_upsells_limit', $limit, $context );
		return $limit > 0 ? $limit : self::DEFAULT_LIMIT;
	}

	/**
	 * Resolve cart upsell limit from filter.
	 *
	 * @param mixed $context Context passed to filter.
	 * @return int
	 */
	private function resolve_card_limit( $context ): int {
		$limit = (int) apply_filters( 'foodord_card_upsells_limit', self::DEFAULT_LIMIT, $context );
		return $limit > 0 ? $limit : self::DEFAULT_LIMIT;
	}

	/**
	 * Normalize a list of product IDs.
	 *
	 * @param int[] $product_ids Product IDs.
	 * @return int[]
	 */
	private function normalize_product_ids( array $product_ids ): array {
		$product_ids = array_map( 'absint', $product_ids );
		$product_ids = array_filter( $product_ids );

		return array_values( array_unique( $product_ids ) );
	}

	/**
	 * Check if an upsell product should be rendered in modal.
	 *
	 * @param WC_Product|mixed $product Upsell product candidate.
	 * @return bool
	 */
	private function get_ineligible_reason( $product ) {
		if ( ! $product instanceof WC_Product ) {
			return 'invalid_product';
		}

		if ( 'publish' !== $product->get_status() ) {
			return 'status_' . $product->get_status();
		}

		if ( ! $product->is_visible() ) {
			return 'not_visible';
		}

		if ( ! $product->is_in_stock() ) {
			return 'out_of_stock';
		}

		if ( ! $product->is_purchasable() ) {
			return 'not_purchasable';
		}

		if ( class_exists( 'Foodord_Product_Availability' ) && ! Foodord_Product_Availability::is_available( $product->get_id() ) ) {
			return 'foodord_unavailable';
		}

		return null;
	}

	/**
	 * Determine whether a product has topping configuration.
	 *
	 * @param int $product_id Product ID.
	 * @return bool
	 */
	private function product_has_toppings( int $product_id ): bool {
		$product_id = absint( $product_id );

		if ( $product_id <= 0 ) {
			return false;
		}

		$selected_sets = get_post_meta( $product_id, '_foodord_topping_sets', true );
		$selected_sets = is_array( $selected_sets ) ? array_values( array_unique( array_map( 'sanitize_text_field', $selected_sets ) ) ) : array();

		$excluded_sets = get_post_meta( $product_id, '_foodord_topping_sets_excluded', true );
		$excluded_sets = is_array( $excluded_sets ) ? array_values( array_unique( array_map( 'sanitize_text_field', $excluded_sets ) ) ) : array();

		$available_sets = get_option( 'foodord_topping_sets', array() );
		if ( empty( $available_sets ) || ! is_array( $available_sets ) ) {
			return false;
		}

		$product_term_ids = wp_get_post_terms( $product_id, 'product_cat', array( 'fields' => 'ids' ) );
		$product_term_ids = is_array( $product_term_ids ) ? array_map( 'absint', $product_term_ids ) : array();
		$mapped_set_ids   = array();

		if ( ! empty( $product_term_ids ) ) {
			foreach ( $available_sets as $set ) {
				if ( ! is_array( $set ) ) {
					continue;
				}

				$set_id     = isset( $set['id'] ) ? sanitize_text_field( (string) $set['id'] ) : '';
				$categories = isset( $set['categories'] ) && is_array( $set['categories'] ) ? array_map( 'absint', $set['categories'] ) : array();

				if ( '' === $set_id || empty( $categories ) ) {
					continue;
				}

				if ( array_intersect( $categories, $product_term_ids ) ) {
					$mapped_set_ids[] = $set_id;
				}
			}
		}

		$mapped_set_ids = array_values( array_unique( $mapped_set_ids ) );

		if ( ! empty( $excluded_sets ) && ! empty( $mapped_set_ids ) ) {
			$mapped_set_ids = array_values( array_diff( $mapped_set_ids, $excluded_sets ) );
		}

		$selected_sets = empty( $selected_sets ) ? $mapped_set_ids : array_values( array_unique( array_merge( $selected_sets, $mapped_set_ids ) ) );

		if ( empty( $selected_sets ) ) {
			return false;
		}

		foreach ( $available_sets as $set ) {
			if ( ! is_array( $set ) ) {
				continue;
			}

			$set_id = isset( $set['id'] ) ? sanitize_text_field( (string) $set['id'] ) : '';
			if ( '' === $set_id || ! in_array( $set_id, $selected_sets, true ) ) {
				continue;
			}

			$items = isset( $set['items'] ) && is_array( $set['items'] ) ? $set['items'] : array();
			foreach ( $items as $item ) {
				if ( ! is_array( $item ) ) {
					continue;
				}

				$label = isset( $item['label'] ) ? sanitize_text_field( (string) $item['label'] ) : '';
				if ( '' !== $label ) {
					return true;
				}
			}
		}

		return false;
	}
}
