<?php
declare( strict_types=1 );

namespace FiboFilters\WooCommerce;

use WC_Log_Handler_File;
use WC_Log_Levels;
use WC_Queue_Interface;
use WooCommerce;

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

/**
 * WooCommerce helper
 */
class Helper {
	public function WC(): WooCommerce { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid
		return WC();
	}

	public function get_wc_log_handler_file(): WC_Log_Handler_File {
		return new WC_Log_Handler_File();
	}

	/**
	 * @return WC_Queue_Interface|null
	 */
	public function get_queue() {
		$queue = null;
		$wc    = $this->WC();

		if ( method_exists( $wc, 'queue' ) ) {
			$wc_queue = $wc->queue();
			if ( is_object( $wc_queue ) && method_exists( $wc_queue, 'schedule_recurring' ) ) {
				$queue = $wc_queue;
			}
		}

		return $queue;
	}

	public function is_valid_log_level( string $level ): bool {
		return WC_Log_Levels::is_valid_level( $level );
	}

	/**
	 * Get taxonomies of products attributes
	 *
	 * @return array
	 */
	public function get_attribute_taxonomies(): array {
		return apply_filters( 'fibofilters/woocommerce/attribute_taxonomies', wc_get_attribute_taxonomies() );
	}

	/**
	 * Get taxonomies of products attributes
	 *
	 * @return string[]
	 */
	public function get_attribute_taxonomy_names(): array {
		$taxonomies = [];

		foreach ( $this->get_attribute_taxonomies() as $taxonomy ) {
			$taxonomies[] = 'pa_' . $taxonomy->attribute_name;
		}

		return apply_filters( 'fibofilters/woocommerce/attribute_taxonomy_names', $taxonomies );
	}

	public function get_total_products_count(): int {
		$products = wp_count_posts( 'product' );

		return isset( $products->publish ) ? (int) $products->publish : 0;
	}

	public function get_total_product_variations_count(): int {
		$products = wp_count_posts( 'product_variation' );

		return isset( $products->publish ) ? (int) $products->publish : 0;
	}

	public function get_product_count_with_taxonomy( string $taxonomy ): int {
		$args  = [
			'fields'         => 'ids',
			'posts_per_page' => - 1,
			'post_type'      => 'product',
			'tax_query'      => [
				[
					'taxonomy' => $taxonomy,
					'operator' => 'EXISTS',
				],
			],
		];
		$query = new \WP_Query( $args );

		return $query->post_count;
	}

	public function get_product_count_with_custom_field( string $custom_field ): int {
		$args  = [
			'fields'         => 'ids',
			'posts_per_page' => - 1,
			'post_type'      => 'product',
			'meta_query'     => [
				[
					'taxonomy' => $custom_field,
					'operator' => 'EXISTS',
				],
			],
		];
		$query = new \WP_Query( $args );

		return $query->post_count;
	}

	public function get_product_count_on_sale(): int {
		/** @var \WC_Product_Data_Store_CPT $data_store */
		$data_store       = \WC_Data_Store::load( 'product' );
		$on_sale_products = $data_store->get_on_sale_products();
		$ids              = array_map(
			function ( $item ): int {
				return $item->parent_id > 0 ? (int) $item->parent_id : (int) $item->id;
			},
			$on_sale_products
		);

		return count( array_values( array_unique( $ids ) ) );
	}

	public function get_product_count_featured(): int {
		/** @var \WC_Product_Data_Store_CPT $data_store */
		$data_store           = \WC_Data_Store::load( 'product' );
		$featured_product_ids = $data_store->get_featured_product_ids();

		return count( $featured_product_ids );
	}

	/**
	 * Get product count - in stock
	 *
	 * @see \WC_Product_Data_Store_CPT::get_on_sale_products
	 */
	public function get_product_count_instock(): int {
		global $wpdb;

		$exclude_term_ids            = [];
		$outofstock_join             = '';
		$outofstock_where            = '';
		$product_visibility_term_ids = wc_get_product_visibility_term_ids();

		if ( get_option( 'woocommerce_hide_out_of_stock_items' ) === 'yes' && $product_visibility_term_ids['outofstock'] ) {
			$exclude_term_ids[] = $product_visibility_term_ids['outofstock'];
		}

		if ( count( $exclude_term_ids ) ) {
			$outofstock_join  = " LEFT JOIN ( SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id IN ( " . implode( ',', array_map( 'absint', $exclude_term_ids ) ) . ' ) ) AS exclude_join ON exclude_join.object_id = id';
			$outofstock_where = ' AND exclude_join.object_id IS NULL';
		}

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$results = $wpdb->get_results(
			"
			SELECT posts.ID as id, posts.post_parent as parent_id
			FROM {$wpdb->posts} AS posts
			INNER JOIN {$wpdb->wc_product_meta_lookup} AS lookup ON posts.ID = lookup.product_id
			$outofstock_join
			WHERE posts.post_type IN ( 'product', 'product_variation' )
			AND posts.post_status = 'publish'
			AND lookup.stock_status = 'instock'
			$outofstock_where
			AND posts.post_parent NOT IN (
				SELECT ID FROM `$wpdb->posts` as posts
				WHERE posts.post_type = 'product'
				AND posts.post_parent = 0
				AND posts.post_status != 'publish'
			)
			GROUP BY posts.ID
			"
		);
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared

		$ids = array_map(
			function ( $item ): int {
				return $item->parent_id > 0 ? (int) $item->parent_id : (int) $item->id;
			},
			$results
		);

		return count( array_values( array_unique( $ids ) ) );
	}

	/**
	 * Get product count - with price
	 *
	 * @see \WC_Product_Data_Store_CPT::get_on_sale_products
	 */
	public function get_product_count_with_price(): int {
		global $wpdb;

		$exclude_term_ids            = [];
		$outofstock_join             = '';
		$outofstock_where            = '';
		$product_visibility_term_ids = wc_get_product_visibility_term_ids();

		if ( get_option( 'woocommerce_hide_out_of_stock_items' ) === 'yes' && $product_visibility_term_ids['outofstock'] ) {
			$exclude_term_ids[] = $product_visibility_term_ids['outofstock'];
		}

		if ( count( $exclude_term_ids ) ) {
			$outofstock_join  = " LEFT JOIN ( SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id IN ( " . implode( ',', array_map( 'absint', $exclude_term_ids ) ) . ' ) ) AS exclude_join ON exclude_join.object_id = id';
			$outofstock_where = ' AND exclude_join.object_id IS NULL';
		}

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$results = $wpdb->get_results(
			"
			SELECT posts.ID as id, posts.post_parent as parent_id
			FROM {$wpdb->posts} AS posts
			INNER JOIN {$wpdb->wc_product_meta_lookup} AS lookup ON posts.ID = lookup.product_id
			$outofstock_join
			WHERE posts.post_type IN ( 'product', 'product_variation' )
			AND posts.post_status = 'publish'
			AND lookup.min_price > 0
			$outofstock_where
			AND posts.post_parent NOT IN (
				SELECT ID FROM `$wpdb->posts` as posts
				WHERE posts.post_type = 'product'
				AND posts.post_parent = 0
				AND posts.post_status != 'publish'
			)
			GROUP BY posts.ID
			"
		);
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared

		$ids = array_map(
			function ( $item ): int {
				return $item->parent_id > 0 ? (int) $item->parent_id : (int) $item->id;
			},
			$results
		);

		return count( array_values( array_unique( $ids ) ) );
	}

	public function get_currency_settings(): array {
		$code = get_woocommerce_currency();

		return [
			'code'              => $code,
			'precision'         => wc_get_price_decimals(),
			'symbol'            => html_entity_decode( (string) get_woocommerce_currency_symbol( $code ) ),
			'symbolPosition'    => get_option( 'woocommerce_currency_pos' ),
			'decimalSeparator'  => wc_get_price_decimal_separator(),
			'thousandSeparator' => wc_get_price_thousand_separator(),
			'priceFormat'       => html_entity_decode( (string) get_woocommerce_price_format() ),
		];
	}

	/**
	 * Checks if a taxonomy is sortable
	 *
	 * @param string $taxonomy The taxonomy to check if it's sortable.
	 */
	public function is_taxonomy_sortable( string $taxonomy ): bool {
		// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
		return in_array( $taxonomy, apply_filters( 'woocommerce_sortable_taxonomies', [ 'product_cat' ] ), true );
	}

	/**
	 * Get the price display mode based on the tax settings
	 *
	 * The price display mode: 'excl' (exclusive of tax), 'incl' (inclusive of tax),
	 * or an empty string if taxes are not enabled or the method of displaying the price is consistent with how it was saved in the product.
	 *
	 * @return string The price display mode.
	 */
	public function get_price_display_mode(): string {
		if ( ! wc_tax_enabled() ) {
			return '';
		}

		if ( ! apply_filters( 'fibofilters/inconsistent_tax_settings_support', '__return_true' ) ) {
			return '';
		}

		$prices_include_tax = wc_prices_include_tax();
		$tax_display_shop   = get_option( 'woocommerce_tax_display_shop' );

		if ( $prices_include_tax && $tax_display_shop === 'excl' ) {
			return 'excl';
		}
		if ( ! $prices_include_tax && $tax_display_shop === 'incl' ) {
			return 'incl';
		}

		return '';
	}

	/**
	 * Output the start of a product loop.
	 *
	 * @return string
	 */
	public function woocommerce_product_loop_start(): string {

		do_action( 'fibofilters/product_loop_start/before_call' );

		$html = (string) woocommerce_product_loop_start( false );

		do_action( 'fibofilters/product_loop_start/after_call' );

		return $html;
	}

	/**
	 * Get all WooCommerce product types that are only wrappers for variations.
	 * In WooCommerce, it's "Variable Product" by default, but other plugins might add their own types,
	 * that behave similar to "Variable Product", but aren't "Variable Product".
	 *
	 * @return array WooCommerce Product types.
	 */
	public function get_variations_wrapper_product_types() {
		return apply_filters( 'fibofilters/woocommerce/variations_wrapper_product_types', [ 'variable' ] );
	}

	/**
	 * Get the default catalog orderby option
	 *
	 * @see woocommerce_catalog_ordering()
	 */
	public function get_default_catalog_orderby(): string {
		// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
		return wc_get_loop_prop( 'is_search' ) ? 'relevance' : apply_filters( 'woocommerce_default_catalog_orderby', get_option( 'woocommerce_default_catalog_orderby', '' ) );
	}

	/**
	 * Priorities assigned to different stock statuses,
	 * which, when the fibofilters/request/product_order/in_stock_first filter is enabled,
	 * will affect how products are grouped—for example,
	 * showing in-stock and backorder items first, and out-of-stock items at the end.
	 *
	 * @return int[]
	 */
	public function stock_status_order_priorities(): array {
		$statuses   = wc_get_product_stock_status_options();
		$priorities = [
			'instock'     => 5,
			'onbackorder' => 5,
			'outofstock'  => 1,
		];

		foreach ( $statuses as $key => $label ) {
			if ( ! isset( $priorities[ $key ] ) ) {
				$priorities[ $key ] = 5;
			}
		}

		return apply_filters( 'fibofilters/product_order/stock_status_priorities', $priorities );
	}

	/**
	 * Generates a SQL CASE expression used for ordering products by stock status.
	 *
	 * @param string $table_name Column name to compare in the CASE expression.
	 *
	 * @return string SQL CASE expression for later usage.
	 */
	public function create_sql_case_expression_for_stock_status( string $table_name ): string {
		global $wpdb;

		$case_sql = ' CASE';
		foreach ( $this->stock_status_order_priorities() as $stock_status => $priority ) {
			$case_sql .= $wpdb->prepare( " WHEN $table_name = %s THEN %d", $stock_status, $priority );
		}
		$case_sql .= ' ELSE 5 END ';

		return $case_sql;
	}

	/**
	 * Regenerate WC Product Attributes Lookup table synchronously (no AS) and block until done.
	 *
	 * @return bool True on success (finished), false if skipped or failed to start.
	 */
	public function regenerate_attributes_lookup_sync(): bool {
		if ( ! apply_filters( 'fibofilters/indexer/attributes_lookup_table/should_regenerate', false ) ) {
			return false;
		}

		if ( ! function_exists( '\wc_get_container' ) ) {
			return false;
		}

		if ( ! class_exists( \Automattic\WooCommerce\Internal\ProductAttributesLookup\DataRegenerator::class ) ) {
			return false;
		}

		try {
			$container          = \wc_get_container();
			$lookup_regenerator = $container->get( \Automattic\WooCommerce\Internal\ProductAttributesLookup\DataRegenerator::class );

			$last_product_id = $lookup_regenerator->initiate_regeneration( false );

			if ( ! $last_product_id ) {
				$lookup_regenerator->finalize_regeneration( true );

				return true;
			}

			do {
				$more = $lookup_regenerator->do_regeneration_step( null, true );
			} while ( $more );

			$lookup_regenerator->finalize_regeneration( true );

			return true;
		} catch ( \Throwable $e ) {
			return false;
		}
	}
}
