<?php
/**
 * SEO Keywords Handler
 *
 * Handles all AJAX actions related to keyword management: scheduling keywords
 * for publication, discovering new keywords via DataForSEO, adding manual
 * keywords, generating smart editorial roadmaps, saving keywords to the
 * keyword library, enriching saved keywords with SEO metrics, and managing
 * the full keyword lifecycle in the saved-keyword store.
 *
 * @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.Security.NonceVerification.Missing -- Nonce verified via AJAX_Security trait

/**
 * Keyword management AJAX handler
 *
 * Provides all keyword-related AJAX endpoints for the SEO Intelligence page.
 * Depends on SEO_Handler_Context for shared repositories and service clients.
 * Cross-handler dependency: load_all_keywords_for_roadmap() delegates snapshot
 * loading to SEO_Analysis_Handler via load_latest_snapshot_public().
 *
 * @package Wisewand
 * @since 2.8.2
 */
class SEO_Keywords_Handler {

	use AJAX_Security;

	/**
	 * Shared dependency context
	 *
	 * @since 2.8.2
	 * @var SEO_Handler_Context
	 */
	private $context;

	/**
	 * Create a new keywords handler instance
	 *
	 * @since 2.8.2
	 *
	 * @param SEO_Handler_Context $context Shared dependency container.
	 */
	public function __construct( SEO_Handler_Context $context ) {
		$this->context = $context;
	}

	// =========================================================================
	// PRIVATE LAZY HELPERS (handler-specific services)
	// =========================================================================

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

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

	/**
	 * Cached Calendar_Entry_Manager instance
	 *
	 * @since 2.8.7
	 * @var \Wisewand\Calendar\Calendar_Entry_Manager|null
	 */
	private $entry_manager = null;

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

	/**
	 * Cached Roadmap_Generator instance
	 *
	 * @since 2.8.7
	 * @var \Wisewand\Calendar\Roadmap_Generator|null
	 */
	private $roadmap_generator = null;

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

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

	/**
	 * AJAX Handler — Schedule a keyword for publication in the editorial calendar
	 *
	 * Creates a new calendar entry for the supplied keyword data and assigns it
	 * to the next available publication date slot.
	 *
	 * @since 2.8.2
	 */
	public function ajax_schedule_keyword() {
		$this->verify_ajax_request( 'wisewand_seo_real' );

		$keyword_data = isset( $_POST['keyword_data'] ) ? map_deep( wp_unslash( $_POST['keyword_data'] ), 'sanitize_text_field' ) : array();

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

		try {
			$payload_builder = $this->get_payload_builder();
			$entry_manager   = $this->get_entry_manager();

			// Check if keyword already scheduled
			$existing = $entry_manager->check_keyword_exists( $keyword_data['keyword'] );
			if ( $existing ) {
				wp_send_json_error(
					array(
						'message'       => sprintf(
							/* translators: %s: Publication date in format YYYY-MM-DD */
							esc_html__( 'This keyword is already scheduled for publication on %s', 'wisewand-ai-content-writer-seo-content-generator' ),
							esc_html( $existing['publication_date'] )
						),
						'existing_id'   => $existing['id'],
						'existing_date' => $existing['publication_date'],
					)
				);
				return;
			}

			// Find next available date
			$upcoming_count = $this->context->calendar_repository->get_upcoming_count_from_date( gmdate( 'Y-m-d' ) );

			$next_date = gmdate( 'Y-m-d', strtotime( '+' . ( $upcoming_count + 1 ) . ' days' ) );

			// Build Wisewand payload
			$wisewand_payload = $payload_builder->build_payload( $keyword_data );

			// Create calendar entry
			$entry_id = $entry_manager->create_entry(
				array(
					'keyword'                => $keyword_data['keyword'],
					'keyword_source'         => 'current_ranking',
					'search_volume'          => $keyword_data['search_volume'] ?? 0,
					'current_position'       => $keyword_data['position'] ?? null,
					'search_intent'          => $keyword_data['search_intent'] ?? 'informational',
					'priority_score'         => $keyword_data['priority'] ?? 0,
					'publication_action'     => 'create',
					'wisewand_payload'       => wp_json_encode( $wisewand_payload ),
					'wisewand_lang'          => $wisewand_payload['lang'],
					'wisewand_type'          => $wisewand_payload['type'],
					'wisewand_length'        => $wisewand_payload['length'],
					'publication_date'       => $next_date,
					'publication_time_range' => 'random',
					'target_post_type'       => 'post',
					'target_author'          => $this->context->options->get( 'wisewand_calendar_default_author', 1 ),
					'created_by'             => get_current_user_id(),
				)
			);

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

			wp_send_json_success(
				array(
					'entry_id'         => $entry_id,
					'publication_date' => $next_date,
					'keyword'          => $keyword_data['keyword'],
					'message'          => esc_html__( 'Keyword scheduled successfully!', 'wisewand-ai-content-writer-seo-content-generator' ),
				)
			);

		} catch ( \Exception $e ) {
			$this->context->logger->error(
				'Failed to schedule keyword',
				array(
					'keyword' => $keyword_data['keyword'] ?? 'unknown',
					'error'   => $e->getMessage(),
				)
			);

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

	/**
	 * AJAX Handler — Discover keywords using DataForSEO keywords_for_site endpoint
	 *
	 * Fetches up to 30 keyword suggestions for the given domain and renders
	 * them as an HTML discovery table for display in the SEO Intelligence panel.
	 *
	 * @since 2.8.2
	 */
	public function ajax_discover_keywords() {
		$this->verify_ajax_request( 'wisewand_seo_real' );

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

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

		try {
			$response = $this->context->dataforseo_client->get_keywords_for_site( $domain, $language_code, $location_code, 50 );

			if ( is_wp_error( $response ) ) {
				throw new \Exception( $response->get_error_message() );
			}

			$keywords = $this->process_discovered_keywords( $response );

			$html = $this->render_discovery_results( $keywords );

			wp_send_json_success(
				array(
					'html'  => $html,
					'count' => count( $keywords ),
				)
			);

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

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

	/**
	 * AJAX Handler — Add manual keywords directly to the editorial calendar
	 *
	 * Parses a newline- or comma-separated list of keywords and creates a
	 * calendar entry for each one that is not already scheduled.
	 *
	 * @since 2.8.2
	 */
	public function ajax_add_manual_keywords() {
		$this->verify_ajax_request( 'wisewand_seo_real' );

		$keywords_text = sanitize_textarea_field( wp_unslash( $_POST['keywords'] ?? '' ) );

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

		try {
			$keywords_array = array_filter( array_map( 'trim', preg_split( '/[\r\n,]+/', $keywords_text ) ) );

			if ( empty( $keywords_array ) ) {
				wp_send_json_error( array( 'message' => esc_html__( 'No valid keywords found', 'wisewand-ai-content-writer-seo-content-generator' ) ) );
			}

			$payload_builder = $this->get_payload_builder();
			$entry_manager   = $this->get_entry_manager();

			$added            = array();
			$skipped          = array();
			$next_date_offset = 1;

			foreach ( $keywords_array as $keyword ) {
				$existing = $entry_manager->check_keyword_exists( $keyword );
				if ( $existing ) {
					$skipped[] = $keyword;
					continue;
				}

				$upcoming_count = $this->context->calendar_repository->get_upcoming_count( current_time( 'Y-m-d' ), gmdate( 'Y-m-d', strtotime( '+30 days' ) ) );

				$publication_date = gmdate( 'Y-m-d', strtotime( '+' . ( $upcoming_count + 1 ) . ' days' ) );

				$keyword_data = array(
					'keyword'       => $keyword,
					'search_intent' => 'informational',
					'content_type'  => 'blog',
				);

				$payload = $payload_builder->build_payload( $keyword_data );

				$entry_id = $entry_manager->create_entry(
					array(
						'keyword'                => $keyword,
						'keyword_source'         => 'manual',
						'search_intent'          => 'informational',
						'wisewand_payload'       => wp_json_encode( $payload ),
						'wisewand_lang'          => $payload['lang'],
						'wisewand_type'          => $payload['type'],
						'wisewand_length'        => $payload['length'],
						'publication_date'       => $publication_date,
						'publication_time_range' => 'random',
						'target_author'          => $this->context->options->get( 'wisewand_calendar_default_author', 1 ),
						'created_by'             => get_current_user_id(),
					)
				);

				if ( ! is_wp_error( $entry_id ) ) {
					$added[] = array(
						'keyword' => $keyword,
						'date'    => $publication_date,
					);
				}
			}

			$message = sprintf(
				/* translators: 1: Number of keywords added, 2: Number of keywords skipped */
				__( '%1$d keyword(s) added to calendar. %2$d skipped (already scheduled).', 'wisewand-ai-content-writer-seo-content-generator' ),
				count( $added ),
				count( $skipped )
			);

			wp_send_json_success(
				array(
					'success_count' => count( $added ),
					'skipped_count' => count( $skipped ),
					'added'         => $added,
					'skipped'       => $skipped,
					'message'       => $message,
					'summary_html'  => $this->render_manual_summary( $added, $skipped ),
				)
			);

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

	/**
	 * AJAX Handler — Generate a smart editorial content roadmap
	 *
	 * Loads all available keywords (rankings, opportunities, saved library),
	 * applies the requested selection strategy and cadence settings, generates
	 * a full calendar plan via Roadmap_Generator, and bulk-creates the entries.
	 * Optionally triggers AI brief generation and/or same-day CRON execution.
	 *
	 * @since 2.8.2
	 */
	public function ajax_generate_smart_roadmap() {
		if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
			error_log( '=== WISEWAND ROADMAP: Handler started ===' );
		}

		check_ajax_referer( 'wisewand_seo_real', 'nonce' );

		if ( ! current_user_can( 'edit_posts' ) ) {
			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				error_log( 'WISEWAND ROADMAP: Permission denied' );
			}
			wp_send_json_error( array( 'message' => esc_html__( 'Insufficient permissions', 'wisewand-ai-content-writer-seo-content-generator' ) ) );
		}

		$domain            = sanitize_text_field( $_POST['domain'] ?? '' );
		$language_code     = sanitize_text_field( $_POST['language_code'] ?? 'en' );
		$location_code     = absint( $_POST['location_code'] ?? 2840 );
		$posts_per_period  = absint( $_POST['posts_per_period'] ?? 1 );
		$period_type       = sanitize_text_field( $_POST['period_type'] ?? 'day' );
		$keyword_selection = sanitize_text_field( $_POST['keyword_selection'] ?? 'all' );

		// Decode JSON first, then sanitize array elements
		$raw_keywords          = isset( $_POST['selected_keywords'] ) ? wp_unslash( $_POST['selected_keywords'] ) : '';
		$selected_keywords_raw = json_decode( $raw_keywords, true );
		if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $selected_keywords_raw ) ) {
			$selected_keywords = array();
		} else {
			$selected_keywords = array_map( 'sanitize_text_field', $selected_keywords_raw );
		}

		// Decode JSON first, then sanitize array elements
		$raw_actions                 = isset( $_POST['cannibalisation_actions'] ) ? wp_unslash( $_POST['cannibalisation_actions'] ) : '';
		$cannibalisation_actions_raw = json_decode( $raw_actions, true );
		if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $cannibalisation_actions_raw ) ) {
			$cannibalisation_actions = array();
		} else {
			$cannibalisation_actions = array_map( 'sanitize_text_field', $cannibalisation_actions_raw );
		}
		$generate_ai_instructions = (int) ( $_POST['generate_ai_instructions'] ?? 0 ) === 1;

		if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
			error_log( 'AI_INSTRUCTIONS: Modal checkbox value (from POST): ' . ( $_POST['generate_ai_instructions'] ?? 'NOT SET' ) );
			error_log( 'AI_INSTRUCTIONS: Parsed as boolean: ' . ( $generate_ai_instructions ? 'TRUE (will use OpenAI)' : 'FALSE (no OpenAI calls)' ) );
			error_log( 'AI_INSTRUCTIONS: Global setting: ' . ( $this->context->options->get( 'wisewand_default_auto_generate_instructions' ) ? 'ENABLED' : 'DISABLED' ) );
			error_log( 'WISEWAND ROADMAP: Parameters - posts: ' . $posts_per_period . ', period: ' . $period_type . ', selection: ' . $keyword_selection . ', AI instructions: ' . ( $generate_ai_instructions ? 'YES' : 'NO' ) );
		}

		// Validate: Default project must be configured
		$default_project = $this->context->options->get( 'wisewand_default_project_id', '' );
		if ( empty( $default_project ) ) {
			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				error_log( 'WISEWAND ROADMAP: No default project configured' );
			}
			wp_send_json_error(
				array(
					'message' => esc_html__( 'Default Wisewand project not configured.', 'wisewand-ai-content-writer-seo-content-generator' ) . "\n\n" .
								__( 'Please go to:', 'wisewand-ai-content-writer-seo-content-generator' ) . "\n" .
								__( 'Settings → Calendar Defaults', 'wisewand-ai-content-writer-seo-content-generator' ) . "\n\n" .
								__( 'And select a default project before generating roadmaps.', 'wisewand-ai-content-writer-seo-content-generator' ),
				)
			);
			return;
		}

		try {
			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				error_log( 'WISEWAND ROADMAP: Entering try block' );
			}

			$this->context->logger->info(
				'Roadmap AJAX called',
				array(
					'domain'            => $domain,
					'posts_per_period'  => $posts_per_period,
					'period_type'       => $period_type,
					'keyword_selection' => $keyword_selection,
				)
			);

			// Load ALL keywords from table (no snapshot requirement!)
			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				error_log( 'WISEWAND ROADMAP: Loading keywords...' );
			}
			$all_keywords = $this->load_all_keywords_for_roadmap( $domain, $language_code, $location_code );
			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				error_log( 'WISEWAND ROADMAP: Loaded ' . count( $all_keywords ) . ' keywords' );
			}

			$this->context->logger->info(
				'Keywords loaded',
				array(
					'count'         => count( $all_keywords ),
					'first_keyword' => ! empty( $all_keywords ) ? $all_keywords[0]['keyword'] : 'none',
				)
			);

			// Apply keyword selection strategy
			if ( 'selected' === $keyword_selection && ! empty( $selected_keywords ) ) {
				$all_keywords = array_filter(
					$all_keywords,
					function ( $kw ) use ( $selected_keywords ) {
						return in_array( $kw['keyword'], $selected_keywords, true );
					}
				);
				$all_keywords = array_values( $all_keywords );
			} elseif ( 'auto' === $keyword_selection ) {
				$all_keywords = array_slice( $all_keywords, 0, 180 );
			}
			// 'all' = use all keywords (no filtering)

			if ( empty( $all_keywords ) ) {
				$this->context->logger->error(
					'No keywords after filtering',
					array(
						'keyword_selection'       => $keyword_selection,
						'selected_keywords_count' => count( $selected_keywords ),
					)
				);
				wp_send_json_error( array( 'message' => esc_html__( 'No keywords available. Add keywords or run SEO analysis first.', 'wisewand-ai-content-writer-seo-content-generator' ) ) );
				return;
			}

			// Initialize roadmap generator via lazy helpers
			$entry_manager     = $this->get_entry_manager();
			$roadmap_generator = $this->get_roadmap_generator();

			$this->context->logger->info(
				'Roadmap generation with enhanced options',
				array(
					'total_keywords'    => count( $all_keywords ),
					'posts_per_period'  => $posts_per_period,
					'period_type'       => $period_type,
					'keyword_selection' => $keyword_selection,
				)
			);

			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				error_log( 'WISEWAND ROADMAP: Calling generate_smart_roadmap()...' );
			}

			// Accept start_date from POST
			$start_date_input = isset( $_POST['start_date'] )
				? sanitize_text_field( wp_unslash( $_POST['start_date'] ) )
				: gmdate( 'Y-m-d' );

			// Validate: Don't allow past dates
			if ( strtotime( $start_date_input ) < strtotime( gmdate( 'Y-m-d' ) ) ) {
				$start_date = gmdate( 'Y-m-d' );
			} else {
				$start_date = $start_date_input;
			}

			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				error_log( 'ROADMAP: Start date - Input: ' . $start_date_input . ', Final: ' . $start_date . ', Today: ' . gmdate( 'Y-m-d' ) );
			}

			$roadmap_options = array(
				'detect_cannibalisation'   => true,
				'cannibalisation_actions'  => $cannibalisation_actions,
				'generate_ai_instructions' => $generate_ai_instructions,
			);

			if ( $generate_ai_instructions && defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				error_log( 'WISEWAND ROADMAP: AI Instructions generation ENABLED for ' . count( $all_keywords ) . ' keywords' );
			}

			$calendar_entries = $roadmap_generator->generate_smart_roadmap(
				$all_keywords,
				$start_date,
				$posts_per_period,
				$period_type,
				$roadmap_options
			);

			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				error_log( 'WISEWAND ROADMAP: Generated ' . count( $calendar_entries ) . ' calendar entries' );
				error_log( 'WISEWAND ROADMAP: Bulk creating entries...' );
			}

			// Bulk create entries
			$result = $entry_manager->bulk_create_entries( $calendar_entries );

			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				error_log( 'WISEWAND ROADMAP: Bulk create result - success: ' . $result['success_count'] . ', duplicates: ' . ( $result['duplicate_count'] ?? 0 ) . ', failed: ' . ( $result['failed_count'] ?? 0 ) );
			}

			// If all were duplicates, send helpful error
			if ( 0 === $result['success_count'] && ( $result['duplicate_count'] ?? 0 ) > 0 ) {
				$msg  = "All {$result['duplicate_count']} keywords are already scheduled in the Editorial Calendar.\n\n";
				$msg .= "Options:\n";
				$msg .= "1. Go to Editorial Calendar and delete existing entries\n";
				$msg .= "2. Select different keywords\n";
				$msg .= '3. Choose a different start date';

				if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
					error_log( 'WISEWAND ROADMAP: All duplicates - sending error' );
				}
				wp_send_json_error( array( 'message' => $msg ) );
				return;
			}

			// Calculate statistics
			$stats = array(
				'total_scheduled' => $result['success_count'],
				'duplicates'      => $result['duplicate_count'] ?? 0,
				'creates'         => count( array_filter( $calendar_entries, fn( $e ) => 'create' === $e['publication_action'] ) ),
				'updates'         => count( array_filter( $calendar_entries, fn( $e ) => 'update' === $e['publication_action'] ) ),
				'skipped'         => count( $all_keywords ) - count( $calendar_entries ),
				'by_risk'         => array(
					'none'   => count( array_filter( $calendar_entries, fn( $e ) => ( $e['cannibalisation_risk'] ?? 'none' ) === 'none' ) ),
					'low'    => count( array_filter( $calendar_entries, fn( $e ) => ( $e['cannibalisation_risk'] ?? 'none' ) === 'low' ) ),
					'medium' => count( array_filter( $calendar_entries, fn( $e ) => ( $e['cannibalisation_risk'] ?? 'none' ) === 'medium' ) ),
					'high'   => count( array_filter( $calendar_entries, fn( $e ) => ( $e['cannibalisation_risk'] ?? 'none' ) === 'high' ) ),
				),
				'date_start'      => $calendar_entries[0]['publication_date'] ?? $start_date,
				'date_end'        => $calendar_entries[ count( $calendar_entries ) - 1 ]['publication_date'] ?? $start_date,
				'total_days'      => $this->calculate_days_between(
					$calendar_entries[0]['publication_date'] ?? $start_date,
					$calendar_entries[ count( $calendar_entries ) - 1 ]['publication_date'] ?? $start_date
				),
			);

			$this->context->logger->info( 'Smart roadmap generated (enhanced)', $stats );

			// Auto-schedule CRON for automation
			$this->ensure_calendar_cron_scheduled();

			// Add entry IDs and AI generation flag to response
			$stats['created_entry_ids']        = $result['created_ids'] ?? array();
			$stats['generate_ai_instructions'] = $generate_ai_instructions;

			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				error_log( 'WISEWAND ROADMAP: Sending success response - scheduled: ' . $result['success_count'] . ', duplicates: ' . ( $result['duplicate_count'] ?? 0 ) . ', AI instructions: ' . ( $generate_ai_instructions ? 'YES' : 'NO' ) );
			}

			// Trigger CRON if same-day roadmap
			$is_same_day = ( $start_date === gmdate( 'Y-m-d' ) );
			if ( $is_same_day && $result['success_count'] > 0 ) {
				if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
					error_log( 'ROADMAP: Same-day roadmap detected! Triggering immediate CRON...' );
				}

				do_action( 'wisewand_editorial_calendar_cron' );

				$this->context->logger->info(
					'Triggered immediate CRON for same-day roadmap',
					array(
						'start_date'      => $start_date,
						'entries_created' => $result['success_count'],
					)
				);

				$stats['immediate_generation_triggered'] = true;
				$stats['message']                        = 'Roadmap created and article generation started immediately!';
			}

			wp_send_json_success( $stats );

			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				error_log( 'WISEWAND ROADMAP: Response sent successfully' );
				error_log( '=== WISEWAND ROADMAP: Handler completed ===' );
			}

		} catch ( \Exception $e ) {
			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				error_log( 'WISEWAND ROADMAP: Exception caught - ' . $e->getMessage() . ' at line ' . $e->getLine() );
			}

			$this->context->logger->error(
				'Smart roadmap generation failed',
				array(
					'domain' => $domain,
					'error'  => $e->getMessage(),
					'file'   => $e->getFile(),
					'line'   => $e->getLine(),
					'trace'  => $e->getTraceAsString(),
				)
			);

			$error_message = 'Exception: ' . $e->getMessage() . ' (File: ' . basename( $e->getFile() ) . ', Line: ' . $e->getLine() . ')';
			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				error_log( 'WISEWAND ROADMAP: Sending error response: ' . $error_message );
			}
			wp_send_json_error( array( 'message' => $error_message ) );

		} catch ( \Error $e ) {
			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				error_log( 'WISEWAND ROADMAP: Fatal error caught - ' . $e->getMessage() . ' at line ' . $e->getLine() );
			}

			$this->context->logger->error(
				'Smart roadmap fatal error',
				array(
					'error' => $e->getMessage(),
					'file'  => $e->getFile(),
					'line'  => $e->getLine(),
				)
			);

			$error_message = 'Fatal error: ' . $e->getMessage() . ' (File: ' . basename( $e->getFile() ) . ', Line: ' . $e->getLine() . ')';
			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				error_log( 'WISEWAND ROADMAP: Sending fatal error response: ' . $error_message );
			}
			wp_send_json_error( array( 'message' => $error_message ) );
		}
	}

	/**
	 * AJAX Handler — Return list of detected languages across all keyword sources
	 *
	 * Loads every keyword regardless of language, counts by language code, and
	 * returns a sorted list with human-readable language names.
	 *
	 * @since 2.8.2
	 */
	public function ajax_get_available_languages() {
		$this->verify_ajax_request( 'wisewand_seo_real' );

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

		try {
			$all_keywords = $this->load_all_keywords_for_roadmap( $domain, 'all', $location_code );

			$language_counts = array();
			foreach ( $all_keywords as $kw ) {
				$lang = $kw['language_code'] ?? 'en';
				if ( ! isset( $language_counts[ $lang ] ) ) {
					$language_counts[ $lang ] = 0;
				}
				++$language_counts[ $lang ];
			}

			arsort( $language_counts );

			$languages      = array();
			$language_names = array(
				'en' => '🇬🇧 English',
				'fr' => '🇫🇷 French',
				'es' => '🇪🇸 Spanish',
				'de' => '🇩🇪 German',
				'it' => '🇮🇹 Italian',
				'pt' => '🇵🇹 Portuguese',
				'nl' => '🇳🇱 Dutch',
				'pl' => '🇵🇱 Polish',
				'ru' => '🇷🇺 Russian',
				'ja' => '🇯🇵 Japanese',
				'zh' => '🇨🇳 Chinese',
				'ar' => '🇸🇦 Arabic',
				'hi' => '🇮🇳 Hindi',
				'tr' => '🇹🇷 Turkish',
				'ko' => '🇰🇷 Korean',
			);

			foreach ( $language_counts as $code => $count ) {
				$languages[] = array(
					'code'  => $code,
					'name'  => $language_names[ $code ] ?? strtoupper( $code ),
					'count' => $count,
				);
			}

			wp_send_json_success(
				array(
					'languages'      => $languages,
					'total_keywords' => count( $all_keywords ),
				)
			);

		} catch ( \Exception $e ) {
			$this->context->logger->error( 'Failed to get available languages', array( 'error' => $e->getMessage() ) );
			wp_send_json_error( array( 'message' => esc_html( 'Failed to detect languages: ' . $e->getMessage() ) ) );
		}
	}

	/**
	 * AJAX Handler — Save keywords to the keyword library
	 *
	 * Parses a newline- or comma-separated keyword list, persists each keyword
	 * via Saved_Keywords_Manager, optionally enriches with DataForSEO metrics,
	 * and copies any matching ranking data from the SEO Intelligence snapshot.
	 *
	 * @since 2.8.2
	 */
	public function ajax_save_keywords_to_library() {
		$this->verify_ajax_request( 'wisewand_seo_real' );

		// CRITICAL FIX: Increase timeout for large keyword batches
		// Large batches with auto_enrich can take 50+ seconds (each keyword = 1 API call + delay)
		// Without this, PHP default timeout (30s) kills the request before completion
		// phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- Required for long-running SEO analysis operations
		set_time_limit( 300 ); // 5 minutes max

		// Increase memory limit if needed for large batches
		if ( function_exists( 'wp_raise_memory_limit' ) ) {
			wp_raise_memory_limit( 'admin' );
		}

		$keywords_text   = sanitize_textarea_field( wp_unslash( $_POST['keywords'] ?? '' ) );
		$skip_duplicates = ! empty( $_POST['skip_duplicates'] );
		$auto_enrich     = ! empty( $_POST['auto_enrich'] );
		$language_code   = sanitize_text_field( $_POST['language_code'] ?? 'en' );
		$location_code   = absint( $_POST['location_code'] ?? 2840 );

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

		try {
			$saved_manager = $this->context->get_saved_keywords_manager();

			$keywords_array = array_filter( array_map( 'trim', preg_split( '/[\r\n,]+/', $keywords_text ) ) );

			$this->context->logger->info(
				'Saving keywords to library',
				array(
					'keywords_count'  => count( $keywords_array ),
					'language_code'   => $language_code,
					'location_code'   => $location_code,
					'skip_duplicates' => $skip_duplicates,
					'auto_enrich'     => $auto_enrich,
				)
			);

			$result = $saved_manager->save_keywords( $keywords_array, $auto_enrich, 'manual', null, $language_code, $location_code, $skip_duplicates );

			// Copy ranking data from SEO Intelligence if available
			$this->copy_ranking_data_to_saved_keywords( $result['saved'], $language_code, $location_code );

			$this->context->logger->info(
				'Keywords saved result',
				array(
					'saved'   => $result['saved_count'],
					'skipped' => $result['skipped_count'],
				)
			);

			// Build enrichment status message
			$enrichment_msg = '';
			if ( $auto_enrich ) {
				if ( \Wisewand\Utils\DataForSEO_Universal::is_configured() ) {
					/* translators: enrichment status message */
					$enrichment_msg = esc_html__( ' (enriched with SEO data)', 'wisewand-ai-content-writer-seo-content-generator' );
				} else {
					/* translators: enrichment status message when DataForSEO not configured */
					$enrichment_msg = esc_html__( ' (enrichment skipped - DataForSEO not configured)', 'wisewand-ai-content-writer-seo-content-generator' );
				}
			}

			wp_send_json_success(
				array(
					'saved_count'   => $result['saved_count'],
					'skipped_count' => $result['skipped_count'],
					'failed_count'  => $result['failed_count'],
					/* translators: 1: Number of keywords saved, 2: Number of keywords skipped, 3: Number of keywords failed, 4: Enrichment status message */
					'message'       => sprintf( esc_html__( '%1$d saved, %2$d skipped, %3$d failed%4$s', 'wisewand-ai-content-writer-seo-content-generator' ), $result['saved_count'], $result['skipped_count'], $result['failed_count'], $enrichment_msg ),
				)
			);

		} catch ( \Exception $e ) {
			$this->context->logger->error(
				'Failed to save keywords',
				array(
					'error' => $e->getMessage(),
					'trace' => $e->getTraceAsString(),
				)
			);
			wp_send_json_error( array( 'message' => esc_html( 'Error: ' . $e->getMessage() ) ) );
		}
	}

	/**
	 * AJAX Handler — Enrich a set of saved keywords with DataForSEO metrics
	 *
	 * Fetches up-to-date search volume, difficulty, and CPC data for each
	 * specified keyword ID and updates the saved keyword record.
	 *
	 * @since 2.8.2
	 */
	public function ajax_enrich_saved_keywords() {
		$this->verify_ajax_request( 'wisewand_seo_real' );

		$keyword_ids = array_map( 'absint', $_POST['keyword_ids'] ?? array() );

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

		try {
			$saved_manager = $this->context->get_saved_keywords_manager();

			$rate_limiter = $this->context->rate_limiter;

			$enriched = 0;
			foreach ( $keyword_ids as $keyword_id ) {
				if ( $saved_manager->enrich_keyword( $keyword_id ) ) {
					++$enriched;
				}
				$rate_limiter->smart_delay( 'dataforseo', 0.5 );
			}

			wp_send_json_success(
				array(
					'enriched' => $enriched,
					/* translators: %d: Number of keywords enriched with SEO data */
					'message'  => sprintf( esc_html__( '%d keyword(s) enriched with SEO data', 'wisewand-ai-content-writer-seo-content-generator' ), $enriched ),
				)
			);

		} catch ( \Exception $e ) {
			wp_send_json_error( array( 'message' => esc_html( $e->getMessage() ) ) );
		}
	}

	/**
	 * AJAX Handler — Schedule a single saved keyword to the editorial calendar
	 *
	 * Looks up the saved keyword by ID, checks it is not already scheduled,
	 * and creates a new calendar entry at the next available date.
	 *
	 * @since 2.8.2
	 */
	public function ajax_schedule_saved_keyword() {
		$this->verify_ajax_request( 'wisewand_seo_real' );

		$keyword_id = absint( $_POST['keyword_id'] ?? 0 );

		try {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Query custom plugin table for saved keyword data
			$saved_kw = $this->context->keyword_repository->get_keyword( $keyword_id );

			if ( ! $saved_kw ) {
				wp_send_json_error( array( 'message' => esc_html__( 'Keyword not found', 'wisewand-ai-content-writer-seo-content-generator' ) ) );
				return;
			}

			$saved_kw = (array) $saved_kw;

			$keyword_data = array(
				'keyword'       => $saved_kw['keyword'],
				'search_volume' => $saved_kw['search_volume'],
				'search_intent' => $saved_kw['search_intent'],
				'content_type'  => $saved_kw['content_type'],
				'priority'      => $saved_kw['priority_score'],
			);

			$payload_builder = $this->get_payload_builder();
			$entry_manager   = $this->get_entry_manager();

			$existing = $entry_manager->check_keyword_exists( $saved_kw['keyword'] );
			if ( $existing ) {
				/* translators: %s: Publication date in format YYYY-MM-DD */
				wp_send_json_error( array( 'message' => sprintf( esc_html__( 'Already scheduled for %s', 'wisewand-ai-content-writer-seo-content-generator' ), $existing['publication_date'] ) ) );
				return;
			}

			$upcoming_count = $this->context->calendar_repository->get_upcoming_count( current_time( 'Y-m-d' ), gmdate( 'Y-m-d', strtotime( '+30 days' ) ) );

			$next_date = gmdate( 'Y-m-d', strtotime( '+' . ( $upcoming_count + 1 ) . ' days' ) );

			$wisewand_payload = $payload_builder->build_payload( $keyword_data );

			$entry_id = $entry_manager->create_entry(
				array(
					'keyword'                => $saved_kw['keyword'],
					'keyword_source'         => 'saved',
					'search_volume'          => $saved_kw['search_volume'],
					'search_intent'          => $saved_kw['search_intent'],
					'priority_score'         => $saved_kw['priority_score'],
					'wisewand_payload'       => wp_json_encode( $wisewand_payload ),
					'wisewand_lang'          => $wisewand_payload['lang'],
					'wisewand_type'          => $wisewand_payload['type'],
					'wisewand_length'        => $wisewand_payload['length'],
					'publication_date'       => $next_date,
					'publication_time_range' => 'random',
					'target_author'          => $this->context->options->get( 'wisewand_calendar_default_author', 1 ),
					'created_by'             => get_current_user_id(),
				)
			);

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

			wp_send_json_success(
				array(
					'entry_id'         => $entry_id,
					'publication_date' => $next_date,
					'keyword'          => $saved_kw['keyword'],
				)
			);

		} catch ( \Exception $e ) {
			wp_send_json_error( array( 'message' => esc_html( $e->getMessage() ) ) );
		}
	}

	/**
	 * AJAX Handler — Schedule multiple saved keywords to the editorial calendar
	 *
	 * Iterates over the supplied keyword IDs and creates a calendar entry for
	 * each keyword that is not already scheduled, incrementing the publication
	 * date offset with each addition.
	 *
	 * @since 2.8.2
	 */
	public function ajax_schedule_saved_keywords() {
		$this->verify_ajax_request( 'wisewand_seo_real' );

		$keyword_ids = array_map( 'absint', $_POST['keyword_ids'] ?? array() );

		try {
			$payload_builder = $this->get_payload_builder();
			$entry_manager   = $this->get_entry_manager();

			$scheduled = 0;

			foreach ( $keyword_ids as $keyword_id ) {
				$saved_kw = $this->context->keyword_repository->get_keyword( $keyword_id );
				if ( $saved_kw ) {
					$saved_kw = (array) $saved_kw;
				}

				if ( ! $saved_kw ) {
					continue;
				}

				$existing = $entry_manager->check_keyword_exists( $saved_kw['keyword'] );
				if ( $existing ) {
					continue;
				}

				$upcoming_count = $this->context->calendar_repository->get_upcoming_count( current_time( 'Y-m-d' ), gmdate( 'Y-m-d', strtotime( '+30 days' ) ) );

				$next_date = gmdate( 'Y-m-d', strtotime( '+' . ( $upcoming_count + 1 ) . ' days' ) );

				$keyword_data = array(
					'keyword'       => $saved_kw['keyword'],
					'search_volume' => $saved_kw['search_volume'],
					'search_intent' => $saved_kw['search_intent'],
					'content_type'  => $saved_kw['content_type'],
					'priority'      => $saved_kw['priority_score'],
				);

				$wisewand_payload = $payload_builder->build_payload( $keyword_data );

				$entry_id = $entry_manager->create_entry(
					array(
						'keyword'                => $saved_kw['keyword'],
						'keyword_source'         => 'saved',
						'search_volume'          => $saved_kw['search_volume'],
						'search_intent'          => $saved_kw['search_intent'],
						'priority_score'         => $saved_kw['priority_score'],
						'wisewand_payload'       => wp_json_encode( $wisewand_payload ),
						'wisewand_lang'          => $wisewand_payload['lang'],
						'wisewand_type'          => $wisewand_payload['type'],
						'wisewand_length'        => $wisewand_payload['length'],
						'publication_date'       => $next_date,
						'publication_time_range' => 'random',
						'target_author'          => $this->context->options->get( 'wisewand_calendar_default_author', 1 ),
						'created_by'             => get_current_user_id(),
					)
				);

				if ( ! is_wp_error( $entry_id ) ) {
					++$scheduled;
				}
			}

			wp_send_json_success( array( 'scheduled' => $scheduled ) );

		} catch ( \Exception $e ) {
			wp_send_json_error( array( 'message' => esc_html( $e->getMessage() ) ) );
		}
	}

	/**
	 * AJAX Handler — Delete a single saved keyword from the library
	 *
	 * @since 2.8.2
	 */
	public function ajax_delete_saved_keyword() {
		$this->verify_ajax_request( 'wisewand_seo_real' );

		$keyword_id = absint( $_POST['keyword_id'] ?? 0 );

		try {
			$saved_manager = $this->context->get_saved_keywords_manager();

			$result = $saved_manager->delete_keyword( $keyword_id );

			if ( is_wp_error( $result ) ) {
				throw new \Exception( $result->get_error_message() );
			}

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

		} catch ( \Exception $e ) {
			wp_send_json_error( array( 'message' => esc_html( $e->getMessage() ) ) );
		}
	}

	/**
	 * AJAX Handler — Fetch keyword ideas for a single seed (supports progress tracking)
	 *
	 * Processes one seed keyword per request to allow the JavaScript layer to
	 * stream progress updates. Saves the seed, fetches ideas from DataForSEO,
	 * enriches each idea with volume data, and saves them to the library.
	 *
	 * @since 2.8.2
	 */
	public function ajax_get_keyword_ideas() {
		$this->verify_ajax_request( 'wisewand_seo_real' );

		$seeds_text    = sanitize_textarea_field( $_POST['seeds'] ?? '' );
		$language_code = sanitize_text_field( $_POST['language_code'] ?? 'en' );
		$location_code = absint( $_POST['location_code'] ?? 2840 );
		$seed_index    = absint( $_POST['seed_index'] ?? 0 );

		if ( empty( $seeds_text ) ) {
			wp_send_json_error( array( 'message' => esc_html__( 'Please enter at least one seed keyword', 'wisewand-ai-content-writer-seo-content-generator' ) ) );
		}

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

		try {
			$seed_keywords = array_filter( array_map( 'trim', preg_split( '/[\r\n,]+/', $seeds_text ) ) );

			if ( empty( $seed_keywords ) || ! isset( $seed_keywords[ $seed_index ] ) ) {
				wp_send_json_error( array( 'message' => esc_html__( 'Invalid seed index', 'wisewand-ai-content-writer-seo-content-generator' ) ) );
			}

			$seed = $seed_keywords[ $seed_index ];

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

			$progress_log = $this->process_single_seed_with_progress( $seed, $saved_manager, $language_code, $location_code, $seed_index + 1, count( $seed_keywords ) );

			wp_send_json_success( $progress_log );

		} catch ( \Exception $e ) {
			$this->context->logger->error(
				'Keyword ideas failed',
				array(
					'error' => $e->getMessage(),
					'file'  => $e->getFile(),
					'line'  => $e->getLine(),
				)
			);
			wp_send_json_error( array( 'message' => esc_html( 'Error: ' . $e->getMessage() ) ) );
		}
	}

	/**
	 * AJAX Handler — Update a saved keyword record
	 *
	 * Accepts an associative array of field updates and applies them via
	 * Saved_Keywords_Manager after sanitizing each value by expected type.
	 *
	 * @since 2.8.2
	 */
	public function ajax_update_saved_keyword() {
		$this->verify_ajax_request( 'wisewand_seo_real' );

		$keyword_id  = absint( $_POST['keyword_id'] ?? 0 );
		$updates_raw = wp_unslash( $_POST['updates'] ?? array() );
		$updates     = array();

		if ( is_array( $updates_raw ) ) {
			foreach ( $updates_raw as $key => $value ) {
				$sanitized_key = sanitize_key( $key );

				if ( in_array( $sanitized_key, array( 'volume', 'difficulty', 'cpc' ), true ) ) {
					$updates[ $sanitized_key ] = is_numeric( $value ) ? floatval( $value ) : 0;
				} elseif ( in_array( $sanitized_key, array( 'is_favorite', 'is_archived' ), true ) ) {
					$updates[ $sanitized_key ] = (bool) $value;
				} else {
					$updates[ $sanitized_key ] = sanitize_text_field( $value );
				}
			}
		}

		if ( ! $keyword_id ) {
			wp_send_json_error( array( 'message' => esc_html__( 'Invalid keyword ID', 'wisewand-ai-content-writer-seo-content-generator' ) ) );
		}

		try {
			$saved_manager = $this->context->get_saved_keywords_manager();

			$result = $saved_manager->update_keyword( $keyword_id, $updates );

			if ( ! $result ) {
				throw new \Exception( 'Failed to update keyword' );
			}

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

		} catch ( \Exception $e ) {
			wp_send_json_error( array( 'message' => esc_html( $e->getMessage() ) ) );
		}
	}

	/**
	 * AJAX Handler — Bulk delete saved keywords
	 *
	 * @since 2.8.2
	 */
	public function ajax_bulk_delete_saved_keywords() {
		$this->verify_ajax_request( 'wisewand_seo_real' );

		$keyword_ids = array_map( 'absint', $_POST['keyword_ids'] ?? array() );

		try {
			$saved_manager = $this->context->get_saved_keywords_manager();

			$deleted = $saved_manager->bulk_delete( $keyword_ids );

			wp_send_json_success( array( 'deleted' => $deleted ) );

		} catch ( \Exception $e ) {
			wp_send_json_error( array( 'message' => esc_html( $e->getMessage() ) ) );
		}
	}

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

	/**
	 * Process raw DataForSEO keywords_for_site response into normalized keyword array
	 *
	 * @since 2.8.2
	 *
	 * @param array $response Raw API response array.
	 * @return array Up to 30 normalized keyword arrays.
	 */
	private function process_discovered_keywords( $response ) {
		$task  = $response[0] ?? array();
		$items = $task['result'][0]['items'] ?? array();

		$keywords = array();
		foreach ( $items as $item ) {
			$kw_data       = $item['keyword_data'] ?? array();
			$keyword       = $kw_data['keyword'] ?? '';
			$search_volume = $kw_data['keyword_info']['search_volume'] ?? 0;

			if ( empty( $keyword ) ) {
				continue;
			}

			$keywords[] = array(
				'keyword'       => $keyword,
				'search_volume' => $search_volume,
				'search_intent' => $this->detect_search_intent( $keyword ),
				'content_type'  => $this->detect_content_type( $keyword ),
			);
		}

		return array_slice( $keywords, 0, 30 );
	}

	/**
	 * Render discovered keywords as an HTML discovery table
	 *
	 * @since 2.8.2
	 *
	 * @param array $keywords Normalized keyword arrays.
	 * @return string HTML table markup.
	 */
	private function render_discovery_results( $keywords ) {
		if ( empty( $keywords ) ) {
			return '<div class="empty-state"><p>No new keywords found</p></div>';
		}

		$html  = '<table class="discovery-table">';
		$html .= '<thead><tr>';
		$html .= '<th>Keyword</th>';
		$html .= '<th>Volume</th>';
		$html .= '<th>Intent</th>';
		$html .= '<th>Action</th>';
		$html .= '</tr></thead><tbody>';

		foreach ( $keywords as $kw ) {
			$keyword_json = htmlspecialchars( wp_json_encode( $kw ), ENT_QUOTES, 'UTF-8' );

			$html .= '<tr>';
			$html .= '<td><strong>' . esc_html( $kw['keyword'] ) . '</strong></td>';
			$html .= '<td>' . number_format( $kw['search_volume'] ) . '/mo</td>';
			$html .= '<td><span class="intent-badge">' . ucfirst( $kw['search_intent'] ) . '</span></td>';
			$html .= '<td><button class="btn-add-discovery" data-keyword="' . $keyword_json . '">&#10133; Add to Calendar</button></td>';
			$html .= '</tr>';
		}

		$html .= '</tbody></table>';
		return $html;
	}

	/**
	 * Render summary HTML for the manual keyword addition result
	 *
	 * @since 2.8.2
	 *
	 * @param array $added   Keywords that were successfully added.
	 * @param array $skipped Keywords that were already scheduled.
	 * @return string HTML summary markup.
	 */
	private function render_manual_summary( $added, $skipped ) {
		if ( empty( $added ) && empty( $skipped ) ) {
			return '';
		}

		$html  = '<div style="margin-top: 16px; padding: 12px; background: #f0f9ff; border-radius: 8px;">';
		$html .= '<strong>Summary:</strong><br>';

		if ( ! empty( $added ) ) {
			$html .= '&#10003; Added: ' . count( $added ) . ' keyword(s)<br>';
		}

		if ( ! empty( $skipped ) ) {
			$html .= 'Skipped: ' . count( $skipped ) . ' (already scheduled)<br>';
		}

		$html .= '</div>';
		return $html;
	}

	/**
	 * Load all keywords from all sources for use in roadmap generation
	 *
	 * Merges current rankings, SEO opportunities, and saved library keywords
	 * into a single array. Rankings and opportunities are sourced by delegating
	 * to SEO_Analysis_Handler::load_latest_snapshot_public(). All keywords are
	 * enriched with editorial calendar status before returning.
	 *
	 * @since 2.8.2
	 *
	 * @param string $domain        Site domain.
	 * @param string $language_code ISO language code, or 'all' to skip filtering.
	 * @param int    $location_code DataForSEO location code.
	 * @return array Merged and enriched keyword arrays.
	 */
	private function load_all_keywords_for_roadmap( $domain, $language_code, $location_code ) {
		$all_keywords = array();

		// Delegate snapshot loading to the analysis handler to avoid duplicating the logic
		if ( ! class_exists( 'Wisewand\\Admin\\SEO\\SEO_Analysis_Handler' ) ) {
			require_once __DIR__ . '/SEO_Analysis_Handler.php';
		}
		$analysis_handler = new SEO_Analysis_Handler( $this->context );
		$snapshot_data    = $analysis_handler->load_latest_snapshot_public( $domain, $language_code, $location_code );

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

		// Build AI data lookup map from saved keywords (for enriching snapshot data)
		$ai_data_map = array();
		foreach ( $saved_keywords as $saved_kw ) {
			$ai_data_map[ $saved_kw['keyword'] ] = array(
				'author'     => $saved_kw['suggested_author_id'] ?? null,
				'categories' => $saved_kw['suggested_categories'] ?? null,
			);
		}

		if ( $snapshot_data ) {
			// Add current rankings (enriched with AI data)
			foreach ( ( $snapshot_data['current_rankings'] ?? array() ) as $kw ) {
				$kw['source'] = 'current_ranking';

				if ( isset( $ai_data_map[ $kw['keyword'] ] ) ) {
					$kw['suggested_author_id']  = $ai_data_map[ $kw['keyword'] ]['author'];
					$kw['suggested_categories'] = $ai_data_map[ $kw['keyword'] ]['categories'];
				}

				$all_keywords[] = $kw;
			}

			// Add opportunities (enriched with AI data)
			foreach ( ( $snapshot_data['opportunities'] ?? array() ) as $kw ) {
				$kw['source'] = 'opportunity';

				if ( isset( $ai_data_map[ $kw['keyword'] ] ) ) {
					$kw['suggested_author_id']  = $ai_data_map[ $kw['keyword'] ]['author'];
					$kw['suggested_categories'] = $ai_data_map[ $kw['keyword'] ]['categories'];
				}

				$all_keywords[] = $kw;
			}
		}

		// Add saved keywords (already have author/categories)
		foreach ( $saved_keywords as $saved_kw ) {
			$keyword_lang = $saved_kw['language_code'] ?? $language_code;

			// Language filtering: Skip if specific language requested and doesn't match
			if ( 'all' !== $language_code && $keyword_lang !== $language_code ) {
				continue;
			}

			$all_keywords[] = array(
				'keyword'              => $saved_kw['keyword'],
				'position'             => null,
				'search_volume'        => $saved_kw['search_volume'] ?? 0,
				'difficulty'           => $saved_kw['difficulty'] ?? null,
				'cpc'                  => $saved_kw['cpc'],
				'competition'          => $saved_kw['competition'],
				'search_intent'        => $saved_kw['search_intent'],
				'content_type'         => $saved_kw['content_type'],
				'priority'             => $saved_kw['priority_score'] ?? 0,
				'language_code'        => $keyword_lang,
				'source'               => $saved_kw['keyword_source'],
				'suggested_author_id'  => $saved_kw['suggested_author_id'] ?? null,
				'suggested_categories' => $saved_kw['suggested_categories'] ?? null,
			);
		}

		// Apply language filter to all keywords
		if ( 'all' !== $language_code ) {
			$all_keywords = array_filter(
				$all_keywords,
				function ( $kw ) use ( $language_code ) {
					return ( $kw['language_code'] ?? 'en' ) === $language_code;
				}
			);
			$all_keywords = array_values( $all_keywords );
		}

		$this->context->logger->info(
			'All keywords loaded for roadmap',
			array(
				'total'           => count( $all_keywords ),
				'language_filter' => $language_code,
				'from_snapshot'   => $snapshot_data ? count( $snapshot_data['current_rankings'] ?? array() ) + count( $snapshot_data['opportunities'] ?? array() ) : 0,
				'from_saved'      => count( $saved_keywords ),
			)
		);

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

		return $all_keywords;
	}

	/**
	 * Calculate the number of days between two dates (inclusive)
	 *
	 * @since 2.8.2
	 *
	 * @param string $start Start date in Y-m-d format.
	 * @param string $end   End date in Y-m-d format.
	 * @return int Number of days including both start and end.
	 */
	private function calculate_days_between( $start, $end ) {
		$start_dt = new \DateTime( $start );
		$end_dt   = new \DateTime( $end );
		$diff     = $start_dt->diff( $end_dt );
		return $diff->days + 1;
	}

	/**
	 * Process a single seed keyword with step-by-step progress tracking
	 *
	 * Saves the seed, fetches keyword ideas from DataForSEO, enriches each idea
	 * with volume data, saves enriched ideas to the library, and returns a
	 * progress log suitable for streaming back to the JavaScript layer.
	 *
	 * @since 2.8.2
	 *
	 * @param string                                  $seed          Seed keyword text.
	 * @param \Wisewand\Utils\Saved_Keywords_Manager $saved_manager Keyword manager instance.
	 * @param string                                  $language_code ISO language code.
	 * @param int                                     $location_code DataForSEO location code.
	 * @param int                                     $seed_number   1-based index of this seed.
	 * @param int                                     $total_seeds   Total number of seeds in batch.
	 * @return array Progress log with seed stats and step-by-step detail.
	 */
	private function process_single_seed_with_progress( $seed, $saved_manager, $language_code, $location_code, $seed_number, $total_seeds ) {
		$steps = array();

		$steps[] = $this->create_progress_step( 'save_seed', 'started', "Processing seed keyword: {$seed}", null, $seed );

		try {
			if ( ! $saved_manager->keyword_exists( $seed, $language_code, $location_code ) ) {
				$seed_id = $saved_manager->save_keyword( $seed, 'manual', null, $language_code, $location_code );

				if ( ! is_wp_error( $seed_id ) ) {
					$saved_manager->enrich_keyword( $seed_id );

					$this->context->keyword_repository->update_keyword( $seed_id, array( 'tags' => 'seed_keyword' ) );

					$steps[] = $this->create_progress_step( 'save_seed', 'success', 'Seed keyword saved and enriched', 'keywords_data/google_ads/search_volume/live', $seed );
				}
			} else {
				$steps[] = $this->create_progress_step( 'save_seed', 'skipped', 'Seed keyword already exists', null, $seed );
			}
		} catch ( \Exception $e ) {
			$steps[] = $this->create_progress_step( 'save_seed', 'warning', 'Seed already exists', null, $seed );
		}

		// Step 2: Fetch keyword ideas
		$steps[] = $this->create_progress_step( 'fetch_ideas', 'started', 'Fetching keyword ideas from DataForSEO...', 'keywords_data/google_ads/keyword_ideas/live', $seed );

		$response = $this->context->dataforseo_client->get_keyword_ideas( array( $seed ), $language_code, $location_code, 10 );

		if ( is_wp_error( $response ) ) {
			$steps[] = $this->create_progress_step( 'fetch_ideas', 'error', 'Failed: ' . $response->get_error_message(), null, $seed );

			return array(
				'seed'           => $seed,
				'seed_number'    => $seed_number,
				'total_seeds'    => $total_seeds,
				'ideas_found'    => 0,
				'ideas_enriched' => 0,
				'ideas_saved'    => 0,
				'steps'          => $steps,
				'completed'      => $seed_number >= $total_seeds,
			);
		}

		$ideas       = $this->process_keyword_ideas( $response, $seed );
		$ideas_count = count( $ideas );

		$steps[] = $this->create_progress_step( 'fetch_ideas', 'success', "{$ideas_count} keyword ideas retrieved", null, $seed );

		// Step 3: Enrich each keyword with volume data
		$enriched_ideas = array();

		if ( $ideas_count > 0 ) {
			$steps[] = $this->create_progress_step( 'enrich_batch', 'started', "Enriching {$ideas_count} keywords with metrics...", null, $seed );

			foreach ( $ideas as $idea_index => $idea ) {
				$keyword = $idea['keyword'];

				$steps[] = $this->create_progress_step(
					'enrich_keyword',
					'processing',
					"Enriching: {$keyword}",
					'keywords_data/google_ads/search_volume/live',
					$keyword,
					$idea_index + 1,
					$ideas_count
				);

				try {
					$enriched_data = $this->enrich_keyword_idea( $keyword, $seed, $language_code, $location_code );

					if ( $enriched_data ) {
						$enriched_ideas[] = $enriched_data;

						$steps[] = $this->create_progress_step(
							'enrich_keyword',
							'success',
							"&#10003; {$keyword} (Vol: " . number_format( $enriched_data['search_volume'] ) . ', Diff: ' . ( $enriched_data['difficulty'] ?? 'N/A' ) . ')',
							null,
							$keyword,
							$idea_index + 1,
							$ideas_count
						);
					}

					$this->context->rate_limiter->smart_delay( 'dataforseo', 0.3 );
				} catch ( \Exception $e ) {
					$steps[] = $this->create_progress_step( 'enrich_keyword', 'error', "&#10007; {$keyword}: " . $e->getMessage(), null, $keyword );
				}
			}
		}

		// Step 4: Save enriched keywords
		$saved_count   = 0;
		$skipped_count = 0;

		if ( ! empty( $enriched_ideas ) ) {
			$steps[] = $this->create_progress_step( 'save_batch', 'started', 'Saving ' . count( $enriched_ideas ) . ' keywords to library...', null, $seed );

			$result        = $saved_manager->save_keyword_ideas( $enriched_ideas, $seed );
			$saved_count   = $result['saved'];
			$skipped_count = $result['skipped'];

			$steps[] = $this->create_progress_step( 'save_batch', 'success', "Saved {$saved_count} new keywords, skipped {$skipped_count} duplicates", null, $seed );
		}

		return array(
			'seed'           => $seed,
			'seed_number'    => $seed_number,
			'total_seeds'    => $total_seeds,
			'ideas_found'    => $ideas_count,
			'ideas_enriched' => count( $enriched_ideas ),
			'ideas_saved'    => $saved_count,
			'ideas_skipped'  => $skipped_count,
			'steps'          => $steps,
			'completed'      => $seed_number >= $total_seeds,
		);
	}

	/**
	 * Build a single progress step descriptor
	 *
	 * @since 2.8.2
	 *
	 * @param string      $step_type Step identifier string.
	 * @param string      $status    One of: started, success, skipped, warning, error, processing.
	 * @param string      $message   Human-readable status message.
	 * @param string|null $endpoint  Optional DataForSEO endpoint path for display.
	 * @param string|null $keyword   Optional keyword being processed.
	 * @param int|null    $current   Optional current position within a sub-loop.
	 * @param int|null    $total     Optional total count for the sub-loop.
	 * @return array Progress step array.
	 */
	private function create_progress_step( $step_type, $status, $message, $endpoint = null, $keyword = null, $current = null, $total = null ) {
		$step = array(
			'step'      => $step_type,
			'status'    => $status,
			'message'   => $message,
			'timestamp' => current_time( 'mysql' ),
		);

		if ( $endpoint ) {
			$step['endpoint'] = $endpoint;
		}

		if ( $keyword ) {
			$step['keyword'] = $keyword;
		}

		if ( null !== $current && null !== $total ) {
			$step['progress'] = array(
				'current' => $current,
				'total'   => $total,
			);
		}

		return $step;
	}

	/**
	 * Process raw DataForSEO keyword_ideas API response into normalized idea array
	 *
	 * Returns up to 10 ideas sorted by relevance. Volume data is intentionally
	 * omitted here (it will be enriched separately via search_volume endpoint).
	 *
	 * @since 2.8.2
	 *
	 * @param array  $response     Raw API response.
	 * @param string $seed_keyword Seed keyword used in the request.
	 * @return array Up to 10 normalized keyword idea arrays.
	 */
	private function process_keyword_ideas( $response, $seed_keyword ) {
		$task   = $response[0] ?? array();
		$result = $task['result'] ?? array();

		$items = array();
		if ( isset( $result[0]['items'] ) ) {
			$items = $result[0]['items'];
		} elseif ( isset( $result['items'] ) ) {
			$items = $result['items'];
		} elseif ( is_array( $result ) && ! empty( $result ) ) {
			$items = $result;
		}

		$this->context->logger->debug(
			'Processing keyword ideas response',
			array(
				'seed'                 => $seed_keyword,
				'task_keys'            => is_array( $task ) ? array_keys( $task ) : 'not_array',
				'result_type'          => gettype( $result ),
				'result_keys'          => is_array( $result ) ? array_keys( $result ) : 'not_array',
				'items_count'          => count( $items ),
				'first_item_structure' => ! empty( $items[0] ) ? array_keys( $items[0] ) : array(),
				'first_item_sample'    => ! empty( $items[0] ) ? $items[0] : null,
			)
		);

		$ideas = array();

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

			$this->context->logger->debug(
				'Parsing keyword ideas item',
				array(
					'keyword'           => $keyword,
					'item_keys'         => array_keys( $item ),
					'has_keyword_data'  => isset( $item['keyword_data'] ),
					'keyword_data_keys' => isset( $item['keyword_data'] ) ? array_keys( $item['keyword_data'] ) : array(),
				)
			);

			if ( empty( $keyword ) ) {
				$this->context->logger->debug( 'Skipping item - no keyword found', array( 'item_keys' => array_keys( $item ) ) );
				continue;
			}

			$search_intent = $this->detect_search_intent( $keyword );
			$content_type  = $this->detect_content_type( $keyword );

			$ideas[] = array(
				'keyword'        => $keyword,
				'search_volume'  => 0,
				'difficulty'     => null,
				'search_intent'  => $search_intent,
				'content_type'   => $content_type,
				'priority_score' => 0,
			);

			$this->context->logger->debug(
				'Added keyword idea for enrichment',
				array(
					'keyword'      => $keyword,
					'intent'       => $search_intent,
					'content_type' => $content_type,
				)
			);
		}

		usort(
			$ideas,
			function ( $a, $b ) {
				$score_a = (float) ( $a['priority_score'] ?? 0 ) + ( (int) ( $a['search_volume'] ?? 0 ) / 100 );
				$score_b = (float) ( $b['priority_score'] ?? 0 ) + ( (int) ( $b['search_volume'] ?? 0 ) / 100 );
				return $score_b <=> $score_a;
			}
		);

		$filtered_ideas = array_slice( $ideas, 0, 10 );

		$this->context->logger->info(
			'Filtered keyword ideas',
			array(
				'seed'         => $seed_keyword,
				'total_found'  => count( $ideas ),
				'selected_top' => count( $filtered_ideas ),
			)
		);

		return $filtered_ideas;
	}

	/**
	 * Enrich a single keyword idea with live volume, difficulty, and priority data
	 *
	 * Calls the DataForSEO search_volume endpoint for a single keyword and
	 * returns a fully populated idea array ready for saving to the library.
	 *
	 * @since 2.8.2
	 *
	 * @param string $keyword       Keyword to enrich.
	 * @param string $seed_keyword  Parent seed keyword (for logging context).
	 * @param string $language_code ISO language code.
	 * @param int    $location_code DataForSEO location code.
	 * @return array|null Enriched keyword array, or null on failure.
	 */
	private function enrich_keyword_idea( $keyword, $seed_keyword, $language_code = 'en', $location_code = 2840 ) {
		if ( ! $this->context->dataforseo_client ) {
			return null;
		}

		try {
			$volume_data = array(
				array(
					'location_code' => $location_code,
					'language_code' => $language_code,
					'keywords'      => array( $keyword ),
				),
			);

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

			if ( is_wp_error( $response ) ) {
				throw new \Exception( $response->get_error_message() );
			}

			$task    = $response[0] ?? array();
			$items   = $task['result'] ?? array();
			$kw_data = $items[0] ?? array();

			$search_volume     = (int) ( $kw_data['search_volume'] ?? 0 );
			$competition_index = $kw_data['competition_index'] ?? null;
			$difficulty        = ( null !== $competition_index && is_numeric( $competition_index ) ) ? (int) $competition_index : null;

			$search_intent = $this->detect_search_intent( $keyword );
			$content_type  = $this->detect_content_type( $keyword );

			$priority = $this->calculate_priority_score( $search_volume, $difficulty );

			return array(
				'keyword'        => $keyword,
				'search_volume'  => $search_volume,
				'difficulty'     => $difficulty,
				'search_intent'  => $search_intent,
				'content_type'   => $content_type,
				'priority_score' => $priority,
				'language_code'  => $language_code,
				'location_code'  => $location_code,
			);

		} catch ( \Exception $e ) {
			$this->context->logger->error(
				'Keyword enrichment failed',
				array(
					'keyword' => $keyword,
					'error'   => $e->getMessage(),
				)
			);
			return null;
		}
	}

	/**
	 * Calculate a 0–70 point priority score based on search volume and difficulty
	 *
	 * @since 2.8.2
	 *
	 * @param int      $search_volume Monthly search volume.
	 * @param int|null $difficulty    Keyword difficulty (0–100), or null if unknown.
	 * @return int Priority score.
	 */
	private function calculate_priority_score( $search_volume, $difficulty ) {
		$priority = 0;

		// Volume score (0-40 points)
		if ( $search_volume > 10000 ) {
			$priority += 40;
		} elseif ( $search_volume > 5000 ) {
			$priority += 30;
		} elseif ( $search_volume > 2000 ) {
			$priority += 20;
		} elseif ( $search_volume > 500 ) {
			$priority += 10;
		}

		// Difficulty score (0-30 points, inverted — lower difficulty = higher score)
		if ( null !== $difficulty ) {
			if ( $difficulty < 30 ) {
				$priority += 30;
			} elseif ( $difficulty < 50 ) {
				$priority += 20;
			} elseif ( $difficulty < 70 ) {
				$priority += 10;
			}
		}

		return $priority;
	}

	/**
	 * Copy ranking data from SEO Intelligence snapshot to saved keywords
	 *
	 * For each saved keyword that also appears in the SEO analysis snapshot,
	 * updates the saved record with the page_url and position_abs so that
	 * roadmap generation can make informed scheduling decisions.
	 *
	 * @since 2.8.2
	 *
	 * @param array  $keywords      List of keyword text strings.
	 * @param string $language_code ISO language code.
	 * @param int    $location_code DataForSEO location code.
	 */
	private function copy_ranking_data_to_saved_keywords( $keywords, $language_code, $location_code ) {
		if ( empty( $keywords ) ) {
			return;
		}

		$this->context->logger->debug(
			'Copying ranking data to saved keywords',
			array(
				'keywords_count' => count( $keywords ),
				'language'       => $language_code,
				'location'       => $location_code,
			)
		);

		$copied_count = 0;

		foreach ( $keywords as $keyword ) {
			$seo_data = $this->context->seo_repository->get_latest_keyword_ranking( $keyword, $language_code, $location_code );

			if ( $seo_data && ! empty( $seo_data->page_url ) ) {
				$updated = $this->context->keyword_repository->update_keyword_by_text(
					$keyword,
					$language_code,
					array(
						'page_url'     => $seo_data->page_url,
						'position_abs' => $seo_data->position_abs,
					)
				);

				if ( false !== $updated ) {
					++$copied_count;
					$this->context->logger->debug(
						'Copied ranking data',
						array(
							'keyword'  => $keyword,
							'page_url' => $seo_data->page_url,
							'position' => $seo_data->position_abs,
						)
					);
				}
			}
		}

		$this->context->logger->info(
			'Ranking data copy complete',
			array(
				'total_keywords' => count( $keywords ),
				'copied'         => $copied_count,
				'skipped'        => count( $keywords ) - $copied_count,
			)
		);
	}

	/**
	 * Detect the likely search intent of a keyword based on trigger words
	 *
	 * Applies a simple rule-based classifier. Returns 'transactional',
	 * 'navigational', 'commercial', or 'informational'.
	 *
	 * @since 2.8.2
	 *
	 * @param string $keyword Keyword to classify.
	 * @return string Intent label.
	 */
	private function detect_search_intent( $keyword ) {
		$keyword_lower = strtolower( $keyword );

		$transactional_signals = array( 'buy', 'price', 'cheap', 'order', 'purchase', 'deal', 'discount', 'coupon', 'shop' );
		foreach ( $transactional_signals as $signal ) {
			if ( false !== strpos( $keyword_lower, $signal ) ) {
				return 'transactional';
			}
		}

		$navigational_signals = array( 'login', 'sign in', 'account', 'dashboard', 'portal' );
		foreach ( $navigational_signals as $signal ) {
			if ( false !== strpos( $keyword_lower, $signal ) ) {
				return 'navigational';
			}
		}

		$commercial_signals = array( 'best', 'top', 'review', 'vs', 'compare', 'comparison', 'alternative' );
		foreach ( $commercial_signals as $signal ) {
			if ( false !== strpos( $keyword_lower, $signal ) ) {
				return 'commercial';
			}
		}

		return 'informational';
	}

	/**
	 * Detect the likely content type for a keyword based on trigger words
	 *
	 * Returns 'product', 'landing', 'video', or 'blog'.
	 *
	 * @since 2.8.2
	 *
	 * @param string $keyword Keyword to classify.
	 * @return string Content type label.
	 */
	private function detect_content_type( $keyword ) {
		$keyword_lower = strtolower( $keyword );

		$product_signals = array( 'buy', 'price', 'product', 'shop', 'store', 'amazon' );
		foreach ( $product_signals as $signal ) {
			if ( false !== strpos( $keyword_lower, $signal ) ) {
				return 'product';
			}
		}

		$landing_signals = array( 'service', 'agency', 'company', 'hire', 'near me' );
		foreach ( $landing_signals as $signal ) {
			if ( false !== strpos( $keyword_lower, $signal ) ) {
				return 'landing';
			}
		}

		$video_signals = array( 'tutorial', 'how to', 'video', 'watch', 'youtube' );
		foreach ( $video_signals as $signal ) {
			if ( false !== strpos( $keyword_lower, $signal ) ) {
				return 'video';
			}
		}

		return 'blog';
	}

	/**
	 * Ensure the editorial calendar CRON job is scheduled
	 *
	 * Called after roadmap generation to guarantee the daily CRON is active.
	 * Schedules the next run for 2 AM tomorrow if not already scheduled.
	 *
	 * @since 2.8.2
	 */
	private function ensure_calendar_cron_scheduled() {
		if ( ! function_exists( 'wp_next_scheduled' ) || ! function_exists( 'wp_schedule_event' ) ) {
			return;
		}

		if ( wp_next_scheduled( 'wisewand_editorial_calendar_cron' ) ) {
			$this->context->logger->debug(
				'CRON already scheduled',
				array(
					'next_run' => gmdate( 'Y-m-d H:i:s', wp_next_scheduled( 'wisewand_editorial_calendar_cron' ) ),
				)
			);
			return;
		}

		$tomorrow_2am = strtotime( 'tomorrow 2:00 AM' );
		$scheduled    = wp_schedule_event( $tomorrow_2am, 'daily', 'wisewand_editorial_calendar_cron' );

		if ( false !== $scheduled ) {
			$this->context->logger->info(
				'Editorial calendar CRON auto-scheduled after roadmap generation',
				array(
					'first_run' => gmdate( 'Y-m-d H:i:s', $tomorrow_2am ),
				)
			);
		} else {
			$this->context->logger->error( 'Failed to auto-schedule CRON after roadmap generation' );
		}
	}
}
