<?php
/**
 * Bulk Post Generator Class
 *
 * Generates multiple posts using AI providers
 *
 * @package SmartyPress_AI_Engine
 * @since 1.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Bulk Post Generator class
 */
class SmartyPress_AI_Bulk_Post_Generator extends SmartyPress_AI_Abstract_Post_Generator {

	const JOB_OPTION_PREFIX = 'smartypress_ai_bulk_job_';
	const CRON_HOOK         = 'smartypress_ai_process_bulk_job';

	/**
	 * Default prompts for bulk generation
	 *
	 * @var array
	 */
	private $default_prompts = array();

	/**
	 * Legacy bulk prompt service.
	 *
	 * @var SmartyPress_AI_Content_Generator_Bulk_Prompt_Service
	 */
	private $prompt_service;

	/**
	 * Bulk job repository.
	 *
	 * @var SmartyPress_AI_Content_Generator_Bulk_Job_Repository
	 */
	private $job_repository;

	/**
	 * Cost estimator.
	 *
	 * @var SmartyPress_AI_Content_Generator_Generation_Cost_Estimator
	 */
	private $cost_estimator;

	/**
	 * Bulk legacy pipeline.
	 *
	 * @var SmartyPress_AI_Content_Generator_Bulk_Legacy_Generation_Pipeline
	 */
	private $legacy_pipeline;

	/**
	 * Constructor
	 *
	 * @param array $providers AI providers instances.
	 * @param array $settings Plugin settings.
	 */
	public function __construct( $providers, $settings ) {
		parent::__construct( $providers, $settings );
		$this->prompt_service  = new SmartyPress_AI_Content_Generator_Bulk_Prompt_Service();
		$this->job_repository  = new SmartyPress_AI_Content_Generator_Bulk_Job_Repository( self::JOB_OPTION_PREFIX, self::CRON_HOOK );
		$this->cost_estimator  = new SmartyPress_AI_Content_Generator_Generation_Cost_Estimator();
		$item_context_service  = new SmartyPress_AI_Content_Generator_Bulk_Item_Context_Service( $this->post_type_extension_registry );
		$this->legacy_pipeline = new SmartyPress_AI_Content_Generator_Bulk_Legacy_Generation_Pipeline( $this->prompt_service, $item_context_service );
		$this->init_default_prompts();
		add_action( self::CRON_HOOK, array( $this, 'process_scheduled_job' ), 10, 1 );
	}

	/**
	 * Initialize default prompts for bulk generation
	 */
	private function init_default_prompts() {
		$this->default_prompts = $this->prompt_service->get_default_prompts();
	}

	/**
	 * Get default prompts
	 *
	 * @return array Default prompts.
	 */
	public function get_default_prompts() {
		return $this->default_prompts;
	}

	/**
	 * Parse topics from multiline text
	 *
	 * @param string $topics_text Topics text with each topic on a new line.
	 * @return array Array of topics.
	 */
	public function parse_topics( $topics_text ) {
		if ( empty( $topics_text ) ) {
			return array();
		}

		// Split by newlines and filter empty lines
		$lines = preg_split( '/[\r\n]+/', $topics_text );
		$topics = array();

		foreach ( $lines as $line ) {
			$line = trim( $line );
			if ( ! empty( $line ) ) {
				$topics[] = $line;
			}
		}

		return $topics;
	}

	/**
	 * Generate single post for bulk operation
	 *
	 * @param string $topic Topic for generation.
	 * @param array  $settings Settings array with prompts, language, context, etc.
	 * @return array Response with post data or error.
	 */
	public function generate_single_bulk_post( $topic, $settings = array() ) {
		return $this->legacy_pipeline->generate_item( $this, $topic, $settings );
	}

	/**
	 * Build context for one queue item, including row-specific metadata.
	 *
	 * @param string $topic Topic.
	 * @param string $context Shared context.
	 * @param array  $item Queue item.
	 * @param array  $settings Generation settings.
	 * @return string
	 */
	private function build_item_context( $topic, $context, $item, $settings ) {
		$parts = array();

		if ( ! empty( $context ) ) {
			$parts[] = $context;
		}

		$row_lines = array();
		foreach ( array(
			'focus_keyword'      => __( 'Focus keyword', 'smartypress-ai-engine' ),
			'category'           => __( 'Category', 'smartypress-ai-engine' ),
			'product_attributes' => __( 'Product attributes', 'smartypress-ai-engine' ),
			'notes'              => __( 'Row notes', 'smartypress-ai-engine' ),
		) as $key => $label ) {
			if ( ! empty( $item[ $key ] ) ) {
				$row_lines[] = $label . ': ' . sanitize_textarea_field( $item[ $key ] );
			}
		}

		if ( ! empty( $row_lines ) ) {
			$parts[] = __( 'Row-specific context:', 'smartypress-ai-engine' ) . "\n" . implode( "\n", $row_lines );
		}

		if ( 'product' === sanitize_key( $settings['post_type'] ?? '' ) ) {
			$parts[] = implode(
				"\n",
				array(
					__( 'WooCommerce product rules:', 'smartypress-ai-engine' ),
					__( '- Generate a real product name, not a marketing headline.', 'smartypress-ai-engine' ),
					__( '- Keep product identity in the title: brand, model, product type, and provided attributes.', 'smartypress-ai-engine' ),
					__( '- Put benefits, buyer intent, and SEO wording into description, excerpt, and SEO metadata.', 'smartypress-ai-engine' ),
					__( '- Do not invent product attributes that are not present in the topic or row context.', 'smartypress-ai-engine' ),
				)
			);
		}

		return trim( implode( "\n\n", array_filter( $parts ) ) );
	}

	/**
	 * Generate multiple posts from topics list
	 *
	 * @param array $topics Array of topics.
	 * @param array $settings Settings array.
	 * @param callable|null $progress_callback Optional callback for progress updates.
	 * @return array Results with created posts and errors.
	 */
	public function generate_bulk_posts( $topics, $settings = array(), $progress_callback = null ) {
		if ( empty( $topics ) || ! is_array( $topics ) ) {
			return array(
				'success' => false,
				'error'   => __( 'No topics provided', 'smartypress-ai-engine' ),
			);
		}

		$max_topics = apply_filters( 'smartypress_ai_bulk_max_topics', 50 );
		if ( count( $topics ) > $max_topics ) {
			return array(
				'success' => false,
				'error'   => sprintf(
					/* translators: %d: Maximum number of topics */
					__( 'Maximum %d topics allowed', 'smartypress-ai-engine' ),
					$max_topics
				),
			);
		}

		$results = array(
			'success'        => true,
			'total'          => count( $topics ),
			'created'        => 0,
			'failed'         => 0,
			'posts'          => array(),
			'errors'         => array(),
		);

		foreach ( $topics as $index => $topic ) {
			$topic = trim( $topic );
			if ( empty( $topic ) ) {
				continue;
			}

			// Call progress callback if provided
			if ( is_callable( $progress_callback ) ) {
				call_user_func( $progress_callback, array(
					'current_index' => $index,
					'current_topic' => $topic,
					'total'         => count( $topics ),
					'status'        => 'generating',
				) );
			}

			// Generate and create post
			$result = $this->generate_single_bulk_post( $topic, $settings );

			if ( $result['success'] ) {
				$results['created']++;
				$results['posts'][] = array(
					'topic'    => $topic,
					'post_id'  => $result['post_id'],
					'title'    => $result['title'],
					'edit_url' => $result['edit_url'],
					'view_url' => $result['view_url'],
					'status'   => 'completed',
				);
			} else {
				$results['failed']++;
				$results['errors'][] = array(
					'topic' => $topic,
					'error' => $result['error'] ?? __( 'Unknown error', 'smartypress-ai-engine' ),
				);
				$results['posts'][] = array(
					'topic'  => $topic,
					'status' => 'failed',
					'error'  => $result['error'] ?? __( 'Unknown error', 'smartypress-ai-engine' ),
				);
			}
		}

		return $results;
	}

	// ========================================
	// REST API Endpoints
	// ========================================

	/**
	 * REST API endpoint: Generate bulk posts
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function generate_bulk_posts_endpoint( $request ) {
		$topics_text = $request->get_param( 'topics' );
		$context     = sanitize_textarea_field( $request->get_param( 'context' ) );
		$language    = sanitize_text_field( $request->get_param( 'language' ) );
		$post_type   = sanitize_text_field( $request->get_param( 'post_type' ) ?: 'post' );
		$post_status = sanitize_text_field( $request->get_param( 'post_status' ) ?: 'draft' );
		$number_of_sections = sanitize_text_field( $request->get_param( 'number_of_sections' ) ?: '2' );
		$paragraphs_per_section = sanitize_text_field( $request->get_param( 'paragraphs_per_section' ) ?: '3' );
		$provider    = $this->validate_provider( $request->get_param( 'provider' ) );
		$model       = sanitize_text_field( $request->get_param( 'model' ) );
		$temperature = floatval( $request->get_param( 'temperature' ) ?: 0.8 );
		$max_tokens  = absint( $request->get_param( 'max_tokens' ) );
		$prompts     = $request->get_param( 'prompts' );

		// Parse topics
		$topics = $this->parse_topics( $topics_text );

		if ( empty( $topics ) ) {
			return new WP_REST_Response(
				array(
					'success' => false,
					'error'   => __( 'No valid topics provided', 'smartypress-ai-engine' ),
				),
				400
			);
		}

		$settings = array(
			'provider'              => $provider,
			'model'                 => $model,
			'language'              => $language,
			'context'               => $context,
			'post_type'             => $post_type,
			'post_status'           => $post_status,
			'number_of_sections'    => $number_of_sections,
			'paragraphs_per_section' => $paragraphs_per_section,
			'temperature'           => $temperature,
			'max_tokens'            => $max_tokens,
			'prompts'               => is_array( $prompts ) ? array_map( 'sanitize_textarea_field', $prompts ) : array(),
		);

		$result = $this->generate_bulk_posts( $topics, $settings );

		return new WP_REST_Response( $result, $result['success'] ? 200 : 500 );
	}

	/**
	 * REST API endpoint: Create a persistent bulk job.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function create_bulk_job_endpoint( $request ) {
		$items = $this->sanitize_job_items( $request->get_param( 'items' ), $request->get_param( 'topics' ) );

		if ( empty( $items ) ) {
			return new WP_REST_Response(
				array(
					'success' => false,
					'error'   => __( 'No valid topics provided', 'smartypress-ai-engine' ),
				),
				400
			);
		}

		$max_topics = apply_filters( 'smartypress_ai_bulk_max_topics', 50 );
		if ( count( $items ) > $max_topics ) {
			return new WP_REST_Response(
				array(
					'success' => false,
					'error'   => sprintf(
						/* translators: %d: Maximum number of topics */
						__( 'Maximum %d topics allowed', 'smartypress-ai-engine' ),
						$max_topics
					),
				),
				400
			);
		}

		$settings = $this->sanitize_job_settings( $request );
		$limits   = array(
			'max_errors' => max( 0, absint( $request->get_param( 'max_errors' ) ?: 0 ) ),
			'cost_limit' => max( 0, (float) ( $request->get_param( 'cost_limit' ) ?: 0 ) ),
		);
		$estimate = $this->estimate_bulk_cost( count( $items ), $settings['provider'], $settings['model'] );
		$job_id   = str_replace( '-', '', wp_generate_uuid4() );
		$now      = current_time( 'mysql' );

		$job = array(
			'id'          => $job_id,
			'status'      => 'queued',
			'created_at'  => $now,
			'updated_at'  => $now,
			'settings'    => $settings,
			'limits'      => $limits,
			'estimate'    => $estimate,
			'summary'     => array(
				'total'   => count( $items ),
				'created' => 0,
				'failed'  => 0,
				'skipped' => 0,
			),
			'items'       => array_map(
				function ( $item, $index ) {
					$item['id']         = (string) ( $index + 1 );
					$item['status']     = 'pending';
					$item['stage']      = 'queued';
					$item['title']      = '';
					$item['post_id']    = 0;
					$item['edit_url']   = '';
					$item['view_url']   = '';
					$item['error']      = '';
					$item['updated_at'] = current_time( 'mysql' );

					return $item;
				},
				$items,
				array_keys( $items )
			),
		);

		$this->save_job( $job );
		$this->schedule_job( $job_id );

		return new WP_REST_Response(
			array(
				'success' => true,
				'job'     => $job,
			),
			200
		);
	}

	/**
	 * REST API endpoint: Get a bulk job.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function get_bulk_job_endpoint( $request ) {
		$job = $this->get_job( $request->get_param( 'job_id' ) );

		if ( empty( $job ) ) {
			return new WP_REST_Response(
				array(
					'success' => false,
					'error'   => __( 'Bulk job not found', 'smartypress-ai-engine' ),
				),
				404
			);
		}

		return new WP_REST_Response(
			array(
				'success' => true,
				'job'     => $job,
			),
			200
		);
	}

	/**
	 * REST API endpoint: Process next item in a job.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function process_bulk_job_endpoint( $request ) {
		$result = $this->process_next_job_item( $request->get_param( 'job_id' ), true );

		return new WP_REST_Response( $result, ! empty( $result['success'] ) ? 200 : 400 );
	}

	/**
	 * REST API endpoint: Pause, resume, cancel, or retry a job.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function bulk_job_action_endpoint( $request ) {
		$job    = $this->get_job( $request->get_param( 'job_id' ) );
		$action = sanitize_key( $request->get_param( 'action' ) );
		$item_id = sanitize_text_field( $request->get_param( 'item_id' ) );

		if ( empty( $job ) ) {
			return new WP_REST_Response(
				array(
					'success' => false,
					'error'   => __( 'Bulk job not found', 'smartypress-ai-engine' ),
				),
				404
			);
		}

		switch ( $action ) {
			case 'pause':
				$job['status'] = 'paused';
				break;
			case 'resume':
				if ( 'cancelled' !== $job['status'] ) {
					$job['status'] = 'queued';
					$this->schedule_job( $job['id'] );
				}
				break;
			case 'cancel':
				$job['status'] = 'cancelled';
				foreach ( $job['items'] as &$item ) {
					if ( in_array( $item['status'], array( 'pending', 'generating', 'saving' ), true ) ) {
						$item['status'] = 'skipped';
						$item['stage']  = 'cancelled';
					}
				}
				unset( $item );
				break;
			case 'retry_failed':
				foreach ( $job['items'] as &$item ) {
					if ( 'failed' === $item['status'] ) {
						$item['status'] = 'pending';
						$item['stage']  = 'queued';
						$item['error']  = '';
					}
				}
				unset( $item );
				$job['summary'] = $this->build_summary( $job['items'] );
				$job['status']  = 'queued';
				$this->schedule_job( $job['id'] );
				break;
			case 'retry_item':
				$job = $this->update_job_item(
					$job,
					$item_id,
					function ( $item ) {
						$item['status'] = 'pending';
						$item['stage']  = 'queued';
						$item['error']  = '';

						return $item;
					}
				);
				$job['summary'] = $this->build_summary( $job['items'] );
				$job['status']  = 'queued';
				$this->schedule_job( $job['id'] );
				break;
			case 'skip_item':
				$job = $this->update_job_item(
					$job,
					$item_id,
					function ( $item ) {
						$item['status'] = 'skipped';
						$item['stage']  = 'skipped';
						$item['error']  = '';

						return $item;
					}
				);
				$job['summary'] = $this->build_summary( $job['items'] );
				break;
			case 'update_item':
				$patch = $request->get_param( 'item' );
				$patch = is_array( $patch ) ? $this->sanitize_job_items( array( $patch ) )[0] ?? array() : array();
				$job = $this->update_job_item(
					$job,
					$item_id,
					function ( $item ) use ( $patch ) {
						$item = array_merge( $item, $patch );
						$item['status'] = 'pending';
						$item['stage']  = 'queued';
						$item['error']  = '';

						return $item;
					}
				);
				$job['summary'] = $this->build_summary( $job['items'] );
				$job['status']  = 'queued';
				break;
			default:
				return new WP_REST_Response(
					array(
						'success' => false,
						'error'   => __( 'Unknown bulk job action', 'smartypress-ai-engine' ),
					),
					400
				);
		}

		$job['updated_at'] = current_time( 'mysql' );
		$this->save_job( $job );

		return new WP_REST_Response(
			array(
				'success' => true,
				'job'     => $job,
			),
			200
		);
	}

	/**
	 * Update a job item by ID.
	 *
	 * @param array    $job Job data.
	 * @param string   $item_id Item ID.
	 * @param callable $callback Update callback.
	 * @return array
	 */
	private function update_job_item( $job, $item_id, $callback ) {
		foreach ( $job['items'] as &$item ) {
			if ( (string) ( $item['id'] ?? '' ) === (string) $item_id ) {
				$item = call_user_func( $callback, $item );
				$item['updated_at'] = current_time( 'mysql' );
				break;
			}
		}
		unset( $item );

		return $job;
	}

	/**
	 * REST API endpoint: Estimate bulk generation cost.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function estimate_bulk_job_endpoint( $request ) {
		$items    = $this->sanitize_job_items( $request->get_param( 'items' ), $request->get_param( 'topics' ) );
		$settings = $this->sanitize_job_settings( $request );

		return new WP_REST_Response(
			array(
				'success'  => true,
				'estimate' => $this->estimate_bulk_cost( count( $items ), $settings['provider'], $settings['model'] ),
			),
			200
		);
	}

	/**
	 * Process scheduled job from WP-Cron.
	 *
	 * @param string $job_id Job ID.
	 * @return void
	 */
	public function process_scheduled_job( $job_id ) {
		$this->process_next_job_item( $job_id, false );
	}

	/**
	 * Process the next pending item in a job.
	 *
	 * @param string $job_id Job ID.
	 * @param bool   $manual Whether request came from REST.
	 * @return array
	 */
	private function process_next_job_item( $job_id, $manual = false ) {
		$job = $this->get_job( $job_id );

		if ( empty( $job ) ) {
			return array(
				'success' => false,
				'error'   => __( 'Bulk job not found', 'smartypress-ai-engine' ),
			);
		}

		if ( in_array( $job['status'], array( 'paused', 'cancelled', 'completed' ), true ) ) {
			return array(
				'success' => true,
				'job'     => $job,
			);
		}

		if ( $this->has_reached_limits( $job ) ) {
			$job['status']     = 'paused';
			$job['updated_at'] = current_time( 'mysql' );
			$this->save_job( $job );

			return array(
				'success' => true,
				'job'     => $job,
			);
		}

		$lock_time = absint( $job['processing_lock'] ?? 0 );
		if ( $lock_time && ( time() - $lock_time ) < 600 ) {
			return array(
				'success' => true,
				'job'     => $job,
			);
		}

		$job['processing_lock'] = time();
		$this->save_job( $job );

		$index = $this->find_next_pending_item_index( $job['items'] );
		if ( null === $index ) {
			$job['status']     = 'completed';
			$job['summary']    = $this->build_summary( $job['items'] );
			$job['updated_at'] = current_time( 'mysql' );
			unset( $job['processing_lock'] );
			$this->save_job( $job );

			return array(
				'success' => true,
				'job'     => $job,
			);
		}

		$job['status']                       = 'running';
		$job['items'][ $index ]['status']    = 'generating';
		$job['items'][ $index ]['stage']     = 'generating';
		$job['items'][ $index ]['provider']  = $job['settings']['provider'] ?? '';
		$job['items'][ $index ]['model']     = $job['settings']['model'] ?? '';
		$job['items'][ $index ]['updated_at'] = current_time( 'mysql' );
		$this->save_job( $job );

		$item     = $job['items'][ $index ];
		$settings = $job['settings'];
		$settings['item']         = $item;
		$settings['product_meta'] = $this->build_product_meta_from_item( $item );
		$settings['stage_callback'] = function ( $stage ) use ( $job_id, $index ) {
			$current_job = $this->get_job( $job_id );
			if ( empty( $current_job['items'][ $index ] ) ) {
				return;
			}

			$current_job['items'][ $index ]['stage']      = sanitize_key( $stage );
			$current_job['items'][ $index ]['updated_at'] = current_time( 'mysql' );
			$current_job['updated_at']                    = current_time( 'mysql' );
			$this->save_job( $current_job );
		};

		$result = $this->generate_single_bulk_post( $item['topic'], $settings );
		$job    = $this->get_job( $job_id );

		if ( empty( $job ) ) {
			return array(
				'success' => false,
				'error'   => __( 'Bulk job not found', 'smartypress-ai-engine' ),
			);
		}

		if ( ! empty( $result['success'] ) ) {
			$job['items'][ $index ]['status']   = 'created';
			$job['items'][ $index ]['stage']    = 'created';
			$job['items'][ $index ]['title']    = sanitize_text_field( $result['title'] ?? '' );
			$job['items'][ $index ]['post_id']  = absint( $result['post_id'] ?? 0 );
			$job['items'][ $index ]['edit_url'] = esc_url_raw( $result['edit_url'] ?? '' );
			$job['items'][ $index ]['view_url'] = esc_url_raw( $result['view_url'] ?? '' );
			$job['items'][ $index ]['seo_keywords'] = array_map( 'sanitize_text_field', (array) ( $result['seo_keywords'] ?? array() ) );
			$job['items'][ $index ]['seo_source']   = sanitize_key( $result['seo_source'] ?? '' );
		} else {
			$job['items'][ $index ]['status'] = 'failed';
			$job['items'][ $index ]['stage']  = 'failed';
			$job['items'][ $index ]['error']  = wp_kses_post( $result['error'] ?? __( 'Generation failed', 'smartypress-ai-engine' ) );
		}

		$job['items'][ $index ]['updated_at'] = current_time( 'mysql' );
		$job['summary']                       = $this->build_summary( $job['items'] );
		$job['updated_at']                    = current_time( 'mysql' );
		unset( $job['processing_lock'] );

		if ( null === $this->find_next_pending_item_index( $job['items'] ) ) {
			$job['status'] = 'completed';
		} elseif ( ! $this->has_reached_limits( $job ) ) {
			$job['status'] = 'running';
			if ( ! $manual ) {
				$this->schedule_job( $job_id );
			}
		} else {
			$job['status'] = 'paused';
		}

		$this->save_job( $job );

		return array(
			'success' => true,
			'job'     => $job,
		);
	}

	/**
	 * Sanitize job settings from REST request.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return array
	 */
	private function sanitize_job_settings( $request ) {
		$prompts = $request->get_param( 'prompts' );

		return array(
			'provider'               => $this->validate_provider( $request->get_param( 'provider' ) ),
			'model'                  => sanitize_text_field( $request->get_param( 'model' ) ),
			'language'               => sanitize_text_field( $request->get_param( 'language' ) ),
			'context'                => sanitize_textarea_field( $request->get_param( 'context' ) ),
			'post_type'              => sanitize_key( $request->get_param( 'post_type' ) ?: 'post' ),
			'post_status'            => sanitize_key( $request->get_param( 'post_status' ) ?: 'draft' ),
			'number_of_sections'     => sanitize_text_field( $request->get_param( 'number_of_sections' ) ?: '2' ),
			'paragraphs_per_section' => sanitize_text_field( $request->get_param( 'paragraphs_per_section' ) ?: '3' ),
			'temperature'            => (float) ( $request->get_param( 'temperature' ) ?: 0.8 ),
			'max_tokens'             => absint( $request->get_param( 'max_tokens' ) ),
			'prompts'                => is_array( $prompts ) ? array_map( 'sanitize_textarea_field', $prompts ) : array(),
		);
	}

	/**
	 * Sanitize job items.
	 *
	 * @param mixed  $items Raw items.
	 * @param string $topics_text Fallback topics text.
	 * @return array
	 */
	private function sanitize_job_items( $items, $topics_text = '' ) {
		if ( empty( $items ) || ! is_array( $items ) ) {
			$items = array_map(
				function ( $topic ) {
					return array( 'topic' => $topic );
				},
				$this->parse_topics( $topics_text )
			);
		}

		$clean = array();
		foreach ( $items as $item ) {
			$item  = is_array( $item ) ? $item : array( 'topic' => $item );
			$topic = sanitize_text_field( $item['topic'] ?? '' );

			if ( '' === $topic ) {
				continue;
			}

			$clean[] = array(
				'topic'              => $topic,
				'focus_keyword'      => sanitize_text_field( $item['focus_keyword'] ?? '' ),
				'category'           => sanitize_text_field( $item['category'] ?? '' ),
				'product_attributes' => sanitize_textarea_field( $item['product_attributes'] ?? '' ),
				'notes'              => sanitize_textarea_field( $item['notes'] ?? '' ),
				'context'            => sanitize_textarea_field( $item['context'] ?? '' ),
				'sku'                => sanitize_text_field( $item['sku'] ?? '' ),
			);
		}

		return $clean;
	}

	/**
	 * Build product metadata from a queue item.
	 *
	 * @param array $item Queue item.
	 * @return array
	 */
	private function build_product_meta_from_item( $item ) {
		return array(
			'sku'        => sanitize_text_field( $item['sku'] ?? '' ),
			'attributes' => sanitize_textarea_field( $item['product_attributes'] ?? '' ),
		);
	}

	/**
	 * Get job option name.
	 *
	 * @param string $job_id Job ID.
	 * @return string
	 */
	private function get_job_option_name( $job_id ) {
		return $this->job_repository->get_option_name( $job_id );
	}

	/**
	 * Get job.
	 *
	 * @param string $job_id Job ID.
	 * @return array|null
	 */
	private function get_job( $job_id ) {
		return $this->job_repository->get( $job_id );
	}

	/**
	 * Save job.
	 *
	 * @param array $job Job data.
	 * @return void
	 */
	private function save_job( $job ) {
		$this->job_repository->save( $job );
	}

	/**
	 * Schedule a job if it is not already scheduled.
	 *
	 * @param string $job_id Job ID.
	 * @return void
	 */
	private function schedule_job( $job_id ) {
		$this->job_repository->schedule( $job_id );
	}

	/**
	 * Find next pending item.
	 *
	 * @param array $items Job items.
	 * @return int|null
	 */
	private function find_next_pending_item_index( $items ) {
		foreach ( $items as $index => $item ) {
			if ( 'pending' === ( $item['status'] ?? '' ) ) {
				return $index;
			}
		}

		return null;
	}

	/**
	 * Build job summary.
	 *
	 * @param array $items Job items.
	 * @return array
	 */
	private function build_summary( $items ) {
		$summary = array(
			'total'   => count( $items ),
			'created' => 0,
			'failed'  => 0,
			'skipped' => 0,
		);

		foreach ( $items as $item ) {
			if ( 'created' === ( $item['status'] ?? '' ) ) {
				$summary['created']++;
			} elseif ( 'failed' === ( $item['status'] ?? '' ) ) {
				$summary['failed']++;
			} elseif ( 'skipped' === ( $item['status'] ?? '' ) ) {
				$summary['skipped']++;
			}
		}

		return $summary;
	}

	/**
	 * Check job limits.
	 *
	 * @param array $job Job data.
	 * @return bool
	 */
	private function has_reached_limits( $job ) {
		$limits  = $job['limits'] ?? array();
		$summary = $job['summary'] ?? array();

		if ( ! empty( $limits['max_errors'] ) && (int) ( $summary['failed'] ?? 0 ) >= (int) $limits['max_errors'] ) {
			return true;
		}

		if ( ! empty( $limits['cost_limit'] ) && ! empty( $job['estimate']['per_item_cost'] ) ) {
			$processed      = (int) ( $summary['created'] ?? 0 ) + (int) ( $summary['failed'] ?? 0 );
			$estimated_cost = $processed * (float) $job['estimate']['per_item_cost'];

			return $estimated_cost >= (float) $limits['cost_limit'];
		}

		return false;
	}

	/**
	 * Estimate cost.
	 *
	 * @param int    $total Total items.
	 * @param string $provider Provider.
	 * @param string $model Model.
	 * @return array
	 */
	private function estimate_bulk_cost( $total, $provider, $model ) {
		return $this->cost_estimator->estimate_legacy_bulk_cost( $total, $provider, $model );
	}

	/**
	 * REST API endpoint: Generate single post for bulk (streaming progress)
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function generate_single_post_endpoint( $request ) {
		$topic       = sanitize_text_field( $request->get_param( 'topic' ) );
		$context     = sanitize_textarea_field( $request->get_param( 'context' ) );
		$language    = sanitize_text_field( $request->get_param( 'language' ) );
		$post_type   = sanitize_text_field( $request->get_param( 'post_type' ) ?: 'post' );
		$post_status = sanitize_text_field( $request->get_param( 'post_status' ) ?: 'draft' );
		$number_of_sections = sanitize_text_field( $request->get_param( 'number_of_sections' ) ?: '2' );
		$paragraphs_per_section = sanitize_text_field( $request->get_param( 'paragraphs_per_section' ) ?: '3' );
		$provider    = $this->validate_provider( $request->get_param( 'provider' ) );
		$model       = sanitize_text_field( $request->get_param( 'model' ) );
		$temperature = floatval( $request->get_param( 'temperature' ) ?: 0.8 );
		$max_tokens  = absint( $request->get_param( 'max_tokens' ) );
		$prompts     = $request->get_param( 'prompts' );

		if ( empty( $topic ) ) {
			return new WP_REST_Response(
				array(
					'success' => false,
					'error'   => __( 'Topic is required', 'smartypress-ai-engine' ),
				),
				400
			);
		}

		$settings = array(
			'provider'              => $provider,
			'model'                 => $model,
			'language'              => $language,
			'context'               => $context,
			'post_type'             => $post_type,
			'post_status'           => $post_status,
			'number_of_sections'    => $number_of_sections,
			'paragraphs_per_section' => $paragraphs_per_section,
			'temperature'           => $temperature,
			'max_tokens'            => $max_tokens,
			'prompts'               => is_array( $prompts ) ? array_map( 'sanitize_textarea_field', $prompts ) : array(),
		);

		$result = $this->generate_single_bulk_post( $topic, $settings );

		if ( $result['success'] ) {
			return new WP_REST_Response(
				array(
					'success'  => true,
					'topic'    => $topic,
					'post_id'  => $result['post_id'],
					'title'    => $result['title'],
					'edit_url' => $result['edit_url'],
					'view_url' => $result['view_url'],
				),
				200
			);
		}

		return new WP_REST_Response(
			array(
				'success' => false,
				'topic'   => $topic,
				'error'   => $result['error'] ?? __( 'Generation failed', 'smartypress-ai-engine' ),
			),
			500
		);
	}

	/**
	 * REST API endpoint: Get default prompts for bulk generation
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function get_prompts_endpoint( $request ) {
		return new WP_REST_Response(
			array(
				'success' => true,
				'prompts' => $this->default_prompts,
			),
			200
		);
	}
}

