<?php
declare( strict_types=1 );

namespace FiboFilters\SeoRules;

use FiboFilters\WPDBException;

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

/**
 * Facade for SEO rules:
 *  - generates potential URLs for all rules
 *  - filters URLs to only those with at least N products (global SEO threshold
 *  - provides a simple global Crawl Budget overview
 */
class SeoRuleEngine {
	/**
	 * @var SeoRuleUrlGenerator
	 */
	private SeoRuleUrlGenerator $url_generator;
	/**
	 * @var SeoRuleProductEstimator
	 */
	private SeoRuleProductEstimator $product_estimator;

	public function __construct(
		SeoRuleUrlGenerator $url_generator,
		SeoRuleProductEstimator $product_estimator
	) {
		$this->url_generator     = $url_generator;
		$this->product_estimator = $product_estimator;
	}

	/**
	 * Returns all potential URLs based on SEO rules (without checking product counts).
	 *
	 * @param string $lang Language code.
	 */
	public function get_potential_urls( string $lang = '' ): array {
		return $this->url_generator->generate_urls_for_rules( $lang );
	}

	/**
	 * Returns a flat list of all unique *qualified* (indexable) URLs across all SEO rules.
	 *
	 * @param string $lang Language code.
	 *
	 * @return array List of unique indexable URLs.
	 * @throws WPDBException When a database error occurred.
	 */
	public function get_all_indexable_urls( string $lang = '' ): array {
		$rules_data = $this->get_qualified_urls( $lang );

		$all_urls = [];

		foreach ( $rules_data as $data ) {
			$urls = $data['urls'] ?? [];

			if ( empty( $urls ) || ! is_array( $urls ) ) {
				continue;
			}

			foreach ( $urls as $url ) {
				if ( ! is_string( $url ) || $url === '' ) {
					continue;
				}
				$all_urls[] = $url;
			}
		}

		$unique_urls = array_values( array_unique( $all_urls ) );

		sort( $unique_urls, SORT_STRING );

		return $unique_urls;
	}

	/**
	 * Returns only URLs that have >= N products.
	 *
	 * Flow:
	 *  - take potential URLs,
	 *  - collect all value_ids used in the rule,
	 *  - build a value index (1 SQL query per rule),
	 *  - for each URL check intersection in PHP,
	 *  - drop URLs that do not meet the threshold.
	 *
	 * @param string $lang Language code.
	 *
	 * @throws WPDBException When a database error occurred.
	 */
	public function get_qualified_urls( string $lang = '' ): array {

		$rules_data = $this->get_potential_urls( $lang );
		$result     = [];

		foreach ( $rules_data as $rule_id => $data ) {

			/** @var SeoRule $rule */
			$rule         = $data['rule'];
			$value_sets   = $data['value_sets'] ?? [];
			$urls         = $data['urls'] ?? [];
			$max_possible = (int) ( $data['max_possible'] ?? 0 );

			$all_value_ids = $this->collect_all_value_ids_for_rule( $value_sets );

			if ( empty( $all_value_ids ) || empty( $urls ) ) {
				$result[ $rule_id ] = $this->build_rule_result(
					$rule,
					$max_possible,
					[]
				);
				continue;
			}

			if ( $this->is_product_cat_any_rule( $rule ) ) {
				$qualified_urls = $this->get_qualified_urls_for_any_product_cat_rule(
					$all_value_ids,
					$urls
				);
			} else {
				$qualified_urls = $this->get_qualified_urls_for_standard_rule(
					$rule,
					$all_value_ids,
					$urls
				);
			}

			$result[ $rule_id ] = $this->build_rule_result(
				$rule,
				$max_possible,
				$qualified_urls
			);
		}

		return $result;
	}

	/**
	 * Calculates a global "crawl budget" overview for all SEO rules.
	 *
	 * - sums up ALL qualified URLs across all rules,
	 * - compares the total against fixed thresholds:
	 *   - warning  >= 1000 URLs
	 *   - critical >= 5000 URLs
	 *
	 * @param string $lang Language code.
	 *
	 * @return array
	 * @throws WPDBException When a database error occurred.
	 */
	public function get_crawl_budget_overview( string $lang = '' ): array {
		$all_urls = $this->get_all_indexable_urls( $lang );

		$total_qualified_urls = count( $all_urls );

		$thresholds = (array) apply_filters(
			'fibofilters/seo/crawl_budget_thresholds',
			[
				'warning'  => 1000,
				'critical' => 5000,
			]
		);

		$warning_threshold  = (int) ( $thresholds['warning'] ?? 1000 );
		$critical_threshold = (int) ( $thresholds['critical'] ?? 5000 );

		if ( 0 === $total_qualified_urls ) {
			$status = 'empty';
		} elseif ( $total_qualified_urls >= $critical_threshold ) {
			$status = 'critical';
		} elseif ( $total_qualified_urls >= $warning_threshold ) {
			$status = 'warning';
		} else {
			$status = 'ok';
		}

		return [
			'total_qualified_urls' => $total_qualified_urls,
			'status'               => $status,
		];
	}

	/**
	 * Checks if the rule is of type "product_cat:any".
	 *
	 * @param SeoRule $rule SEO rule.
	 */
	private function is_product_cat_any_rule( SeoRule $rule ): bool {
		$base_view = $rule->get_base_view();

		list( $type, $arg ) = SeoRuleUrlResolver::parse_base_view( $base_view );

		return ( $type === 'product_cat' && $arg === 'any' );
	}

	/**
	 * Base case:
	 *  - build an index only for value_ids from filters
	 *  - filter URLs by the minimum number of products.
	 *
	 * @param SeoRule $rule SEO rule.
	 * @param int[]   $all_value_ids All value_ids used in the rule.
	 * @param array   $urls List of URL data.
	 *
	 * @return string[]
	 *
	 * @throws WPDBException When a database error occurred.
	 */
	private function get_qualified_urls_for_standard_rule(
		SeoRule $rule,
		array $all_value_ids,
		array $urls
	): array {

		$value_index = $this->product_estimator->build_value_index_for_rule(
			$rule,
			$all_value_ids
		);

		if ( empty( $value_index ) ) {
			return [];
		}

		return $this->filter_urls_by_min_products(
			$urls,
			$value_index
		);
	}

	/**
	 * Case for "product_cat:any"
	 *
	 * @param int[] $all_value_ids  All value_ids used in the rule.
	 * @param array $urls List of URL data.
	 *
	 * @return array
	 *
	 * @throws WPDBException When a database error occurred.
	 */
	private function get_qualified_urls_for_any_product_cat_rule(
		array $all_value_ids,
		array $urls
	): array {

		$term_ids = $this->collect_product_cat_term_ids_from_urls( $urls );

		if ( empty( $term_ids ) ) {
			return [];
		}

		$mapping = $this->map_term_ids_to_value_ids( $term_ids );

		$term_to_value      = $mapping['term_to_value'];
		$taxonomy_value_ids = $mapping['taxonomy_value_ids'];

		if ( empty( $taxonomy_value_ids ) ) {
			return [];
		}

		$value_index = $this->product_estimator->build_value_index_for_any_taxonomy(
			$all_value_ids,
			$taxonomy_value_ids
		);

		if ( empty( $value_index ) ) {
			return [];
		}

		return $this->filter_urls_by_min_products_for_any_taxonomy(
			$urls,
			$value_index,
			$term_to_value
		);
	}

	/**
	 * Collects all term_ids of the product_cat category from the URL context.
	 *
	 * @param array $urls List of URL data.
	 *
	 * @return array
	 */
	private function collect_product_cat_term_ids_from_urls( array $urls ): array {
		$term_ids = [];

		foreach ( $urls as $url_data ) {
			$context  = $url_data['context'] ?? [];
			$taxonomy = $context['taxonomy'] ?? '';
			$term_id  = isset( $context['term_id'] ) ? (int) $context['term_id'] : 0;

			if ( $taxonomy === 'product_cat' && $term_id > 0 ) {
				$term_ids[] = $term_id;
			}
		}

		$term_ids = array_map( 'intval', $term_ids );
		$term_ids = array_filter(
			$term_ids,
			static function ( int $id ): bool {
				return $id > 0;
			}
		);

		return array_values( array_unique( $term_ids ) );
	}

	/**
	 * Maps the category term_id to the value_id from the index:
	 *  - returns map [term_id => taxonomy_value_id]
	 *  - returns a list of unique taxonomy_value_ids.
	 *
	 * @param array $term_ids List of term IDs.
	 *
	 * @return array
	 *
	 * @throws WPDBException When a database error occurred.
	 */
	private function map_term_ids_to_value_ids( array $term_ids ): array {
		$term_to_value      = [];
		$taxonomy_value_ids = [];

		foreach ( $term_ids as $term_id ) {
			$term_id = (int) $term_id;
			if ( $term_id <= 0 ) {
				continue;
			}

			$taxonomy_value_id = $this->product_estimator->get_taxonomy_value_id_for_term(
				'product_cat',
				$term_id
			);

			if ( $taxonomy_value_id <= 0 ) {
				continue;
			}

			$term_to_value[ $term_id ] = $taxonomy_value_id;
			$taxonomy_value_ids[]      = $taxonomy_value_id;
		}

		$taxonomy_value_ids = array_values( array_unique( $taxonomy_value_ids ) );

		return [
			'term_to_value'      => $term_to_value,
			'taxonomy_value_ids' => $taxonomy_value_ids,
		];
	}

	/**
	 * Collects all value_id entries used in rule value_sets.
	 *
	 * @param array $value_sets Value sets for a single rule.
	 *
	 * @return int[]
	 */
	private function collect_all_value_ids_for_rule( array $value_sets ): array {
		$all_value_ids = [];

		foreach ( $value_sets as $values_for_slug ) {
			if ( empty( $values_for_slug ) || ! is_array( $values_for_slug ) ) {
				continue;
			}

			foreach ( $values_for_slug as $value_row ) {
				if ( ! empty( $value_row['value_id'] ) ) {
					$all_value_ids[] = (int) $value_row['value_id'];
				}
			}
		}

		return array_values( array_unique( $all_value_ids ) );
	}

	/**
	 * For a single rule:
	 *  - iterates over URLs,
	 *  - keeps only those that meet the global min products threshold.
	 *
	 * Returns just a flat list of URL strings.
	 *
	 * @param array $urls           List of URL data arrays.
	 * @param array $value_index Value index for the rule.
	 *
	 * @return array List of qualified URL strings.
	 */
	private function filter_urls_by_min_products(
		array $urls,
		array $value_index
	): array {

		$qualified_urls = [];

		foreach ( $urls as $url_data ) {
			$filters = $url_data['filters'] ?? [];

			$has_enough_products = $this->product_estimator->has_enough_products_for_filters(
				$filters,
				$value_index
			);

			if ( ! $has_enough_products ) {
				continue;
			}

			if ( empty( $url_data['url'] ) ) {
				continue;
			}

			$qualified_urls[] = (string) $url_data['url'];
		}

		return $qualified_urls;
	}

	/**
	 * For a single rule:
	 * - iterates over URLs,
	 *  - for each URL we take the term_id from ['context']['term_id'],
	 *  - replace it with taxonomy_value_id (map $term_to_value),
	 *  - add this value to the filters as an additional value_id.
	 *
	 * @param array $urls           List of URL data arrays.
	 * @param array $value_index    Combined value index (filters + categories).
	 * @param array $term_to_value  Map: [term_id => taxonomy_value_id].
	 *
	 * @return array List of qualified URL strings.
	 */
	private function filter_urls_by_min_products_for_any_taxonomy(
		array $urls,
		array $value_index,
		array $term_to_value
	): array {

		$qualified_urls = [];

		foreach ( $urls as $url_data ) {
			$filters = $url_data['filters'] ?? [];
			$context = $url_data['context'] ?? [];

			$term_id = isset( $context['term_id'] ) ? (int) $context['term_id'] : 0;

			if ( $term_id <= 0 ) {
				continue;
			}

			$taxonomy_value_id = $term_to_value[ $term_id ] ?? 0;

			if ( $taxonomy_value_id <= 0 ) {
				continue;
			}

			// Add "category" as an additional value_id so that the intersection also includes the category membership.
			$filters_with_tax              = $filters;
			$filters_with_tax['_taxonomy'] = [
				'value_id' => $taxonomy_value_id,
			];

			$has_enough_products = $this->product_estimator->has_enough_products_for_filters(
				$filters_with_tax,
				$value_index
			);

			if ( ! $has_enough_products ) {
				continue;
			}

			if ( empty( $url_data['url'] ) ) {
				continue;
			}

			$qualified_urls[] = (string) $url_data['url'];
		}

		return $qualified_urls;
	}

	/**
	 * Builds final result structure for a single rule.
	 *
	 * @param SeoRule $rule         SEO rule.
	 * @param int     $max_possible Maximum possible URLs for the rule.
	 * @param array   $qualified_urls List of qualified URL strings.
	 *
	 * @return array
	 */
	private function build_rule_result(
		SeoRule $rule,
		int $max_possible,
		array $qualified_urls
	): array {

		sort( $qualified_urls, SORT_STRING );

		return [
			'rule'            => $rule,
			'max_possible'    => $max_possible,
			'qualified_count' => count( $qualified_urls ),
			'urls'            => $qualified_urls,
		];
	}
}
