<?php
/**
 * SEO AI Matcher Handler
 *
 * Handles all AJAX requests related to AI-powered category matching: running
 * the OpenAI/Category_Matcher pipeline against saved keywords, counting
 * keywords by filter mode, saving inline author/category edits, and checking
 * for ranking conflicts before calendar scheduling. Extracted from
 * SEO_Intelligence_Real for single-responsibility organisation.
 *
 * @package Wisewand
 * @since 2.8.2
 * @license GPL-2.0-or-later
 */

namespace Wisewand\Admin\SEO;

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

use Wisewand\Admin\Traits\AJAX_Security;

// phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_error_log, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- error_log() used within WP_DEBUG conditionals; all AJAX handlers verify nonce before processing $_POST data; direct database calls required for custom plugin table operations

/**
 * Handles SEO AI category matching AJAX requests
 *
 * Runs the OpenAI-backed Category_Matcher pipeline against batches of saved
 * keywords, returns keyword counts by filter mode for the UI, allows inline
 * author/category editing, and checks for ranking conflicts using the
 * Keyword_Deduplication_Engine. Depends on SEO_Handler_Context for shared
 * services.
 *
 * @package Wisewand
 * @since 2.8.2
 */
class SEO_AI_Matcher_Handler {

	use AJAX_Security;

	/**
	 * Shared handler context (dependencies)
	 *
	 * @since 2.8.2
	 * @var SEO_Handler_Context
	 */
	private $context;

	/**
	 * Create a new AI matcher handler
	 *
	 * @since 2.8.2
	 *
	 * @param SEO_Handler_Context $context Shared dependencies DTO.
	 */
	public function __construct( SEO_Handler_Context $context ) {
		$this->context = $context;
	}

	// =========================================================================
	// PRIVATE LAZY HELPERS
	// =========================================================================

	/**
	 * Cached Category_Matcher instance
	 *
	 * @since 2.8.7
	 * @var \Wisewand\Utils\Category_Matcher|null
	 */
	private $category_matcher = null;

	/**
	 * Get (or create) the Category_Matcher instance
	 *
	 * @since 2.8.7
	 *
	 * @return \Wisewand\Utils\Category_Matcher
	 */
	private function get_category_matcher() {
		if ( null === $this->category_matcher ) {
			if ( ! class_exists( 'Wisewand\\Utils\\Category_Matcher' ) ) {
				require_once WISEWAND_PLUGIN_DIR . 'includes/Utils/Category_Matcher.php';
			}
			$this->category_matcher = new \Wisewand\Utils\Category_Matcher(
				$this->context->options,
				$this->context->logger
			);
		}
		return $this->category_matcher;
	}

	// =========================================================================
	// PUBLIC AJAX HANDLERS
	// =========================================================================

	/**
	 * Save keyword author and categories via inline table edit
	 *
	 * Updates the suggested author and/or category assignments for a single
	 * keyword record in the saved-keywords library. Either or both fields may
	 * be present in the POST payload; at least one must be supplied.
	 *
	 * @since 2.8.2
	 */
	public function ajax_save_keyword_author_categories() {
		$this->verify_ajax_request( 'wisewand_seo_real' );

		$keyword_id = absint( $_POST['keyword_id'] ?? 0 );
		if ( ! $keyword_id ) {
			wp_send_json_error( array( 'message' => esc_html__( 'Invalid keyword ID', 'wisewand-ai-content-writer-seo-content-generator' ) ) );
			return;
		}

		$update_data = array();

		// Update author if provided.
		if ( isset( $_POST['author_id'] ) ) {
			$update_data['suggested_author_id'] = absint( $_POST['author_id'] );
		}

		// Update categories if provided.
		if ( isset( $_POST['category_ids'] ) ) {
			$category_ids                        = array_map( 'absint', $_POST['category_ids'] );
			$update_data['suggested_categories'] = ! empty( $category_ids ) ? wp_json_encode( $category_ids ) : null;
		}

		if ( empty( $update_data ) ) {
			wp_send_json_error( array( 'message' => esc_html__( 'No data to update', 'wisewand-ai-content-writer-seo-content-generator' ) ) );
			return;
		}

		$result = $this->context->keyword_repository->update_keyword( $keyword_id, $update_data );

		if ( false === $result ) {
			wp_send_json_error( array( 'message' => esc_html__( 'Database error', 'wisewand-ai-content-writer-seo-content-generator' ) ) );
			return;
		}

		wp_send_json_success( array( 'message' => esc_html__( 'Saved', 'wisewand-ai-content-writer-seo-content-generator' ) ) );
	}

	/**
	 * AI-match WordPress categories to saved keywords
	 *
	 * Operates in two modes depending on whether keyword_ids is present in
	 * the POST payload:
	 *
	 * - Init mode (no keyword_ids): filters saved keywords according to
	 *   filter_mode, returns the matching IDs and batch parameters for
	 *   client-side batching so the browser can drive progress.
	 *
	 * - Batch mode (keyword_ids present): loads the specified keywords, passes
	 *   them through Category_Matcher (OpenAI embeddings), saves the matches,
	 *   and returns per-batch statistics.
	 *
	 * Requires OpenAI to be configured; returns a redirect URL to Settings
	 * when it is not.
	 *
	 * @since 2.8.2
	 */
	public function ajax_ai_match_categories() {
		$this->verify_ajax_request( 'wisewand_seo_real' );

		try {
			if ( ! class_exists( 'Wisewand\\Utils\\OpenAI_Client' ) ) {
				require_once WISEWAND_PLUGIN_DIR . 'includes/Utils/OpenAI_Client.php';
			}

			// Check if OpenAI is configured.
			if ( ! \Wisewand\Utils\OpenAI_Client::is_configured() ) {
				wp_send_json_error(
					array(
						'message'      => esc_html__( 'OpenAI API key not configured. Please add your API key in Settings.', 'wisewand-ai-content-writer-seo-content-generator' ),
						'redirect_url' => admin_url( 'admin.php?page=wisewand-settings' ),
					)
				);
				return;
			}

			$category_matcher = $this->get_category_matcher();
			$saved_manager    = $this->context->get_saved_keywords_manager();

			// Check if this is a batch request (client-side batching to avoid timeout).
			$keyword_ids = isset( $_POST['keyword_ids'] ) ? array_map( 'absint', $_POST['keyword_ids'] ) : array();
			$filter_mode = sanitize_text_field( $_POST['filter_mode'] ?? 'uncategorized' );

			if ( empty( $keyword_ids ) ) {
				// Initial call - return keyword IDs for client-side batching.
				$all_keywords = $saved_manager->get_all_saved_keywords();

				if ( empty( $all_keywords ) ) {
					wp_send_json_error( array( 'message' => esc_html__( 'No keywords in library', 'wisewand-ai-content-writer-seo-content-generator' ) ) );
					return;
				}

				// Apply filter based on mode.
				if ( 'uncategorized' === $filter_mode ) {
					// Filter to only keywords WITHOUT categories.
					$all_keywords = array_filter(
						$all_keywords,
						function ( $kw ) {
							$suggested_categories = $kw['suggested_categories'] ?? null;

							// Check if empty or null.
							if ( empty( $suggested_categories ) || '[]' === $suggested_categories || 'null' === $suggested_categories ) {
								return true;
							}

							// Check if JSON decodes to empty array.
							$cat_ids = json_decode( $suggested_categories, true );
							return empty( $cat_ids );
						}
					);

					$all_keywords = array_values( $all_keywords ); // Re-index.

					$this->context->logger->info(
						'AI Matching: Filtered to uncategorized keywords',
						array(
							'filter_mode'        => $filter_mode,
							'total_after_filter' => count( $all_keywords ),
						)
					);
				} elseif ( 'exclude_ranking' === $filter_mode ) {
					// Exclude keywords with ranking data.
					$all_keywords = array_filter(
						$all_keywords,
						function ( $kw ) {
							$has_categories = ! empty( $kw['suggested_categories'] ) && '[]' !== $kw['suggested_categories'] && 'null' !== $kw['suggested_categories'];
							$has_ranking    = ! empty( $kw['page_url'] ) && ! empty( $kw['position_abs'] );

							// Keep if: no categories AND no ranking data.
							return ! $has_categories && ! $has_ranking;
						}
					);

					$all_keywords = array_values( $all_keywords );

					$this->context->logger->info(
						'AI Matching: Excluded ranking keywords',
						array(
							'filter_mode'        => $filter_mode,
							'total_after_filter' => count( $all_keywords ),
						)
					);
				} elseif ( 'only_ranking' === $filter_mode ) {
					// Only keywords with ranking URLs.
					$all_keywords = array_filter(
						$all_keywords,
						function ( $kw ) {
							return ! empty( $kw['page_url'] ) && ! empty( $kw['position_abs'] );
						}
					);

					$all_keywords = array_values( $all_keywords );

					$this->context->logger->info(
						'AI Matching: Only ranking keywords',
						array(
							'filter_mode'        => $filter_mode,
							'total_after_filter' => count( $all_keywords ),
						)
					);
				} elseif ( 'without_position' === $filter_mode ) {
					// Keywords never analyzed in SEO Intelligence.
					$all_keywords = array_filter(
						$all_keywords,
						function ( $kw ) {
							$has_categories = ! empty( $kw['suggested_categories'] ) && '[]' !== $kw['suggested_categories'] && 'null' !== $kw['suggested_categories'];
							$has_position   = ! empty( $kw['position_abs'] ) || ! empty( $kw['page_url'] );

							return ! $has_categories && ! $has_position;
						}
					);

					$all_keywords = array_values( $all_keywords );

					$this->context->logger->info(
						'AI Matching: Only keywords without position data',
						array(
							'filter_mode'        => $filter_mode,
							'total_after_filter' => count( $all_keywords ),
						)
					);
				}
				// else: 'all' mode - use all keywords (no filter).

				$all_keyword_ids = array_column( $all_keywords, 'id' );

				wp_send_json_success(
					array(
						'mode'           => 'init',
						'keyword_ids'    => $all_keyword_ids,
						'total_keywords' => count( $all_keyword_ids ),
						'filter_mode'    => $filter_mode,
						'batch_size'     => 10, // Reduced from 20 to 10 for better reliability.
						'total_batches'  => ceil( count( $all_keyword_ids ) / 10 ),
						'message'        => 'Ready for client-side batch processing',
					)
				);
				return;
			}

			// Process this specific batch.
			$batch_keywords = $this->context->keyword_repository->get_keywords_by_ids( $keyword_ids );

			if ( empty( $batch_keywords ) ) {
				wp_send_json_error( array( 'message' => esc_html__( 'No keywords found for this batch', 'wisewand-ai-content-writer-seo-content-generator' ) ) );
				return;
			}

			// Determine if we should respect manual categories.
			// Only "all" mode ignores manual categories (re-match everything).
			$respect_manual = ( 'all' !== $filter_mode );

			$this->context->logger->info(
				'Processing AI matching batch',
				array(
					'filter_mode'    => $filter_mode,
					'respect_manual' => $respect_manual,
					'batch_size'     => count( $batch_keywords ),
				)
			);

			// Match this batch to categories using AI with smart URL detection.
			$matches = $category_matcher->match_keywords_to_categories( $batch_keywords, null, $respect_manual );

			if ( is_wp_error( $matches ) ) {
				wp_send_json_error(
					array(
						'message'        => $matches->get_error_message(),
						'batch_keywords' => count( $batch_keywords ),
					)
				);
				return;
			}

			// Save matches to database with author assignment.
			$saved_count = $category_matcher->save_matches( $matches, $batch_keywords );

			// Return batch result.
			wp_send_json_success(
				array(
					'mode'          => 'batch_complete',
					'batch_size'    => count( $batch_keywords ),
					'matched_count' => count( $matches ),
					'saved_count'   => $saved_count,
					'message'       => sprintf(
						/* translators: 1: Number of keywords matched with categories, 2: Total number of keywords in batch */
						__( 'Batch complete: %1$d/%2$d matched', 'wisewand-ai-content-writer-seo-content-generator' ),
						count( $matches ),
						count( $batch_keywords )
					),
				)
			);

		} catch ( \Exception $e ) {
			$this->context->logger->error( 'AI category matching failed', array( 'error' => $e->getMessage() ) );
			wp_send_json_error( array( 'message' => esc_html( $e->getMessage() ) ) );
		}
	}

	/**
	 * Count saved keywords eligible for AI matching by filter mode
	 *
	 * Returns totals for the UI progress display so the user can see how many
	 * keywords will be processed before starting the matching run.
	 *
	 * @since 2.8.2
	 */
	public function ajax_count_keywords_for_matching() {
		$this->verify_ajax_request( 'wisewand_seo_real' );

		try {
			$filter_mode = sanitize_text_field( $_POST['filter_mode'] ?? 'uncategorized' );

			$stats = $this->context->keyword_repository->get_matching_statistics( $filter_mode );

			$this->context->logger->debug(
				'Count keywords for matching',
				array(
					'filter_mode'    => $filter_mode,
					'total'          => $stats['total'],
					'auto_matchable' => $stats['auto_matchable'],
					'ai_needed'      => $stats['ai_needed'],
				)
			);

			wp_send_json_success(
				array(
					'total_keywords' => $stats['total'],
					'auto_matchable' => $stats['auto_matchable'],
					'ai_needed'      => $stats['ai_needed'],
					'filter_mode'    => $filter_mode,
				)
			);

		} catch ( \Exception $e ) {
			$this->context->logger->error( 'Failed to count keywords', array( 'error' => $e->getMessage() ) );
			wp_send_json_error( array( 'message' => esc_html( $e->getMessage() ) ) );
		}
	}

	/**
	 * Check for ranking conflicts before scheduling a keyword to the calendar
	 *
	 * Uses Keyword_Deduplication_Engine to detect existing rankings in
	 * positions 1-50 that would cause keyword cannibalization if a new
	 * article targeted the same keyword.
	 *
	 * @since 2.8.2
	 */
	public function ajax_check_ranking_conflicts() {
		$this->verify_ajax_request( 'wisewand_seo_real' );

		$keyword       = sanitize_text_field( $_POST['keyword'] ?? '' );
		$language_code = sanitize_text_field( $_POST['language_code'] ?? 'en' );

		if ( empty( $keyword ) ) {
			wp_send_json_error( array( 'message' => esc_html__( 'Keyword is required', 'wisewand-ai-content-writer-seo-content-generator' ) ) );
		}

		try {
			$dedup_engine = $this->context->get_deduplication_engine();

			// Check for ranking conflicts.
			$conflicts = $dedup_engine->check_ranking_conflicts( $keyword, $language_code );

			wp_send_json_success(
				array(
					'keyword'        => $keyword,
					'conflicts'      => $conflicts,
					'has_conflicts'  => $conflicts['has_conflicts'],
					'risk_level'     => $conflicts['risk_level'],
					'recommendation' => $conflicts['recommendation'],
				)
			);

		} catch ( \Exception $e ) {
			$this->context->logger->error(
				'Failed to check ranking conflicts',
				array(
					'keyword' => $keyword,
					'error'   => $e->getMessage(),
				)
			);

			wp_send_json_error( array( 'message' => esc_html( $e->getMessage() ) ) );
		}
	}
}
