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

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

use WP_Error;
use DateTime;
use Wisewand\Utils\Options;
use Wisewand\Logging\Logger;
use Wisewand\Utils\Wisewand_Payload_Builder;
use Wisewand\Utils\Cannibalisation_Detector;

// phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_error_log, WordPress.PHP.DevelopmentFunctions.error_log_print_r -- error_log() and print_r() are only used within WP_DEBUG conditionals for debugging

class Roadmap_Generator {

	private $options;
	private $logger;
	private $payload_builder;
	private $entry_manager;
	private $cannibalisation_detector;
	private $rate_limiter;

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

		// Initialize cannibalisation detector
		if ( ! class_exists( 'Wisewand\\Utils\\Cannibalisation_Detector' ) ) {
			require_once WISEWAND_PLUGIN_DIR . 'includes/Utils/Cannibalisation_Detector.php';
		}
		$this->cannibalisation_detector = new Cannibalisation_Detector( $logger );
		$this->rate_limiter = $rate_limiter ?? new \Wisewand\Utils\Rate_Limiter( $this->logger );
	}

	/**
	 * Generate smart roadmap with flexible frequency
	 *
	 * @param array  $keywords Keywords to schedule
	 * @param string $start_date Start date (Y-m-d)
	 * @param int    $posts_per_period Number of posts per period
	 * @param string $period_type 'day', 'week', or 'custom'
	 * @param array  $options Additional options (detect_cannibalisation, etc.)
	 * @return array Calendar entries ready for bulk insert
	 */
	public function generate_smart_roadmap( $keywords, $start_date, $posts_per_period = 1, $period_type = 'day', $options = array() ) {
		$detect_cannibalisation   = $options['detect_cannibalisation'] ?? true;
		$cannibalisation_actions  = $options['cannibalisation_actions'] ?? array();
		$generate_ai_instructions = $options['generate_ai_instructions'] ?? false;

		$this->logger->info(
			'Generating smart roadmap (enhanced)',
			array(
				'total_keywords'           => count( $keywords ),
				'start_date'               => $start_date,
				'posts_per_period'         => $posts_per_period,
				'period_type'              => $period_type,
				'detect_cannibalisation'   => $detect_cannibalisation,
				'generate_ai_instructions' => $generate_ai_instructions,
			)
		);

		// Step 1: Detect cannibalisation if enabled
		if ( $detect_cannibalisation ) {
			$keywords = $this->apply_cannibalisation_detection( $keywords, $cannibalisation_actions );
		}

		// Step 2: AI Instructions - Skip here (would cause timeout for many keywords)
		// Instead: Will be generated client-side after roadmap created
		// User will see progress bar, entries update as AI completes

		// Step 3: Calculate ease scores (easiest first - DataForSEO approach)
		$scored_keywords = $this->calculate_ease_scores( $keywords );

		// Step 4: Distribute with flexible frequency
		$scheduled_keywords = $this->distribute_by_frequency( $scored_keywords, $start_date, $posts_per_period, $period_type );

		// Step 5: Prepare calendar entries
		$calendar_entries = $this->prepare_calendar_entries( $scheduled_keywords );

		return $calendar_entries;
	}

	/**
	 * Apply cannibalisation detection and user actions
	 */
	private function apply_cannibalisation_detection( $keywords, $user_actions ) {
		$processed = array();

		foreach ( $keywords as $kw ) {
			$keyword = $kw['keyword'];

			// Check if user provided action for this keyword
			if ( isset( $user_actions[ $keyword ] ) ) {
				$action = $user_actions[ $keyword ];

				if ( $action['action'] === 'skip' ) {
					// Skip this keyword
					$this->logger->info( 'Skipping keyword due to cannibalisation', array( 'keyword' => $keyword ) );
					continue;
				}

				if ( $action['action'] === 'update' && isset( $action['post_id'] ) ) {
					$kw['publication_action']      = 'update';
					$kw['existing_post_id']        = $action['post_id'];
					$kw['cannibalisation_risk']    = $action['risk'] ?? 'medium';
					$kw['cannibalisation_post_id'] = $action['post_id'];
				} else {
					$kw['publication_action']   = 'create';
					$kw['cannibalisation_risk'] = 'none';
				}
			} else {
				// No user action - use detector recommendation with user's default preference
				$analysis = $this->cannibalisation_detector->analyze_keyword( $keyword, $kw );

				$kw['cannibalisation_risk']    = $analysis['risk_level'];
				$kw['cannibalisation_post_id'] = $analysis['primary_conflict']['post_id'] ?? null;

				// Change 3: Respect user's default cannibalization action setting
				$default_action = $this->options->get( 'wisewand_cannibalization_default_action', 'create' );

				if ( $analysis['recommended_action'] === 'update' && $default_action === 'update' ) {
					// Only auto-update if detector recommends AND user setting allows it
					$kw['publication_action'] = 'update';
					$kw['existing_post_id']   = $analysis['primary_conflict']['post_id'] ?? null;
				} elseif ( $default_action === 'skip' && $analysis['risk_level'] !== 'none' ) {
					// Skip keyword if user prefers to skip conflicts
					continue; // Don't add to processed array
				} else {
					// Default: create new content (safest option)
					$kw['publication_action'] = 'create';
				}
			}

			$processed[] = $kw;
		}

		return $processed;
	}

	/**
	 * Calculate ease scores (DataForSEO recommendation: easier keywords first)
	 */
	private function calculate_ease_scores( $keywords ) {
		$scored = array();

		foreach ( $keywords as $kw ) {
			$ease = 100; // Start at maximum ease

			// Factor 1: Keyword Difficulty (45% weight) - Lower is easier
			$difficulty         = $kw['difficulty'] ?? 50;
			$difficulty_penalty = ( $difficulty / 100 ) * 45;
			$ease              -= $difficulty_penalty;

			// Factor 2: Position (20% weight) - Better position = easier to improve
			$position = $kw['position'] ?? null;
			if ( $position ) {
				// Normalize position (0 = top, 1 = position 50+)
				$position_norm    = min( 1, $position / 50 );
				$position_penalty = $position_norm * 20;
				$ease            -= $position_penalty;
			} else {
				// Not ranking = harder
				$ease -= 20;
			}

			// Factor 3: Competition (15% weight)
			$competition    = $kw['competition'] ?? 'MEDIUM';
			$comp_penalties = array(
				'LOW'    => 0,
				'MEDIUM' => 7.5,
				'HIGH'   => 15,
			);
			$ease          -= $comp_penalties[ $competition ] ?? 7.5;

			// Factor 4: Search Volume (10% weight) - Higher volume = more momentum
			$volume       = $kw['search_volume'] ?? 0;
			$volume_bonus = min( 10, ( $volume / 10000 ) * 10 );
			$ease        += $volume_bonus;

			// Factor 5: Existing Content (10% weight) - Updates easier than creates
			if ( ( $kw['publication_action'] ?? 'create' ) === 'update' ) {
				$ease += 10;
			}

			// Clamp to 0-100 range
			$ease = max( 0, min( 100, $ease ) );

			$kw['ease_score']    = round( $ease, 2 );
			$kw['roadmap_score'] = $kw['ease_score']; // Backwards compatibility
			$scored[]            = $kw;
		}

		// Sort by ease DESC (easiest first)
		usort(
			$scored,
			function ( $a, $b ) {
				return $b['ease_score'] <=> $a['ease_score'];
			}
		);

		$this->logger->info(
			'Ease scores calculated',
			array(
				'total_keywords' => count( $scored ),
				'avg_ease'       => round( array_sum( array_column( $scored, 'ease_score' ) ) / count( $scored ), 2 ),
				'easiest'        => $scored[0]['keyword'] ?? 'none',
				'easiest_score'  => $scored[0]['ease_score'] ?? 0,
			)
		);

		return $scored;
	}

	/**
	 * Distribute keywords with flexible frequency
	 */
	private function distribute_by_frequency( $keywords, $start_date, $posts_per_period, $period_type ) {
		$schedule = array();
		$date     = new \DateTime( $start_date );

		// Get allowed publication days from settings
		$allowed_days = $this->options->get( 'wisewand_allowed_publication_days', array( '1', '2', '3', '4', '5' ) );

		// Determine increment days based on period type
		$increment_days = 1; // Default: daily
		if ( 'week' === $period_type ) {
			$increment_days = 7;
		} elseif ( $period_type === 'custom' && isset( $options['custom_days'] ) ) {
			$increment_days = $options['custom_days'];
		}

		// Group keywords into batches
		$batches      = array_chunk( $keywords, $posts_per_period );
		$batch_number = 1;

		foreach ( $batches as $batch_index => $batch ) {
			$period_start_date = clone $date;
			$posts_in_batch    = count( $batch );

			if ( 'week' === $period_type ) {
				// Get allowed dates for this week
				$week_allowed_dates = $this->get_allowed_dates_in_period( $period_start_date, 7, $allowed_days );

				if ( count( $week_allowed_dates ) === 0 ) {
					// No allowed days in the week? Log error and skip
					$this->logger->error(
						'No allowed publication days configured',
						array(
							'batch'        => $batch_number,
							'period_start' => $period_start_date->format( 'Y-m-d' ),
						)
					);
					if ( WP_DEBUG ) {
						error_log( 'ROADMAP: No allowed publication days configured!' );
					}
					break;
				}

				// More posts than allowed days? Expand across multiple weeks with random selection
				if ( $posts_in_batch > count( $week_allowed_dates ) ) {
					$post_index = 0;

					// Use ALL days in current week in chronological order.
					$current_week_indices = range( 0, count( $week_allowed_dates ) - 1 );

					if ( WP_DEBUG ) {
						$date_strs = array_map(
							function ( $idx ) use ( $week_allowed_dates ) {
								return $week_allowed_dates[ $idx ]->format( 'Y-m-d (D)' );
							},
							$current_week_indices
						);
						error_log( 'ROADMAP RANDOM (overflow): Batch ' . $batch_number . ' - Using ALL ' . count( $week_allowed_dates ) . ' days in week: ' . implode( ', ', $date_strs ) );
					}

					foreach ( $batch as $kw ) {
						if ( $post_index < count( $week_allowed_dates ) ) {
							// Within current week - use all available days
							$day_index = $current_week_indices[ $post_index ];
							$post_date = $week_allowed_dates[ $day_index ];

							if ( WP_DEBUG ) {
								error_log( 'ROADMAP RANDOM (overflow): Post ' . ( $post_index + 1 ) . ' "' . $kw['keyword'] . '" -> ' . $post_date->format( 'Y-m-d (D)' ) );
							}
						} else {
							// Need to expand to next week
							$next_week_start = clone $period_start_date;
							$next_week_start->modify( '+7 days' );
							$next_week_dates = $this->get_allowed_dates_in_period( $next_week_start, 7, $allowed_days );

							$overflow_count = $posts_in_batch - count( $week_allowed_dates );
							$overflow_index = $post_index - count( $week_allowed_dates );

							if ( $overflow_count <= count( $next_week_dates ) ) {
								// Randomly select days for overflow
								$overflow_indices = $this->random_select_indices( count( $next_week_dates ), $overflow_count );
								$day_index        = $overflow_indices[ $overflow_index ] ?? 0;
							} else {
								// More overflow than days - use all days
								$day_index = $overflow_index % count( $next_week_dates );
							}

							$post_date = $next_week_dates[ $day_index ] ?? $next_week_dates[0];

							if ( WP_DEBUG ) {
								error_log( 'ROADMAP RANDOM (overflow->next week): Post ' . ( $post_index + 1 ) . ' "' . $kw['keyword'] . '" -> ' . $post_date->format( 'Y-m-d (D)' ) );
							}
						}

						$kw['scheduled_date']   = $post_date->format( 'Y-m-d' );
						$kw['scheduling_batch'] = $batch_number;
						$schedule[]             = $kw;
						++$post_index;
					}
				} else {
					// Fewer or equal posts than allowed days - RANDOM selection then sort chronologically
					$selected_indices = $this->random_select_indices( count( $week_allowed_dates ), $posts_in_batch );

					if ( WP_DEBUG ) {
						$available_strs = array_map(
							function ( $d ) {
								return $d->format( 'Y-m-d (D)' );
							},
							$week_allowed_dates
						);
						$selected_strs  = array_map(
							function ( $idx ) use ( $week_allowed_dates ) {
								return $week_allowed_dates[ $idx ]->format( 'Y-m-d (D)' );
							},
							$selected_indices
						);
						error_log( 'ROADMAP RANDOM: Batch ' . $batch_number . ' - Available: [' . implode( ', ', $available_strs ) . ']' );
						error_log( 'ROADMAP RANDOM: Batch ' . $batch_number . ' - Randomly selected ' . $posts_in_batch . ' of ' . count( $week_allowed_dates ) . ' days: [' . implode( ', ', $selected_strs ) . ']' );
					}

					foreach ( $batch as $post_index => $kw ) {
						$day_index = $selected_indices[ $post_index ];
						$post_date = $week_allowed_dates[ $day_index ];

						if ( WP_DEBUG ) {
							error_log( 'ROADMAP RANDOM: Keyword "' . $kw['keyword'] . '" (ease: ' . ( $kw['ease_score'] ?? 'N/A' ) . ') -> ' . $post_date->format( 'Y-m-d (D)' ) );
						}

						$kw['scheduled_date']   = $post_date->format( 'Y-m-d' );
						$kw['scheduling_batch'] = $batch_number;
						$schedule[]             = $kw;
					}
				}
			} else {
				// Daily mode - skip disallowed days
				foreach ( $batch as $kw ) {
					$post_date = $this->get_next_allowed_date( $period_start_date, $allowed_days );

					$kw['scheduled_date']   = $post_date->format( 'Y-m-d' );
					$kw['scheduling_batch'] = $batch_number;
					$schedule[]             = $kw;

					// Move to next day for next keyword
					$period_start_date = clone $post_date;
					$period_start_date->modify( '+1 day' );
				}
			}

			// Move to next period.
			if ( 'week' === $period_type ) {
				$date->modify( "+{$increment_days} days" );
			} else {
				// Daily mode: advance past already-scheduled days to prevent
				// weekend pile-up (Sat/Sun/Mon all mapping to Monday).
				$date = clone $period_start_date;
			}
			++$batch_number;
		}

		$total_days = ( $batch_number - 1 ) * $increment_days;

		$this->logger->info(
			'Keywords distributed by frequency',
			array(
				'total_keywords'   => count( $keywords ),
				'posts_per_period' => $posts_per_period,
				'period_type'      => $period_type,
				'allowed_days'     => $allowed_days,
				'total_batches'    => $batch_number - 1,
				'total_days'       => $total_days,
				'date_range'       => array(
					'start' => $schedule[0]['scheduled_date'] ?? null,
					'end'   => $schedule[ count( $schedule ) - 1 ]['scheduled_date'] ?? null,
				),
			)
		);

		return $schedule;
	}

	/**
	 * Prepare calendar entries with new fields
	 */
	private function prepare_calendar_entries( $scheduled_keywords ) {
		$entries = array();

		foreach ( $scheduled_keywords as $kw ) {
			$payload = $this->payload_builder->build_payload( $kw );

			// Override payload with AI-generated instructions if available
			if ( ! empty( $kw['ai_search_intention'] ) ) {
				$payload['search_intention_answer'] = $kw['ai_search_intention'];
				if ( WP_DEBUG ) {
					error_log( "ROADMAP: Using AI-generated search intention for: {$kw['keyword']}" );
				}
			}
			if ( ! empty( $kw['ai_additional_info'] ) ) {
				$payload['additional_information'] = $kw['ai_additional_info'];
			}
			if ( ! empty( $kw['ai_avoid_info'] ) ) {
				$payload['avoid_information'] = $kw['ai_avoid_info'];
			}

			// Determine author: Use language-specific author settings only
			// RSS v2.2: Removed AI-matched author (suggested_author_id) - use language-based settings only
			$keyword_lang  = $kw['language_code'] ?? 'en';
			$target_author = $this->options->get_author_for_language( $keyword_lang );
			if ( WP_DEBUG ) {
				error_log( "ROADMAP: Using language-based author {$target_author} for '{$keyword_lang}' keyword: {$kw['keyword']}" );
			}

			// Determine categories: Use keyword's AI-matched categories, fallback to empty
			$wordpress_categories = null;
			if ( ! empty( $kw['suggested_categories'] ) ) {
				// suggested_categories is JSON string from keyword table
				$wordpress_categories = $kw['suggested_categories']; // Already JSON-encoded
				if ( WP_DEBUG ) {
					error_log( "ROADMAP: Keyword '{$kw['keyword']}' has categories: {$wordpress_categories}" );
				}
			} elseif ( WP_DEBUG ) {
					error_log( "ROADMAP: Keyword '{$kw['keyword']}' has NO suggested_categories field. Full keyword data: " . print_r( array_keys( $kw ), true ) );
			}

			$entry = array(
				'keyword'                 => $kw['keyword'],
				'keyword_source'          => $kw['source'] ?? 'current_ranking',
				'search_volume'           => $kw['search_volume'] ?? 0,
				'current_position'        => $kw['position'] ?? null,
				'search_intent'           => $kw['search_intent'] ?? 'informational',
				'difficulty'              => $kw['difficulty'] ?? null,
				'priority_score'          => $kw['roadmap_score'] ?? 0,
				'ease_score'              => $kw['ease_score'] ?? 0,
				'cannibalisation_risk'    => $kw['cannibalisation_risk'] ?? 'none',
				'cannibalisation_post_id' => $kw['cannibalisation_post_id'] ?? null,
				'publication_action'      => $kw['publication_action'] ?? 'create',
				'existing_post_id'        => $kw['existing_post_id'] ?? null,
				'wisewand_payload'        => wp_json_encode( $payload ),
				'wisewand_lang'           => $payload['lang'],
				'wisewand_type'           => $payload['type'],
				'wisewand_length'         => $payload['length'],
				'wisewand_project_id'     => $payload['project_id'] ?? '',
				'wisewand_persona_id'     => $payload['persona_id'] ?? '',
				'publication_date'        => $kw['scheduled_date'],
				'publication_time_range'  => $this->options->get( 'wisewand_calendar_time_range', 'random' ),
				'publication_time'        => $this->generate_random_time( $this->options->get( 'wisewand_calendar_time_range', 'random' ) ), // ✅ Pre-generate exact time
				'scheduling_batch'        => $kw['scheduling_batch'] ?? 1,
				'auto_publish'            => $this->options->get( 'wisewand_calendar_auto_publish', 0 ),
				// Change 3: Safe merge strategy - use setting instead of forcing 'replace'
				'content_merge_strategy'  => $kw['publication_action'] === 'update'
					? $this->options->get( 'wisewand_default_content_merge_strategy', 'append' )
					: null,
				'target_post_type'        => 'post',
				'target_author'           => $target_author, // ✅ Now uses AI-matched author if available!
				'wordpress_categories'    => $wordpress_categories, // ✅ Now includes AI-matched categories!
				'created_by'              => get_current_user_id(),
			);

			$entries[] = $entry;
		}

		return $entries;
	}

	/**
	 * Legacy method for backwards compatibility
	 */
	private function calculate_priorities( $keywords ) {
		// Redirect to new ease-based scoring
		return $this->calculate_ease_scores( $keywords );
	}

	/**
	 * Legacy method for backwards compatibility
	 */
	private function distribute_intelligently( $keywords, $start_date, $days ) {
		// Redirect to new frequency-based distribution (1 per day = legacy behavior)
		return $this->distribute_by_frequency( $keywords, $start_date, 1, 'day' );
	}

	/**
	 * Check if content exists for keyword (legacy method - kept for compatibility)
	 */
	private function check_content_exists( $keyword ) {
		global $wpdb;

		$like_keyword = '%' . $wpdb->esc_like( $keyword ) . '%';

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct query more efficient than WP_Query for simple title/content lookup
		$existing = $wpdb->get_row(
			$wpdb->prepare(
				"
            SELECT ID, post_title, post_modified, post_content
            FROM {$wpdb->posts}
            WHERE (post_title LIKE %s OR post_content LIKE %s)
            AND post_status IN ('publish', 'draft', 'pending')
            AND post_type IN ('post', 'page', 'product')
            ORDER BY post_modified DESC
            LIMIT 1
        ",
				$like_keyword,
				$like_keyword
			)
		);

		return $existing;
	}

	/**
	 * Validate roadmap preview
	 */
	public function validate_roadmap_preview( $scheduled_keywords ) {
		$preview = array(
			'total_entries' => count( $scheduled_keywords ),
			'creates'       => 0,
			'updates'       => 0,
			'by_risk'       => array(
				'none'   => 0,
				'low'    => 0,
				'medium' => 0,
				'high'   => 0,
			),
			'date_range'    => array(
				'start' => null,
				'end'   => null,
			),
			'warnings'      => array(),
		);

		foreach ( $scheduled_keywords as $kw ) {
			if ( $kw['publication_action'] === 'create' ) {
				++$preview['creates'];
			} elseif ( $kw['publication_action'] === 'update' ) {
				++$preview['updates'];
			}

			$risk = $kw['cannibalisation_risk'] ?? 'none';
			++$preview['by_risk'][ $risk ];

			if ( ! $preview['date_range']['start'] || $kw['scheduled_date'] < $preview['date_range']['start'] ) {
				$preview['date_range']['start'] = $kw['scheduled_date'];
			}
			if ( ! $preview['date_range']['end'] || $kw['scheduled_date'] > $preview['date_range']['end'] ) {
				$preview['date_range']['end'] = $kw['scheduled_date'];
			}
		}

		if ( $preview['creates'] > 50 ) {
			$preview['warnings'][] = 'Creating ' . $preview['creates'] . ' new articles may impact your Wisewand credits.';
		}

		if ( $preview['by_risk']['high'] > 0 ) {
			$preview['warnings'][] = $preview['by_risk']['high'] . ' keywords have HIGH cannibalisation risk.';
		}

		return $preview;
	}

	/**
	 * Generate AI Instructions for all keywords using OpenAI
	 * Called during roadmap generation if user enables the option
	 *
	 * @param array $keywords Keywords to enrich with AI instructions
	 * @return array Keywords with AI instructions added
	 */
	private function generate_ai_instructions_for_keywords( $keywords ) {
		$this->logger->info(
			'Generating AI Instructions for keywords',
			array(
				'count' => count( $keywords ),
			)
		);

		if ( WP_DEBUG ) {
			error_log( 'AI INSTRUCTIONS: Starting generation for ' . count( $keywords ) . ' keywords...' );
		}

		$enriched_keywords = array();

		foreach ( $keywords as $index => $kw ) {
			if ( WP_DEBUG ) {
				error_log( 'AI INSTRUCTIONS: Processing keyword ' . ( $index + 1 ) . '/' . count( $keywords ) . ': ' . $kw['keyword'] );
			}

			// Generate AI instructions for this keyword
			$instructions = $this->payload_builder->generate_ai_instructions( $kw );

			if ( is_wp_error( $instructions ) ) {
				// Log error but continue with next keyword
				$this->logger->warning(
					'Failed to generate AI instructions',
					array(
						'keyword' => $kw['keyword'],
						'error'   => $instructions->get_error_message(),
					)
				);
				if ( WP_DEBUG ) {
					error_log( 'AI INSTRUCTIONS: Failed for "' . $kw['keyword'] . '": ' . $instructions->get_error_message() );
				}

				// Keep keyword without AI instructions
				$enriched_keywords[] = $kw;
				continue;
			}

			// Add AI instructions to keyword data
			$kw['ai_search_intention'] = $instructions['search_intention_answer'] ?? '';
			$kw['ai_additional_info']  = $instructions['additional_information'] ?? '';
			$kw['ai_avoid_info']       = $instructions['avoid_information'] ?? '';

			if ( WP_DEBUG ) {
				error_log( 'AI INSTRUCTIONS: Generated for "' . $kw['keyword'] . '" (confidence: ' . ( $instructions['confidence'] ?? 0 ) . ')' );
			}

			$enriched_keywords[] = $kw;

			// Rate limiting: 1 second delay between OpenAI calls
			if ( $index < count( $keywords ) - 1 ) {
				$this->rate_limiter->smart_delay( 'wisewand_api', 1.0 );
			}
		}

		$this->logger->info(
			'AI Instructions generation completed',
			array(
				'total_keywords' => count( $keywords ),
				'successful'     => count( $enriched_keywords ),
			)
		);

		if ( WP_DEBUG ) {
			error_log( 'AI INSTRUCTIONS: Completed for ' . count( $enriched_keywords ) . ' keywords' );
		}

		return $enriched_keywords;
	}

	/**
	 * Get allowed dates within a period
	 *
	 * @param DateTime $start_date Start date of the period
	 * @param int      $days Number of days in the period
	 * @param array    $allowed_days Array of allowed day numbers (1=Monday, 7=Sunday)
	 * @return array Array of DateTime objects for allowed dates
	 */
	private function get_allowed_dates_in_period( $start_date, $days, $allowed_days ) {
		$dates   = array();
		$current = clone $start_date;

		for ( $i = 0; $i < $days; $i++ ) {
			$day_of_week = $current->format( 'N' ); // 1=Monday, 7=Sunday

			if ( in_array( $day_of_week, $allowed_days, true ) ) {
				$dates[] = clone $current;
			}

			$current->modify( '+1 day' );
		}

		if ( WP_DEBUG && count( $dates ) > 0 ) {
			error_log( 'ROADMAP: Found ' . count( $dates ) . ' allowed dates in period starting ' . $start_date->format( 'Y-m-d' ) );
		}

		return $dates;
	}

	/**
	 * Get next allowed date from current date
	 *
	 * @param DateTime $from_date Starting date to search from
	 * @param array    $allowed_days Array of allowed day numbers (1=Monday, 7=Sunday)
	 * @return DateTime Next allowed date (may be same as $from_date)
	 */
	private function get_next_allowed_date( $from_date, $allowed_days ) {
		$current        = clone $from_date;
		$max_iterations = 14; // Prevent infinite loop (2 weeks max)

		for ( $i = 0; $i < $max_iterations; $i++ ) {
			$day_of_week = $current->format( 'N' ); // 1=Monday, 7=Sunday

			if ( in_array( $day_of_week, $allowed_days, true ) ) {
				if ( WP_DEBUG && $i > 0 ) {
					error_log( 'ROADMAP: Next allowed date from ' . $from_date->format( 'Y-m-d' ) . ' is ' . $current->format( 'Y-m-d' ) . ' (skipped ' . $i . ' days)' );
				}
				return $current;
			}

			$current->modify( '+1 day' );
		}

		// Fallback: return original date if no allowed days found (shouldn't happen)
		$this->logger->warning(
			'Could not find allowed publication date within 14 days',
			array(
				'start_date'   => $from_date->format( 'Y-m-d' ),
				'allowed_days' => $allowed_days,
			)
		);

		return $from_date;
	}

	/**
	 * Generate random publication time within specified range
	 * Used to pre-generate exact times during roadmap creation
	 *
	 * @param string $range Time range (random, morning, afternoon, evening)
	 * @return string Time in H:i:s format
	 */
	private function generate_random_time( $range = 'random' ) {
		$ranges = array(
			'random'    => array( '09:00:00', '20:00:00' ),
			'morning'   => array( '09:00:00', '12:00:00' ),
			'afternoon' => array( '12:00:00', '17:00:00' ),
			'evening'   => array( '17:00:00', '20:00:00' ),
		);

		$time_range = $ranges[ $range ] ?? $ranges['random'];
		$start      = strtotime( $time_range[0] );
		$end        = strtotime( $time_range[1] );

		// Generate random timestamp between start and end
		$random_timestamp = wp_rand( $start, $end );

		// Return in H:i:s format
		return gmdate( 'H:i:s', $random_timestamp );
	}

	/**
	 * Randomly select N indices from a range, maintaining chronological order
	 *
	 * This helper ensures:
	 * 1. Random selection of days within a week
	 * 2. No duplicate days within the same week
	 * 3. Selected days are sorted chronologically
	 *
	 * @param int $total_available Total number of available indices (e.g., 5 allowed days)
	 * @param int $count_needed    Number of indices to select (e.g., 3 posts)
	 * @return array Array of selected indices, sorted ascending
	 */
	private function random_select_indices( $total_available, $count_needed ) {
		// Edge case: need more than available - return all indices
		if ( $count_needed >= $total_available ) {
			if ( WP_DEBUG ) {
				error_log( 'ROADMAP RANDOM: Need ' . $count_needed . ' days but only ' . $total_available . ' available - using all days' );
			}
			return range( 0, $total_available - 1 );
		}

		// Edge case: only one needed - random single selection
		if ( $count_needed === 1 ) {
			$selected = wp_rand( 0, $total_available - 1 );
			if ( WP_DEBUG ) {
				error_log( 'ROADMAP RANDOM: Single post - randomly selected index ' . $selected . ' of ' . $total_available );
			}
			return array( $selected );
		}

		// Edge case: only one available - no choice
		if ( $total_available === 1 ) {
			if ( WP_DEBUG ) {
				error_log( 'ROADMAP RANDOM: Only 1 day available - no randomization possible' );
			}
			return array( 0 );
		}

		// Standard case: randomly select $count_needed unique indices
		$all_indices = range( 0, $total_available - 1 );
		shuffle( $all_indices ); // Randomize order

		// Take first $count_needed
		$selected = array_slice( $all_indices, 0, $count_needed );

		// Sort to maintain chronological order
		sort( $selected );

		return $selected;
	}
}
