<?php
/**
 * This file is part of the Wisewand WordPress plugin.
 *
 * @package Wisewand
 * @since 1.0.0
 * @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/>.
 */

/**
 * AI-Powered Category Matcher
 *
 * Uses OpenAI to intelligently match keywords to WordPress categories
 *
 * @package Wisewand
 * @subpackage Utils
 * @version 1.0.0
 */

namespace Wisewand\Utils;

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


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

// phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_error_log -- error_log() is only used within WP_DEBUG conditionals for debugging

class Category_Matcher {

	private $logger;
	private $options;
	private $openai_client;
	private $rate_limiter;

	public function __construct( Options $options, Logger $logger, $rate_limiter = null ) {
		$this->options = $options;
		$this->logger  = $logger;

		$api_key = OpenAI_Client::get_stored_api_key();
		if ( $api_key ) {
			$this->openai_client = new OpenAI_Client( $api_key, $logger );
		}
		$this->rate_limiter = $rate_limiter ?? new \Wisewand\Utils\Rate_Limiter( $this->logger );
	}

	/**
	 * Check if AI matching is available
	 *
	 * @return bool True if OpenAI is configured
	 */
	public function is_available() {
		return OpenAI_Client::is_configured();
	}

	/**
	 * Match a single article to categories using AI
	 *
	 * Uses OpenAI to intelligently match article title and content to the best
	 * WordPress categories. Returns 1-3 category IDs based on relevance.
	 *
	 * @param string $title Article title
	 * @param string $content Article content
	 * @param string $language_code Language code (e.g., 'en', 'fr')
	 * @return array|WP_Error Array of category IDs or WP_Error on failure
	 */
	public function match_article_to_category( $title, $content, $language_code = 'en' ) {
		if ( ! $this->openai_client ) {
			return new \WP_Error( 'no_api_key', 'OpenAI API key not configured' );
		}

		if ( WP_DEBUG ) {
			error_log( 'AI_CATEGORY_MATCH: Matching article to categories' );
			error_log( '  Title: ' . substr( $title, 0, 100 ) );
			error_log( '  Language: ' . $language_code );
		}

		// Get categories for the specified language
		$categories = $this->get_categories_for_ai( $language_code );

		if ( empty( $categories ) ) {
			$this->logger->warning(
				'No categories available for AI matching',
				array(
					'language' => $language_code,
				)
			);

			if ( WP_DEBUG ) {
				error_log( 'AI_CATEGORY_MATCH: No categories found for language: ' . $language_code );
			}

			return new \WP_Error(
				'no_categories',
				sprintf(
					/* translators: %s: language code */
					__( 'No categories available for language: %s', 'wisewand-ai-content-writer-seo-content-generator' ),
					$language_code
				)
			);
		}

		if ( WP_DEBUG ) {
			error_log( 'AI_CATEGORY_MATCH: Found ' . count( $categories ) . ' categories for language: ' . $language_code );
		}

		// Build prompt for single article matching
		$messages = array(
			array(
				'role'    => 'system',
				'content' => $this->get_article_matching_system_prompt(),
			),
			array(
				'role'    => 'user',
				'content' => $this->build_article_matching_prompt( $title, $content, $categories, $language_code ),
			),
		);

		// Use Structured Outputs for guaranteed valid response
		$response = $this->openai_client->chat_completion(
			$messages,
			array(
				'temperature'     => 0.3, // Low temperature for consistent results
				'max_tokens'      => 500,
				'response_format' => array(
					'type'        => 'json_schema',
					'json_schema' => array(
						'name'   => 'article_category_matching',
						'strict' => true,
						'schema' => array(
							'type'                 => 'object',
							'properties'           => array(
								'category_ids' => array(
									'type'        => 'array',
									'description' => 'Array of matched category IDs (1-3 max, can be empty)',
									'items'       => array(
										'type' => 'integer',
									),
								),
								'confidence'   => array(
									'type'        => 'number',
									'description' => 'Confidence score 0.0 to 1.0',
								),
								'reasoning'    => array(
									'type'        => 'string',
									'description' => 'Brief explanation of the match',
								),
							),
							'required'             => array( 'category_ids', 'confidence', 'reasoning' ),
							'additionalProperties' => false,
						),
					),
				),
			)
		);

		if ( is_wp_error( $response ) ) {
			$this->logger->error(
				'OpenAI API call failed for article matching',
				array(
					'error' => $response->get_error_message(),
					'title' => substr( $title, 0, 100 ),
				)
			);

			if ( WP_DEBUG ) {
				error_log( 'AI_CATEGORY_MATCH: OpenAI API error - ' . $response->get_error_message() );
			}

			return $response;
		}

		// Validate and extract category IDs
		$category_ids = $this->validate_article_match_response( $response, $categories );

		if ( WP_DEBUG ) {
			if ( ! empty( $category_ids ) ) {
				error_log( 'AI_CATEGORY_MATCH: Successfully matched to ' . count( $category_ids ) . ' categories' );
				error_log( '  Confidence: ' . ( $response['confidence'] ?? 'N/A' ) );
				error_log( '  Reasoning: ' . ( $response['reasoning'] ?? 'N/A' ) );
			} else {
				error_log( 'AI_CATEGORY_MATCH: No categories matched (confidence too low or no match)' );
			}
		}

		return $category_ids;
	}

	/**
	 * Get system prompt for single article matching
	 *
	 * @return string System prompt
	 */
	private function get_article_matching_system_prompt() {
		return 'You are an expert WordPress content strategist specializing in SEO and content categorization.

MISSION: Match this article to the most relevant WordPress categories based on its title and content.

CRITICAL INSTRUCTIONS:
1. Return ONLY valid JSON in the exact format specified
2. Use ONLY category IDs from the provided categories list
3. NEVER invent or suggest category IDs not in the list
4. Match 1-3 categories maximum (quality over quantity)
5. First category = PRIMARY (most relevant), then secondary categories
6. Consider: semantic relevance, target audience, content topic, and language

CONFIDENCE GUIDELINES:
- Match if confidence >= 0.55 (reasonably confident)
- 0.85-1.0: Excellent match (exact semantic match)
- 0.70-0.84: Strong match (clear relationship)
- 0.55-0.69: Good match (related topic) - STILL MATCH
- < 0.55: Too weak - return empty array

RESPONSE FORMAT (JSON ONLY):
{
  "category_ids": [5, 12],  // Array of integers (1-3 max, can be empty)
  "confidence": 0.75,       // Float 0.0-1.0
  "reasoning": "Brief explanation of why these categories match"
}

IMPORTANT:
- Better to match with 0.55-0.65 confidence than return nothing
- Use parent/broader categories if no specific match exists
- Consider language: match article language to category language
- Empty array only if truly no relevant category exists';
	}

	/**
	 * Build prompt for article matching
	 *
	 * @param string $title Article title
	 * @param string $content Article content
	 * @param array  $categories Available categories
	 * @param string $language_code Language code
	 * @return string JSON prompt
	 */
	private function build_article_matching_prompt( $title, $content, $categories, $language_code ) {
		// Prepare categories data
		$categories_data = array();
		foreach ( $categories as $category ) {
			$categories_data[] = array(
				'id'          => intval( $category->term_id ),
				'name'        => $category->name,
				'slug'        => $category->slug,
				'parent_id'   => intval( $category->parent ),
				'parent_name' => $category->parent ? ( get_category( $category->parent )->name ?? '' ) : '',
				'description' => $category->description ?: 'No description',
				'count'       => intval( $category->count ),
			);
		}

		// Truncate content if too long (keep first 2000 chars)
		$content_excerpt = strlen( $content ) > 2000 ? substr( $content, 0, 2000 ) . '...' : $content;

		$prompt_data = array(
			'task'         => 'match_article_to_categories',
			'instructions' => 'Match this article to the most relevant WordPress categories. Return ONLY the JSON structure specified in the system prompt. Use ONLY category IDs from the provided list. Match with confidence 0.55-0.69 is GOOD and should be included.',
			'article'      => array(
				'title'    => $title,
				'content'  => $content_excerpt,
				'language' => $language_code,
			),
			'categories'   => $categories_data,
			'guidelines'   => array(
				'Match if confidence >= 0.55',
				'Better to match at 0.55-0.65 than return nothing',
				'Use broader/parent categories if no specific match',
				'Consider article topic, tone, and target audience',
				'Match article language to category language',
			),
		);

		return wp_json_encode( $prompt_data, JSON_PRETTY_PRINT );
	}

	/**
	 * Validate article match response from OpenAI
	 *
	 * @param array $response OpenAI response
	 * @param array $categories Available categories
	 * @return array Validated category IDs
	 */
	private function validate_article_match_response( $response, $categories ) {
		if ( ! isset( $response['category_ids'] ) || ! is_array( $response['category_ids'] ) ) {
			if ( WP_DEBUG ) {
				error_log( 'AI_CATEGORY_MATCH: Invalid response structure - missing category_ids array' );
			}
			return array();
		}

		// Get valid category IDs
		$valid_category_ids = array_map(
			function ( $cat ) {
				return intval( $cat->term_id );
			},
			$categories
		);

		// Filter to only valid category IDs
		$category_ids = array_filter(
			$response['category_ids'],
			function ( $id ) use ( $valid_category_ids ) {
				return in_array( intval( $id ), $valid_category_ids );
			}
		);

		// Convert to integers
		$category_ids = array_map( 'intval', array_values( $category_ids ) );

		// Limit to 3 categories max
		if ( count( $category_ids ) > 3 ) {
			$category_ids = array_slice( $category_ids, 0, 3 );
		}

		// Log if AI suggested invalid categories
		$invalid_ids = array_diff( $response['category_ids'], $category_ids );
		if ( ! empty( $invalid_ids ) && WP_DEBUG ) {
			error_log( 'AI_CATEGORY_MATCH: Warning - AI suggested non-existent category IDs: ' . implode( ', ', $invalid_ids ) );
		}

		return $category_ids;
	}

	/**
	 * Match keywords to categories using AI (language-aware)
	 *
	 * Groups keywords by language and matches each group to language-specific categories
	 * for improved accuracy and relevance.
	 *
	 * SMART MATCHING FLOW:
	 * 1. Auto-match from URLs (positioned keywords with ranking pages)
	 * 2. Filter out keywords with manual categories (protect user choices)
	 * 3. Send remaining keywords to AI for matching
	 *
	 * @param array    $keywords Array of keyword data
	 * @param callable $progress_callback Optional progress callback
	 * @param bool     $respect_manual Respect existing manual categories (default: true)
	 * @return array Match results
	 */
	public function match_keywords_to_categories( $keywords, $progress_callback = null, $respect_manual = true ) {
		if ( ! $this->openai_client ) {
			return new \WP_Error( 'no_api_key', 'OpenAI API key not configured' );
		}

		// Detect matching mode and provide user feedback
		$matching_mode = $this->detect_matching_mode();
		$this->log_matching_mode_info( $matching_mode );

		$this->logger->info(
			'AI Category Matching: Starting smart matching',
			array(
				'total_keywords' => count( $keywords ),
				'respect_manual' => $respect_manual,
				'matching_mode'  => $matching_mode,
			)
		);

		// STEP 1: Auto-match from URLs (fast, accurate, free!)
		$url_matches = $this->auto_match_from_urls( $keywords, $progress_callback );

		$this->logger->info(
			'URL-based auto-matching complete',
			array(
				'url_matches'       => count( $url_matches ),
				'original_keywords' => count( $keywords ),
			)
		);

		// STEP 2: Filter keywords for AI processing
		$keywords_for_ai = $this->filter_keywords_for_ai( $keywords, $url_matches, $respect_manual );

		$this->logger->info(
			'Keywords filtered for AI processing',
			array(
				'url_matched'    => count( $url_matches ),
				'needs_ai'       => count( $keywords_for_ai ),
				'skipped_manual' => $respect_manual ? count( $keywords ) - count( $url_matches ) - count( $keywords_for_ai ) : 0,
			)
		);

		// STEP 3: If no keywords need AI, return URL matches only
		if ( empty( $keywords_for_ai ) ) {
			$this->logger->info( 'All keywords handled by URL matching or have manual categories' );
			return $url_matches;
		}

		// STEP 4: Group remaining keywords by language for AI matching
		$keywords_by_language = $this->group_keywords_by_language( $keywords_for_ai );

		$this->logger->info(
			'AI Category Matching started (language-aware)',
			array(
				'total_keywords'        => count( $keywords ),
				'languages_detected'    => array_keys( $keywords_by_language ),
				'language_distribution' => array_map( 'count', $keywords_by_language ),
			)
		);

		if ( WP_DEBUG ) {
			error_log( 'AI Category Matching: ' . count( $keywords ) . ' keywords grouped into ' . count( $keywords_by_language ) . ' languages' );
		}

		$this->log_progress(
			$progress_callback,
			array(
				'step'           => 'init',
				'total_keywords' => count( $keywords ),
				'languages'      => count( $keywords_by_language ),
			)
		);

		$all_matches     = array();
		$language_number = 0;
		$total_languages = count( $keywords_by_language );

		// Process each language group separately
		foreach ( $keywords_by_language as $language_code => $lang_keywords ) {
			++$language_number;

			$this->logger->info(
				'Processing language group',
				array(
					'language'       => $language_code,
					'keywords_count' => count( $lang_keywords ),
					'progress'       => "{$language_number}/{$total_languages}",
				)
			);

			if ( WP_DEBUG ) {
				error_log( "AI Matching: Processing language '{$language_code}' ({$language_number}/{$total_languages}) with " . count( $lang_keywords ) . ' keywords' );
			}

			// Get categories ONLY for this language
			$categories = $this->get_categories_for_ai( $language_code );

			if ( empty( $categories ) ) {
				$this->logger->warning(
					'No categories found for language, skipping',
					array(
						'language'          => $language_code,
						'keywords_affected' => count( $lang_keywords ),
					)
				);

				if ( WP_DEBUG ) {
					error_log( "AI Matching: No categories for language '{$language_code}', skipping " . count( $lang_keywords ) . ' keywords' );
				}

				// Log as no matches (strict mode: no categories = no matches)
				$this->log_progress(
					$progress_callback,
					array(
						'step'             => 'language_skipped',
						'language'         => $language_code,
						'reason'           => 'no_categories',
						'keywords_skipped' => count( $lang_keywords ),
					)
				);

				continue;
			}

			$this->log_progress(
				$progress_callback,
				array(
					'step'       => 'language_start',
					'language'   => $language_code,
					'keywords'   => count( $lang_keywords ),
					'categories' => count( $categories ),
				)
			);

			// Process this language's keywords in batches
			$batch_size    = 10; // Smaller batches for better reliability
			$total_batches = ceil( count( $lang_keywords ) / $batch_size );
			$current_batch = 0;

			for ( $i = 0; $i < count( $lang_keywords ); $i += $batch_size ) {
				$batch = array_slice( $lang_keywords, $i, $batch_size );
				++$current_batch;

				$this->log_progress(
					$progress_callback,
					array(
						'step'          => 'batch_start',
						'language'      => $language_code,
						'batch'         => $current_batch,
						'total_batches' => $total_batches,
						'batch_size'    => count( $batch ),
					)
				);

				if ( WP_DEBUG ) {
					error_log( "AI Matching [{$language_code}]: Batch {$current_batch}/{$total_batches} with " . count( $batch ) . ' keywords, ' . count( $categories ) . ' categories' );
				}

				$batch_matches = $this->process_batch( $batch, $categories );

				if ( is_wp_error( $batch_matches ) ) {
					$this->logger->error(
						'Batch processing failed',
						array(
							'language' => $language_code,
							'batch'    => $current_batch,
							'error'    => $batch_matches->get_error_message(),
						)
					);
					continue;
				}

				$this->logger->info(
					'Batch processed',
					array(
						'language'         => $language_code,
						'batch'            => $current_batch,
						'matches_returned' => count( $batch_matches ),
					)
				);

				if ( empty( $batch_matches ) ) {
					$this->logger->warning(
						'Batch returned 0 matches',
						array(
							'language'          => $language_code,
							'batch'             => $current_batch,
							'keywords_in_batch' => array_column( $batch, 'keyword' ),
						)
					);
				}

				$all_matches = array_merge( $all_matches, $batch_matches );

				if ( WP_DEBUG ) {
					error_log( "AI Matching [{$language_code}]: Batch {$current_batch}/{$total_batches} completed - " . count( $batch_matches ) . ' matches' );
				}

				$this->log_progress(
					$progress_callback,
					array(
						'step'         => 'batch_complete',
						'language'     => $language_code,
						'batch'        => $current_batch,
						'matches'      => count( $batch_matches ),
						'total_so_far' => count( $all_matches ),
					)
				);

				// Rate limiting: 500ms delay between batches
				if ( $current_batch < $total_batches ) {
					$this->rate_limiter->smart_delay( 'openai', 0.5 );
				}
			}

			$this->log_progress(
				$progress_callback,
				array(
					'step'               => 'language_complete',
					'language'           => $language_code,
					'keywords_processed' => count( $lang_keywords ),
					'matches_found'      => count(
						array_filter(
							$all_matches,
							function ( $m ) use ( $lang_keywords ) {
								return in_array( $m['keyword_id'], array_column( $lang_keywords, 'id' ) );
							}
						)
					),
				)
			);
		}

		// STEP 5: Merge URL matches with AI matches
		$final_matches = array_merge( $url_matches, $all_matches );

		$this->log_progress(
			$progress_callback,
			array(
				'step'                => 'complete',
				'url_matches'         => count( $url_matches ),
				'ai_matches'          => count( $all_matches ),
				'total_matches'       => count( $final_matches ),
				'languages_processed' => count( $keywords_by_language ),
			)
		);

		if ( WP_DEBUG ) {
			error_log( 'AI Category Matching: Complete! URL matches: ' . count( $url_matches ) . ', AI matches: ' . count( $all_matches ) . ', Total: ' . count( $final_matches ) );
		}

		$this->logger->info(
			'Smart matching complete',
			array(
				'url_based' => count( $url_matches ),
				'ai_based'  => count( $all_matches ),
				'total'     => count( $final_matches ),
			)
		);

		return $final_matches;
	}

	/**
	 * Auto-match keywords to categories from ranking URLs
	 *
	 * For keywords with ranking URLs, match the URL to a WordPress post
	 * and extract its categories. This is fast, accurate, and free!
	 *
	 * @param array         $keywords Keywords to check
	 * @param callable|null $progress_callback Progress callback
	 * @return array Array of matches with category_ids
	 */
	private function auto_match_from_urls( $keywords, $progress_callback = null ) {
		// Load URL Post Matcher
		if ( ! class_exists( 'Wisewand\\Utils\\URL_Post_Matcher' ) ) {
			require_once WISEWAND_PLUGIN_DIR . 'includes/Utils/URL_Post_Matcher.php';
		}

		$url_matcher = new URL_Post_Matcher( $this->logger );
		$matches     = array();

		$this->log_progress(
			$progress_callback,
			array(
				'step'     => 'url_matching_start',
				'keywords' => count( $keywords ),
			)
		);

		$matched_count = 0;

		foreach ( $keywords as $keyword ) {
			// Skip if no URL data
			if ( empty( $keyword['page_url'] ) ) {
				continue;
			}

			$language_code = $keyword['language_code'] ?? 'en';

			// Try to match URL to post
			$match_result = $url_matcher->match_url_to_post( $keyword['page_url'], $language_code );

			if ( $match_result && ! empty( $match_result['category_ids'] ) ) {
				global $wpdb;
				$table = $wpdb->prefix . 'wisewand_saved_keywords';

				// Update keyword with matched post and categories
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom plugin table update for URL-based category matching
				$wpdb->update(
					$table,
					array(
						'wordpress_post_id'         => $match_result['post_id'],
						'auto_categorized_from_url' => 1,
						'url_match_confidence'      => $match_result['confidence'],
						'url_matched_at'            => current_time( 'mysql' ),
					),
					array( 'id' => $keyword['id'] ),
					array( '%d', '%d', '%f', '%s' ),
					array( '%d' )
				);

				// Store match
				$matches[] = array(
					'keyword_id'   => $keyword['id'],
					'category_ids' => $match_result['category_ids'],
					'confidence'   => $match_result['confidence'],
					'reasoning'    => 'Auto-detected from ranking URL (post #' . $match_result['post_id'] . ', ' . $match_result['strategy'] . ')',
					'ai_model'     => 'url_matcher',
				);

				++$matched_count;

				$this->logger->info(
					'URL match success',
					array(
						'keyword_id'       => $keyword['id'],
						'keyword'          => $keyword['keyword'],
						'post_id'          => $match_result['post_id'],
						'categories_found' => count( $match_result['category_ids'] ),
						'confidence'       => $match_result['confidence'],
					)
				);
			}
		}

		$this->log_progress(
			$progress_callback,
			array(
				'step'           => 'url_matching_complete',
				'matched'        => $matched_count,
				'total_keywords' => count( $keywords ),
			)
		);

		$this->logger->info(
			'URL-based matching complete',
			array(
				'total_keywords'    => count( $keywords ),
				'matched_from_urls' => $matched_count,
				'match_rate'        => count( $keywords ) > 0 ? round( ( $matched_count / count( $keywords ) ) * 100, 2 ) . '%' : '0%',
			)
		);

		return $matches;
	}

	/**
	 * Filter keywords that need AI processing
	 *
	 * Removes:
	 * - Keywords already matched via URL
	 * - Keywords with manual categories (if respect_manual is true)
	 *
	 * @param array $keywords All keywords
	 * @param array $url_matches Keywords matched via URL
	 * @param bool  $respect_manual Respect manual categories
	 * @return array Keywords that need AI processing
	 */
	private function filter_keywords_for_ai( $keywords, $url_matches, $respect_manual ) {
		$url_matched_ids = array_column( $url_matches, 'keyword_id' );

		$filtered = array_filter(
			$keywords,
			function ( $kw ) use ( $url_matched_ids, $respect_manual ) {
				// Skip if already matched via URL
				if ( in_array( $kw['id'], $url_matched_ids ) ) {
					return false;
				}

				// Check if has manual categories (respect user choices)
				if ( $respect_manual ) {
					$suggested_categories = $kw['suggested_categories'] ?? null;

					// Has manual categories if:
					// - suggested_categories is not empty
					// - AND it was set before URL matching (check ai_matched_at)
					// - AND it's not auto-generated (check auto_categorized_from_url)
					if ( ! empty( $suggested_categories ) &&
					$suggested_categories !== '[]' &&
					$suggested_categories !== 'null' &&
					empty( $kw['auto_categorized_from_url'] ) ) {

						$this->logger->debug(
							'Skipping keyword with manual categories',
							array(
								'keyword_id' => $kw['id'],
								'keyword'    => $kw['keyword'],
							)
						);

						return false;
					}
				}

				return true;
			}
		);

		return array_values( $filtered );
	}

	/**
	 * Process a batch of keywords
	 *
	 * @param array $batch Keywords batch
	 * @param array $categories Available categories
	 * @return array|WP_Error Matches or error
	 */
	private function process_batch( $batch, $categories ) {
		// Build ultra-strict prompt
		$messages = array(
			array(
				'role'    => 'system',
				'content' => $this->get_system_prompt(),
			),
			array(
				'role'    => 'user',
				'content' => $this->build_user_prompt( $batch, $categories ),
			),
		);

		// Use Structured Outputs with JSON Schema for guaranteed valid responses
		$response = $this->openai_client->chat_completion(
			$messages,
			array(
				'temperature'     => 0.3, // Low temperature for consistent results
				'max_tokens'      => 1500,
				'response_format' => array(
					'type'        => 'json_schema',
					'json_schema' => array(
						'name'   => 'category_matching_response',
						'strict' => true,
						'schema' => array(
							'type'                 => 'object',
							'properties'           => array(
								'matches' => array(
									'type'  => 'array',
									'items' => array(
										'type'       => 'object',
										'properties' => array(
											'keyword_id'   => array(
												'type' => 'integer',
												'description' => 'The ID of the keyword being matched',
											),
											'category_ids' => array(
												'type'  => 'array',
												'description' => 'Array of matched category IDs (1-3 max, can be empty)',
												'items' => array(
													'type' => 'integer',
												),
											),
											'confidence'   => array(
												'type' => 'number',
												'description' => 'Confidence score 0.0 to 1.0',
											),
											'reasoning'    => array(
												'type' => 'string',
												'description' => 'Brief explanation of the match',
											),
										),
										'required'   => array( 'keyword_id', 'category_ids', 'confidence', 'reasoning' ),
										'additionalProperties' => false,
									),
								),
							),
							'required'             => array( 'matches' ),
							'additionalProperties' => false,
						),
					),
				),
			)
		);

		if ( is_wp_error( $response ) ) {
			$this->logger->error(
				'OpenAI API call failed in batch',
				array(
					'error'      => $response->get_error_message(),
					'batch_size' => count( $batch ),
				)
			);
			return $response;
		}

		$this->logger->debug(
			'OpenAI response received',
			array(
				'batch_size'           => count( $batch ),
				'response_has_matches' => isset( $response['matches'] ),
				'response_keys'        => array_keys( $response ),
			)
		);

		// Validate and extract matches
		return $this->validate_and_extract_matches( $response, $batch, $categories );
	}

	/**
	 * Get system prompt with enhanced matching guidelines
	 *
	 * Designed for 85-90% completion rate while maintaining quality
	 *
	 * @return string System prompt
	 */
	private function get_system_prompt() {
		return 'You are an expert WordPress content strategist specializing in SEO and content categorization.

CORE MISSION: Match keywords to the most relevant WordPress categories with reasonable confidence.

═══════════════════════════════════════════════════════════════════════
CRITICAL INSTRUCTIONS - FOLLOW EXACTLY:
═══════════════════════════════════════════════════════════════════════

1. You MUST return ONLY valid JSON in the exact format specified
2. You MUST ONLY use category IDs from the provided categories list
3. NEVER invent, create, or suggest category IDs not in the list
4. Match 1-3 categories maximum per keyword (quality over quantity)
5. First category = PRIMARY (most relevant), then secondary/related categories
6. Consider: semantic relevance, target audience, search intent, and language

═══════════════════════════════════════════════════════════════════════
CONFIDENCE GUIDELINES (IMPORTANT - READ CAREFULLY):
═══════════════════════════════════════════════════════════════════════

MATCH if confidence >= 0.55 (reasonably confident)

Confidence Tiers:

🟢 EXCELLENT (0.85-1.0): Exact or near-exact semantic match
   Example: "bedtime stories" → "Bedtime Stories" = 0.95
   Example: "histoires pour enfants" → "Histoires pour Enfants" = 0.92

🟢 STRONG (0.70-0.84): Clear semantic relationship
   Example: "funny bedtime stories" → "Bedtime Stories" = 0.78
   Example: "contes drôles" → "Contes" = 0.75

🟡 GOOD (0.55-0.69): Related topic or broader category
   Example: "short bedtime stories" → "Stories" = 0.62
   Example: "poésie pour enfant" → "Littérature Jeunesse" = 0.58
   ⚠️ STILL MATCH THESE! Better than no category.

🔴 WEAK (0.40-0.54): Tangentially related
   Example: "educational games" → "Education" = 0.48
   → DO NOT match (too weak)

🔴 NO MATCH (0.0-0.39): No relevant category exists
   Example: "quantum physics stories" → (no quantum category) = 0.0
   → Return empty category_ids array

KEY PRINCIPLE: A 0.55-0.65 match is GOOD ENOUGH if the category is semantically related.
PREFER: Matching with medium confidence over no match at all.

═══════════════════════════════════════════════════════════════════════
MATCHING STRATEGY:
═══════════════════════════════════════════════════════════════════════

Step 1: Look for EXACT match (name/slug similarity)
Step 2: Look for SEMANTIC match (meaning/topic similarity)
Step 3: Look for BROADER category (parent topic if no specific match)
Step 4: If all above < 0.55 confidence → Return empty array

LANGUAGE AWARENESS:
- French keywords should match French categories
- English keywords should match English categories
- Pay attention to language field in keyword data
- Consider linguistic nuances and cultural context

═══════════════════════════════════════════════════════════════════════
RESPONSE FORMAT (JSON ONLY):
═══════════════════════════════════════════════════════════════════════

Structure: {"matches": [...]}

Each match object:
{
  "keyword_id": 123,              // Integer from keywords list
  "category_ids": [5, 12],        // Array of integers from categories list (1-3 max)
  "confidence": 0.75,             // Float 0.0-1.0
  "reasoning": "Short explanation" // Brief, clear reasoning
}

═══════════════════════════════════════════════════════════════════════
EXAMPLES OF CORRECT RESPONSES:
═══════════════════════════════════════════════════════════════════════

Example 1 - Excellent match (0.95):
{"matches":[{"keyword_id":123,"category_ids":[5],"confidence":0.95,"reasoning":"Exact match: Bedtime Stories"}]}

Example 2 - Strong match with secondary (0.78):
{"matches":[{"keyword_id":124,"category_ids":[5,12],"confidence":0.78,"reasoning":"Primary: Stories, Secondary: Kids"}]}

Example 3 - Good match, broader category (0.60):
{"matches":[{"keyword_id":125,"category_ids":[18],"confidence":0.60,"reasoning":"Related topic under Literature"}]}

Example 4 - No suitable match (0.0):
{"matches":[{"keyword_id":126,"category_ids":[],"confidence":0.0,"reasoning":"No relevant category available"}]}

Example 5 - Medium confidence but still match (0.58):
{"matches":[{"keyword_id":127,"category_ids":[9],"confidence":0.58,"reasoning":"Semantically related to Education"}]}

═══════════════════════════════════════════════════════════════════════
VALIDATION REQUIREMENTS:
═══════════════════════════════════════════════════════════════════════

✓ Response is valid JSON (no markdown, no code blocks)
✓ All category_ids are integers from the provided categories list
✓ category_ids is array (can be empty if no match >= 0.55)
✓ confidence is float between 0.0 and 1.0
✓ reasoning is string (brief, clear)
✓ Match if confidence >= 0.55

═══════════════════════════════════════════════════════════════════════
REMEMBER:
═══════════════════════════════════════════════════════════════════════

• Better to match with 0.55-0.65 confidence than return empty
• Use parent/broader categories if no specific match exists
• Consider user intent and content type
• Language matters: FR keywords need FR categories
• Quality is important, but so is providing helpful matches

Your goal: Provide the best possible category matches while being reasonably confident (>= 0.55).';
	}

	/**
	 * Build user prompt with keywords and categories
	 *
	 * @param array $batch Keywords batch
	 * @param array $categories Available categories
	 * @return string User prompt as JSON
	 */
	private function build_user_prompt( $batch, $categories ) {
		// Prepare keywords array
		$keywords_data = array();
		foreach ( $batch as $keyword ) {
			$keywords_data[] = array(
				'id'            => intval( $keyword['id'] ),
				'keyword'       => $keyword['keyword'],
				'search_volume' => intval( $keyword['search_volume'] ?? 0 ),
				'search_intent' => $keyword['search_intent'] ?? 'informational',
				'language'      => $keyword['language_code'] ?? 'en',
			);
		}

		// Prepare categories array
		$categories_data = array();
		foreach ( $categories as $category ) {
			$categories_data[] = array(
				'id'          => intval( $category->term_id ),
				'name'        => $category->name,
				'slug'        => $category->slug,
				'parent_id'   => intval( $category->parent ),
				'parent_name' => $category->parent ? ( get_category( $category->parent )->name ?? '' ) : '',
				'description' => $category->description ?: 'No description',
				'count'       => intval( $category->count ),
			);
		}

		$prompt_data = array(
			'task'                => 'match_keywords_to_categories',
			'instructions'        => 'Match each keyword to the most relevant WordPress categories with reasonable confidence (>= 0.55). Return ONLY the JSON structure specified in the system prompt. Use ONLY category IDs from the provided list. Match with confidence 0.55-0.69 is GOOD and should be included. Only return empty array if truly no relevant category exists (<0.55 confidence).',
			'keywords'            => $keywords_data,
			'categories'          => $categories_data,
			'matching_guidelines' => array(
				'Confidence >= 0.85: Excellent match (exact or near-exact)',
				'Confidence 0.70-0.84: Strong match (clear semantic relationship)',
				'Confidence 0.55-0.69: Good match (related topic) - STILL MATCH!',
				'Confidence < 0.55: Too weak - return empty array',
				'Better to match at 0.55-0.65 than return nothing',
				'Use broader/parent categories if no specific match exists',
			),
			'strict_rules'        => array(
				'Use ONLY category IDs from the categories array above',
				'Return category_ids as array of integers (1-3 max)',
				'Match if confidence >= 0.55 (be reasonably confident)',
				'First ID = primary category, rest = secondary',
				'Consider language: FR keywords → FR categories, EN → EN',
				'Consider hierarchy: prefer child categories when specific',
			),
		);

		return wp_json_encode( $prompt_data, JSON_PRETTY_PRINT );
	}

	/**
	 * Validate and extract matches from OpenAI response
	 *
	 * @param array $response OpenAI response
	 * @param array $batch Keywords batch
	 * @param array $categories Available categories
	 * @return array Validated matches
	 */
	private function validate_and_extract_matches( $response, $batch, $categories ) {
		$this->logger->debug(
			'Validating OpenAI response',
			array(
				'response_type'   => gettype( $response ),
				'has_matches_key' => isset( $response['matches'] ),
				'response_keys'   => is_array( $response ) ? array_keys( $response ) : 'not_array',
				'batch_size'      => count( $batch ),
			)
		);

		if ( ! isset( $response['matches'] ) || ! is_array( $response['matches'] ) ) {
			$this->logger->error(
				'OpenAI response missing matches array',
				array(
					'response'           => $response,
					'response_structure' => is_array( $response ) ? 'array but no matches key' : 'not an array',
				)
			);
			return array();
		}

		$this->logger->info(
			'Processing OpenAI matches',
			array(
				'total_matches_from_ai' => count( $response['matches'] ),
				'batch_size'            => count( $batch ),
			)
		);

		$valid_category_ids = array_map(
			function ( $cat ) {
				return intval( $cat->term_id );
			},
			$categories
		);

		$validated_matches = array();

		foreach ( $response['matches'] as $match ) {
			// Validate structure
			if ( ! isset( $match['keyword_id'] ) || ! isset( $match['category_ids'] ) ) {
				$this->logger->warning( 'Invalid match structure', array( 'match' => $match ) );
				continue;
			}

			$keyword_id   = intval( $match['keyword_id'] );
			$category_ids = is_array( $match['category_ids'] ) ? $match['category_ids'] : array();

			// CRITICAL: Filter out any category IDs not in our list
			$filtered_category_ids = array_filter(
				$category_ids,
				function ( $id ) use ( $valid_category_ids ) {
					return in_array( intval( $id ), $valid_category_ids );
				}
			);

			$filtered_category_ids = array_map( 'intval', array_values( $filtered_category_ids ) );

			// Store validated match
			$validated_matches[] = array(
				'keyword_id'   => $keyword_id,
				'category_ids' => $filtered_category_ids,
				'confidence'   => floatval( $match['confidence'] ?? 0.0 ),
				'reasoning'    => sanitize_text_field( $match['reasoning'] ?? '' ),
				'ai_model'     => 'gpt-4o-mini',
			);

			// Log if AI suggested invalid categories
			$invalid_ids = array_diff( $category_ids, $filtered_category_ids );
			if ( ! empty( $invalid_ids ) ) {
				$this->logger->warning(
					'AI suggested non-existent category IDs',
					array(
						'keyword_id'  => $keyword_id,
						'invalid_ids' => $invalid_ids,
						'filtered_to' => $filtered_category_ids,
					)
				);
			}
		}

		$this->logger->info(
			'Validation complete',
			array(
				'original_matches'  => count( $response['matches'] ),
				'validated_matches' => count( $validated_matches ),
				'filtered_out'      => count( $response['matches'] ) - count( $validated_matches ),
			)
		);

		return $validated_matches;
	}

	/**
	 * Group keywords by language for separate processing
	 *
	 * @param array $keywords Keywords to group
	 * @return array Keywords grouped by language code
	 */
	private function group_keywords_by_language( $keywords ) {
		$grouped = array();

		foreach ( $keywords as $kw ) {
			$lang = $kw['language_code'] ?? 'en';

			if ( ! isset( $grouped[ $lang ] ) ) {
				$grouped[ $lang ] = array();
			}

			$grouped[ $lang ][] = $kw;
		}

		// Sort by language code for consistent processing
		ksort( $grouped );

		return $grouped;
	}

	/**
	 * Get WordPress categories for AI matching (language-aware)
	 *
	 * Supports multiple multilingual plugins with graceful fallback:
	 * 1. WPML (most popular)
	 * 2. Polylang
	 * 3. Custom term meta (_category_language)
	 * 4. Fallback: all categories
	 *
	 * @param string $language_code Language code (fr, en, etc.) or 'all'
	 * @return array Filtered categories for the specified language
	 */
	private function get_categories_for_ai( $language_code = 'all' ) {
		// If 'all' requested, return everything (legacy behavior)
		if ( 'all' === $language_code ) {
			$categories = get_categories(
				array(
					'hide_empty' => false,
					'orderby'    => 'name',
					'order'      => 'ASC',
				)
			);

			$this->logger->info(
				'Categories loaded (all languages)',
				array(
					'count' => count( $categories ),
				)
			);

			return $categories;
		}

		$this->logger->info(
			'Loading categories for specific language',
			array(
				'language' => $language_code,
			)
		);

		// Try WPML integration
		if ( $this->options->is_wpml_active() ) {
			$categories = $this->get_wpml_categories( $language_code );

			$this->logger->info(
				'Categories loaded via WPML',
				array(
					'language' => $language_code,
					'count'    => count( $categories ),
				)
			);

			return $categories;
		}

		// Try Polylang integration
		if ( $this->options->is_polylang_active() ) {
			$categories = $this->get_polylang_categories( $language_code );

			$this->logger->info(
				'Categories loaded via Polylang',
				array(
					'language' => $language_code,
					'count'    => count( $categories ),
				)
			);

			return $categories;
		}

		// Try custom term meta
		$categories = $this->get_categories_by_meta_language( $language_code );

		if ( ! empty( $categories ) ) {
			$this->logger->info(
				'Categories loaded via custom meta',
				array(
					'language' => $language_code,
					'count'    => count( $categories ),
				)
			);

			return $categories;
		}

		// No multilingual plugin detected - check configuration mode
		$openai_settings = $this->options->get_openai_settings();
		$matching_mode   = $openai_settings['category_matching_mode'] ?? 'auto';

		// If strict mode is explicitly enabled, return empty
		if ( 'strict' === $matching_mode ) {
			$this->logger->warning(
				'Strict mode enabled - no categories for language',
				array(
					'language'  => $language_code,
					'mode'      => 'strict',
					'returning' => 'empty array',
				)
			);

			return array();
		}

		// Default/Auto/Fallback mode: return all categories
		// This allows single-language sites to use AI matching without WPML/Polylang
		$this->logger->info(
			'No multilingual support detected - using all categories as fallback',
			array(
				'language'        => $language_code,
				'fallback_mode'   => 'all_categories',
				'configured_mode' => $matching_mode,
				'note'            => 'Single-language site or no language meta configured',
			)
		);

		// Fallback: return all categories when no multilingual plugin or meta
		$all_categories = get_categories(
			array(
				'hide_empty' => false,
				'orderby'    => 'name',
				'order'      => 'ASC',
			)
		);

		$this->logger->info(
			'Fallback categories loaded successfully',
			array(
				'language_requested'  => $language_code,
				'categories_returned' => count( $all_categories ),
				'mode'                => 'single_language_fallback',
				'category_names'      => array_slice( array_column( $all_categories, 'name' ), 0, 5 ),
			)
		);

		return $all_categories; // ✅ FIX: Return all categories for single-language sites
	}

	/**
	 * Get categories for specific language using WPML
	 *
	 * @param string $language_code WPML language code
	 * @return array Categories in specified language
	 */
	private function get_wpml_categories( $language_code ) {
		global $sitepress;

		// Fallback if $sitepress not available
		if ( ! $sitepress ) {
			$this->logger->warning( 'WPML active but $sitepress global not available' );
			return array();
		}

		// Save current language
		$original_lang = $sitepress->get_current_language();

		// Switch to target language
		$sitepress->switch_lang( $language_code );

		// Get categories in target language
		$categories = get_categories(
			array(
				'hide_empty' => false,
				'orderby'    => 'name',
				'order'      => 'ASC',
			)
		);

		// Switch back to original language
		$sitepress->switch_lang( $original_lang );

		$this->logger->debug(
			'WPML categories fetched',
			array(
				'language'          => $language_code,
				'original_language' => $original_lang,
				'categories_found'  => count( $categories ),
				'sample_names'      => array_slice( array_column( $categories, 'name' ), 0, 5 ),
			)
		);

		return $categories;
	}

	/**
	 * Get categories for specific language using Polylang
	 *
	 * @param string $language_code Polylang language code
	 * @return array Categories in specified language
	 */
	private function get_polylang_categories( $language_code ) {
		if ( ! function_exists( 'pll_get_term_language' ) ) {
			$this->logger->warning( 'Polylang functions not available' );
			return array();
		}

		// Use get_terms() with 'lang' parameter - Polylang filters automatically!
		$categories = get_terms(
			array(
				'taxonomy'   => 'category',
				'lang'       => $language_code,
				'hide_empty' => false,
				'orderby'    => 'name',
				'order'      => 'ASC',
			)
		);

		// get_terms() can return WP_Error on failure
		if ( is_wp_error( $categories ) ) {
			$this->logger->error(
				'Polylang get_terms failed',
				array(
					'error'    => $categories->get_error_message(),
					'language' => $language_code,
				)
			);
			return array();
		}

		$this->logger->debug(
			'Polylang categories retrieved',
			array(
				'language' => $language_code,
				'count'    => count( $categories ),
			)
		);

		return $categories;
	}

	/**
	 * Get categories by custom language meta field
	 *
	 * Falls back to this if no multilingual plugin detected
	 *
	 * @param string $language_code Language code
	 * @return array Categories with matching language meta
	 */
	private function get_categories_by_meta_language( $language_code ) {
		$all_categories = get_categories(
			array(
				'hide_empty' => false,
				'orderby'    => 'name',
				'order'      => 'ASC',
			)
		);

		$filtered = array_filter(
			$all_categories,
			function ( $cat ) use ( $language_code ) {
				// Check for custom meta field
				$cat_lang = get_term_meta( $cat->term_id, '_category_language', true );

				if ( ! $cat_lang ) {
					// Try alternative meta field names
					$cat_lang = get_term_meta( $cat->term_id, 'language', true );
				}

				return $cat_lang === $language_code;
			}
		);

		$this->logger->debug(
			'Categories filtered by custom meta',
			array(
				'language'         => $language_code,
				'total_categories' => count( $all_categories ),
				'filtered_count'   => count( $filtered ),
			)
		);

		return array_values( $filtered );
	}

	/**
	 * Save matches to database with default author assignment
	 *
	 * @param array $matches Array of match results
	 * @param array $batch_keywords Original keywords for language detection
	 * @return int Number of keywords updated
	 */
	public function save_matches( $matches, $batch_keywords = array() ) {
		global $wpdb;
		$table   = $wpdb->prefix . 'wisewand_saved_keywords';
		$updated = 0;

		// Create lookup for keyword language
		$keyword_langs = array();
		foreach ( $batch_keywords as $kw ) {
			$keyword_langs[ $kw['id'] ] = $kw['language_code'] ?? 'en';
		}

		foreach ( $matches as $match ) {
			// Determine default author based on keyword language
			$keyword_lang      = $keyword_langs[ $match['keyword_id'] ] ?? 'en';
			$default_author_id = $this->get_default_author_for_language( $keyword_lang );

            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom plugin table update for AI category matching
			$result = $wpdb->update(
				$table,
				array(
					'suggested_author_id'  => $default_author_id, // Auto-set based on language
					'suggested_categories' => ! empty( $match['category_ids'] ) ? wp_json_encode( $match['category_ids'] ) : null,
					'ai_matched_at'        => current_time( 'mysql' ),
					'ai_model_used'        => $match['ai_model'] ?? 'gpt-4o-mini',
				),
				array( 'id' => $match['keyword_id'] ),
				array( '%d', '%s', '%s', '%s' ),
				array( '%d' )
			);

			if ( $result ) {
				++$updated;
			}
		}

		$this->logger->info(
			'AI matches saved to database',
			array(
				'total_matches' => count( $matches ),
				'updated'       => $updated,
			)
		);

		return $updated;
	}

	/**
	 * Get default author ID for a language
	 *
	 * Uses centralized Options::get_author_for_language() for consistent fallback logic
	 *
	 * @param string $lang Language code
	 * @return int|null Author ID
	 */
	private function get_default_author_for_language( $lang ) {
		return $this->options->get_author_for_language( $lang );
	}

	/**
	 * Log progress callback
	 */
	private function log_progress( $callback, $data ) {
		if ( $callback && is_callable( $callback ) ) {
			call_user_func( $callback, $data );
		}

		if ( $this->logger && ! empty( $data['step'] ) ) {
			$this->logger->debug( 'AI Category Matching: ' . $data['step'], $data );
		}
	}

	/**
	 * Detect the matching mode based on active plugins
	 *
	 * @return string Matching mode: 'wpml', 'polylang', 'custom_meta', or 'fallback'
	 */
	private function detect_matching_mode() {
		if ( $this->options->is_wpml_active() ) {
			return 'wpml';
		}

		if ( $this->options->is_polylang_active() ) {
			return 'polylang';
		}

		// Check if any categories have custom language meta
		$sample_categories = get_categories(
			array(
				'hide_empty' => false,
				'number'     => 10,
			)
		);

		foreach ( $sample_categories as $cat ) {
			$lang_meta = get_term_meta( $cat->term_id, '_category_language', true );
			if ( ! empty( $lang_meta ) ) {
				return 'custom_meta';
			}
		}

		return 'fallback';
	}

	/**
	 * Log informative message about matching mode
	 *
	 * @param string $mode Matching mode
	 */
	private function log_matching_mode_info( $mode ) {
		$messages = array(
			'wpml'        => '✅ WPML detected - Using language-specific categories',
			'polylang'    => '✅ Polylang detected - Using language-specific categories',
			'custom_meta' => '✅ Custom language meta detected - Using language-specific categories',
			'fallback'    => 'ℹ️  No multilingual plugin detected - Using all categories for matching (single-language mode)',
		);

		$message = $messages[ $mode ] ?? 'Unknown mode';

		$this->logger->info(
			'AI Category Matching Mode: ' . $mode,
			array(
				'mode'            => $mode,
				'message'         => $message,
				'wpml_active'     => $this->options->is_wpml_active(),
				'polylang_active' => $this->options->is_polylang_active(),
			)
		);

		// Also log to error_log for easy debugging
		if ( WP_DEBUG ) {
			error_log( "AI Category Matching: {$message}" );
		}
	}
}
