<?php

declare(strict_types=1);

namespace Automattic\WooCommerce\Internal\ProductFilters;

defined( 'ABSPATH' ) || exit;

/**
 * Class for managing taxonomy hierarchy data with performance optimization.
 *
 * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
 */
class TaxonomyHierarchyData {

	/**
	 * Cache group for taxonomy hierarchy data.
	 */
	private const CACHE_GROUP = 'wc_taxonomy_hierarchy';

	/**
	 * In-memory cache for hierarchy maps.
	 *
	 * @var array
	 */
	private $hierarchy_data = array();

	/**
	 * Get optimized hierarchy map for a taxonomy.
	 *
	 * @param string $taxonomy The taxonomy name.
	 * @return array Hierarchy map structure optimized for the taxonomy size.
	 */
	public function get_hierarchy_map( string $taxonomy ): array {
		if ( ! is_taxonomy_hierarchical( $taxonomy ) ) {
			return array();
		}

		// Check in-memory cache first.
		if ( isset( $this->hierarchy_data[ $taxonomy ] ) ) {
			return $this->hierarchy_data[ $taxonomy ];
		}

		// Check option cache.
		$cache_key  = self::CACHE_GROUP . '_' . $taxonomy;
		$cached_map = null;

		if ( ! ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) {
			$cached_map = get_option( $cache_key );
		}

		if ( ! empty( $cached_map ) && $this->validate_cache( $cached_map ) ) {
			// Cache in memory and return.
			$this->hierarchy_data[ $taxonomy ] = $cached_map;
			return $cached_map;
		}

		// Build the complete hierarchy map with all descendants pre-computed.
		$map = $this->build_full_hierarchy_map( $taxonomy );

		// Cache the map in options and memory.
		if ( ! ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) {
			update_option( $cache_key, $map, false );
		}

		$this->hierarchy_data[ $taxonomy ] = $map;

		return $map;
	}

	/**
	 * Get all descendants for a term.
	 *
	 * @param int    $term_id  The term ID.
	 * @param string $taxonomy The taxonomy name.
	 * @return array Array of all descendant term IDs.
	 */
	public function get_descendants( int $term_id, string $taxonomy ): array {
		$map = $this->get_hierarchy_map( $taxonomy );
		return $map['descendants'][ $term_id ] ?? array();
	}

	/**
	 * Get ancestor chain for batch processing.
	 *
	 * @param int    $term_id  The term ID.
	 * @param string $taxonomy The taxonomy name.
	 * @return array Array of ancestor term IDs (bottom-up).
	 */
	public function get_ancestors( int $term_id, string $taxonomy ): array {
		$map = $this->get_hierarchy_map( $taxonomy );
		return $map['ancestors'][ $term_id ] ?? array();
	}

	/**
	 * Clear hierarchy cache for a taxonomy.
	 *
	 * @param string $taxonomy The taxonomy name.
	 */
	public function clear_cache( string $taxonomy ): void {
		// Clear in-memory cache for this taxonomy.
		unset( $this->hierarchy_data[ $taxonomy ] );

		// Clear only the specific taxonomy's option cache.
		$cache_key = self::CACHE_GROUP . '_' . $taxonomy;
		delete_option( $cache_key );
	}

	/**
	 * Check if the cache is valid.
	 *
	 * @param array $data Cache data.
	 *
	 * @return boolean
	 */
	private function validate_cache( $data ) {
		return is_array( $data ) &&
			array_key_exists( 'descendants', $data ) &&
			array_key_exists( 'ancestors', $data ) &&
			array_key_exists( 'tree', $data );
	}

	/**
	 * Build hierarchy map for FilterData and ProductFilterTaxonomy.
	 *
	 * Pre-computes descendants and ancestor chains for maximum query speed.
	 *
	 * @param string $taxonomy The taxonomy name.
	 * @return array Complete hierarchy map with descendants and ancestor chains.
	 */
	private function build_full_hierarchy_map( string $taxonomy ): array {
		$terms = get_terms(
			array(
				'taxonomy'   => $taxonomy,
				'hide_empty' => false,
				'orderby'    => 'name',
				'order'      => 'ASC',
			)
		);

		if ( is_wp_error( $terms ) || empty( $terms ) ) {
			return array();
		}

		$map = array(
			'descendants' => array(), // term_id => [descendant_ids].
			'ancestors'   => array(), // term_id => [ancestor_ids].
			'tree'        => array(),
		);

		// Build core lookups and temporary structures.
		$temp_children = array();
		$temp_parents  = array();
		$temp_terms    = array();

		// Prime term meta cache in single query to avoid N+1.
		update_termmeta_cache( wp_list_pluck( $terms, 'term_id' ) );

		foreach ( $terms as $term ) {
			$term_id   = $term->term_id;
			$parent_id = $term->parent;

			$temp_parents[ $term_id ] = $parent_id;

			if ( ! isset( $temp_children[ $parent_id ] ) ) {
				$temp_children[ $parent_id ] = array();
			}

			$temp_children[ $parent_id ][] = $term_id;

			// Get the menu_order from term meta (WooCommerce stores category order in 'order' meta).
			$menu_order = get_term_meta( $term_id, 'order', true );

			$temp_terms[ $term_id ] = array(
				'slug'       => $term->slug,
				'name'       => $term->name,
				'parent'     => $parent_id,
				'term_id'    => $term->term_id,
				'menu_order' => is_numeric( $menu_order ) ? (int) $menu_order : 0,
			);
		}

		// Pre-compute descendants and ancestors.
		foreach ( array_keys( $temp_parents ) as $term_id ) {
			$map['descendants'][ $term_id ] = $this->compute_descendants( $term_id, $temp_children );
			$map['ancestors'][ $term_id ]   = $this->compute_ancestors( $term_id, $temp_parents );
		}

		foreach ( $temp_children[0] as $term_id ) {
			$this->build_term_tree( $map['tree'], $term_id, $temp_children, $temp_terms );
		}

		return $map;
	}

	/**
	 * Recursively build hierarchical term tree with depth and parent.
	 *
	 * @param array $tree       Reference to tree array being built.
	 * @param int   $term_id    Current term ID.
	 * @param array $children   Children relationships map (parent_id => [child_ids]).
	 * @param array $temp_terms Term data indexed by term_id.
	 * @param int   $depth      Current depth level in hierarchy.
	 */
	private function build_term_tree( &$tree, $term_id, $children, $temp_terms, $depth = 0 ) {
		$tree[ $term_id ]          = $temp_terms[ $term_id ];
		$tree[ $term_id ]['depth'] = $depth;

		if ( ! empty( $children[ $term_id ] ) ) {
			foreach ( $children[ $term_id ] as $child_id ) {
				$this->build_term_tree( $tree[ $term_id ]['children'], $child_id, $children, $temp_terms, $depth + 1 );
			}
		}
	}

	/**
	 * Compute all descendants of a term.
	 *
	 * @param int   $term_id  The term ID.
	 * @param array $children Children relationships map.
	 * @return array Array of descendant term IDs.
	 */
	private function compute_descendants( int $term_id, array $children ): array {
		$descendants = array();

		if ( ! isset( $children[ $term_id ] ) ) {
			return $descendants;
		}

		foreach ( $children[ $term_id ] as $child_id ) {
			$descendants[] = $child_id;
			$descendants   = array_merge( $descendants, $this->compute_descendants( $child_id, $children ) );
		}

		return array_unique( $descendants );
	}

	/**
	 * Compute ancestor chain for a term.
	 *
	 * @param int   $term_id The term ID.
	 * @param array $parent_lookup Parent relationships.
	 * @return array Array of ancestor term IDs (bottom-up).
	 */
	private function compute_ancestors( int $term_id, array $parent_lookup ): array {
		$ancestors  = array();
		$current_id = $term_id;

		while ( isset( $parent_lookup[ $current_id ] ) && $parent_lookup[ $current_id ] > 0 ) {
			$parent_id   = $parent_lookup[ $current_id ];
			$ancestors[] = $parent_id;
			$current_id  = $parent_id;
		}

		return $ancestors;
	}
}
