<?php
/**
 * SEO Analysis Handler
 *
 * Handles all AJAX requests related to domain SEO analysis: running live
 * analysis via DataForSEO, loading cached snapshots, and rendering the
 * keyword results table. 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;
use Wisewand\Utils\DataForSEO_Universal;

// 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 domain analysis AJAX requests
 *
 * Runs live DataForSEO analysis, loads cached snapshots, and renders the
 * keyword HTML table. Depends on SEO_Handler_Context for shared services.
 *
 * @package Wisewand
 * @since 2.8.2
 */
class SEO_Analysis_Handler {

	use AJAX_Security;

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

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

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

	/**
	 * Run a live SEO analysis for the given domain
	 *
	 * @since 2.8.2
	 */
	public function ajax_run_real_analysis() {
		$this->verify_ajax_request( 'wisewand_seo_real' );

		$domain        = sanitize_text_field( wp_unslash( $_POST['domain'] ?? '' ) );
		$language_code = sanitize_text_field( wp_unslash( $_POST['language_code'] ?? 'en' ) );
		$location_code = absint( $_POST['location_code'] ?? 2840 );

		$debug_info = array(
			'domain'                 => $domain,
			'has_dataforseo_client'  => ! empty( $this->context->dataforseo_client ),
			'credentials_configured' => DataForSEO_Universal::is_configured(),
		);

		$this->context->logger->debug( 'AJAX analysis debug info', $debug_info );

		if ( empty( $domain ) ) {
			wp_send_json_error(
				array(
					'message' => esc_html__( 'Domain is empty', 'wisewand-ai-content-writer-seo-content-generator' ),
					'debug'   => $debug_info,
				)
			);
		}

		if ( ! $this->context->dataforseo_client ) {
			wp_send_json_error(
				array(
					'message' => esc_html__( 'DataForSEO client not initialized', 'wisewand-ai-content-writer-seo-content-generator' ),
					'debug'   => $debug_info,
				)
			);
		}

		if ( ! DataForSEO_Universal::is_configured() ) {
			wp_send_json_error(
				array(
					'message' => esc_html__( 'DataForSEO credentials not configured', 'wisewand-ai-content-writer-seo-content-generator' ),
					'debug'   => $debug_info,
				)
			);
		}

		try {
			$test_result = $this->test_dataforseo_connection();
			if ( is_wp_error( $test_result ) ) {
				throw new \Exception( 'DataForSEO Connection Test Failed: ' . $test_result->get_error_message() );
			}

			$analysis_results = $this->run_real_seo_analysis( $domain, $language_code, $location_code );

			$this->store_analysis_results( $analysis_results );

			$html    = $this->render_analysis_results( $analysis_results );
			$metrics = $this->calculate_real_metrics( $analysis_results );

			wp_send_json_success(
				array(
					'html'           => $html,
					'metrics'        => $metrics,
					'domain'         => $domain,
					'total_keywords' => count( $analysis_results['current_rankings'] ) + count( $analysis_results['opportunities'] ),
				)
			);

		} catch ( \Exception $e ) {
			$this->context->logger->error(
				'Real SEO analysis failed',
				array(
					'domain' => $domain,
					'error'  => $e->getMessage(),
				)
			);

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

	/**
	 * Load and render the most recent cached analysis snapshot
	 *
	 * @since 2.8.2
	 */
	public function ajax_load_latest_snapshot() {
		$this->verify_ajax_request( 'wisewand_seo_real' );

		$domain        = sanitize_text_field( wp_unslash( $_POST['domain'] ?? '' ) );
		$language_code = sanitize_text_field( wp_unslash( $_POST['language_code'] ?? 'en' ) );
		$location_code = absint( $_POST['location_code'] ?? 2840 );

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

		try {
			$snapshot_data = $this->load_latest_snapshot( $domain, $language_code, $location_code );

			if ( ! $snapshot_data ) {
				$snapshot_data = array(
					'domain'           => $domain,
					'language_code'    => $language_code,
					'location_code'    => $location_code,
					'current_rankings' => array(),
					'opportunities'    => array(),
					'analysis_date'    => null,
				);
			}

			$saved_manager  = $this->context->get_saved_keywords_manager();
			$saved_keywords = $saved_manager->get_all_saved_keywords();
			$has_saved      = count( $saved_keywords ) > 0;

			if ( empty( $snapshot_data['current_rankings'] ) && empty( $snapshot_data['opportunities'] ) && ! $has_saved ) {
				wp_send_json_success(
					array(
						'html'       => '',
						'metrics'    => array(
							'current_rankings'  => 0,
							'new_opportunities' => 0,
							'quick_wins'        => 0,
							'top_performers'    => 0,
						),
						'show_empty' => true,
					)
				);
				return;
			}

			$html    = $this->render_analysis_results( $snapshot_data );
			$metrics = $this->calculate_real_metrics( $snapshot_data );

			wp_send_json_success(
				array(
					'html'           => $html,
					'metrics'        => $metrics,
					'domain'         => $domain,
					'total_keywords' => count( $snapshot_data['current_rankings'] ) + count( $snapshot_data['opportunities'] ) + count( $saved_keywords ),
					'snapshot_date'  => $snapshot_data['analysis_date'] ?? null,
				)
			);

		} catch ( \Exception $e ) {
			$this->context->logger->error(
				'Failed to load latest snapshot',
				array(
					'domain' => $domain,
					'error'  => $e->getMessage(),
				)
			);

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

	// =========================================================================
	// PUBLIC API (called by other handlers)
	// =========================================================================

	/**
	 * Load the most recent snapshot for programmatic use by other handlers
	 *
	 * Exposes the private load_latest_snapshot() method to sibling handlers
	 * (e.g. SEO_Keywords_Handler::load_all_keywords_for_roadmap) so they can
	 * retrieve cached analysis data without duplicating the retrieval logic.
	 *
	 * @since 2.8.2
	 *
	 * @param string $domain        Target domain.
	 * @param string $language_code Language code.
	 * @param int    $location_code Location code.
	 * @return array|null Snapshot data array or null if none exists.
	 */
	public function load_latest_snapshot_public( $domain, $language_code, $location_code ) {
		return $this->load_latest_snapshot( $domain, $language_code, $location_code );
	}

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

	/**
	 * Verify the DataForSEO connection is alive using a minimal request
	 *
	 * @since 2.8.2
	 *
	 * @return true|\WP_Error True on success, WP_Error on failure.
	 */
	private function test_dataforseo_connection() {
		$this->context->logger->debug( 'Testing DataForSEO connection' );

		$test_data = array(
			array(
				'target'        => 'google.com',
				'location_code' => 2840,
				'language_code' => 'en',
				'limit'         => 1,
			),
		);

		$response = $this->context->dataforseo_client->request( 'POST', '/dataforseo_labs/google/ranked_keywords/live', $test_data );

		if ( is_wp_error( $response ) ) {
			$this->context->logger->error(
				'DataForSEO connection test failed',
				array(
					'error_code'    => $response->get_error_code(),
					'error_message' => $response->get_error_message(),
				)
			);
			return $response;
		}

		$this->context->logger->debug( 'DataForSEO connection test successful' );
		return true;
	}

	/**
	 * Orchestrate the full SEO analysis pipeline for a domain
	 *
	 * @since 2.8.2
	 *
	 * @param string $domain        Target domain.
	 * @param string $language_code Language code (e.g. "en").
	 * @param int    $location_code DataForSEO location code.
	 * @return array Analysis results with current_rankings and opportunities keys.
	 */
	private function run_real_seo_analysis( $domain, $language_code, $location_code ) {
		$this->context->logger->info(
			'Starting real SEO analysis',
			array(
				'domain'        => $domain,
				'language_code' => $language_code,
				'location_code' => $location_code,
			)
		);

		$results = array(
			'domain'           => $domain,
			'language_code'    => $language_code,
			'location_code'    => $location_code,
			'current_rankings' => array(),
			'opportunities'    => array(),
			'analysis_date'    => current_time( 'mysql' ),
		);

		$results['current_rankings'] = $this->fetch_real_rankings( $domain, $language_code, $location_code );
		$results['opportunities']    = $this->fetch_real_opportunities( $domain, $language_code, $location_code, $results['current_rankings'] );

		return $results;
	}

	/**
	 * Fetch current keyword rankings for the domain from DataForSEO
	 *
	 * @since 2.8.2
	 *
	 * @param string $domain        Target domain.
	 * @param string $language_code Language code.
	 * @param int    $location_code Location code.
	 * @return array Processed keyword ranking records.
	 *
	 * @throws \Exception When DataForSEO request fails.
	 */
	private function fetch_real_rankings( $domain, $language_code, $location_code ) {
		$clean_domain = preg_replace( '/^www\./i', '', $domain );
		$clean_domain = trim( $clean_domain, '/' );

		$ranked_data = array(
			array(
				'target'             => $clean_domain,
				'language_code'      => $language_code,
				'location_code'      => $location_code,
				'limit'              => 500,
				'include_subdomains' => false,
			),
		);

		$this->context->logger->debug(
			'Making DataForSEO ranked keywords request',
			array(
				'endpoint'           => '/dataforseo_labs/google/ranked_keywords/live',
				'original_domain'    => $domain,
				'clean_domain'       => $clean_domain,
				'language_code'      => $language_code,
				'location_code'      => $location_code,
				'include_subdomains' => false,
			)
		);

		$response = $this->context->dataforseo_client->request( 'POST', '/dataforseo_labs/google/ranked_keywords/live', $ranked_data );

		if ( is_wp_error( $response ) ) {
			$error_message = $response->get_error_message();
			$error_code    = $response->get_error_code();

			$this->context->logger->error(
				'DataForSEO ranked keywords request failed',
				array(
					'error_code'    => $error_code,
					'error_message' => $error_message,
					'domain'        => $domain,
				)
			);

			throw new \Exception(
				sprintf(
					'DataForSEO API Error (%s): %s',
					esc_html( $error_code ),
					esc_html( $error_message )
				)
			);
		}

		$this->context->logger->debug(
			'DataForSEO ranked keywords response received',
			array(
				'domain'        => $domain,
				'response_type' => gettype( $response ),
				'has_tasks'     => isset( $response[0] ) && isset( $response[0]['result'] ),
			)
		);

		$keywords = $this->process_real_rankings( $response, $domain );

		$this->enrich_with_real_volumes( $keywords, $language_code, $location_code );

		return $keywords;
	}

	/**
	 * Fetch keyword opportunities using top rankings as seeds
	 *
	 * @since 2.8.2
	 *
	 * @param string $domain            Target domain.
	 * @param string $language_code     Language code.
	 * @param int    $location_code     Location code.
	 * @param array  $existing_rankings Already-fetched current rankings.
	 * @return array Opportunity keyword records.
	 */
	private function fetch_real_opportunities( $domain, $language_code, $location_code, $existing_rankings ) {
		if ( empty( $existing_rankings ) ) {
			return array();
		}

		$seed_keywords     = array_slice( array_column( $existing_rankings, 'keyword' ), 0, 5 );
		$all_opportunities = array();

		foreach ( $seed_keywords as $seed ) {
			$suggestions_data = array(
				array(
					'keyword'              => $seed,
					'location_code'        => $location_code,
					'language_code'        => $language_code,
					'include_seed_keyword' => false,
					'limit'                => 20,
				),
			);

			$response = $this->context->dataforseo_client->request( 'POST', '/dataforseo_labs/google/keyword_suggestions/live', $suggestions_data );

			if ( ! is_wp_error( $response ) ) {
				$opportunities     = $this->process_real_opportunities( $response, array_column( $existing_rankings, 'keyword' ) );
				$all_opportunities = array_merge( $all_opportunities, $opportunities );
			}

			$this->context->rate_limiter->smart_delay( 'dataforseo', 0.2 );
		}

		$unique_opportunities = array();
		foreach ( $all_opportunities as $opp ) {
			$unique_opportunities[ $opp['keyword'] ] = $opp;
		}

		return array_slice( $unique_opportunities, 0, 50 );
	}

	/**
	 * Process the raw DataForSEO ranked_keywords response into keyword records
	 *
	 * Filters out subdomain URLs so only the exact target domain is included.
	 *
	 * @since 2.8.2
	 *
	 * @param array  $response      Raw API response array.
	 * @param string $target_domain Domain to filter for.
	 * @return array Processed keyword records.
	 */
	private function process_real_rankings( $response, $target_domain ) {
		$task  = $response[0] ?? array();
		$items = $task['result'][0]['items'] ?? array();

		$this->context->logger->debug(
			'Processing ranked keywords',
			array(
				'total_items'   => count( $items ),
				'target_domain' => $target_domain,
			)
		);

		$keywords       = array();
		$filtered_count = 0;

		foreach ( $items as $item ) {
			$keyword_data = $item['keyword_data'] ?? array();
			$serp_element = $item['ranked_serp_element'] ?? array();
			$serp_item    = $serp_element['serp_item'] ?? array();

			$keyword  = $keyword_data['keyword'] ?? '';
			$position = $serp_item['rank_absolute'] ?? $serp_item['rank_group'] ?? null;
			$page_url = $serp_item['url'] ?? '';

			if ( empty( $keyword ) || ! $position ) {
				continue;
			}

			if ( ! empty( $page_url ) ) {
				$url_host            = wp_parse_url( $page_url, PHP_URL_HOST );
				$normalized_url_host = preg_replace( '/^www\./i', '', $url_host );
				$normalized_target   = preg_replace( '/^www\./i', '', $target_domain );

				if ( $normalized_url_host !== $normalized_target ) {
					++$filtered_count;
					continue;
				}
			}

			$keywords[] = array(
				'keyword'       => $keyword,
				'position'      => $position,
				'page_url'      => $page_url,
				'search_volume' => 0,
				'content_type'  => $this->detect_content_type( $keyword ),
				'search_intent' => $this->detect_search_intent( $keyword ),
				'priority'      => $this->calculate_priority( $position, 0 ),
				'type'          => 'current',
			);
		}

		$this->context->logger->info(
			'Ranked keywords processed',
			array(
				'total_items'   => count( $items ),
				'filtered_out'  => $filtered_count,
				'kept_keywords' => count( $keywords ),
				'target_domain' => $target_domain,
			)
		);

		return $keywords;
	}

	/**
	 * Process the raw keyword_suggestions response into opportunity records
	 *
	 * @since 2.8.2
	 *
	 * @param array $response          Raw API response array.
	 * @param array $existing_keywords Keyword strings already in current_rankings.
	 * @return array Opportunity keyword records.
	 */
	private function process_real_opportunities( $response, $existing_keywords ) {
		$task  = $response[0] ?? array();
		$items = $task['result'] ?? array();

		$opportunities = array();
		foreach ( $items as $item ) {
			$keyword       = $item['keyword'] ?? '';
			$search_volume = $item['search_volume'] ?? 0;

			if ( empty( $keyword ) || in_array( $keyword, $existing_keywords ) ) {
				continue;
			}

			$opportunities[] = array(
				'keyword'       => $keyword,
				'position'      => null,
				'page_url'      => '',
				'search_volume' => $search_volume,
				'content_type'  => $this->detect_content_type( $keyword ),
				'search_intent' => $this->detect_search_intent( $keyword ),
				'priority'      => $this->calculate_priority( null, $search_volume ),
				'type'          => 'opportunity',
			);
		}

		return $opportunities;
	}

	/**
	 * Enrich a keywords array with real search volume data from DataForSEO
	 *
	 * Modifies the array in-place in batches of 20.
	 *
	 * @since 2.8.2
	 *
	 * @param array  $keywords      Keywords array passed by reference.
	 * @param string $language_code Language code.
	 * @param int    $location_code Location code.
	 */
	private function enrich_with_real_volumes( &$keywords, $language_code, $location_code ) {
		if ( empty( $keywords ) ) {
			return;
		}

		$keyword_names = array_column( $keywords, 'keyword' );

		foreach ( array_chunk( $keyword_names, 20 ) as $batch ) {
			$volume_data = array(
				array(
					'location_code' => $location_code,
					'language_code' => $language_code,
					'keywords'      => $batch,
				),
			);

			$response = $this->context->dataforseo_client->request( 'POST', '/keywords_data/google_ads/search_volume/live', $volume_data );

			if ( ! is_wp_error( $response ) ) {
				$this->update_with_real_volumes( $keywords, $response );
			}

			$this->context->rate_limiter->smart_delay( 'dataforseo', 0.3 );
		}
	}

	/**
	 * Apply volume data from API response to the keywords array
	 *
	 * @since 2.8.2
	 *
	 * @param array $keywords        Keywords array passed by reference.
	 * @param array $volume_response Raw volume API response.
	 */
	private function update_with_real_volumes( &$keywords, $volume_response ) {
		$task  = $volume_response[0] ?? array();
		$items = $task['result'] ?? array();

		foreach ( $items as $item ) {
			$keyword       = $item['keyword'] ?? '';
			$search_volume = $item['search_volume'] ?? 0;

			foreach ( $keywords as &$kw ) {
				if ( $kw['keyword'] === $keyword ) {
					$kw['search_volume'] = $search_volume;
					$kw['priority']      = $this->calculate_priority( $kw['position'], $search_volume );
					break;
				}
			}
		}
	}

	/**
	 * Persist the analysis results to the database via the SEO repository
	 *
	 * @since 2.8.2
	 *
	 * @param array $results Full analysis results array.
	 */
	private function store_analysis_results( $results ) {
		$this->context->logger->info(
			'Storing SEO analysis results',
			array(
				'domain'        => $results['domain'],
				'keyword_count' => count( array_merge( $results['current_rankings'], $results['opportunities'] ) ),
			)
		);

		$success = $this->context->seo_repository->store_analysis_results( $results );

		if ( $success ) {
			$this->context->logger->info( 'SEO analysis results stored successfully' );
		} else {
			$this->context->logger->error( 'Failed to store SEO analysis results: ' . $this->context->seo_repository->get_last_error() );
		}
	}

	/**
	 * Load the latest stored snapshot for a domain from the database
	 *
	 * Returns null when no snapshot exists. Includes position comparison
	 * data against the previous snapshot.
	 *
	 * @since 2.8.2
	 *
	 * @param string $domain        Target domain.
	 * @param string $language_code Language code.
	 * @param int    $location_code Location code.
	 * @return array|null Snapshot data array or null if none exists.
	 */
	private function load_latest_snapshot( $domain, $language_code, $location_code ) {
		$latest_snapshot = $this->context->seo_repository->get_latest_snapshot_id( $domain, $language_code, $location_code );

		if ( ! $latest_snapshot ) {
			return null;
		}

		$keywords = $this->context->seo_repository->get_keywords_by_snapshot( $latest_snapshot );

		if ( empty( $keywords ) ) {
			return null;
		}

		$previous_snapshot = $this->context->seo_repository->get_previous_snapshot_id( $domain, $language_code, $location_code, $latest_snapshot );

		$previous_data = array();
		if ( $previous_snapshot ) {
			$previous_keywords = $this->context->seo_repository->get_keywords_by_snapshot( $previous_snapshot );
			foreach ( $previous_keywords as $kw ) {
				$previous_data[ $kw['keyword'] ] = $kw['position_abs'];
			}
		}

		$current_rankings = array();
		$opportunities    = array();

		foreach ( $keywords as $kw ) {
			$keyword_data = array(
				'keyword'       => $kw['keyword'],
				'position'      => $kw['position_abs'],
				'page_url'      => $kw['page_url'],
				'search_volume' => $kw['search_volume'],
				'content_type'  => $kw['content_type'],
				'search_intent' => $kw['search_intent'],
				'priority'      => $kw['priority_score'],
				'type'          => $kw['keyword_type'],
				'language_code' => $kw['language_code'] ?? 'en',
			);

			if ( isset( $previous_data[ $kw['keyword'] ] ) ) {
				$old_position = $previous_data[ $kw['keyword'] ];
				$new_position = $kw['position_abs'];

				if ( $old_position && $new_position ) {
					$keyword_data['position_change']   = $old_position - $new_position;
					$keyword_data['previous_position'] = $old_position;
				}
			} else {
				$keyword_data['is_new'] = true;
			}

			if ( 'current' === $kw['keyword_type'] ) {
				$current_rankings[] = $keyword_data;
			} else {
				$opportunities[] = $keyword_data;
			}
		}

		$snapshot_date = $this->context->seo_repository->get_snapshot_date( $latest_snapshot );

		return array(
			'domain'           => $domain,
			'language_code'    => $language_code,
			'location_code'    => $location_code,
			'current_rankings' => $current_rankings,
			'opportunities'    => $opportunities,
			'analysis_date'    => $snapshot_date,
			'snapshot_id'      => $latest_snapshot,
		);
	}

	/**
	 * Render the full HTML keyword results table
	 *
	 * Merges current rankings, opportunities, and saved keywords into a
	 * single sortable table with bulk-action bars.
	 *
	 * @since 2.8.2
	 *
	 * @param array $results Analysis results array.
	 * @return string HTML string.
	 */
	private function render_analysis_results( $results ) {
		$all_keywords  = array();
		$language_code = $results['language_code'] ?? 'en';

		foreach ( $results['current_rankings'] as $ranking ) {
			$ranking['type']          = 'current';
			$ranking['can_edit']      = false;
			$ranking['can_delete']    = false;
			$ranking['language_code'] = $language_code;
			$all_keywords[]           = $ranking;
		}

		foreach ( $results['opportunities'] as $opportunity ) {
			$opportunity['type']          = 'opportunity';
			$opportunity['can_edit']      = false;
			$opportunity['can_delete']    = false;
			$opportunity['language_code'] = $language_code;
			$all_keywords[]               = $opportunity;
		}

		$saved_manager  = $this->context->get_saved_keywords_manager();
		$saved_keywords = $saved_manager->get_all_saved_keywords();

		$this->context->logger->debug(
			'Loading saved keywords for table',
			array(
				'total_saved_keywords' => count( $saved_keywords ),
				'enriched_count'       => count( array_filter( $saved_keywords, fn( $k ) => $k['is_enriched'] ) ),
				'sample_saved_keyword' => ! empty( $saved_keywords[0] ) ? array(
					'keyword'        => $saved_keywords[0]['keyword'],
					'is_enriched'    => $saved_keywords[0]['is_enriched'],
					'search_volume'  => $saved_keywords[0]['search_volume'],
					'difficulty'     => $saved_keywords[0]['difficulty'] ?? null,
					'keyword_source' => $saved_keywords[0]['keyword_source'],
					'tags'           => $saved_keywords[0]['tags'],
				) : null,
			)
		);

		foreach ( $saved_keywords as $saved_kw ) {
			$type = 'saved';
			if ( 'keyword_ideas' === $saved_kw['keyword_source'] ) {
				$type = 'ideas';
			} elseif ( 'competitor_spy' === $saved_kw['keyword_source'] ) {
				$type = 'competitor_spy';
			} elseif ( 'seed_keyword' === $saved_kw['tags'] ) {
				$type = 'seed';
			}

			$all_keywords[] = array(
				'keyword'              => $saved_kw['keyword'],
				'keyword_id'           => $saved_kw['id'],
				'position'             => null,
				'page_url'             => '',
				'search_volume'        => $saved_kw['search_volume'] ?? 0,
				'difficulty'           => $saved_kw['difficulty'] ?? null,
				'cpc'                  => $saved_kw['cpc'] ?? null,
				'competition'          => $saved_kw['competition'] ?? null,
				'content_type'         => $saved_kw['content_type'],
				'search_intent'        => $saved_kw['search_intent'],
				'priority'             => $saved_kw['priority_score'] ?? 0,
				'type'                 => $type,
				'language_code'        => $saved_kw['language_code'] ?? 'en',
				'seed_keyword'         => $saved_kw['seed_keyword'],
				'suggested_author_id'  => $saved_kw['suggested_author_id'] ?? null,
				'suggested_categories' => $saved_kw['suggested_categories'] ?? null,
				'ai_matched_at'        => $saved_kw['ai_matched_at'] ?? null,
				'ai_model_used'        => $saved_kw['ai_model_used'] ?? null,
				'can_edit'             => true,
				'can_delete'           => true,
			);

			$this->context->logger->debug(
				'Added saved keyword to table',
				array(
					'keyword'     => $saved_kw['keyword'],
					'type'        => $type,
					'is_enriched' => $saved_kw['is_enriched'] ?? 0,
					'volume'      => $saved_kw['search_volume'] ?? 0,
				)
			);
		}

		// Enrich with calendar status via shared Saved_Keywords_Manager
		$all_keywords = $this->context->get_saved_keywords_manager()->enrich_with_calendar_status( $all_keywords );

		if ( empty( $all_keywords ) ) {
			return '<div class="empty-state"><div class="empty-icon">📊</div><h4>No data found</h4><p>No keywords found for this domain in the selected market.</p></div>';
		}

		$this->context->logger->debug(
			'Table rendering sample data',
			array(
				'total_keywords' => count( $all_keywords ),
				'sample_keyword' => ! empty( $all_keywords[0] ) ? array(
					'keyword'       => $all_keywords[0]['keyword'] ?? 'missing',
					'type'          => $all_keywords[0]['type'] ?? 'missing',
					'search_volume' => $all_keywords[0]['search_volume'] ?? 'missing',
					'difficulty'    => $all_keywords[0]['difficulty'] ?? 'missing',
					'priority'      => $all_keywords[0]['priority'] ?? 'missing',
				) : 'no_keywords',
			)
		);

		$date_info = '';
		if ( isset( $results['analysis_date'] ) ) {
			$date_info = '<div class="analysis-date">📅 Last updated: ' . gmdate( 'F j, Y @ g:i A', strtotime( $results['analysis_date'] ) ) . '</div>';
		}

		$html = $date_info;

		$html .= '<div class="bulk-actions-bar bulk-actions-top" id="bulk-actions-top" style="display: none;">';
		$html .= '<div class="bulk-info"><span class="selected-count-display">0</span> keywords selected</div>';
		$html .= '<button type="button" class="bulk-enrich-btn btn-bulk-action">📊 Enrich</button>';
		$html .= '<button type="button" class="bulk-schedule-btn btn-bulk-action">📅 Schedule</button>';
		$html .= '<button type="button" class="bulk-delete-btn btn-bulk-action btn-danger">🗑️ Delete</button>';
		$html .= '</div>';

		$html .= '<div class="table-responsive">';
		$html .= '<table class="seo-table">';
		$html .= '<thead><tr>';
		$html .= '<th class="col-checkbox"><input type="checkbox" id="select-all-keywords"></th>';
		$html .= '<th class="sortable col-keyword" data-sort="keyword">🎯 Keyword <span class="sort-arrow"></span></th>';
		$html .= '<th class="col-source">🏷️ Source</th>';
		$html .= '<th class="col-author" data-column="author">👤 Author</th>';
		$html .= '<th class="sortable col-categories" data-sort="categories" data-column="categories">📁 Categories <span class="sort-arrow"></span></th>';
		$html .= '<th class="col-language" data-column="language">🌍 Lang</th>';
		$html .= '<th class="sortable col-position" data-sort="position" data-column="position">📍 Position <span class="sort-arrow"></span></th>';
		$html .= '<th class="col-change" data-column="change">📈 Change</th>';
		$html .= '<th class="col-page" data-column="page">📄 Page</th>';
		$html .= '<th class="sortable col-volume" data-sort="volume" data-column="volume">📊 Volume <span class="sort-arrow"></span></th>';
		$html .= '<th class="sortable col-difficulty" data-sort="difficulty" data-column="difficulty">⚡ Difficulty <span class="sort-arrow"></span></th>';
		$html .= '<th class="sortable col-cpc" data-sort="cpc" data-column="cpc">💰 CPC <span class="sort-arrow"></span></th>';
		$html .= '<th class="col-competition" data-column="competition">🎯 Comp</th>';
		$html .= '<th class="col-intent" data-column="intent">🧠 Intent</th>';
		$html .= '<th class="sortable col-priority" data-sort="priority" data-column="priority">🎯 Priority <span class="sort-arrow"></span></th>';
		$html .= '<th class="col-calendar" data-column="calendar">📅 Calendar</th>';
		$html .= '<th class="col-action">🚀 Actions</th>';
		$html .= '</tr></thead><tbody>';

		$all_users = get_users(
			array(
				'capability' => 'edit_posts',
				'orderby'    => 'display_name',
				'order'      => 'ASC',
			)
		);

		$category_map = array();

		if ( isset( $GLOBALS['sitepress'] ) ) {
			global $sitepress;
			$active_languages = $sitepress->get_active_languages();

			if ( ! empty( $active_languages ) ) {
				foreach ( $active_languages as $lang_code => $lang_data ) {
					$sitepress->switch_lang( $lang_code );
					$lang_categories = get_categories( array( 'hide_empty' => false ) );
					foreach ( $lang_categories as $cat ) {
						$category_map[ $cat->term_id ] = $cat->name;
					}
				}
				$sitepress->switch_lang( $sitepress->get_current_language() );
			} else {
				$all_categories = get_categories( array( 'hide_empty' => false ) );
				foreach ( $all_categories as $cat ) {
					$category_map[ $cat->term_id ] = $cat->name;
				}
			}
		} else {
			$all_categories = get_categories( array( 'hide_empty' => false ) );
			foreach ( $all_categories as $cat ) {
				$category_map[ $cat->term_id ] = $cat->name;
			}
		}

		foreach ( $all_keywords as $kw ) {
			$keyword_id = $kw['keyword_id'] ?? 0;
			$can_edit   = $kw['can_edit'] ?? false;
			$can_delete = $kw['can_delete'] ?? false;

			$html .= '<tr data-type="' . $kw['type'] . '" data-language="' . ( $kw['language_code'] ?? 'en' ) . '" data-keyword-id="' . $keyword_id . '">';

			$html .= '<td class="col-checkbox">';
			if ( $can_edit || $can_delete ) {
				$html .= '<input type="checkbox" class="select-keyword-row" value="' . $keyword_id . '" data-type="' . $kw['type'] . '">';
			}
			$html .= '</td>';

			$html .= '<td class="col-keyword">';
			$html .= '<strong>' . esc_html( $kw['keyword'] ) . '</strong>';
			if ( 'ideas' === $kw['type'] && ! empty( $kw['seed_keyword'] ) ) {
				$html .= '<br><span class="seed-source">from: ' . esc_html( $kw['seed_keyword'] ) . '</span>';
			}
			$html .= '</td>';

			$html .= '<td class="col-source">';
			if ( 'current' === $kw['type'] ) {
				$html .= '<span class="keyword-source-badge badge-ranking">📊 Ranking</span>';
			} elseif ( 'opportunity' === $kw['type'] ) {
				$html .= '<span class="keyword-source-badge badge-opportunity">🔍 Opportunity</span>';
			} elseif ( 'saved' === $kw['type'] ) {
				$html .= '<span class="keyword-source-badge badge-manual">💾 Manual</span>';
			} elseif ( 'competitor_spy' === $kw['type'] ) {
				$html .= '<span class="keyword-source-badge badge-competitor">🕵️ Competitor</span>';
			} elseif ( 'ideas' === $kw['type'] ) {
				$html .= '<span class="keyword-source-badge badge-idea">🆕 Idea</span>';
			} elseif ( 'seed' === $kw['type'] ) {
				$html .= '<span class="keyword-source-badge badge-seed">🌱 Seed</span>';
			}
			$html .= '</td>';

			$html      .= '<td class="col-author" data-column-target="author">';
			$author_id  = $kw['suggested_author_id'] ?? null;
			if ( ! $author_id ) {
				$lang      = $kw['language_code'] ?? 'en';
				$author_id = $this->context->options->get_author_for_language( $lang );
			}
			$html .= '<select class="keyword-author-select" data-keyword-id="' . $keyword_id . '" style="font-size: 11px; padding: 3px 6px; border: 1px solid #d1d5db; border-radius: 4px; background: #ffffff; width: 100%; max-width: 120px;">';
			foreach ( $all_users as $user ) {
				$selected = ( $user->ID == $author_id ) ? 'selected' : '';
				$html    .= '<option value="' . $user->ID . '" ' . $selected . '>' . esc_html( $user->display_name ) . '</option>';
			}
			$html .= '</select>';
			$html .= '</td>';

			$html                 .= '<td class="col-categories" data-column-target="categories">';
			$suggested_categories  = $kw['suggested_categories'] ?? null;
			$current_category_ids  = array();
			$cat_names             = array();

			if ( $suggested_categories ) {
				$current_category_ids = json_decode( $suggested_categories, true ) ?: array();
				foreach ( $current_category_ids as $cat_id ) {
					if ( isset( $category_map[ $cat_id ] ) ) {
						$cat_names[] = $category_map[ $cat_id ];
					}
				}
			}

			$html .= '<div class="open-category-modal" data-keyword-id="' . $keyword_id . '" data-category-ids="' . esc_attr( wp_json_encode( $current_category_ids ) ) . '" style="cursor: pointer; min-height: 24px; display: flex; align-items: center; gap: 4px; flex-wrap: wrap; padding: 4px;">';
			if ( ! empty( $cat_names ) ) {
				foreach ( array_slice( $cat_names, 0, 2 ) as $name ) {
					$html .= '<span style="font-size: 10px; color: #1e293b; background: #e0f2fe; padding: 2px 6px; border-radius: 4px;">' . esc_html( $name ) . '</span>';
				}
				if ( count( $cat_names ) > 2 ) {
					$html .= '<span style="font-size: 10px; color: #64748b;">+' . ( count( $cat_names ) - 2 ) . '</span>';
				}
				$html .= '<span style="font-size: 12px; color: var(--brand-primary); margin-left: 4px;">✏️</span>';
			} else {
				$html .= '<span style="font-size: 10px; color: #94a3b8;">None</span>';
				$html .= '<span style="font-size: 10px; color: var(--brand-primary); margin-left: 4px;">+ Add</span>';
			}
			$html .= '</div>';
			$html .= '</td>';

			$html       .= '<td class="col-language" data-column-target="language">';
			$lang_code   = $kw['language_code'] ?? 'en';
			$lang_emojis = array(
				'en' => '🇺🇸',
				'fr' => '🇫🇷',
				'de' => '🇩🇪',
				'es' => '🇪🇸',
				'it' => '🇮🇹',
			);
			$emoji       = $lang_emojis[ $lang_code ] ?? '🌍';
			$html       .= '<span style="font-size: 18px;" title="' . strtoupper( $lang_code ) . '">' . $emoji . '</span>';
			$html       .= '</td>';

			$html .= '<td class="col-position" data-column-target="position" data-position="' . ( $kw['position'] ?? 999 ) . '">';
			if ( $kw['position'] ) {
				$pos_class = $kw['position'] <= 10 ? 'pos-top-10' : ( $kw['position'] <= 20 ? 'pos-top-20' : 'pos-top-50' );
				$html     .= '<span class="position-badge ' . $pos_class . '">#' . $kw['position'] . '</span>';
			} else {
				$html .= '<span style="color: #94a3b8;">—</span>';
			}
			$html .= '</td>';

			$html .= '<td class="col-change" data-column-target="change">';
			if ( isset( $kw['is_new'] ) && $kw['is_new'] ) {
				$html .= '<span class="change-badge new-keyword">🆕 New</span>';
			} elseif ( isset( $kw['position_change'] ) ) {
				$change = $kw['position_change'];
				if ( $change > 0 ) {
					$html .= '<span class="change-badge change-up">↗️ +' . abs( $change ) . '</span>';
				} elseif ( $change < 0 ) {
					$html .= '<span class="change-badge change-down">↘️ ' . $change . '</span>';
				} else {
					$html .= '<span class="change-badge change-stable">➡️ 0</span>';
				}
			} else {
				$html .= '<span style="color: #94a3b8;">—</span>';
			}
			$html .= '</td>';

			$html .= '<td class="col-page" data-column-target="page">';
			if ( $kw['page_url'] ) {
				$path       = wp_parse_url( $kw['page_url'], PHP_URL_PATH ) ?: '/';
				$short_path = strlen( $path ) > 30 ? substr( $path, 0, 27 ) . '...' : $path;
				$html      .= '<a href="' . esc_url( $kw['page_url'] ) . '" target="_blank" class="page-link" title="' . esc_attr( $path ) . '">' . esc_html( $short_path ) . '</a>';
			} else {
				$html .= '<span style="color: #94a3b8;">New content</span>';
			}
			$html .= '</td>';

			$html .= '<td class="col-volume" data-column-target="volume" data-volume="' . ( $kw['search_volume'] ?? 0 ) . '">';
			$html .= ( $kw['search_volume'] ?? 0 ) > 0 ? '<strong>' . number_format( $kw['search_volume'] ) . '</strong>' : '—';
			$html .= '</td>';

			$html .= '<td class="col-difficulty" data-column-target="difficulty" data-difficulty="' . ( $kw['difficulty'] ?? 0 ) . '">';
			if ( isset( $kw['difficulty'] ) && $kw['difficulty'] > 0 ) {
				$diff_class = $kw['difficulty'] < 30 ? 'diff-easy' : ( $kw['difficulty'] < 60 ? 'diff-medium' : 'diff-hard' );
				$html      .= '<span class="difficulty-badge ' . $diff_class . '">' . $kw['difficulty'] . '%</span>';
			} else {
				$html .= '<span style="color: #94a3b8;">—</span>';
			}
			$html .= '</td>';

			$html .= '<td class="col-cpc" data-column-target="cpc" data-cpc="' . ( $kw['cpc'] ?? 0 ) . '">';
			if ( isset( $kw['cpc'] ) && $kw['cpc'] > 0 ) {
				$html .= '<span style="color: var(--brand-primary); font-weight: 600;">💰 $' . number_format( $kw['cpc'], 2 ) . '</span>';
			} else {
				$html .= '<span style="color: #94a3b8;">—</span>';
			}
			$html .= '</td>';

			$html            .= '<td class="col-competition" data-column-target="competition">';
			$competition_text = $kw['competition'] ?? '';
			if ( $competition_text ) {
				$comp_badge_class = '';
				$comp_icon        = '';
				switch ( strtoupper( $competition_text ) ) {
					case 'LOW':
						$comp_badge_class = 'diff-easy';
						$comp_icon        = '🟢';
						break;
					case 'MEDIUM':
						$comp_badge_class = 'diff-medium';
						$comp_icon        = '🟡';
						break;
					case 'HIGH':
						$comp_badge_class = 'diff-hard';
						$comp_icon        = '🔴';
						break;
				}
				$html .= '<span class="difficulty-badge ' . $comp_badge_class . '">' . $comp_icon . ' ' . ucfirst( strtolower( $competition_text ) ) . '</span>';
			} else {
				$html .= '<span style="color: #94a3b8;">—</span>';
			}
			$html .= '</td>';

			$html .= '<td class="col-intent" data-column-target="intent"><span class="intent-badge">' . ucfirst( $kw['search_intent'] ?? 'informational' ) . '</span></td>';

			$html          .= '<td class="col-priority" data-priority="' . ( $kw['priority'] ?? 0 ) . '">';
			$priority_class = ( $kw['priority'] ?? 0 ) >= 70 ? 'priority-high' : ( ( $kw['priority'] ?? 0 ) >= 40 ? 'priority-medium' : 'priority-low' );
			$html          .= '<span class="priority-badge ' . $priority_class . '">' . round( $kw['priority'] ?? 0 ) . '</span>';
			$html          .= '</td>';

			$html .= '<td class="col-calendar" data-column-target="calendar">';
			if ( ! empty( $kw['calendar_status'] ) ) {
				$cal_status = $kw['calendar_status'];
				$date       = gmdate( 'M j', strtotime( $kw['calendar_date'] ) );
				$tooltip    = 'Scheduled: ' . $date;
				if ( $kw['calendar_time'] ) {
					$tooltip .= ' at ' . gmdate( 'g:i A', strtotime( $kw['calendar_time'] ) );
				}
				$tooltip .= "\nStatus: " . $cal_status['label'];
				if ( $kw['calendar_post_id'] ) {
					$tooltip .= "\nPost ID: " . $kw['calendar_post_id'];
				}

				$html .= '<div style="display: flex; align-items: center; gap: 4px; justify-content: center;">';
				$html .= '<span style="cursor: help; font-size: 11px;" title="' . esc_attr( $tooltip ) . '">';
				$html .= '<span style="font-size: 14px;">' . $cal_status['icon'] . '</span> ';
				$html .= '<span style="color: ' . $cal_status['color'] . '; font-weight: 600;">' . $date . '</span>';
				$html .= '</span>';
				$html .= '<button class="btn-remove-from-calendar" data-calendar-id="' . ( $kw['calendar_id'] ?? 0 ) . '" data-keyword="' . esc_attr( $kw['keyword'] ) . '" title="Remove from calendar" style="background: none; border: none; cursor: pointer; padding: 2px 4px; font-size: 12px; color: #EF4444;">🗑️</button>';
				$html .= '</div>';
			} else {
				$html .= '<span style="color: #64748b; font-size: 11px;">—</span>';
			}
			$html .= '</td>';

			$html .= '<td class="col-action">';
			$html .= '<div class="action-buttons">';

			if ( $can_edit && $keyword_id ) {
				$html .= '<button class="action-btn btn-edit" data-keyword-id="' . $keyword_id . '" title="Edit">✏️</button>';
			}

			$keyword_json = htmlspecialchars(
				wp_json_encode(
					array(
						'keyword'       => $kw['keyword'],
						'keyword_id'    => $keyword_id,
						'position'      => $kw['position'] ?? null,
						'search_volume' => $kw['search_volume'] ?? 0,
						'difficulty'    => $kw['difficulty'] ?? null,
						'search_intent' => $kw['search_intent'] ?? 'informational',
						'content_type'  => $kw['content_type'] ?? 'blog',
						'priority'      => $kw['priority'] ?? 0,
						'page_url'      => $kw['page_url'] ?? '',
						'type'          => $kw['type'],
					)
				),
				ENT_QUOTES,
				'UTF-8'
			);

			$html .= '<button class="action-btn btn-schedule" data-keyword-data="' . $keyword_json . '" title="Schedule">📅</button>';

			if ( $can_delete && $keyword_id ) {
				$html .= '<button class="action-btn btn-delete-keyword" data-keyword-id="' . $keyword_id . '" data-keyword="' . esc_attr( $kw['keyword'] ) . '" title="Delete">🗑️</button>';
			}

			$html .= '</div>';
			$html .= '</td>';

			$html .= '</tr>';
		}

		$html .= '</tbody></table>';
		$html .= '</div>';

		$html .= '<div class="bulk-actions-bar bulk-actions-bottom" id="bulk-actions-bottom" style="display: none;">';
		$html .= '<div class="bulk-info"><span class="selected-count-display">0</span> keywords selected</div>';
		$html .= '<button type="button" class="bulk-enrich-btn btn-bulk-action">📊 Enrich</button>';
		$html .= '<button type="button" class="bulk-schedule-btn btn-bulk-action">📅 Schedule</button>';
		$html .= '<button type="button" class="bulk-delete-btn btn-bulk-action btn-danger">🗑️ Delete</button>';
		$html .= '</div>';

		return $html;
	}

	/**
	 * Calculate summary metrics from analysis results
	 *
	 * @since 2.8.2
	 *
	 * @param array $results Analysis results array.
	 * @return array Metrics: current_rankings, new_opportunities, quick_wins, top_performers.
	 */
	private function calculate_real_metrics( $results ) {
		$current_count       = count( $results['current_rankings'] );
		$opportunities_count = count( $results['opportunities'] );

		$quick_wins = count(
			array_filter(
				$results['current_rankings'],
				function ( $k ) {
					return ( $k['position'] ?? 999 ) >= 11 && ( $k['position'] ?? 999 ) <= 20;
				}
			)
		);

		$top_performers = count(
			array_filter(
				$results['current_rankings'],
				function ( $k ) {
					return ( $k['position'] ?? 999 ) <= 10;
				}
			)
		);

		return array(
			'current_rankings'  => $current_count,
			'new_opportunities' => $opportunities_count,
			'quick_wins'        => $quick_wins,
			'top_performers'    => $top_performers,
		);
	}

	/**
	 * Detect content type from keyword text
	 *
	 * @since 2.8.2
	 *
	 * @param string $keyword Keyword string.
	 * @return string Content type: affiliate|product|category|blog.
	 */
	private function detect_content_type( $keyword ) {
		$kw = strtolower( $keyword );
		if ( preg_match( '/\b(acheter|prix|avis|meilleur|comparaison)\b/', $kw ) ) {
			return 'affiliate';
		}
		if ( preg_match( '/\b(app|outil|générateur|logiciel)\b/', $kw ) ) {
			return 'product';
		}
		if ( preg_match( '/\b(catégorie|collection|liste)\b/', $kw ) ) {
			return 'category';
		}
		return 'blog';
	}

	/**
	 * Detect search intent from keyword text
	 *
	 * @since 2.8.2
	 *
	 * @param string $keyword Keyword string.
	 * @return string Intent: transactional|commercial|informational.
	 */
	private function detect_search_intent( $keyword ) {
		$kw = strtolower( $keyword );
		if ( preg_match( '/\b(acheter|prix|gratuit)\b/', $kw ) ) {
			return 'transactional';
		}
		if ( preg_match( '/\b(meilleur|avis|comparaison)\b/', $kw ) ) {
			return 'commercial';
		}
		return 'informational';
	}

	/**
	 * Calculate a priority score for a keyword
	 *
	 * @since 2.8.2
	 *
	 * @param int|null $position      Current Google position (null for opportunities).
	 * @param int      $search_volume Monthly search volume.
	 * @return int Priority score 0-100+.
	 */
	private function calculate_priority( $position = null, $search_volume = 0 ) {
		if ( $position ) {
			$position_score  = max( 0, 100 - $position );
			$volume_score    = min( $search_volume / 100, 20 );
			$quick_win_bonus = ( $position >= 11 && $position <= 20 ) ? 25 : 0;
			return max( 0, round( $position_score + $volume_score + $quick_win_bonus ) );
		} else {
			$volume_score      = min( $search_volume / 100, 60 );
			$opportunity_bonus = 20;
			return max( 0, round( $volume_score + $opportunity_bonus ) );
		}
	}

	/**
	 * AJAX handler: get DataForSEO credit balance
	 *
	 * @since 2.8.7
	 */
	public function ajax_get_dataforseo_balance() {
		$this->verify_ajax_request( 'wisewand_seo_real' );

		if ( ! $this->context->dataforseo_client ) {
			wp_send_json_error( array(
				'message' => esc_html__( 'DataForSEO not configured', 'wisewand-ai-content-writer-seo-content-generator' ),
			) );
		}

		$account_info = $this->context->dataforseo_client->get_account_info();

		if ( is_wp_error( $account_info ) ) {
			wp_send_json_error( array(
				'message' => esc_html( $account_info->get_error_message() ),
			) );
		}

		wp_send_json_success( $account_info );
	}
}
