<?php
/**
 * Calendar AJAX Handler
 *
 * Handles all AJAX requests for the Editorial Calendar admin page.
 * Extracted from Editorial_Calendar to separate AJAX logic from rendering concerns.
 *
 * @package Wisewand
 * @since 2.8.2
 * @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\Admin;

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

use Wisewand\Utils\Options;
use Wisewand\Logging\Logger;
use Wisewand\Admin\Traits\AJAX_Security;

// 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

/**
 * Handles all AJAX actions for the Editorial Calendar
 *
 * Provides methods for fetching, saving, deleting, generating and importing
 * calendar entries via WordPress AJAX. All public methods are registered as
 * AJAX handlers in Plugin.php.
 *
 * @package Wisewand
 * @since 2.8.2
 */
class Calendar_AJAX_Handler {

	use AJAX_Security;

	/**
	 * Options wrapper instance.
	 *
	 * @since 2.8.2
	 * @var Options
	 */
	private $options;

	/**
	 * Logger instance.
	 *
	 * @since 2.8.2
	 * @var Logger
	 */
	private $logger;

	/**
	 * Calendar entry manager instance.
	 *
	 * @since 2.8.2
	 * @var \Wisewand\Calendar\Calendar_Entry_Manager
	 */
	private $entry_manager;

	/**
	 * Payload builder instance.
	 *
	 * @since 2.8.2
	 * @var \Wisewand\Utils\Wisewand_Payload_Builder
	 */
	private $payload_builder;

	/**
	 * Panel renderer instance for generating side panel HTML.
	 *
	 * @since 2.8.2
	 * @var Calendar_Panel_Renderer
	 */
	private $panel_renderer;

	/**
	 * Debug info for last error (used in exception handling across boundaries).
	 *
	 * @since 2.8.2
	 * @var array|null
	 */
	private $_last_error_debug = null;

	/**
	 * Constructor
	 *
	 * @since 2.8.2
	 *
	 * @param Options                                   $options         Options wrapper instance.
	 * @param Logger                                    $logger          Logger instance.
	 * @param \Wisewand\Calendar\Calendar_Entry_Manager $entry_manager   Calendar entry manager instance.
	 * @param \Wisewand\Utils\Wisewand_Payload_Builder  $payload_builder Payload builder instance.
	 */
	public function __construct( Options $options, Logger $logger, $entry_manager, $payload_builder ) {
		$this->options         = $options;
		$this->logger          = $logger;
		$this->entry_manager   = $entry_manager;
		$this->payload_builder = $payload_builder;
		$this->panel_renderer  = new Calendar_Panel_Renderer( $options, $logger );
	}

	/**
	 * AJAX Handler — Get Calendar Events
	 *
	 * Fetches calendar entries filtered by date range and keyword source, then
	 * transforms them into FullCalendar-compatible event objects.
	 *
	 * @since 2.8.2
	 */
	public function ajax_get_calendar_events() {
		$this->verify_ajax_request( 'wisewand_calendar' );

		$start_date    = isset( $_POST['start_date'] ) ? sanitize_text_field( wp_unslash( $_POST['start_date'] ) ) : '';
		$end_date      = isset( $_POST['end_date'] ) ? sanitize_text_field( wp_unslash( $_POST['end_date'] ) ) : '';
		$source_filter = isset( $_POST['source_filter'] ) ? sanitize_text_field( wp_unslash( $_POST['source_filter'] ) ) : 'all';

		try {
			$filters = array(
				'publication_date_start' => $start_date,
				'publication_date_end'   => $end_date,
			);

			// Add keyword_source filter if not "all"
			if ( $source_filter !== 'all' ) {
				$filters['keyword_source'] = $source_filter;
			}

			$entries = $this->entry_manager->get_entries( $filters );

			$events = $this->transform_entries_to_fullcalendar_events( $entries );

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

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

	/**
	 * AJAX Handler — Update Entry Date (from drag and drop)
	 *
	 * Updates the publication date (and optionally time) of a calendar entry
	 * when the user drags it to a new position in the FullCalendar view.
	 *
	 * @since 2.8.2
	 */
	public function ajax_update_entry_gmdate() {
		$this->verify_ajax_request( 'wisewand_calendar' );

		$entry_id = absint( $_POST['entry_id'] ?? 0 );
		$new_date = isset( $_POST['new_date'] ) ? sanitize_text_field( wp_unslash( $_POST['new_date'] ) ) : '';

		if ( ! $entry_id || ! $new_date ) {
			wp_send_json_error( array( 'message' => __( 'Invalid parameters', 'wisewand-ai-content-writer-seo-content-generator' ) ) );
		}

		try {
			$updates = array();

			// Check if it's a full datetime (contains 'T') - e.g. from Week/Day view
			if ( strpos( $new_date, 'T' ) !== false ) {
				try {
					// Parse ISO8601 date from FullCalendar (which includes offset)
					$date = new \DateTime( $new_date );

					// Convert to WordPress timezone
					$date->setTimezone( wp_timezone() );

					$updates['publication_date'] = $date->format( 'Y-m-d' );
					$updates['publication_time'] = $date->format( 'H:i:s' );
				} catch ( \Exception $e ) {
					// Fallback if parsing fails
					$updates['publication_date'] = substr( $new_date, 0, 10 );
				}
			} else {
				// Just date - e.g. from Month view
				// Only update date, preserve existing time
				$updates['publication_date'] = $new_date;
			}

			$result = $this->entry_manager->update_entry(
				$entry_id,
				$updates
			);

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

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

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

	/**
	 * AJAX Handler — Get Entry Details for Side Panel
	 *
	 * Fetches a single calendar entry and returns its rendered HTML form
	 * for display in the side panel, along with the raw entry data.
	 *
	 * @since 2.8.2
	 */
	public function ajax_get_entry_details() {
		$this->verify_ajax_request( 'wisewand_calendar' );

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

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

		try {
			$entry = $this->entry_manager->get_entry( $entry_id );

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

			$html = $this->panel_renderer->render_entry_panel_form( $entry );

			wp_send_json_success(
				array(
					'html'  => $html,
					'entry' => $entry,
				)
			);

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

	/**
	 * AJAX Handler — Save Entry
	 *
	 * Saves all editable fields for a calendar entry including publication
	 * date/time, article configuration payload, author, categories, and notes.
	 * For RSS entries, only publication date/time and WordPress settings are saved.
	 *
	 * @since 2.8.2
	 */
	public function ajax_save_entry() {
		$this->verify_ajax_request( 'wisewand_calendar' );

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

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

		try {
			$existing_entry = $this->entry_manager->get_entry( $entry_id );

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

			// For RSS entries, only allow editing specific fields
			$is_rss_entry = isset( $existing_entry['keyword_source'] ) && $existing_entry['keyword_source'] === 'rss';

			$existing_payload = $existing_entry['wisewand_payload_decoded'] ?? array();

			// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Payload is a complex JSON string, decoded and sanitized later
			// jQuery sends objects as arrays, not JSON strings - no need to json_decode
			// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Complex array sanitized by sanitize_payload_types()
			$raw_payload = isset( $_POST['wisewand_payload'] ) ? $this->sanitize_payload_types( wp_unslash( $_POST['wisewand_payload'] ) ) : array();

			// Process CTAs array
			$ctas = array();
			if ( ! empty( $raw_payload['cta_anchors'] ) && ! empty( $raw_payload['cta_urls'] ) ) {
				$anchors = $raw_payload['cta_anchors'];
				$urls    = $raw_payload['cta_urls'];

				for ( $i = 0; $i < count( $anchors ); $i++ ) {
					if ( ! empty( $anchors[ $i ] ) && ! empty( $urls[ $i ] ) ) {
						$ctas[] = array(
							'anchor' => sanitize_text_field( $anchors[ $i ] ),
							'url'    => esc_url_raw( $urls[ $i ] ),
						);
					}
				}
			}

			if ( ! empty( $ctas ) ) {
				$raw_payload['ctas']    = $ctas;
				$raw_payload['use_cta'] = true;
			} else {
				$raw_payload['ctas']    = array();
				$raw_payload['use_cta'] = false;
			}

			// Remove temporary arrays
			unset( $raw_payload['cta_anchors'] );
			unset( $raw_payload['cta_urls'] );

			// Process social networks array
			if ( isset( $raw_payload['social_networks'] ) && is_array( $raw_payload['social_networks'] ) ) {
				$raw_payload['social_networks'] = array_map( 'sanitize_text_field', $raw_payload['social_networks'] );
			} else {
				$raw_payload['social_networks'] = array();
			}

			// Process internal links WP status array
			if ( isset( $raw_payload['internal_links_wp_status'] ) && is_array( $raw_payload['internal_links_wp_status'] ) ) {
				$raw_payload['internal_links_wp_status'] = array_map( 'sanitize_text_field', $raw_payload['internal_links_wp_status'] );
			} else {
				$raw_payload['internal_links_wp_status'] = array( 'publish' );
			}

			// Process Affiliation Links - Product Review
			$affiliation_links = array();
			if ( ! empty( $raw_payload['affiliation_link_urls'] ) && ! empty( $raw_payload['affiliation_link_anchors'] ) ) {
				$urls    = $raw_payload['affiliation_link_urls'];
				$anchors = $raw_payload['affiliation_link_anchors'];

				for ( $i = 0; $i < count( $urls ); $i++ ) {
					if ( ! empty( $urls[ $i ] ) && ! empty( $anchors[ $i ] ) ) {
						$affiliation_links[] = array(
							'url'    => esc_url_raw( $urls[ $i ] ),
							'anchor' => sanitize_text_field( $anchors[ $i ] ),
						);
					}
				}
			}
			if ( ! empty( $affiliation_links ) ) {
				$raw_payload['affiliation_links'] = $affiliation_links;
			}
			unset( $raw_payload['affiliation_link_urls'] );
			unset( $raw_payload['affiliation_link_anchors'] );

			// Process Affiliation Links - Compare Products Product 1
			$affiliation_links_1 = array();
			if ( ! empty( $raw_payload['affiliation_product_1_link_urls'] ) && ! empty( $raw_payload['affiliation_product_1_link_anchors'] ) ) {
				$urls    = $raw_payload['affiliation_product_1_link_urls'];
				$anchors = $raw_payload['affiliation_product_1_link_anchors'];

				for ( $i = 0; $i < count( $urls ); $i++ ) {
					if ( ! empty( $urls[ $i ] ) && ! empty( $anchors[ $i ] ) ) {
						$affiliation_links_1[] = array(
							'url'    => esc_url_raw( $urls[ $i ] ),
							'anchor' => sanitize_text_field( $anchors[ $i ] ),
						);
					}
				}
			}
			if ( ! empty( $affiliation_links_1 ) ) {
				$raw_payload['affiliation_links_product_1'] = $affiliation_links_1;
			}
			unset( $raw_payload['affiliation_product_1_link_urls'] );
			unset( $raw_payload['affiliation_product_1_link_anchors'] );

			// Process Affiliation Links - Compare Products Product 2
			$affiliation_links_2 = array();
			if ( ! empty( $raw_payload['affiliation_product_2_link_urls'] ) && ! empty( $raw_payload['affiliation_product_2_link_anchors'] ) ) {
				$urls    = $raw_payload['affiliation_product_2_link_urls'];
				$anchors = $raw_payload['affiliation_product_2_link_anchors'];

				for ( $i = 0; $i < count( $urls ); $i++ ) {
					if ( ! empty( $urls[ $i ] ) && ! empty( $anchors[ $i ] ) ) {
						$affiliation_links_2[] = array(
							'url'    => esc_url_raw( $urls[ $i ] ),
							'anchor' => sanitize_text_field( $anchors[ $i ] ),
						);
					}
				}
			}
			if ( ! empty( $affiliation_links_2 ) ) {
				$raw_payload['affiliation_links_product_2'] = $affiliation_links_2;
			}
			unset( $raw_payload['affiliation_product_2_link_urls'] );
			unset( $raw_payload['affiliation_product_2_link_anchors'] );

			// Process Top Products - Rename from top_products_data to affiliation_top_products
			if ( isset( $raw_payload['top_products_data'] ) ) {
				$raw_payload['affiliation_top_products'] = $raw_payload['top_products_data'];
				unset( $raw_payload['top_products_data'] );
			}

			$updated_payload = array_merge( $existing_payload, $raw_payload );

			// Get API endpoint to determine which fields to keep
			$api_endpoint = isset( $_POST['api_endpoint'] ) ? sanitize_text_field( wp_unslash( $_POST['api_endpoint'] ) ) : 'articles';

			// Auto-set fields
			$updated_payload['subject']     = $existing_entry['keyword'];
			$updated_payload['use_webhook'] = false;

			// Clean up legacy/deprecated fields
			unset( $updated_payload['use_inline_images'] ); // Wrong field name - API uses use_inlineimages (no underscore)

			// Clean up endpoint-specific fields
			if ( $api_endpoint === 'articles' ) {
				// Remove fields that don't exist for articles endpoint
				unset( $updated_payload['website'] ); // Set by user input, but articles use home_url()
				unset( $updated_payload['writing_style'] ); // Articles use writing_style_mode instead

				// Clean up affiliation fields if NOT an affiliation type
				$article_type = $updated_payload['type'] ?? 'blog';
				if ( ! in_array( $article_type, array( 'affiliation-product-review', 'affiliation-compare-products', 'affiliation-top-product' ) ) ) {
					// Remove all affiliation fields for non-affiliation article types
					unset( $updated_payload['affiliation_product_name'] );
					unset( $updated_payload['affiliation_product_review_urls'] );
					unset( $updated_payload['affiliation_links'] );
					unset( $updated_payload['affiliation_promo_code'] );
					unset( $updated_payload['affiliation_promo_percentage'] );
					unset( $updated_payload['affiliation_product_1_name'] );
					unset( $updated_payload['affiliation_product_1_review_urls'] );
					unset( $updated_payload['affiliation_links_product_1'] );
					unset( $updated_payload['affiliation_promo_code_1'] );
					unset( $updated_payload['affiliation_promo_percentage_1'] );
					unset( $updated_payload['affiliation_product_2_name'] );
					unset( $updated_payload['affiliation_product_2_review_urls'] );
					unset( $updated_payload['affiliation_links_product_2'] );
					unset( $updated_payload['affiliation_promo_code_2'] );
					unset( $updated_payload['affiliation_promo_percentage_2'] );
					unset( $updated_payload['affiliation_top_products'] );
				}

				// Clean up type-specific affiliation fields
				if ( $article_type !== 'affiliation-product-review' ) {
					unset( $updated_payload['affiliation_product_name'] );
					unset( $updated_payload['affiliation_product_review_urls'] );
					unset( $updated_payload['affiliation_links'] );
					unset( $updated_payload['affiliation_promo_code'] );
					unset( $updated_payload['affiliation_promo_percentage'] );
				}

				if ( $article_type !== 'affiliation-compare-products' ) {
					unset( $updated_payload['affiliation_product_1_name'] );
					unset( $updated_payload['affiliation_product_1_review_urls'] );
					unset( $updated_payload['affiliation_links_product_1'] );
					unset( $updated_payload['affiliation_promo_code_1'] );
					unset( $updated_payload['affiliation_promo_percentage_1'] );
					unset( $updated_payload['affiliation_product_2_name'] );
					unset( $updated_payload['affiliation_product_2_review_urls'] );
					unset( $updated_payload['affiliation_links_product_2'] );
					unset( $updated_payload['affiliation_promo_code_2'] );
					unset( $updated_payload['affiliation_promo_percentage_2'] );
				}

				if ( $article_type !== 'affiliation-top-product' ) {
					unset( $updated_payload['affiliation_top_products'] );
				}
			} else {
				// Category/Product/Discover pages
				// Remove fields that don't exist for these endpoints
				unset( $updated_payload['keep_title'] ); // Only for articles
				unset( $updated_payload['search_intention_answer'] ); // Only for articles
				unset( $updated_payload['avoid_information'] ); // Only for articles
				unset( $updated_payload['type'] ); // Only for articles

				// Remove ALL affiliation fields (only for articles endpoint)
				unset( $updated_payload['affiliation_product_name'] );
				unset( $updated_payload['affiliation_product_review_urls'] );
				unset( $updated_payload['affiliation_links'] );
				unset( $updated_payload['affiliation_promo_code'] );
				unset( $updated_payload['affiliation_promo_percentage'] );
				unset( $updated_payload['affiliation_product_1_name'] );
				unset( $updated_payload['affiliation_product_1_review_urls'] );
				unset( $updated_payload['affiliation_links_product_1'] );
				unset( $updated_payload['affiliation_promo_code_1'] );
				unset( $updated_payload['affiliation_promo_percentage_1'] );
				unset( $updated_payload['affiliation_product_2_name'] );
				unset( $updated_payload['affiliation_product_2_review_urls'] );
				unset( $updated_payload['affiliation_links_product_2'] );
				unset( $updated_payload['affiliation_promo_code_2'] );
				unset( $updated_payload['affiliation_promo_percentage_2'] );
				unset( $updated_payload['affiliation_top_products'] );

				// For categorypages and productpages (not discoverarticles)
				if ( $api_endpoint === 'categorypages' || $api_endpoint === 'productpages' ) {
					unset( $updated_payload['keywords_secondary'] ); // Only for articles + discover
				}
			}

			// Ensure website field for categorypages/productpages/discoverarticles
			if ( in_array( $api_endpoint, array( 'categorypages', 'productpages', 'discoverarticles' ) ) ) {
				if ( empty( $updated_payload['website'] ) ) {
					$updated_payload['website'] = home_url();
				}
			}

			// Process target_author (from form)
			$target_author = absint( $_POST['target_author'] ?? 0 );
			if ( ! $target_author ) {
				$target_author = get_current_user_id(); // Fallback to current user
			}

			// Process wordpress_categories (array of category IDs)
			$wordpress_categories = array();
			if ( ! empty( $_POST['wordpress_categories'] ) && is_array( $_POST['wordpress_categories'] ) ) {
				$wordpress_categories = array_map( 'absint', $_POST['wordpress_categories'] );
			}

			// Process publication_time - add :00 seconds if not present
			$publication_time = isset( $_POST['publication_time'] ) ? sanitize_text_field( wp_unslash( $_POST['publication_time'] ) ) : '';
			if ( ! empty( $publication_time ) && strlen( $publication_time ) === 5 ) {
				$publication_time .= ':00'; // Add seconds (HH:MM -> HH:MM:00)
			}

			$updates = array(
				'publication_date'       => isset( $_POST['publication_date'] ) ? sanitize_text_field( wp_unslash( $_POST['publication_date'] ) ) : '',
				'publication_time'       => $publication_time,
				'publication_time_range' => isset( $_POST['publication_time_range'] ) ? sanitize_text_field( wp_unslash( $_POST['publication_time_range'] ) ) : 'random',
				'content_merge_strategy' => isset( $_POST['content_merge_strategy'] ) ? sanitize_text_field( wp_unslash( $_POST['content_merge_strategy'] ) ) : 'replace',
				'existing_post_id'       => absint( $_POST['existing_post_id'] ?? 0 ) ?: null,
				'target_author'          => $target_author,
				'wordpress_categories'   => ! empty( $wordpress_categories ) ? wp_json_encode( $wordpress_categories ) : null,
				'notes'                  => isset( $_POST['notes'] ) ? sanitize_textarea_field( wp_unslash( $_POST['notes'] ) ) : '',
				'wisewand_payload'       => wp_json_encode( $updated_payload ),
				'wisewand_lang'          => $updated_payload['lang'],
				'wisewand_type'          => $updated_payload['type'] ?? $api_endpoint, // Store type or endpoint
				'wisewand_length'        => $updated_payload['length'],
			);

			if ( $updates['existing_post_id'] ) {
				$updates['publication_action'] = 'update';
			} else {
				$updates['publication_action'] = 'create';
			}

			// For RSS entries, filter update_data to only allowed fields
			if ( $is_rss_entry ) {
				$allowed_fields_for_rss = array(
					'publication_date',
					'publication_time',
					'publication_time_range',
					'target_author',
					'wordpress_categories',
					'notes',
					'content_merge_strategy',
					'existing_post_id',
				);

				$filtered_update_data = array();
				foreach ( $updates as $key => $value ) {
					if ( in_array( $key, $allowed_fields_for_rss, true ) ) {
						$filtered_update_data[ $key ] = $value;
					}
				}
				$updates = $filtered_update_data;
			}

			$result = $this->entry_manager->update_entry( $entry_id, $updates );

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

			wp_send_json_success( array( 'message' => __( 'Entry saved successfully', 'wisewand-ai-content-writer-seo-content-generator' ) ) );

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

	/**
	 * AJAX Handler — Delete Entry
	 *
	 * Permanently removes a calendar entry. RSS entries cannot be deleted via
	 * this handler as they are managed by the RSS feed sync.
	 *
	 * @since 2.8.2
	 */
	public function ajax_delete_entry() {
		$this->verify_ajax_request( 'wisewand_calendar' );

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

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

		// Check if this is an RSS entry - RSS entries cannot be deleted from calendar
		$entry = $this->entry_manager->get_entry( $entry_id );
		if ( $entry && isset( $entry['keyword_source'] ) && $entry['keyword_source'] === 'rss' ) {
			wp_send_json_error(
				array(
					'message' => __( 'RSS entries cannot be deleted directly. These entries are synced from RSS feeds. To remove them, disable the RSS feed sync or edit the feed settings.', 'wisewand-ai-content-writer-seo-content-generator' ),
				)
			);
		}

		try {
			$result = $this->entry_manager->delete_entry( $entry_id );

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

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

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

	/**
	 * AJAX Handler — Generate AI Instructions (Template)
	 *
	 * Generates article brief instructions using pre-built templates based on
	 * keyword data such as search intent, volume, and current ranking position.
	 * Free and instant — does not use OpenAI.
	 *
	 * @since 2.8.2
	 */
	public function ajax_generate_instructions_template() {
		$this->verify_ajax_request( 'wisewand_calendar' );

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

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

		try {
			// Get entry to extract keyword data
			$entry = $this->entry_manager->get_entry( $entry_id );

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

			// Build keyword data from entry
			$keyword_data = array(
				'keyword'          => $entry['keyword'],
				'search_intent'    => $entry['search_intent'] ?? 'informational',
				'search_volume'    => $entry['search_volume'] ?? 0,
				'difficulty'       => $entry['difficulty'] ?? 50,
				'current_position' => $entry['current_position'] ?? null,
			);

			// Use existing template methods
			if ( ! class_exists( 'Wisewand\\Utils\\Wisewand_Payload_Builder' ) ) {
				require_once WISEWAND_PLUGIN_DIR . 'includes/Utils/Wisewand_Payload_Builder.php';
			}

			$payload_builder = new \Wisewand\Utils\Wisewand_Payload_Builder( $this->options, $this->logger );

			// Generate using templates
			$position = $keyword_data['current_position'];
			$keyword  = $keyword_data['keyword'];
			$intent   = $keyword_data['search_intent'];

			$instructions = array();

			if ( $position && $position <= 20 ) {
				$instructions['search_intention_answer'] = sprintf(
					'This article aims to improve our current ranking (position #%d) for "%s" by providing more comprehensive, up-to-date information and better user experience.',
					$position,
					$keyword
				);
				$instructions['additional_information']  = sprintf(
					'Focus on improving existing content for "%s" (currently ranked #%d). Add fresh insights, update statistics to %s, include practical examples, and ensure the content is more comprehensive than competing pages.',
					$keyword,
					$position,
					gmdate( 'Y' )
				);
			} else {
				$intent_descriptions = array(
					'informational' => 'provide comprehensive, factual information to educate readers about',
					'commercial'    => 'help readers make informed purchase decisions regarding',
					'transactional' => 'guide users through the process of',
				);
				$description         = $intent_descriptions[ $intent ] ?? $intent_descriptions['informational'];

				$instructions['search_intention_answer'] = sprintf( 'This article will %s "%s" with practical examples and actionable insights.', $description, $keyword );

				$intent_instructions = array(
					'informational' => 'Provide in-depth, well-researched information with clear structure, practical examples, and actionable takeaways.',
					'commercial'    => 'Include detailed comparisons, pros/cons, pricing information, and clear recommendations to help buyers make decisions.',
					'transactional' => 'Create step-by-step guides with clear instructions, tips for success, and common pitfalls to avoid.',
				);
				$base_instruction    = $intent_instructions[ $intent ] ?? $intent_instructions['informational'];

				$instructions['additional_information'] = sprintf(
					'Create content about "%s". %s Focus on %s best practices and current trends.',
					$keyword,
					$base_instruction,
					gmdate( 'Y' )
				);
			}

			$instructions['avoid_information'] = sprintf(
				'Avoid outdated information, unverified claims, and overly promotional content. Do not include generic filler content or information not directly relevant to "%s". Avoid speculation without factual basis.',
				$keyword
			);

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

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

	/**
	 * AJAX Handler — Generate AI Instructions (OpenAI)
	 *
	 * Generates article brief instructions using GPT-4o-mini for context-aware
	 * content briefs. Requires an OpenAI API key to be configured in settings.
	 * Generated instructions are saved to the calendar entry payload.
	 *
	 * @since 2.8.2
	 */
	public function ajax_generate_instructions_ai() {
		$this->verify_ajax_request( 'wisewand_calendar' );

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

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

		try {
			// Get entry to extract keyword data
			$entry = $this->entry_manager->get_entry( $entry_id );

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

			// Build keyword data from entry
			$keyword_data = array(
				'keyword'          => $entry['keyword'],
				'search_intent'    => $entry['search_intent'] ?? 'informational',
				'search_volume'    => $entry['search_volume'] ?? 0,
				'difficulty'       => $entry['difficulty'] ?? 50,
				'current_position' => $entry['current_position'] ?? null,
				'language_code'    => $entry['wisewand_lang'] ?? 'en',
			);

			// Use AI generation
			if ( ! class_exists( 'Wisewand\\Utils\\Wisewand_Payload_Builder' ) ) {
				require_once WISEWAND_PLUGIN_DIR . 'includes/Utils/Wisewand_Payload_Builder.php';
			}

			$payload_builder = new \Wisewand\Utils\Wisewand_Payload_Builder( $this->options, $this->logger );
			$result          = $payload_builder->generate_ai_instructions( $keyword_data );

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

			// Update calendar entry's payload with generated instructions
			$current_payload                            = $entry['wisewand_payload_decoded'] ?? array();
			$current_payload['search_intention_answer'] = $result['search_intention_answer'] ?? '';
			$current_payload['additional_information']  = $result['additional_information'] ?? '';
			$current_payload['avoid_information']       = $result['avoid_information'] ?? '';

			// Save updated payload to database
			$update_result = $this->entry_manager->update_entry(
				$entry_id,
				array(
					'wisewand_payload' => wp_json_encode( $current_payload ),
				)
			);

			if ( is_wp_error( $update_result ) ) {
				throw new \Exception( 'Failed to save instructions: ' . $update_result->get_error_message() );
			}

			$this->logger->info(
				'AI Instructions generated and saved',
				array(
					'entry_id' => $entry_id,
					'keyword'  => $entry['keyword'],
				)
			);

			wp_send_json_success(
				array(
					'instructions' => $result,
					'saved'        => true,
				)
			);

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

	/**
	 * AJAX Handler — Generate Now
	 *
	 * Saves the current entry payload and immediately triggers article generation
	 * via the Wisewand API. Sets entry status to 'creating' before the API call,
	 * then to 'generating' on success or 'failed' on error.
	 *
	 * COMPLETELY REWRITTEN — v3.1 — 2025-10-03 17:15 — PRODUCTION READY
	 *
	 * @since 2.8.2
	 */
	public function ajax_generate_now() {
		// VERSION MARKER - This will appear in AJAX response if v3.1 is running
		$version_marker = 'WISEWAND_V3.1_2025-10-03_17:15_FINAL_PRODUCTION';
		if ( WP_DEBUG ) {
			error_log( "=== $version_marker - Function Called ===" );
		}

		check_ajax_referer( 'wisewand_calendar', 'nonce' );

		if ( ! current_user_can( 'edit_posts' ) ) {
			wp_send_json_error(
				array(
					'message' => __( 'Insufficient permissions', 'wisewand-ai-content-writer-seo-content-generator' ),
					'version' => $version_marker,
				)
			);
		}

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

		if ( ! $entry_id ) {
			wp_send_json_error(
				array(
					'message' => __( 'Invalid entry ID', 'wisewand-ai-content-writer-seo-content-generator' ),
					'version' => $version_marker,
				)
			);
		}

		try {
			// Get entry
			$entry = $this->entry_manager->get_entry( $entry_id );
			if ( is_wp_error( $entry ) ) {
				throw new \Exception( $entry->get_error_message() );
			}

			// Get API key
			$api_key = $this->options->get( 'wisewand_api_key' );
			if ( empty( $api_key ) ) {
				throw new \Exception( __( 'Wisewand API key not configured', 'wisewand-ai-content-writer-seo-content-generator' ) );
			}

			// Process submitted form data (same as ajax_save_entry)
			// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Payload is a complex JSON string, decoded and sanitized later
			// jQuery sends objects as arrays, not JSON strings - no need to json_decode
			// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Complex array sanitized by sanitize_payload_types()
			$raw_payload  = isset( $_POST['wisewand_payload'] ) ? $this->sanitize_payload_types( wp_unslash( $_POST['wisewand_payload'] ) ) : array();
			$api_endpoint = isset( $_POST['api_endpoint'] ) ? sanitize_text_field( wp_unslash( $_POST['api_endpoint'] ) ) : 'articles';

			$existing_entry = $this->entry_manager->get_entry( $entry_id );
			if ( is_wp_error( $existing_entry ) ) {
				throw new \Exception( $existing_entry->get_error_message() );
			}

			// Process submitted payload (same logic as ajax_save_entry)
			$existing_payload = $existing_entry['wisewand_payload_decoded'] ?? array();
			// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Payload is a complex JSON string, decoded and sanitized later
			// jQuery sends objects as arrays, not JSON strings - no need to json_decode
			// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Complex array sanitized by sanitize_payload_types()
			$raw_payload = isset( $_POST['wisewand_payload'] ) ? $this->sanitize_payload_types( wp_unslash( $_POST['wisewand_payload'] ) ) : array();

			// Process CTAs array
			$ctas = array();
			if ( ! empty( $raw_payload['cta_anchors'] ) && ! empty( $raw_payload['cta_urls'] ) ) {
				$anchors = $raw_payload['cta_anchors'];
				$urls    = $raw_payload['cta_urls'];
				for ( $i = 0; $i < count( $anchors ); $i++ ) {
					if ( ! empty( $anchors[ $i ] ) && ! empty( $urls[ $i ] ) ) {
						$ctas[] = array(
							'anchor' => sanitize_text_field( $anchors[ $i ] ),
							'url'    => esc_url_raw( $urls[ $i ] ),
						);
					}
				}
			}
			if ( ! empty( $ctas ) ) {
				$raw_payload['ctas']    = $ctas;
				$raw_payload['use_cta'] = true;
			}
			unset( $raw_payload['cta_anchors'] );
			unset( $raw_payload['cta_urls'] );

			// Process social networks
			if ( isset( $raw_payload['social_networks'] ) && is_array( $raw_payload['social_networks'] ) ) {
				$raw_payload['social_networks'] = array_map( 'sanitize_text_field', $raw_payload['social_networks'] );
			}

			// Process internal links WP status array
			if ( isset( $raw_payload['internal_links_wp_status'] ) && is_array( $raw_payload['internal_links_wp_status'] ) ) {
				$raw_payload['internal_links_wp_status'] = array_map( 'sanitize_text_field', $raw_payload['internal_links_wp_status'] );
			}

			// Process Affiliation Links - Product Review
			$affiliation_links = array();
			if ( ! empty( $raw_payload['affiliation_link_urls'] ) && ! empty( $raw_payload['affiliation_link_anchors'] ) ) {
				$urls    = $raw_payload['affiliation_link_urls'];
				$anchors = $raw_payload['affiliation_link_anchors'];

				for ( $i = 0; $i < count( $urls ); $i++ ) {
					if ( ! empty( $urls[ $i ] ) && ! empty( $anchors[ $i ] ) ) {
						$affiliation_links[] = array(
							'url'    => esc_url_raw( $urls[ $i ] ),
							'anchor' => sanitize_text_field( $anchors[ $i ] ),
						);
					}
				}
			}
			if ( ! empty( $affiliation_links ) ) {
				$raw_payload['affiliation_links'] = $affiliation_links;
			}
			unset( $raw_payload['affiliation_link_urls'] );
			unset( $raw_payload['affiliation_link_anchors'] );

			// Process Affiliation Links - Compare Products Product 1
			$affiliation_links_1 = array();
			if ( ! empty( $raw_payload['affiliation_product_1_link_urls'] ) && ! empty( $raw_payload['affiliation_product_1_link_anchors'] ) ) {
				$urls    = $raw_payload['affiliation_product_1_link_urls'];
				$anchors = $raw_payload['affiliation_product_1_link_anchors'];

				for ( $i = 0; $i < count( $urls ); $i++ ) {
					if ( ! empty( $urls[ $i ] ) && ! empty( $anchors[ $i ] ) ) {
						$affiliation_links_1[] = array(
							'url'    => esc_url_raw( $urls[ $i ] ),
							'anchor' => sanitize_text_field( $anchors[ $i ] ),
						);
					}
				}
			}
			if ( ! empty( $affiliation_links_1 ) ) {
				$raw_payload['affiliation_links_product_1'] = $affiliation_links_1;
			}
			unset( $raw_payload['affiliation_product_1_link_urls'] );
			unset( $raw_payload['affiliation_product_1_link_anchors'] );

			// Process Affiliation Links - Compare Products Product 2
			$affiliation_links_2 = array();
			if ( ! empty( $raw_payload['affiliation_product_2_link_urls'] ) && ! empty( $raw_payload['affiliation_product_2_link_anchors'] ) ) {
				$urls    = $raw_payload['affiliation_product_2_link_urls'];
				$anchors = $raw_payload['affiliation_product_2_link_anchors'];

				for ( $i = 0; $i < count( $urls ); $i++ ) {
					if ( ! empty( $urls[ $i ] ) && ! empty( $anchors[ $i ] ) ) {
						$affiliation_links_2[] = array(
							'url'    => esc_url_raw( $urls[ $i ] ),
							'anchor' => sanitize_text_field( $anchors[ $i ] ),
						);
					}
				}
			}
			if ( ! empty( $affiliation_links_2 ) ) {
				$raw_payload['affiliation_links_product_2'] = $affiliation_links_2;
			}
			unset( $raw_payload['affiliation_product_2_link_urls'] );
			unset( $raw_payload['affiliation_product_2_link_anchors'] );

			// Process Top Products
			if ( isset( $raw_payload['top_products_data'] ) ) {
				$raw_payload['affiliation_top_products'] = $raw_payload['top_products_data'];
				unset( $raw_payload['top_products_data'] );
			}

			$updated_payload = array_merge( $existing_payload, $raw_payload );
			$api_endpoint    = isset( $_POST['api_endpoint'] ) ? sanitize_text_field( wp_unslash( $_POST['api_endpoint'] ) ) : 'articles';

			// Clean up endpoint-specific fields (same as save handler)
			$updated_payload['subject']     = $existing_entry['keyword'];
			$updated_payload['use_webhook'] = false;

			// Clean up legacy/deprecated fields
			unset( $updated_payload['use_inline_images'] ); // Wrong field name - API uses use_inlineimages (no underscore)

			// Field cleanup logic (COMPLETE - same as ajax_save_entry)
			if ( $api_endpoint === 'articles' ) {
				unset( $updated_payload['website'] );
				unset( $updated_payload['writing_style'] );

				// Clean up affiliation fields if NOT an affiliation type
				$article_type = $updated_payload['type'] ?? 'blog';
				if ( ! in_array( $article_type, array( 'affiliation-product-review', 'affiliation-compare-products', 'affiliation-top-product' ) ) ) {
					// Remove ALL affiliation fields for non-affiliation article types
					unset( $updated_payload['affiliation_product_name'] );
					unset( $updated_payload['affiliation_product_review_urls'] );
					unset( $updated_payload['affiliation_links'] );
					unset( $updated_payload['affiliation_promo_code'] );
					unset( $updated_payload['affiliation_promo_percentage'] );
					unset( $updated_payload['affiliation_product_1_name'] );
					unset( $updated_payload['affiliation_product_1_review_urls'] );
					unset( $updated_payload['affiliation_links_product_1'] );
					unset( $updated_payload['affiliation_promo_code_1'] );
					unset( $updated_payload['affiliation_promo_percentage_1'] );
					unset( $updated_payload['affiliation_product_2_name'] );
					unset( $updated_payload['affiliation_product_2_review_urls'] );
					unset( $updated_payload['affiliation_links_product_2'] );
					unset( $updated_payload['affiliation_promo_code_2'] );
					unset( $updated_payload['affiliation_promo_percentage_2'] );
					unset( $updated_payload['affiliation_top_products'] );
				}

				// Clean up type-specific affiliation fields
				if ( $article_type !== 'affiliation-product-review' ) {
					unset( $updated_payload['affiliation_product_name'] );
					unset( $updated_payload['affiliation_product_review_urls'] );
					unset( $updated_payload['affiliation_links'] );
					unset( $updated_payload['affiliation_promo_code'] );
					unset( $updated_payload['affiliation_promo_percentage'] );
				}
				if ( $article_type !== 'affiliation-compare-products' ) {
					unset( $updated_payload['affiliation_product_1_name'] );
					unset( $updated_payload['affiliation_product_1_review_urls'] );
					unset( $updated_payload['affiliation_links_product_1'] );
					unset( $updated_payload['affiliation_promo_code_1'] );
					unset( $updated_payload['affiliation_promo_percentage_1'] );
					unset( $updated_payload['affiliation_product_2_name'] );
					unset( $updated_payload['affiliation_product_2_review_urls'] );
					unset( $updated_payload['affiliation_links_product_2'] );
					unset( $updated_payload['affiliation_promo_code_2'] );
					unset( $updated_payload['affiliation_promo_percentage_2'] );
				}
				if ( $article_type !== 'affiliation-top-product' ) {
					unset( $updated_payload['affiliation_top_products'] );
				}
			} else {
				// Category/Product/Discover pages
				unset( $updated_payload['keep_title'] );
				unset( $updated_payload['search_intention_answer'] );
				unset( $updated_payload['avoid_information'] );
				unset( $updated_payload['type'] );

				// Remove ALL affiliation fields (only for articles endpoint)
				unset( $updated_payload['affiliation_product_name'] );
				unset( $updated_payload['affiliation_product_review_urls'] );
				unset( $updated_payload['affiliation_links'] );
				unset( $updated_payload['affiliation_promo_code'] );
				unset( $updated_payload['affiliation_promo_percentage'] );
				unset( $updated_payload['affiliation_product_1_name'] );
				unset( $updated_payload['affiliation_product_1_review_urls'] );
				unset( $updated_payload['affiliation_links_product_1'] );
				unset( $updated_payload['affiliation_promo_code_1'] );
				unset( $updated_payload['affiliation_promo_percentage_1'] );
				unset( $updated_payload['affiliation_product_2_name'] );
				unset( $updated_payload['affiliation_product_2_review_urls'] );
				unset( $updated_payload['affiliation_links_product_2'] );
				unset( $updated_payload['affiliation_promo_code_2'] );
				unset( $updated_payload['affiliation_promo_percentage_2'] );
				unset( $updated_payload['affiliation_top_products'] );

				if ( $api_endpoint === 'categorypages' || $api_endpoint === 'productpages' ) {
					unset( $updated_payload['keywords_secondary'] );
				}
			}

			// SAVE THE UPDATED ENTRY
			$updates = array(
				'wisewand_payload' => wp_json_encode( $updated_payload ),
				'wisewand_lang'    => $updated_payload['lang'],
				'wisewand_type'    => $updated_payload['type'] ?? $api_endpoint,
				'wisewand_length'  => $updated_payload['length'],
				'wisewand_status'  => 'creating', // Set to creating
				'wisewand_error'   => null,
			);

			$this->entry_manager->update_entry( $entry_id, $updates );

			// STEP 2: NOW GENERATE with the fresh payload
			$api_key = $this->options->get( 'wisewand_api_key' );
			if ( empty( $api_key ) ) {
				throw new \Exception( __( 'Wisewand API key not configured', 'wisewand-ai-content-writer-seo-content-generator' ) );
			}

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

			$api_client = new \Wisewand\Utils\Wisewand_API_Client( $api_key, $this->logger );

			// Build clean payload using shared whitelist
			$article_type = $updated_payload['type'] ?? 'blog';

			if ( ! class_exists( 'Wisewand\\Utils\\API_Payload_Cleaner' ) ) {
				require_once WISEWAND_PLUGIN_DIR . 'includes/Utils/API_Payload_Cleaner.php';
			}
			$clean_payload = \Wisewand\Utils\API_Payload_Cleaner::clean( $updated_payload, $api_endpoint, $article_type );

			// DETAILED LOGGING: Log exact payload being sent to API
			if ( WP_DEBUG ) {
				error_log( '=== WISEWAND API REQUEST ===' );
				error_log( 'Entry ID: ' . $entry_id );
				error_log( 'Endpoint: ' . $api_endpoint );
				error_log( 'Article Type: ' . $article_type );
				error_log( 'Clean Payload Fields: ' . implode( ', ', array_keys( $clean_payload ) ) );
				error_log( 'Full Clean Payload JSON: ' . wp_json_encode( $clean_payload, JSON_PRETTY_PRINT ) );
				error_log( '===========================' );
			}

			$this->logger->info(
				'Generate Now: Sending to API',
				array(
					'entry_id'           => $entry_id,
					'endpoint'           => $api_endpoint,
					'article_type'       => $article_type,
					'clean_fields_count' => count( $clean_payload ),
					'clean_fields'       => array_keys( $clean_payload ),
				)
			);

			// Send payload directly (NO wrapper - API expects fields at root level)
			$article = $api_client->create_content( $api_endpoint, $clean_payload );

			if ( is_wp_error( $article ) ) {
				$error_message = esc_html( $article->get_error_message() );
				$error_data    = $article->get_error_data();

				// Enhanced error logging
				if ( WP_DEBUG ) {
					error_log( '=== GENERATE NOW ERROR ===' );
					error_log( 'Error Message: ' . $error_message );
					error_log( 'Error Data: ' . print_r( $error_data, true ) );
					error_log( '=========================' );
				}

				$this->logger->error(
					'Generate Now: API rejected payload',
					array(
						'error_message' => $error_message,
						'error_data'    => $error_data,
						'http_status'   => $error_data['status_code'] ?? 'unknown',
						'api_response'  => $error_data['response_data'] ?? 'none',
						'endpoint'      => $api_endpoint,
						'entry_id'      => $entry_id,
						'sent_payload'  => $clean_payload,
					)
				);

				// Build detailed error with payload info for debugging
				$payload_fields = array_keys( $clean_payload );
				sort( $payload_fields );

				$user_error = 'Generation failed: ' . $error_message;

				// Detect insufficient credits.
				$is_credit_error = (
					( isset( $error_data['status_code'] ) && 402 === (int) $error_data['status_code'] ) ||
					false !== stripos( $error_message, 'credit' ) ||
					false !== stripos( $error_message, 'insufficient' ) ||
					false !== stripos( $error_message, 'quota' )
				);

				if ( $is_credit_error ) {
					$user_error = __( 'Insufficient WiseWand credits.', 'wisewand-ai-content-writer-seo-content-generator' )
						. ' ' . __( 'Buy more at', 'wisewand-ai-content-writer-seo-content-generator' )
						. ' https://app.wisewand.ai/buy-credits';
					delete_transient( 'wisewand_credit_balance' );
				}

				if ( ! empty( $error_data['status_code'] ) && ! $is_credit_error ) {
					$user_error .= ' (HTTP ' . $error_data['status_code'] . ')';
				}
				if ( ! empty( $error_data['response_data']['error'] ) ) {
					$user_error .= "\n\nAPI Error: " . $error_data['response_data']['error'];
				}
				if ( ! empty( $error_data['response_data']['message'] ) ) {
					$user_error .= "\n\nDetails: " . $error_data['response_data']['message'];
				}

				// Create detailed exception with payload data
				$exception = new \Exception( $user_error );

				// Store payload info to include in JSON response
				$this->_last_error_debug = array(
					'sent_fields'      => $payload_fields,
					'sent_field_count' => count( $payload_fields ),
					'clean_payload'    => $clean_payload,
				);

				throw $exception;
			}

			// Invalidate credit balance cache after successful creation.
			delete_transient( 'wisewand_credit_balance' );

			$article_id = $article['id'] ?? null;
			if ( ! $article_id ) {
				throw new \Exception( __( 'No content ID returned from Wisewand', 'wisewand-ai-content-writer-seo-content-generator' ) );
			}

			// Store article ID
			$this->entry_manager->update_entry(
				$entry_id,
				array(
					'wisewand_article_id' => $article_id,
				)
			);

			// Trigger generation
			$run_result = $api_client->run_content_generation( $api_endpoint, $article_id );

			if ( is_wp_error( $run_result ) ) {
				$error_msg = esc_html( $run_result->get_error_message() );

				// Special case: "already running or in queue" is not a real error
				// Article was created successfully, just can't run it right now
				if ( stripos( $error_msg, 'already running' ) !== false || stripos( $error_msg, 'in queue' ) !== false ) {
					// Update status to generating (it's already queued)
					$this->entry_manager->update_entry(
						$entry_id,
						array(
							'wisewand_status'         => 'generating',
							'generation_triggered_at' => current_time( 'mysql' ),
						)
					);

					$this->logger->info(
						'Generate Now: Article already in queue',
						array(
							'entry_id'   => $entry_id,
							'article_id' => $article_id,
						)
					);

					wp_send_json_success(
						array(
							'message'      => __( 'Article created and queued for generation!', 'wisewand-ai-content-writer-seo-content-generator' ),
							'article_id'   => $article_id,
							'wisewand_url' => 'https://app.wisewand.ai/articles/' . $article_id . '/result',
							'status'       => 'generating',
							'note'         => 'Generation already in progress',
							'version'      => $version_marker,
						)
					);
				}

				// Real error - throw it
				throw new \Exception( $error_msg );
			}

			// Update status to generating
			$this->entry_manager->update_entry(
				$entry_id,
				array(
					'wisewand_status'         => 'generating',
					'generation_triggered_at' => current_time( 'mysql' ),
				)
			);

			$this->logger->info(
				'Generate Now: Success',
				array(
					'entry_id'   => $entry_id,
					'article_id' => $article_id,
				)
			);

			wp_send_json_success(
				array(
					'message'      => __( 'Article generation started successfully!', 'wisewand-ai-content-writer-seo-content-generator' ),
					'article_id'   => $article_id,
					'wisewand_url' => 'https://app.wisewand.ai/articles/' . $article_id . '/result',
					'status'       => 'generating',
					'version'      => $version_marker,
				)
			);

		} catch ( \Exception $e ) {
			$this->logger->error(
				'Generate Now failed',
				array(
					'entry_id' => $entry_id,
					'error'    => $e->getMessage(),
				)
			);

			$this->entry_manager->update_entry(
				$entry_id,
				array(
					'wisewand_status' => 'failed',
					'wisewand_error'  => $e->getMessage(),
				)
			);

			// Include debug info if available
			$error_response = array(
				'message' => $e->getMessage(),
				'version' => $version_marker,
			);

			// Add payload debugging info if we have it
			if ( isset( $this->_last_error_debug ) ) {
				$error_response['debug'] = $this->_last_error_debug;
			}

			wp_send_json_error( $error_response );
		}
	}

	/**
	 * AJAX Handler — Import Now (manual immediate import)
	 *
	 * Immediately imports a ready calendar entry as a WordPress post, bypassing
	 * the CRON schedule. Uses Calendar_CRON's import_and_publish_entry via
	 * reflection since that method is private.
	 *
	 * @since 2.8.2
	 */
	public function ajax_import_now() {
		$this->verify_ajax_request( 'wisewand_calendar' );

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

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

		try {
			// Get entry
			$entry = $this->entry_manager->get_entry( $entry_id );
			if ( is_wp_error( $entry ) ) {
				throw new \Exception( esc_html( $entry->get_error_message() ) );
			}

			// Check if already imported
			if ( ! empty( $entry['wordpress_post_id'] ) ) {
				throw new \Exception( __( 'This entry has already been imported', 'wisewand-ai-content-writer-seo-content-generator' ) );
			}

			// Check if content is ready
			if ( $entry['wisewand_status'] !== 'ready' ) {
				throw new \Exception( __( 'Content is not ready for import yet', 'wisewand-ai-content-writer-seo-content-generator' ) );
			}

			// Load Calendar_CRON
			if ( ! class_exists( 'Wisewand\\Automation\\Calendar_CRON' ) ) {
				require_once WISEWAND_PLUGIN_DIR . 'includes/Automation/Calendar_CRON.php';
			}

			// Load Content_Manager
			if ( ! class_exists( 'Wisewand\\Content\\Content_Manager' ) ) {
				require_once WISEWAND_PLUGIN_DIR . 'includes/Content/Content_Manager.php';
			}

			$content_manager = new \Wisewand\Content\Content_Manager( $this->options, $this->logger );
			$calendar_cron   = new \Wisewand\Automation\Calendar_CRON(
				$this->options,
				$this->logger,
				$content_manager
			);

			// Import using reflection to access private method
			$reflection = new \ReflectionClass( $calendar_cron );
			$method     = $reflection->getMethod( 'import_and_publish_entry' );
			$method->setAccessible( true );
			$result = $method->invoke( $calendar_cron, $entry );

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

			$this->logger->info(
				'Manual import successful',
				array(
					'entry_id' => $entry_id,
					'post_id'  => $result,
				)
			);

			wp_send_json_success(
				array(
					'message'  => __( 'Article imported successfully!', 'wisewand-ai-content-writer-seo-content-generator' ),
					'post_id'  => $result,
					'edit_url' => get_edit_post_link( $result, 'raw' ),
					'view_url' => get_permalink( $result ),
				)
			);

		} catch ( \Exception $e ) {
			$this->logger->error(
				'Manual import failed',
				array(
					'entry_id' => $entry_id,
					'error'    => $e->getMessage(),
				)
			);

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

	/**
	 * AJAX Handler — Get Projects List
	 *
	 * Fetches all Wisewand projects for the current API key. Used to populate
	 * the project selector in the entry side panel.
	 *
	 * @since 2.8.2
	 */
	public function ajax_get_projects() {
		$this->logger->info( 'AJAX: ajax_get_projects called' );

		check_ajax_referer( 'wisewand_calendar', 'nonce' );

		if ( ! current_user_can( 'edit_posts' ) ) {
			$this->logger->error( 'AJAX: Insufficient permissions' );
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions', 'wisewand-ai-content-writer-seo-content-generator' ) ) );
		}

		try {
			$api_key = $this->options->get( 'wisewand_api_key' );

			if ( empty( $api_key ) ) {
				$this->logger->error( 'AJAX: API key not configured' );
				wp_send_json_error( array( 'message' => __( 'Wisewand API key not configured', 'wisewand-ai-content-writer-seo-content-generator' ) ) );
				return;
			}

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

			$api_client = new \Wisewand\Utils\Wisewand_API_Client( $api_key, $this->logger );
			$this->logger->info( 'AJAX: Calling list_projects with force_refresh=true' );
			$result = $api_client->list_projects( array( 'take' => 100 ), true );

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

			$projects = array();
			foreach ( $result['items'] ?? array() as $project ) {
				$projects[] = array(
					'id'      => $project['id'] ?? '',
					'name'    => ! empty( $project['website'] ) ? $project['website'] : ( $project['name'] ?? $project['id'] ?? '' ),
					'website' => $project['website'] ?? '',
				);
			}

			$this->logger->info( 'AJAX: Returning ' . count( $projects ) . ' projects' );
			wp_send_json_success( array( 'projects' => $projects ) );

		} catch ( \Exception $e ) {
			$this->logger->error( 'AJAX: Failed to fetch projects', array( 'error' => $e->getMessage() ) );
			wp_send_json_error( array( 'message' => esc_html( $e->getMessage() ) ) );
		}
	}

	/**
	 * AJAX Handler — Get Single Project Details (including brief)
	 *
	 * Fetches details for a single Wisewand project by ID, including the project
	 * brief, website, and name. Used when a project is selected in the entry panel.
	 *
	 * @since 2.8.2
	 */
	public function ajax_get_project_details() {
		$this->verify_ajax_request( 'wisewand_settings' );

		$project_id = isset( $_POST['project_id'] ) ? sanitize_text_field( wp_unslash( $_POST['project_id'] ) ) : '';

		if ( empty( $project_id ) ) {
			wp_send_json_error( array( 'message' => __( 'Project ID required', 'wisewand-ai-content-writer-seo-content-generator' ) ) );
		}

		try {
			$api_key = $this->options->get( 'wisewand_api_key' );

			if ( empty( $api_key ) ) {
				throw new \Exception( __( 'Wisewand API key not configured', 'wisewand-ai-content-writer-seo-content-generator' ) );
			}

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

			$api_client = new \Wisewand\Utils\Wisewand_API_Client( $api_key, $this->logger );
			$result     = $api_client->get_project( $project_id );

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

			$project = array(
				'id'      => $result['id'] ?? $project_id,
				'name'    => ! empty( $result['website'] ) ? $result['website'] : ( $result['name'] ?? $project_id ),
				'brief'   => $result['brief'] ?? '',
				'website' => $result['website'] ?? '',
			);

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

		} catch ( \Exception $e ) {
			$this->logger->error(
				'Failed to fetch project details',
				array(
					'project_id' => $project_id,
					'error'      => $e->getMessage(),
				)
			);
			wp_send_json_error( array( 'message' => esc_html( $e->getMessage() ) ) );
		}
	}

	/**
	 * AJAX Handler — Get Personas List
	 *
	 * Fetches all Wisewand personas for the current API key. Used to populate
	 * the persona selector in the entry side panel writing style section.
	 *
	 * @since 2.8.2
	 */
	public function ajax_get_personas() {
		$this->logger->info( 'AJAX: ajax_get_personas called' );

		check_ajax_referer( 'wisewand_calendar', 'nonce' );

		if ( ! current_user_can( 'edit_posts' ) ) {
			$this->logger->error( 'AJAX: Insufficient permissions' );
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions', 'wisewand-ai-content-writer-seo-content-generator' ) ) );
		}

		try {
			$api_key = $this->options->get( 'wisewand_api_key' );

			if ( empty( $api_key ) ) {
				$this->logger->error( 'AJAX: API key not configured' );
				wp_send_json_error( array( 'message' => __( 'Wisewand API key not configured', 'wisewand-ai-content-writer-seo-content-generator' ) ) );
				return;
			}

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

			$api_client = new \Wisewand\Utils\Wisewand_API_Client( $api_key, $this->logger );
			$this->logger->info( 'AJAX: Calling list_personas with force_refresh=true' );
			$result = $api_client->list_personas( array( 'take' => 100 ), true );

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

			$personas = array();
			foreach ( $result['items'] ?? array() as $persona ) {
				$personas[] = array(
					'id'          => $persona['id'] ?? '',
					'name'        => $persona['name'] ?? 'Unnamed Persona',
					'description' => $persona['description'] ?? '',
				);
			}

			$this->logger->info( 'AJAX: Returning ' . count( $personas ) . ' personas' );
			wp_send_json_success( array( 'personas' => $personas ) );

		} catch ( \Exception $e ) {
			$this->logger->error( 'AJAX: Failed to fetch personas', array( 'error' => $e->getMessage() ) );
			wp_send_json_error( array( 'message' => esc_html( $e->getMessage() ) ) );
		}
	}

	/**
	 * AJAX Handler — Manual CRON Trigger
	 *
	 * Manually runs the Calendar_CRON automation cycle, which checks for entries
	 * due for generation or import and processes them. Requires manage_options.
	 *
	 * @since 2.8.2
	 */
	public function ajax_trigger_manual_cron() {
		$this->verify_ajax_request( 'wisewand_calendar', 'manage_options' );

		try {
			$this->logger->info( 'Manual CRON trigger started' );

			$start_time = microtime( true );

			if ( ! class_exists( 'Wisewand\\Automation\\Calendar_CRON' ) ) {
				require_once WISEWAND_PLUGIN_DIR . 'includes/Automation/Calendar_CRON.php';
			}

			$plugin          = \Wisewand\Core\Plugin::instance();
			$content_manager = $plugin->get_content_manager();

			if ( ! $content_manager ) {
				throw new \Exception( 'Content Manager not initialized' );
			}

			$calendar_cron = new \Wisewand\Automation\Calendar_CRON(
				$this->options,
				$this->logger,
				$content_manager
			);

			$calendar_cron->run();

			$elapsed = round( microtime( true ) - $start_time, 2 );

			$summary            = $calendar_cron->get_cron_summary();
			$summary['elapsed'] = $elapsed;

			wp_send_json_success(
				array(
					'summary' => $summary,
					'message' => __( 'Automation completed successfully', 'wisewand-ai-content-writer-seo-content-generator' ),
				)
			);

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

	/**
	 * Transform calendar entries to FullCalendar event objects
	 *
	 * Converts raw database entry arrays into the format expected by FullCalendar,
	 * including title with status emoji, event color, class names, and extended
	 * properties for filtering and detail display.
	 *
	 * @since 2.8.2
	 *
	 * @param array $entries Array of calendar entry arrays from the database.
	 * @return array Array of FullCalendar event objects.
	 */
	private function transform_entries_to_fullcalendar_events( $entries ) {
		$events = array();

		foreach ( $entries as $entry ) {
			$color = $this->get_event_color( $entry );
			$title = $entry['keyword'];

			// Add status indicator
			if ( in_array( $entry['wordpress_status'], array( 'imported_draft', 'imported_published', 'imported_scheduled' ) ) ) {
				$title = '🟢 ' . $title; // Green for imported (success)
			} elseif ( $entry['wisewand_status'] === 'ready' ) {
				$title = '🟣 ' . $title; // Purple for ready
			} elseif ( $entry['wisewand_status'] === 'generating' ) {
				$title = '🟡 ' . $title; // Yellow for generating
			} elseif ( $entry['wisewand_status'] === 'failed' || $entry['wordpress_status'] === 'failed' ) {
				$title = '🔴 ' . $title; // Red for failed
			}

			/**
			 * Display publication time inline with article title
			 *
			 * Format: [emoji] [time] Article Title
			 * Time format respects WordPress Settings > General > Time Format.
			 * Time is displayed inline to save vertical space in calendar view.
			 *
			 * @since 1.0.0 Changed from multi-line to inline format, added wp_date() for timezone support
			 */
			// Add scheduled time info inline (for all entries, regardless of status)
			if ( ! empty( $entry['publication_time'] ) ) {
				// Show exact time - respects WordPress timezone and format settings
				$time_format = get_option( 'time_format', 'g:i A' );
				$timestamp   = strtotime( $entry['publication_time'] );
				if ( false !== $timestamp ) {
					// v1.2.7: Use date() not wp_date() - publication_time already in local timezone
					// phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date -- Intentional: displaying already-local times from database
					$formatted_time = date( $time_format, $timestamp );
					if ( false !== $formatted_time ) {
						// Prepend time to beginning of title instead of appending
						$title = $formatted_time . ' ' . $title;
					}
				}
			}

			// Build class names for CSS targeting
			$class_names = array();
			if ( isset( $entry['keyword_source'] ) ) {
				$class_names[] = 'event-' . $entry['keyword_source'];
			}

			$events[] = array(
				'id'              => $entry['id'],
				'title'           => $title,
				'start'           => $entry['publication_date'],
				'backgroundColor' => $color,
				'borderColor'     => $color,
				'classNames'      => $class_names,
				'extendedProps'   => array(
					'keyword'                => $entry['keyword'],
					'wisewand_status'        => $entry['wisewand_status'],
					'wordpress_status'       => $entry['wordpress_status'],
					'search_volume'          => $entry['search_volume'],
					'current_position'       => $entry['current_position'],
					'publication_time'       => $entry['publication_time'] ?? null,
					'publication_time_range' => $entry['publication_time_range'] ?? 'random',
					'keyword_source'         => $entry['keyword_source'] ?? 'manual',
					'source_metadata'        => $entry['source_metadata'] ?? null,
					'rss_article_id'         => $entry['rss_article_id'] ?? null,
					'rss_feed_id'            => $entry['rss_feed_id'] ?? null,
				),
			);
		}

		return $events;
	}

	/**
	 * Get the background color for a calendar event based on entry status
	 *
	 * Determines the FullCalendar event color by checking keyword_source, then
	 * wordpress_status, then wisewand_status in priority order.
	 *
	 * @since 2.8.2
	 *
	 * @param array $entry Calendar entry array from the database.
	 * @return string Hex color code.
	 */
	private function get_event_color( $entry ) {
		// RSS entries - use feed_color from source_metadata (green #10b981)
		if ( isset( $entry['keyword_source'] ) && $entry['keyword_source'] === 'rss' ) {
			if ( ! empty( $entry['source_metadata'] ) ) {
				$metadata = json_decode( $entry['source_metadata'], true );
				if ( isset( $metadata['feed_color'] ) ) {
					return $metadata['feed_color']; // Custom feed color (default: #10b981)
				}
			}
			return '#10b981'; // Default green for RSS entries
		}

		// Imported statuses (green - success state)
		if ( in_array( $entry['wordpress_status'], array( 'imported_draft', 'imported_published', 'imported_scheduled' ) ) ) {
			return '#10b981'; // Green
		}

		// Failed status (red)
		if ( $entry['wisewand_status'] === 'failed' || $entry['wordpress_status'] === 'failed' ) {
			return '#ef4444'; // Red
		}

		// Ready status (purple - distinct from generating)
		if ( $entry['wisewand_status'] === 'ready' ) {
			return '#8b5cf6'; // Purple
		}

		// Generating status (amber)
		if ( $entry['wisewand_status'] === 'generating' ) {
			return '#f59e0b'; // Amber
		}

		// Importing status (yellow)
		if ( $entry['wordpress_status'] === 'importing' ) {
			return '#eab308'; // Yellow
		}

		// Default: Scheduled (blue)
		return '#3b82f6'; // Blue
	}

	/**
	 * Determine API endpoint string from wisewand_type stored value
	 *
	 * Maps legacy single-word type values and normalised endpoint strings to the
	 * four valid API endpoints: articles, categorypages, productpages, discoverarticles.
	 *
	 * @since 2.8.2
	 *
	 * @param string $wisewand_type The wisewand_type value stored in the database.
	 * @return string Normalised API endpoint string.
	 */
	private function determine_endpoint_from_type( $wisewand_type ) {
		if ( $wisewand_type === 'categorypage' || $wisewand_type === 'categorypages' ) {
			return 'categorypages';
		}
		if ( $wisewand_type === 'productpage' || $wisewand_type === 'productpages' ) {
			return 'productpages';
		}
		if ( $wisewand_type === 'discoverarticles' ) {
			return 'discoverarticles';
		}
		return 'articles';
	}

	/**
	 * Sanitize payload data with type-aware handling
	 *
	 * Handles type preservation for integer fields that get converted to strings
	 * by map_deep() sanitization.
	 *
	 * @since 1.4.3
	 *
	 * @param array $payload Raw payload data.
	 * @return array Sanitized payload with correct types.
	 */
	private function sanitize_payload_types( $payload ) {
		// Apply text sanitization to all fields first
		$sanitized = map_deep( wp_unslash( $payload ), 'sanitize_text_field' );

		// Integer fields needing type preservation
		$integer_fields = array( 'inlineimages_count' );

		foreach ( $integer_fields as $int_field ) {
			if ( isset( $sanitized[ $int_field ] ) && is_numeric( $sanitized[ $int_field ] ) ) {
				$sanitized[ $int_field ] = absint( $sanitized[ $int_field ] );
			}
		}

		return $sanitized;
	}

}
