<?php
/**
 * This file is part of the Wisewand WordPress plugin.
 *
 * Handles all taxonomy-related processing for content imports: category extraction,
 * hierarchical parent resolution, AI category matching, tag processing, and custom
 * taxonomy assignment.
 *
 * @package Wisewand
 * @since 2.8.2
 * @license GPL-2.0-or-later
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */
namespace Wisewand\Content;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use WP_Error;
use Wisewand\Utils\Options;
use Wisewand\Logging\Logger;

// phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_error_log, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- error_log() used within WP_DEBUG conditionals; dynamic placeholder generation for IN clauses; direct database calls required for custom plugin table operations

/**
 * Taxonomy Processor
 *
 * Responsible for all taxonomy operations during content import: extracting and
 * validating category IDs before wp_insert_post(), resolving hierarchical parents,
 * AI-based category matching, tag and custom taxonomy processing, and WPML taxonomy
 * language assignment.
 *
 * @package Wisewand
 * @since 2.8.2
 */
class Taxonomy_Processor {

	/**
	 * Options wrapper instance.
	 *
	 * @var Options
	 */
	private $options;

	/**
	 * Logger instance.
	 *
	 * @var Logger
	 */
	private $logger;

	/**
	 * Content_Manager reference for determine_language() calls.
	 *
	 * @var Content_Manager|null
	 */
	private $content_manager = null;

	/**
	 * Constructor.
	 *
	 * @since 2.8.2
	 *
	 * @param Options $options Options wrapper instance.
	 * @param Logger  $logger  Logger instance.
	 */
	public function __construct( Options $options, Logger $logger ) {
		$this->options = $options;
		$this->logger  = $logger;
	}

	/**
	 * Set Content_Manager reference for cross-class method calls.
	 *
	 * Required for determine_language() used in process_language_for_taxonomy().
	 *
	 * @since 2.8.2
	 *
	 * @param Content_Manager $manager Content_Manager instance.
	 */
	public function set_content_manager( Content_Manager $manager ) {
		$this->content_manager = $manager;
	}

	/**
	 * Extract and prepare category IDs BEFORE wp_insert_post().
	 *
	 * CRITICAL FIX: Prevents WordPress from auto-assigning default_category.
	 * By including post_category in wp_insert_post(), we tell WordPress exactly
	 * which categories to use, preventing the "Assurance" bug.
	 *
	 * PRIORITY CASCADE:
	 * 1. Manual categories (RSS feed target_categories)
	 * 2. API categories (publishwordpress_category)
	 * 3. AI matching (if enabled and no manual)
	 * 4. Default category (last resort)
	 *
	 * @since 2.8.2
	 *
	 * @param array $payload Content payload with categories.
	 * @return array Category IDs ready for wp_insert_post().
	 */
	public function extract_and_prepare_categories( array $payload ) {
		$content_settings = $this->options->get_content_settings();
		$category_ids     = array();

		if ( WP_DEBUG ) {
			error_log( 'CATEGORY_EXTRACT: Starting category extraction' );
			error_log( 'CATEGORY_EXTRACT: Payload categories: ' . ( isset( $payload['categories'] ) ? wp_json_encode( $payload['categories'] ) : 'NOT SET' ) );
		}

		// PRIORITY 1 & 2: Manual or API categories.
		if ( isset( $payload['categories'] ) && ! empty( $payload['categories'] ) ) {
			if ( WP_DEBUG ) {
				error_log( 'CATEGORY_EXTRACT: Processing provided categories...' );
			}

			// Use existing process_category_terms method (validates, creates if needed).
			$processed_ids = $this->process_category_terms(
				$payload['categories'],
				$content_settings['auto_create_categories']
			);

			if ( is_wp_error( $processed_ids ) ) {
				if ( WP_DEBUG ) {
					error_log( 'CATEGORY_EXTRACT: ERROR processing categories: ' . $processed_ids->get_error_message() );
				}
				$category_ids = array();
			} else {
				$category_ids = $processed_ids;
				if ( WP_DEBUG ) {
					error_log( 'CATEGORY_EXTRACT: Processed category IDs: ' . implode( ', ', $category_ids ) );
				}
			}
		}

		// FIX #2: Auto-include hierarchical parents.
		if ( ! empty( $category_ids ) ) {
			$category_ids = $this->ensure_hierarchical_parents_included( $category_ids );
			if ( WP_DEBUG ) {
				error_log( 'CATEGORY_EXTRACT: After parent inclusion: ' . implode( ', ', $category_ids ) );
			}
		}

		// PRIORITY 3: AI Category Matching (if enabled and no categories yet).
		if ( empty( $category_ids ) ) {
			if ( WP_DEBUG ) {
				error_log( 'CATEGORY_EXTRACT: No manual categories provided' );
			}

			$use_ai_matching = $payload['use_ai_category_matching'] ?? false;

			if ( WP_DEBUG ) {
				error_log( 'CATEGORY_EXTRACT: AI matching flag: ' . ( $use_ai_matching ? 'ENABLED' : 'DISABLED' ) );
			}

			if ( $use_ai_matching ) {
				$ai_result = $this->match_article_to_categories( $payload );

				if ( ! is_wp_error( $ai_result ) && ! empty( $ai_result ) ) {
					$category_ids = $ai_result;

					if ( WP_DEBUG ) {
						error_log( '✅ AI_CATEGORY_MATCH: Successfully matched to categories: ' . implode( ', ', $category_ids ) );
					}
				} elseif ( is_wp_error( $ai_result ) ) {
					if ( WP_DEBUG ) {
						error_log( '❌ AI_CATEGORY_MATCH: Failed - ' . $ai_result->get_error_message() );
					}
				} elseif ( WP_DEBUG ) {
						error_log( 'AI_CATEGORY_MATCH: No categories matched' );
				}
			}
		}

		// PRIORITY 4: Fallback - WordPress default_category (last resort).
		if ( empty( $category_ids ) ) {
			$default_cat = get_option( 'default_category' );
			if ( $default_cat && term_exists( (int) $default_cat, 'category' ) ) {
				$category_ids = array( (int) $default_cat );
				if ( WP_DEBUG ) {
					error_log( 'CATEGORY_EXTRACT: Using WordPress default_category: ' . $default_cat );
				}
			} else {
				// Find first available category (last resort).
				$all_categories = get_terms(
					array(
						'taxonomy'   => 'category',
						'hide_empty' => false,
						'number'     => 1,
						'orderby'    => 'name',
						'order'      => 'ASC',
					)
				);

				if ( ! is_wp_error( $all_categories ) && ! empty( $all_categories ) ) {
					$first_category = $all_categories[0];
					$category_ids   = array( (int) $first_category->term_id );
					if ( WP_DEBUG ) {
						error_log( 'CATEGORY_EXTRACT: Using first available category: "' . $first_category->name . '" (ID: ' . $first_category->term_id . ')' );
					}
				}
			}
		}

		if ( WP_DEBUG ) {
			error_log( 'CATEGORY_EXTRACT: Final category IDs for wp_insert_post: ' . implode( ', ', $category_ids ) );

			foreach ( $category_ids as $cat_id ) {
				$cat = get_category( $cat_id );
				if ( $cat && ! is_wp_error( $cat ) ) {
					error_log( '  - ' . $cat->name . ' (ID: ' . $cat_id . ', Parent: ' . $cat->parent . ')' );
				}
			}
		}

		return $category_ids;
	}

	/**
	 * Ensure hierarchical parent categories are included.
	 *
	 * WordPress hierarchical categories work best when both parent and child are assigned.
	 * This prevents issues with themes/plugins that expect the full category tree.
	 *
	 * Example: If assigning "Électrique" (child of "Voiture"), also assign "Voiture" parent.
	 *
	 * @since 2.8.2
	 *
	 * @param array $category_ids Original category IDs.
	 * @return array Category IDs with parents included.
	 */
	public function ensure_hierarchical_parents_included( array $category_ids ) {
		$complete_ids = array();

		foreach ( $category_ids as $cat_id ) {
			$cat_id = (int) $cat_id;

			// Add the original category.
			if ( ! in_array( $cat_id, $complete_ids, true ) ) {
				$complete_ids[] = $cat_id;
			}

			// Walk up the category tree to root.
			$current = get_category( $cat_id );

			while ( $current && ! is_wp_error( $current ) && $current->parent > 0 ) {
				$parent_id = (int) $current->parent;

				if ( ! in_array( $parent_id, $complete_ids, true ) ) {
					$complete_ids[] = $parent_id;
					if ( WP_DEBUG ) {
						$parent = get_category( $parent_id );
						if ( $parent && ! is_wp_error( $parent ) ) {
							error_log( 'CATEGORY_HIERARCHY: Auto-included parent "' . $parent->name . '" (ID: ' . $parent_id . ') for child ID: ' . $cat_id );
						}
					}
				}

				// Move to next parent up the tree.
				$current = get_category( $parent_id );
			}
		}

		if ( WP_DEBUG && count( $complete_ids ) > count( $category_ids ) ) {
			error_log( 'CATEGORY_HIERARCHY: Expanded ' . count( $category_ids ) . ' categories to ' . count( $complete_ids ) . ' (with parents)' );
		}

		return $complete_ids;
	}

	/**
	 * Match article to categories using AI.
	 *
	 * Uses Category_Matcher to intelligently match article content to best categories
	 * when use_ai_category_matching flag is enabled and no manual categories provided.
	 *
	 * @since 2.8.2
	 *
	 * @param array $payload Content payload with title and content.
	 * @return array|WP_Error Array of category IDs or WP_Error on failure.
	 */
	public function match_article_to_categories( array $payload ) {
		if ( WP_DEBUG ) {
			$title = $payload['title'] ?? $payload['post_title'] ?? 'Unknown';
			error_log( '🤖 AI_CATEGORY_MATCH: Starting AI matching for article: "' . substr( $title, 0, 50 ) . '..."' );
		}

		// Check if Category_Matcher class exists.
		if ( ! class_exists( 'Wisewand\\Utils\\Category_Matcher' ) ) {
			if ( WP_DEBUG ) {
				error_log( 'AI_CATEGORY_MATCH: Category_Matcher class not found' );
			}
			return new WP_Error(
				'category_matcher_not_found',
				__( 'Category_Matcher class not available', 'wisewand-ai-content-writer-seo-content-generator' )
			);
		}

		// Instantiate Category_Matcher.
		try {
			$category_matcher = new \Wisewand\Utils\Category_Matcher( $this->options, $this->logger );

			// Check if OpenAI is configured.
			if ( ! $category_matcher->is_available() ) {
				if ( WP_DEBUG ) {
					error_log( 'AI_CATEGORY_MATCH: OpenAI not configured' );
				}
				return new WP_Error(
					'openai_not_configured',
					__( 'OpenAI API not configured', 'wisewand-ai-content-writer-seo-content-generator' )
				);
			}

			if ( WP_DEBUG ) {
				error_log( 'AI_CATEGORY_MATCH: Calling Category_Matcher->match_article_to_category()' );
			}

			// Extract article data.
			$title   = $payload['title'] ?? $payload['post_title'] ?? '';
			$content = $payload['content'] ?? $payload['post_content'] ?? '';
			$lang    = $payload['lang'] ?? $payload['language'] ?? 'en';

			// Call AI matching.
			$result = $category_matcher->match_article_to_category( $title, $content, $lang );

			if ( is_wp_error( $result ) ) {
				if ( WP_DEBUG ) {
					error_log( 'AI_CATEGORY_MATCH: Matching failed - ' . $result->get_error_message() );
				}
				return $result;
			}

			if ( WP_DEBUG ) {
				if ( ! empty( $result ) ) {
					$category_names = array();
					foreach ( $result as $cat_id ) {
						$cat = get_category( $cat_id );
						if ( $cat && ! is_wp_error( $cat ) ) {
							$category_names[] = $cat->name . ' (ID: ' . $cat_id . ')';
						}
					}
					error_log( '✅ AI_CATEGORY_MATCH: Matched to categories: ' . implode( ', ', $category_names ) );
				} else {
					error_log( 'AI_CATEGORY_MATCH: No suitable categories found' );
				}
			}

			return $result;

		} catch ( \Exception $e ) {
			if ( WP_DEBUG ) {
				error_log( 'AI_CATEGORY_MATCH: Exception - ' . $e->getMessage() );
			}
			return new WP_Error(
				'ai_matching_exception',
				sprintf(
					/* translators: %s: error message */
					__( 'AI matching failed: %s', 'wisewand-ai-content-writer-seo-content-generator' ),
					$e->getMessage()
				)
			);
		}
	}

	/**
	 * Process all taxonomies for a post after insertion.
	 *
	 * Verifies category assignment made by wp_insert_post(), corrects any
	 * discrepancies, assigns tags, and sets custom taxonomy terms.
	 *
	 * @since 2.8.2
	 *
	 * @param int   $post_id Post ID to process taxonomies for.
	 * @param array $payload Content payload containing taxonomy data.
	 * @return bool|WP_Error True on success, WP_Error on failure.
	 */
	public function process_taxonomies( $post_id, array $payload ) {
		$content_settings = $this->options->get_content_settings();

		if ( WP_DEBUG ) {
			error_log( 'TAXONOMY: Processing taxonomies for post ' . $post_id );
			error_log( 'TAXONOMY: Payload categories: ' . ( isset( $payload['categories'] ) ? wp_json_encode( $payload['categories'] ) : 'NOT SET' ) );
		}

		// OPTIMIZED: Categories were set in wp_insert_post() via post_category parameter.
		// This method now VERIFIES they were set correctly and fixes if needed.
		$current_categories  = wp_get_post_categories( $post_id );
		$expected_categories = $payload['categories'] ?? array();

		if ( WP_DEBUG ) {
			error_log( 'TAXONOMY: Verifying category assignment for post ' . $post_id );
			error_log( 'TAXONOMY: Expected categories: ' . ( is_array( $expected_categories ) ? wp_json_encode( $expected_categories ) : 'NOT ARRAY' ) );
			error_log( 'TAXONOMY: Current assigned categories: ' . ( empty( $current_categories ) ? 'NONE' : implode( ', ', $current_categories ) ) );

			if ( ! empty( $current_categories ) ) {
				foreach ( $current_categories as $cat_id ) {
					$cat = get_category( $cat_id );
					if ( $cat && ! is_wp_error( $cat ) ) {
						error_log( 'TAXONOMY: - Currently assigned: ' . $cat->name . ' (ID: ' . $cat_id . ', Parent: ' . $cat->parent . ')' );
					}
				}
			}
		}

		// Re-process categories if provided in payload.
		// We always re-extract to ensure we have the correct list (including hierarchy).
		$category_ids = array();

		if ( isset( $payload['categories'] ) && ! empty( $payload['categories'] ) ) {
			if ( WP_DEBUG ) {
				error_log( 'TAXONOMY: Re-processing categories from payload to verify correctness...' );
			}

			$category_ids = $this->process_category_terms( $payload['categories'], $content_settings['auto_create_categories'] );

			if ( is_wp_error( $category_ids ) ) {
				if ( WP_DEBUG ) {
					error_log( 'TAXONOMY: ERROR processing categories: ' . $category_ids->get_error_message() );
				}
				$category_ids = array();
			} else {
				// Include hierarchical parents.
				$category_ids = $this->ensure_hierarchical_parents_included( $category_ids );

				if ( WP_DEBUG ) {
					error_log( 'TAXONOMY: Re-processed category IDs (with parents): ' . implode( ', ', $category_ids ) );
				}

				// CRITICAL: Check if WordPress assigned all the categories we wanted.
				$missing_categories = array_diff( $category_ids, $current_categories );

				if ( ! empty( $missing_categories ) || count( $current_categories ) !== count( $category_ids ) ) {
					if ( WP_DEBUG ) {
						error_log( '⚠️  TAXONOMY: WordPress did not assign all categories correctly!' );
						error_log( '  - Expected: ' . implode( ', ', $category_ids ) );
						error_log( '  - Got: ' . implode( ', ', $current_categories ) );
						error_log( '  - Missing: ' . implode( ', ', $missing_categories ) );
						error_log( '  - Will call wp_set_post_categories to fix...' );
					}
					// Will be fixed below by wp_set_post_categories.
				} elseif ( WP_DEBUG ) {
						error_log( '✅ TAXONOMY: All categories correctly assigned by wp_insert_post!' );
				}
			}
		} else {
			if ( WP_DEBUG ) {
				error_log( 'TAXONOMY: No categories in payload, keeping WordPress-assigned categories' );
			}
			$category_ids = $current_categories;
		}

		// CRITICAL FIX: If no categories assigned, ensure post has at least one category.
		// This is REQUIRED for permalink structures with /%category%/
		// MUST work on ANY WordPress site (not hardcode specific category names).
		if ( empty( $category_ids ) && empty( $current_categories ) ) {
			if ( WP_DEBUG ) {
				error_log( 'TAXONOMY: No categories assigned, searching for suitable category...' );
			}

			$default_cat = get_option( 'default_category' );
			if ( WP_DEBUG ) {
				error_log( 'TAXONOMY: WordPress default_category option: "' . ( $default_cat ?: 'EMPTY' ) . '"' );
			}

			// If default_category option is not set or invalid, find ANY existing category.
			if ( empty( $default_cat ) || ! term_exists( (int) $default_cat, 'category' ) ) {
				if ( WP_DEBUG ) {
					error_log( 'TAXONOMY: Default category not configured, finding first available category...' );
				}

				// Get the first available category (sorted by name).
				$all_categories = get_terms(
					array(
						'taxonomy'   => 'category',
						'hide_empty' => false,
						'number'     => 1,
						'orderby'    => 'name',
						'order'      => 'ASC',
					)
				);

				if ( ! is_wp_error( $all_categories ) && ! empty( $all_categories ) ) {
					$first_category = $all_categories[0];
					$default_cat    = $first_category->term_id;
					if ( WP_DEBUG ) {
						error_log( 'TAXONOMY: Found first category: "' . $first_category->name . '" (ID: ' . $default_cat . ')' );
					}

					// Set as WordPress default for future imports.
					update_option( 'default_category', $default_cat );
					if ( WP_DEBUG ) {
						error_log( 'TAXONOMY: Set as WordPress default_category for future imports' );
					}
				} else {
					// NO categories exist on this WordPress site!
					if ( WP_DEBUG ) {
						error_log( 'TAXONOMY: No categories exist in WordPress, creating "Uncategorized"...' );
					}

					// Create standard "Uncategorized" category.
					$new_cat = wp_insert_term(
						'Uncategorized',
						'category',
						array(
							'slug'        => 'uncategorized',
							'description' => 'Default category for imported content',
						)
					);

					if ( ! is_wp_error( $new_cat ) ) {
						$default_cat = $new_cat['term_id'];
						if ( WP_DEBUG ) {
							error_log( 'TAXONOMY: Created "Uncategorized" category - ID: ' . $default_cat );
						}

						// Set as WordPress default.
						update_option( 'default_category', $default_cat );
						if ( WP_DEBUG ) {
							error_log( 'TAXONOMY: Set "Uncategorized" as WordPress default_category' );
						}
					} elseif ( WP_DEBUG ) {
							error_log( '❌ TAXONOMY: CRITICAL - Could not create category: ' . $new_cat->get_error_message() );
					}
				}
			}

			// Validate we have a valid category ID.
			if ( $default_cat ) {
				$default_term = get_term( $default_cat, 'category' );
				if ( $default_term && ! is_wp_error( $default_term ) ) {
					$category_ids = array( (int) $default_cat );
					if ( WP_DEBUG ) {
						error_log( '✅ TAXONOMY: Will assign category "' . $default_term->name . '" (ID: ' . $default_cat . ')' );
					}
				} elseif ( WP_DEBUG ) {
						error_log( '❌ TAXONOMY: Category ID ' . $default_cat . ' is invalid after all checks!' );
				}
			} elseif ( WP_DEBUG ) {
					error_log( '❌ TAXONOMY: Could not determine any category to assign!' );
			}
		}

		// CRITICAL FIX: Always call wp_set_post_categories if we have categories to assign.
		// This ensures ALL selected categories are assigned, even if wp_insert_post() was incomplete.
		if ( ! empty( $category_ids ) ) {
			// Check if we need to update categories.
			$needs_update = false;

			if ( empty( $current_categories ) ) {
				$needs_update = true;
				if ( WP_DEBUG ) {
					error_log( 'TAXONOMY: No categories currently assigned - needs update' );
				}
			} else {
				// Check if current categories match expected categories.
				$current_sorted  = $current_categories;
				$expected_sorted = $category_ids;
				sort( $current_sorted );
				sort( $expected_sorted );

				if ( $current_sorted !== $expected_sorted ) {
					$needs_update = true;
					if ( WP_DEBUG ) {
						error_log( 'TAXONOMY: Categories mismatch - needs update' );
						error_log( '  - Current (sorted): ' . implode( ', ', $current_sorted ) );
						error_log( '  - Expected (sorted): ' . implode( ', ', $expected_sorted ) );
					}
				}
			}

			if ( $needs_update ) {
				if ( WP_DEBUG ) {
					error_log( 'TAXONOMY: Calling wp_set_post_categories with ' . count( $category_ids ) . ' categories: ' . implode( ', ', $category_ids ) );
				}

				$result = wp_set_post_categories( $post_id, $category_ids );

				if ( is_wp_error( $result ) ) {
					if ( WP_DEBUG ) {
						error_log( '❌ TAXONOMY: ERROR - wp_set_post_categories failed: ' . $result->get_error_message() );
					}
				} elseif ( WP_DEBUG ) {
						error_log( 'TAXONOMY: wp_set_post_categories returned: ' . wp_json_encode( $result ) );

						// VERIFY assignment worked.
						$assigned = wp_get_post_categories( $post_id );
					if ( empty( $assigned ) ) {
						error_log( '❌ TAXONOMY: CRITICAL - Categories not assigned! Verification failed!' );
					} else {
						error_log( '✅ TAXONOMY: Categories verified - ' . count( $assigned ) . ' categories assigned: ' . implode( ', ', $assigned ) );

						foreach ( $assigned as $cat_id ) {
							$cat = get_category( $cat_id );
							if ( $cat && ! is_wp_error( $cat ) ) {
								error_log( '   - ' . $cat->name . ' (ID: ' . $cat_id . ', Slug: ' . $cat->slug . ', Parent: ' . $cat->parent . ')' );
							}
						}
					}
				}
			} elseif ( WP_DEBUG ) {
					error_log( '✅ TAXONOMY: Categories already correct (' . count( $category_ids ) . ' categories), no update needed' );
			}
		} elseif ( empty( $category_ids ) && empty( $current_categories ) ) {
			if ( WP_DEBUG ) {
				error_log( '❌ TAXONOMY: CRITICAL - No categories to assign! Post will have NO categories!' );
			}
		}

		// Process tags.
		if ( isset( $payload['tags'] ) ) {
			$tag_ids = $this->process_tag_terms( $payload['tags'], $content_settings['auto_create_tags'] );
			if ( ! is_wp_error( $tag_ids ) ) {
				wp_set_post_tags( $post_id, $tag_ids );
			}
		}

		// Process custom taxonomies.
		$custom_taxonomies = get_object_taxonomies( get_post_type( $post_id ), 'objects' );
		foreach ( $custom_taxonomies as $taxonomy ) {
			if ( in_array( $taxonomy->name, array( 'category', 'post_tag' ), true ) ) {
				continue; // Skip built-in taxonomies already processed.
			}

			if ( isset( $payload['taxonomies'][ $taxonomy->name ] ) ) {
				$term_ids = $this->process_custom_taxonomy_terms(
					$payload['taxonomies'][ $taxonomy->name ],
					$taxonomy->name,
					$content_settings['auto_create_categories'] // Use same setting.
				);

				if ( ! is_wp_error( $term_ids ) ) {
					wp_set_object_terms( $post_id, $term_ids, $taxonomy->name );
				}
			}
		}

		return true;
	}

	/**
	 * Process and resolve category terms from mixed IDs and name/slug inputs.
	 *
	 * Uses batch queries to minimise database round-trips. Validates numeric IDs,
	 * matches names/slugs, and optionally creates new categories.
	 *
	 * @since 2.8.2
	 *
	 * @param array|string $categories  Category IDs, names, or slugs.
	 * @param bool         $auto_create Whether to create categories that do not exist.
	 * @return array Validated category term IDs.
	 */
	public function process_category_terms( $categories, $auto_create ) {
		if ( ! is_array( $categories ) ) {
			$categories = array( $categories );
		}

		$category_ids = array();

		// Separate numeric IDs from names/slugs for batch processing.
		$numeric_ids = array();
		$names_slugs = array();

		foreach ( $categories as $category ) {
			if ( is_numeric( $category ) ) {
				$numeric_ids[] = (int) $category;
			} else {
				$names_slugs[] = $category;
			}
		}

		// BATCH QUERY 1: Validate all numeric IDs at once (eliminates N queries).
		if ( ! empty( $numeric_ids ) ) {
			global $wpdb;
			$placeholders = implode( ',', array_fill( 0, count( $numeric_ids ), '%d' ) );
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Dynamic placeholders for IN clause, direct query more efficient than get_terms()
			$valid_ids = $wpdb->get_col(
				$wpdb->prepare(
					"SELECT t.term_id FROM {$wpdb->terms} t
                 INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
                 WHERE t.term_id IN ($placeholders) AND tt.taxonomy = 'category'",
					$numeric_ids
				)
			);

			if ( ! empty( $valid_ids ) ) {
				$category_ids = array_merge( $category_ids, array_map( 'intval', $valid_ids ) );
			}
		}

		// BATCH QUERY 2: Find all names/slugs at once (eliminates 2N queries).
		if ( ! empty( $names_slugs ) ) {
			global $wpdb;
			$sanitized_names = array_map( 'sanitize_text_field', $names_slugs );
			$sanitized_slugs = array_map( 'sanitize_title', $names_slugs );

			$placeholders = implode( ',', array_fill( 0, count( $sanitized_names ), '%s' ) );

			// Single query to find terms by name OR slug.
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Dynamic placeholders for IN clause, direct query more efficient than get_terms()
			$found_terms = $wpdb->get_results(
				$wpdb->prepare(
					"SELECT t.term_id, t.name, t.slug FROM {$wpdb->terms} t
                 INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
                 WHERE tt.taxonomy = 'category'
                 AND (t.name IN ($placeholders) OR t.slug IN ($placeholders))",
					array_merge( $sanitized_names, $sanitized_slugs )
				),
				OBJECT_K
			);

			// Build lookup maps for O(1) access.
			$terms_by_name = array();
			$terms_by_slug = array();
			foreach ( $found_terms as $term ) {
				$terms_by_name[ strtolower( $term->name ) ] = $term->term_id;
				$terms_by_slug[ $term->slug ]               = $term->term_id;
			}

			// Match categories to found terms.
			foreach ( $names_slugs as $category ) {
				$sanitized_name = sanitize_text_field( $category );
				$sanitized_slug = sanitize_title( $category );

				$found_id = null;

				// Try slug match first (more reliable).
				if ( isset( $terms_by_slug[ $sanitized_slug ] ) ) {
					$found_id = $terms_by_slug[ $sanitized_slug ];
				}
				// Try name match (case-insensitive).
				elseif ( isset( $terms_by_name[ strtolower( $sanitized_name ) ] ) ) {
					$found_id = $terms_by_name[ strtolower( $sanitized_name ) ];
				}

				if ( $found_id ) {
					$category_ids[] = (int) $found_id;
				} elseif ( $auto_create ) {
					// Create new category.
					$new_term = wp_insert_term( $sanitized_name, 'category' );
					if ( ! is_wp_error( $new_term ) ) {
						$category_ids[] = $new_term['term_id'];
					}
				}
			}
		}

		return array_unique( $category_ids );
	}

	/**
	 * Process and resolve tag terms from mixed IDs and name inputs.
	 *
	 * Uses batch queries to minimise database round-trips. Converts numeric IDs
	 * to names and optionally filters to only existing tags.
	 *
	 * @since 2.8.2
	 *
	 * @param array|string $tags        Tag IDs or names.
	 * @param bool         $auto_create Whether to create tags that do not exist.
	 * @return array Tag names for wp_set_post_tags().
	 */
	public function process_tag_terms( $tags, $auto_create ) {
		if ( ! is_array( $tags ) ) {
			$tags = array( $tags );
		}

		$tag_names = array();

		// Separate numeric IDs from names for batch processing.
		$numeric_ids = array();
		$name_tags   = array();

		foreach ( $tags as $tag ) {
			if ( is_numeric( $tag ) ) {
				$numeric_ids[] = (int) $tag;
			} else {
				$name_tags[] = sanitize_text_field( $tag );
			}
		}

		// BATCH QUERY 1: Convert all numeric IDs to names at once (eliminates N queries).
		if ( ! empty( $numeric_ids ) ) {
			global $wpdb;
			$placeholders = implode( ',', array_fill( 0, count( $numeric_ids ), '%d' ) );
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Dynamic placeholders for IN clause, direct query more efficient than get_terms()
			$found_tags = $wpdb->get_col(
				$wpdb->prepare(
					"SELECT t.name FROM {$wpdb->terms} t
                 INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
                 WHERE t.term_id IN ($placeholders) AND tt.taxonomy = 'post_tag'",
					$numeric_ids
				)
			);

			if ( ! empty( $found_tags ) ) {
				$tag_names = array_merge( $tag_names, $found_tags );
			}
		}

		// BATCH QUERY 2: Check if name tags exist (if not auto-creating).
		if ( ! empty( $name_tags ) ) {
			if ( $auto_create ) {
				// If auto-creating, just add all names (wp_set_post_tags handles creation).
				$tag_names = array_merge( $tag_names, $name_tags );
			} else {
				global $wpdb;
				$placeholders = implode( ',', array_fill( 0, count( $name_tags ), '%s' ) );
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Dynamic placeholders for IN clause, direct query more efficient than get_terms()
				$existing_tags = $wpdb->get_col(
					$wpdb->prepare(
						"SELECT t.name FROM {$wpdb->terms} t
                     INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
                     WHERE tt.taxonomy = 'post_tag'
                     AND (t.name IN ($placeholders) OR t.slug IN ($placeholders))",
						array_merge( $name_tags, array_map( 'sanitize_title', $name_tags ) )
					)
				);

				if ( ! empty( $existing_tags ) ) {
					$tag_names = array_merge( $tag_names, $existing_tags );
				}
			}
		}

		return array_unique( $tag_names );
	}

	/**
	 * Process and resolve custom taxonomy terms from mixed IDs and name/slug inputs.
	 *
	 * Uses batch queries for efficiency. Validates numeric IDs, matches names/slugs,
	 * and optionally creates new terms for a given taxonomy.
	 *
	 * @since 2.8.2
	 *
	 * @param array|string $terms       Term IDs, names, or slugs.
	 * @param string       $taxonomy    Target taxonomy name.
	 * @param bool         $auto_create Whether to create terms that do not exist.
	 * @return array Validated term IDs.
	 */
	public function process_custom_taxonomy_terms( $terms, $taxonomy, $auto_create ) {
		if ( ! is_array( $terms ) ) {
			$terms = array( $terms );
		}

		$term_ids = array();

		// Separate numeric IDs from names/slugs for batch processing.
		$numeric_ids = array();
		$names_slugs = array();

		foreach ( $terms as $term ) {
			if ( is_numeric( $term ) ) {
				$numeric_ids[] = (int) $term;
			} else {
				$names_slugs[] = $term;
			}
		}

		// BATCH QUERY 1: Validate all numeric IDs at once (eliminates N queries).
		if ( ! empty( $numeric_ids ) ) {
			global $wpdb;
			$placeholders = implode( ',', array_fill( 0, count( $numeric_ids ), '%d' ) );
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Dynamic placeholders for IN clause, direct query more efficient than get_terms()
			$valid_ids = $wpdb->get_col(
				$wpdb->prepare(
					"SELECT t.term_id FROM {$wpdb->terms} t
                 INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
                 WHERE t.term_id IN ($placeholders) AND tt.taxonomy = %s",
					array_merge( $numeric_ids, array( $taxonomy ) )
				)
			);

			if ( ! empty( $valid_ids ) ) {
				$term_ids = array_merge( $term_ids, array_map( 'intval', $valid_ids ) );
			}
		}

		// BATCH QUERY 2: Find all names/slugs at once (eliminates 2N queries).
		if ( ! empty( $names_slugs ) ) {
			global $wpdb;
			$sanitized_names = array_map( 'sanitize_text_field', $names_slugs );
			$sanitized_slugs = array_map( 'sanitize_title', $names_slugs );

			$placeholders = implode( ',', array_fill( 0, count( $sanitized_names ), '%s' ) );

			// Single query to find terms by name OR slug.
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Dynamic placeholders for IN clause, direct query more efficient than get_terms()
			$found_terms = $wpdb->get_results(
				$wpdb->prepare(
					"SELECT t.term_id, t.name, t.slug FROM {$wpdb->terms} t
                 INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
                 WHERE tt.taxonomy = %s
                 AND (t.name IN ($placeholders) OR t.slug IN ($placeholders))",
					array_merge( array( $taxonomy ), $sanitized_names, $sanitized_slugs )
				),
				OBJECT_K
			);

			// Build lookup maps for O(1) access.
			$terms_by_name = array();
			$terms_by_slug = array();
			foreach ( $found_terms as $term ) {
				$terms_by_name[ strtolower( $term->name ) ] = $term->term_id;
				$terms_by_slug[ $term->slug ]               = $term->term_id;
			}

			// Match terms to found results.
			foreach ( $names_slugs as $term_input ) {
				$sanitized_name = sanitize_text_field( $term_input );
				$sanitized_slug = sanitize_title( $term_input );

				$found_id = null;

				// Try slug match first.
				if ( isset( $terms_by_slug[ $sanitized_slug ] ) ) {
					$found_id = $terms_by_slug[ $sanitized_slug ];
				}
				// Try name match.
				elseif ( isset( $terms_by_name[ strtolower( $sanitized_name ) ] ) ) {
					$found_id = $terms_by_name[ strtolower( $sanitized_name ) ];
				}

				if ( $found_id ) {
					$term_ids[] = (int) $found_id;
				} elseif ( $auto_create ) {
					// Create new term.
					$new_term = wp_insert_term( $sanitized_name, $taxonomy );
					if ( ! is_wp_error( $new_term ) ) {
						$term_ids[] = $new_term['term_id'];
					}
				}
			}
		}

		return array_unique( $term_ids );
	}

	/**
	 * Set WPML language on a taxonomy term.
	 *
	 * Delegates language determination to Content_Manager if available, then
	 * calls WPML sitepress API to register the language on the term.
	 *
	 * @since 2.8.2
	 *
	 * @param int   $term_id Term ID to set language for.
	 * @param array $payload Content payload containing language data.
	 * @return string|null Language code set, or null if WPML inactive.
	 */
	public function process_language_for_taxonomy( $term_id, array $payload ) {
		if ( ! $this->options->is_wpml_active() ) {
			return null;
		}

		// Delegate to Content_Manager for language determination.
		if ( ! $this->content_manager ) {
			return null;
		}

		$language = $this->content_manager->determine_language( $payload );
		if ( ! $language ) {
			return null;
		}

		// Set taxonomy term language in WPML.
		global $sitepress;
		if ( $sitepress ) {
			$sitepress->set_element_language_details(
				$term_id,
				'tax_category',
				false,
				$language
			);

			$this->logger->info(
				'Set category language in WPML',
				array(
					'category_id' => $term_id,
					'language'    => $language,
				)
			);
		}

		return $language;
	}

	/**
	 * Get the appropriate taxonomy name for a given post type.
	 *
	 * Maps WooCommerce product types to product_cat and everything else to
	 * the standard WordPress category taxonomy.
	 *
	 * @since 2.8.2
	 *
	 * @param string $post_type Post type to resolve taxonomy for.
	 * @return string Taxonomy name (e.g. 'category', 'product_cat').
	 */
	public function get_taxonomy_for_post_type( $post_type ) {
		switch ( $post_type ) {
			case 'product_cat':
				return 'product_cat'; // WooCommerce product categories.
			case 'category':
			default:
				return 'category'; // WordPress categories.
		}
	}
}
