<?php
/**
 * Abstract Post Generator Class
 *
 * Base class for post generation with common methods
 *
 * @package SmartyPress_AI_Engine
 * @since 1.0.0
 */

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

/**
 * Abstract Post Generator class
 */
abstract class SmartyPress_AI_Abstract_Post_Generator {

	/**
	 * AI Providers
	 *
	 * @var array
	 */
	protected $providers;

	/**
	 * Plugin settings
	 *
	 * @var array
	 */
	protected $settings;

	/**
	 * Content plan parser.
	 *
	 * @var SmartyPress_AI_Content_Generator_Plan_Parser
	 */
	protected $plan_parser;

	/**
	 * Content plan normalizer.
	 *
	 * @var SmartyPress_AI_Content_Generator_Plan_Normalizer
	 */
	protected $plan_normalizer;

	/**
	 * Dynamic plan summary builder.
	 *
	 * @var SmartyPress_AI_Content_Generator_Plan_Summary_Builder
	 */
	protected $plan_summary_builder;

	/**
	 * Generated draft parser.
	 *
	 * @var SmartyPress_AI_Content_Generator_Draft_Parser
	 */
	protected $draft_parser;

	/**
	 * Markdown to Gutenberg converter.
	 *
	 * @var SmartyPress_AI_Content_Generator_Markdown_Block_Converter
	 */
	protected $markdown_converter;

	/**
	 * Structured article block renderer.
	 *
	 * @var SmartyPress_AI_Content_Generator_Article_Block_Renderer
	 */
	protected $article_block_renderer;

	/**
	 * SEO metadata preparation service.
	 *
	 * @var SmartyPress_AI_Content_Generator_SEO_Generation_Metadata_Service
	 */
	protected $seo_metadata_service;

	/**
	 * SEO plugin metadata writer.
	 *
	 * @var SmartyPress_AI_Content_Generator_SEO_Plugin_Metadata_Writer
	 */
	protected $seo_metadata_writer;

	/**
	 * SEO optimization service.
	 *
	 * @var SmartyPress_AI_Content_Generator_SEO_Optimization_Service
	 */
	protected $seo_optimization_service;

	/**
	 * Post persistence service.
	 *
	 * @var SmartyPress_AI_Content_Generator_Post_Persistence_Service
	 */
	protected $post_persistence;

	/**
	 * Legacy post generation service.
	 *
	 * @var SmartyPress_AI_Content_Generator_Legacy_Post_Generation_Service
	 */
	protected $legacy_post_generation;

	/**
	 * Plan draft generation service.
	 *
	 * @var SmartyPress_AI_Content_Generator_Plan_Draft_Generation_Service
	 */
	protected $plan_draft_generation;

	/**
	 * Dynamic plan generation service.
	 *
	 * @var SmartyPress_AI_Content_Generator_Plan_Generation_Service
	 */
	protected $plan_generation;

	/**
	 * Language instruction service.
	 *
	 * @var SmartyPress_AI_Content_Generator_Generation_Language_Service
	 */
	protected $language_service;

	/**
	 * Narrow legacy step service.
	 *
	 * @var SmartyPress_AI_Content_Generator_Generation_Step_Service
	 */
	protected $generation_step_service;

	/**
	 * Post type extension registry.
	 *
	 * @var SmartyPress_AI_Post_Type_Extension_Registry
	 */
	protected $post_type_extension_registry;

	/**
	 * Constructor
	 *
	 * @param array $providers AI providers instances.
	 * @param array $settings Plugin settings.
	 */
	public function __construct( $providers, $settings ) {
		$this->providers             = $providers;
		$this->settings              = $settings;
		$this->plan_parser           = new SmartyPress_AI_Content_Generator_Plan_Parser();
		$this->plan_normalizer       = new SmartyPress_AI_Content_Generator_Plan_Normalizer();
		$this->plan_summary_builder  = new SmartyPress_AI_Content_Generator_Plan_Summary_Builder();
		$this->draft_parser          = new SmartyPress_AI_Content_Generator_Draft_Parser();
		$this->markdown_converter    = new SmartyPress_AI_Content_Generator_Markdown_Block_Converter();
		$this->article_block_renderer = new SmartyPress_AI_Content_Generator_Article_Block_Renderer();
		$this->seo_metadata_service  = new SmartyPress_AI_Content_Generator_SEO_Generation_Metadata_Service();
		$this->seo_metadata_writer   = new SmartyPress_AI_Content_Generator_SEO_Plugin_Metadata_Writer();
		$this->language_service      = new SmartyPress_AI_Content_Generator_Generation_Language_Service( $this->get_site_language() );
		$this->seo_optimization_service = new SmartyPress_AI_Content_Generator_SEO_Optimization_Service( $this->seo_metadata_service, $this->language_service );
		$this->generation_step_service = new SmartyPress_AI_Content_Generator_Generation_Step_Service( $this );
		$this->post_type_extension_registry = new SmartyPress_AI_Post_Type_Extension_Registry(
			array(
				new SmartyPress_AI_WooCommerce_Product_Extension(),
			)
		);
		$this->post_persistence      = new SmartyPress_AI_Content_Generator_Post_Persistence_Service(
			$this->markdown_converter,
			$this->article_block_renderer,
			$this->seo_metadata_service,
			$this->seo_metadata_writer
		);
		$this->legacy_post_generation = new SmartyPress_AI_Content_Generator_Legacy_Post_Generation_Service(
			new SmartyPress_AI_Content_Generator_Generated_Title_Validator(),
			new SmartyPress_AI_Content_Generator_SEO_Content_Optimization_Service()
		);
		$this->plan_generation = new SmartyPress_AI_Content_Generator_Plan_Generation_Service(
			$this->plan_parser,
			$this->plan_normalizer
		);
		$this->plan_draft_generation = new SmartyPress_AI_Content_Generator_Plan_Draft_Generation_Service(
			$this->plan_normalizer,
			$this->plan_summary_builder,
			$this->draft_parser
		);
	}

	/**
	 * Get site language
	 *
	 * @return string Language name.
	 */
	protected function get_site_language() {
		return smartypress_ai_get_language_by_locale();
	}

	/**
	 * Add unique identifier to system prompt to prevent AI provider prompt caching
	 *
	 * @param string $system_prompt System prompt.
	 * @return string System prompt with cache buster.
	 */
	protected function add_cache_buster( $system_prompt ) {
		return $system_prompt . ' [rid:' . wp_generate_uuid4() . ']';
	}

	/**
	 * Build a strict target-language instruction for legacy generation prompts.
	 *
	 * @param string $language Target output language.
	 * @param string $artifact Reader-facing artifact type.
	 * @return string
	 */
	protected function get_output_language_instruction( $language, $artifact = 'content' ) {
		return $this->language_service->get_output_language_instruction( $language, $artifact );
	}

	/**
	 * Get provider instance
	 *
	 * @param string $provider_name Provider name.
	 * @return SmartyPress_AI_AI_Provider|null
	 */
	protected function get_provider( $provider_name = '' ) {
		if ( empty( $provider_name ) ) {
			$provider_name = $this->settings['default_provider'] ?? 'openai';
		}

		if ( class_exists( 'SmartyPress_AI_Token_Tracker' ) ) {
			SmartyPress_AI_Token_Tracker::set_module( 'content-generation' );
		}

		$provider = $this->providers[ $provider_name ] ?? null;

		return $provider;
	}

	/**
	 * Optimize generated post fields for SEO.
	 *
	 * @param array $args Optimization arguments.
	 * @return array
	 */
	public function optimize_generated_post_for_seo( array $args ) {
		$provider = $this->get_provider( $args['provider'] ?? '' );

		return $this->seo_optimization_service->optimize( $provider, $args );
	}

	/**
	 * Get SEO rules from post type extensions.
	 *
	 * @param string $post_type Post type.
	 * @param array  $settings Generation settings.
	 * @return array
	 */
	public function get_post_type_seo_rules( $post_type, array $settings ) {
		return $this->post_type_extension_registry->get_seo_rules( $post_type, $settings );
	}

	/**
	 * Validate and sanitize provider name
	 *
	 * @param string $provider Provider name from request.
	 * @return string Validated provider name or empty string.
	 */
	protected function validate_provider( $provider ) {
		$allowed_providers = array( 'openai', 'deepseek', 'anthropic', 'openrouter', 'yandex', 'gigachat' );
		$provider_input    = sanitize_text_field( $provider );
		
		if ( in_array( $provider_input, $allowed_providers, true ) ) {
			return $provider_input;
		}
		
		return '';
	}

	/**
	 * Generate title
	 *
	 * @param string $context Context for title generation.
	 * @param string $provider_name Provider name.
	 * @param array  $options Additional options (prompt_template, language, model, temperature).
	 * @return array Response with title or error.
	 */
	public function generate_title( $context, $provider_name = '', $options = array() ) {
		$provider = $this->get_provider( $provider_name );

		if ( ! $provider ) {
			$error_response = array(
				'success'    => false,
				'error'      => __( 'API key not configured. Go to SmartyPress AI > Settings and add your OpenAI or Deepseek API key.', 'smartypress-ai-engine' ),
				'error_code' => 'api_key_missing',
			);
			
			// Add debug info if WP_DEBUG is enabled
			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				$error_response['debug_info'] = array(
					'requested_provider'  => $provider_name ?: ( $this->settings['default_provider'] ?? 'openai' ),
					'available_providers' => ! empty( $this->providers ) ? implode( ', ', array_keys( $this->providers ) ) : 'none',
					'has_settings'        => ! empty( $this->settings ),
					'settings_keys'       => ! empty( $this->settings ) ? array_keys( $this->settings ) : array(),
				);
			}
			
			return $error_response;
		}

		$language = $options['language'] ?? $this->get_site_language();
		$temperature = $options['temperature'] ?? $this->settings['temperature'] ?? 0.7;
		$prompt_template = $options['prompt_template'] ?? '';
		$model = sanitize_text_field( $options['model'] ?? '' );
		$language_instruction = $this->get_output_language_instruction( $language, 'title' );
		
		// Use custom prompt if provided
		if ( ! empty( $prompt_template ) ) {
			$prompt_context = $options['context'] ?? $context;
			$variables = array(
				'LANGUAGE' => $language,
				'TOPIC'    => $context,
				'CONTEXT'  => $prompt_context,
			);
			$user_prompt = SmartyPress_AI_Prompt_Processor::process_prompt( $prompt_template, $variables );
			$system_prompt = sprintf(
				/* translators: %s: Language name */
				__( 'You are a professional content writer. Return ONLY plain text, no HTML tags, no Markdown formatting. Always wrap your response with ===CONTENT_START=== and ===CONTENT_END=== markers. Language: %s', 'smartypress-ai-engine' ),
				$language
			);
		} else {
			// Default prompt
			$system_prompt = sprintf(
				/* translators: %s: Language name */
				__( 'You are a professional content writer. Generate engaging and SEO-optimized titles. Return ONLY plain text, no HTML tags, no Markdown formatting. Always wrap your response with ===CONTENT_START=== and ===CONTENT_END=== markers. Language: %s', 'smartypress-ai-engine' ),
				$language
			);

			$user_prompt = sprintf(
				/* translators: %s: Context */
				__( 'Generate a compelling title for a blog post about: %s. Wrap your response with ===CONTENT_START=== and ===CONTENT_END=== markers.', 'smartypress-ai-engine' ),
				$context
			);
		}

		$system_prompt .= "\n\n" . $language_instruction;
		$user_prompt   .= "\n\nIMPORTANT LANGUAGE RULE: " . $language_instruction;

		// Add "generate different" instruction if current title exists
		$current_title = $options['current_title'] ?? '';
		if ( ! empty( $current_title ) ) {
			$user_prompt .= sprintf(
				"\n\nIMPORTANT: Do NOT reuse or repeat this existing title: \"%s\". Generate a completely different, unique title.",
				$current_title
			);
		}

		$params = array(
			'system_prompt' => $this->add_cache_buster( $system_prompt ),
			'temperature'   => $temperature,
			'max_tokens'    => 100,
		);

		if ( ! empty( $model ) ) {
			$params['model'] = $model;
		}

		$result = $provider->generate_content( $user_prompt, $params );

		if ( $result['success'] ) {
			$result['title'] = trim( $result['content'] );
			// Clean AI markers first
			$result['title'] = SmartyPress_AI_Prompt_Processor::clean_generated_content( $result['title'] );
			// Remove quotes if present (AI often wraps titles in quotes)
			$result['title'] = preg_replace( '/^["\'"]+|["\'"]+$/u', '', $result['title'] );
			$result['title'] = trim( $result['title'] );
		}

		return $result;
	}

	/**
	 * Generate sections
	 *
	 * @param string $topic Topic/context for sections generation.
	 * @param string $title Post title (optional).
	 * @param string $provider_name Provider name.
	 * @param array  $options Additional options (prompt_template, language, number_of_sections, model, temperature).
	 * @return array Response with sections or error.
	 */
	public function generate_sections( $topic, $title = '', $provider_name = '', $options = array() ) {
		$provider = $this->get_provider( $provider_name );

		if ( ! $provider ) {
			$error_response = array(
				'success'    => false,
				'error'      => __( 'API key not configured. Go to SmartyPress AI > Settings and add your OpenAI or Deepseek API key.', 'smartypress-ai-engine' ),
				'error_code' => 'api_key_missing',
			);
			
			// Add debug info if WP_DEBUG is enabled
			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				$error_response['debug_info'] = array(
					'requested_provider'  => $provider_name ?: ( $this->settings['default_provider'] ?? 'openai' ),
					'available_providers' => ! empty( $this->providers ) ? implode( ', ', array_keys( $this->providers ) ) : 'none',
					'has_settings'        => ! empty( $this->settings ),
					'settings_keys'       => ! empty( $this->settings ) ? array_keys( $this->settings ) : array(),
				);
			}
			
			return $error_response;
		}

		$language = $options['language'] ?? $this->get_site_language();
		$temperature = $options['temperature'] ?? $this->settings['temperature'] ?? 0.7;
		$number_of_sections = $options['number_of_sections'] ?? '2';
		$prompt_template = $options['prompt_template'] ?? '';
		$model = sanitize_text_field( $options['model'] ?? '' );
		$language_instruction = $this->get_output_language_instruction( $language, 'sections' );
		
		// Use custom prompt if provided
		if ( ! empty( $prompt_template ) ) {
			$variables = array(
				'LANGUAGE'           => $language,
				'TOPIC'              => $topic,
				'TITLE'              => $title,
				'NUMBER_OF_SECTIONS' => $number_of_sections,
				'SECTIONS_COUNT'     => $number_of_sections,
				'CONTEXT'            => $options['context'] ?? '',
			);
			$user_prompt = SmartyPress_AI_Prompt_Processor::process_prompt( $prompt_template, $variables );
			$system_prompt = sprintf(
				/* translators: %s: Language name */
				__( 'You are a professional content writer. Create a structured outline with sections. Use Markdown format: ## for each section heading. NEVER use HTML tags. Output only headings, nothing else. Always wrap your response with ===CONTENT_START=== and ===CONTENT_END=== markers. Language: %s', 'smartypress-ai-engine' ),
				$language
			);
		} else {
			// Default prompt
			$system_prompt = sprintf(
				/* translators: 1: Number of sections, 2: Language name */
				__( 'You are a professional content writer. Create a structured outline with %1$s sections. Use Markdown format: ## for each section heading. NEVER use HTML tags. Output only headings, nothing else. Always wrap your response with ===CONTENT_START=== and ===CONTENT_END=== markers. Language: %2$s', 'smartypress-ai-engine' ),
				$number_of_sections,
				$language
			);

			$user_prompt = sprintf(
				/* translators: 1: Number of sections, 2: Topic, 3: Optional title */
				__( 'Create %1$s sections for a blog post about: %2$s%3$s Wrap your response with ===CONTENT_START=== and ===CONTENT_END=== markers.', 'smartypress-ai-engine' ),
				$number_of_sections,
				$topic,
				! empty( $title ) ? sprintf(
					/* translators: %s: Post title */
					__( ' (Title: %s)', 'smartypress-ai-engine' ),
					$title
				) : ''
			);
		}

		$system_prompt .= "\n\n" . $language_instruction;
		$user_prompt   .= "\n\nIMPORTANT LANGUAGE RULE: " . $language_instruction;

		$params = array(
			'system_prompt' => $this->add_cache_buster( $system_prompt ),
			'temperature'   => $temperature,
			'max_tokens'    => 500,
		);

		if ( ! empty( $model ) ) {
			$params['model'] = $model;
		}

		$result = $provider->generate_content( $user_prompt, $params );

		if ( $result['success'] ) {
			$result['sections'] = trim( $result['content'] );
			// Clean AI comments
			$result['sections'] = SmartyPress_AI_Prompt_Processor::clean_generated_content( $result['sections'] );
		}

		return $result;
	}

	/**
	 * Generate content
	 *
	 * @param string $title Post title.
	 * @param string $context Additional context.
	 * @param string $provider_name Provider name.
	 * @param array  $options Additional options (prompt_template, language, sections, paragraphs_per_section, model, temperature).
	 * @return array Response with content or error.
	 */
	public function generate_content( $title, $context = '', $provider_name = '', $options = array() ) {
		$provider = $this->get_provider( $provider_name );

		if ( ! $provider ) {
			$error_response = array(
				'success'    => false,
				'error'      => __( 'API key not configured. Go to SmartyPress AI > Settings and add your OpenAI or Deepseek API key.', 'smartypress-ai-engine' ),
				'error_code' => 'api_key_missing',
			);
			
			// Add debug info if WP_DEBUG is enabled
			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				$error_response['debug_info'] = array(
					'requested_provider'  => $provider_name ?: ( $this->settings['default_provider'] ?? 'openai' ),
					'available_providers' => ! empty( $this->providers ) ? implode( ', ', array_keys( $this->providers ) ) : 'none',
					'has_settings'        => ! empty( $this->settings ),
					'settings_keys'       => ! empty( $this->settings ) ? array_keys( $this->settings ) : array(),
				);
			}
			
			return $error_response;
		}

		$language = $options['language'] ?? $this->get_site_language();
		$temperature = $options['temperature'] ?? $this->settings['temperature'] ?? 0.7;
		$max_tokens = $options['max_tokens'] ?? $this->settings['max_tokens'] ?? 2000;
		$sections = $options['sections'] ?? '';
		$paragraphs_per_section = $options['paragraphs_per_section'] ?? '3';
		$prompt_template = $options['prompt_template'] ?? '';
		$model = sanitize_text_field( $options['model'] ?? '' );
		$language_instruction = $this->get_output_language_instruction( $language, 'content' );
		
		// Use custom prompt if provided
		if ( ! empty( $prompt_template ) ) {
			$variables = array(
				'LANGUAGE'              => $language,
				'TITLE'                 => $title,
				'TOPIC'                 => $context,
				'SECTIONS'              => $sections,
				'PARAGRAPHS_PER_SECTION' => $paragraphs_per_section,
				'CONTEXT'               => $options['context'] ?? '',
			);
			$user_prompt = SmartyPress_AI_Prompt_Processor::process_prompt( $prompt_template, $variables );
			$system_prompt = sprintf(
				/* translators: %s: Language name */
				__( 'You are a professional content writer. Create well-structured, informative, and engaging blog posts. FORMAT RULES (STRICT): Always use Markdown for formatting (## for headings, **bold**, *italic*, lists, etc.). NEVER use HTML tags in your response (no <h2>, <p>, <strong>, <div>, etc.). If the user prompt contains HTML, ignore the HTML formatting and return pure Markdown instead. Use ## for section headings (not **bold text**). Always wrap your response with ===CONTENT_START=== and ===CONTENT_END=== markers. Do not include the title in the content. Language: %s', 'smartypress-ai-engine' ),
				$language
			);
		} else {
			// Default prompt
			$system_prompt = sprintf(
				/* translators: %s: Language name */
				__( 'You are a professional content writer. Create well-structured, informative, and engaging blog posts. FORMAT RULES (STRICT): Always use Markdown for formatting (## for headings, **bold**, *italic*, lists, etc.). NEVER use HTML tags in your response (no <h2>, <p>, <strong>, <div>, etc.). If the user prompt contains HTML, ignore the HTML formatting and return pure Markdown instead. Use ## for section headings (not **bold text**). Always wrap your response with ===CONTENT_START=== and ===CONTENT_END=== markers. Do not include the title in the content. Language: %s', 'smartypress-ai-engine' ),
				$language
			);

			$user_prompt = sprintf(
				/* translators: 1: Title, 2: Context */
				__( 'Write a comprehensive blog post with the title: "%1$s". %2$s Wrap your response with ===CONTENT_START=== and ===CONTENT_END=== markers.', 'smartypress-ai-engine' ),
				$title,
				! empty( $context ) ? sprintf(
					/* translators: %s: Context information */
					__( 'Additional context: %s', 'smartypress-ai-engine' ),
					$context
				) : ''
			);
		}

		// Add instruction to generate new content and enforce Markdown format
		$user_prompt .= "\n\nIMPORTANT: Generate completely NEW and ORIGINAL content. Do not repeat or paraphrase any existing content from the post.";
		$user_prompt .= "\n\nFORMAT: Return content in Markdown only. Use ## for section headings. Do NOT use HTML tags.";
		$user_prompt .= "\n\nSEO QUALITY RULES: write at least 600 words unless the user explicitly requested a shorter text; use the main topic phrase naturally in the introduction, one ##/### heading, and several body paragraphs without keyword stuffing; avoid long URLs or CTA blocks; do not add a sales call-to-action at the end.";
		$system_prompt .= "\n\n" . $language_instruction;
		$user_prompt   .= "\n\nIMPORTANT LANGUAGE RULE: " . $language_instruction;

		$params = array(
			'system_prompt'    => $this->add_cache_buster( $system_prompt ),
			'temperature'      => $temperature,
			'max_tokens'       => $max_tokens,
			'request_timeout'  => 180,
		);

		if ( ! empty( $model ) ) {
			$params['model'] = $model;
		}

		$result = $provider->generate_content( $user_prompt, $params );

		// Clean content from AI comments and title headers; keep as Markdown for preview
		if ( $result['success'] && ! empty( $result['content'] ) ) {
			$raw_content                       = (string) $result['content'];
			$result['raw_content']            = $raw_content;
			$result['expects_content_marker'] = true;
			$result['has_content_end_marker'] = false !== strpos( $raw_content, '===CONTENT_END===' );
			$result['content'] = SmartyPress_AI_Prompt_Processor::clean_generated_content( $result['content'], $title );
		}

		return $result;
	}

	/**
	 * Generate excerpt
	 *
	 * @param string $content Post content.
	 * @param string $provider_name Provider name.
	 * @param array  $options Additional options (prompt_template, title, language, model, temperature).
	 * @return array Response with excerpt or error.
	 */
	public function generate_excerpt( $content, $provider_name = '', $options = array() ) {
		$provider = $this->get_provider( $provider_name );

		if ( ! $provider ) {
			$error_response = array(
				'success'    => false,
				'error'      => __( 'API key not configured. Go to SmartyPress AI > Settings and add your OpenAI or Deepseek API key.', 'smartypress-ai-engine' ),
				'error_code' => 'api_key_missing',
			);
			
			// Add debug info if WP_DEBUG is enabled
			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				$error_response['debug_info'] = array(
					'requested_provider'  => $provider_name ?: ( $this->settings['default_provider'] ?? 'openai' ),
					'available_providers' => ! empty( $this->providers ) ? implode( ', ', array_keys( $this->providers ) ) : 'none',
					'has_settings'        => ! empty( $this->settings ),
					'settings_keys'       => ! empty( $this->settings ) ? array_keys( $this->settings ) : array(),
				);
			}
			
			return $error_response;
		}

		$language = $options['language'] ?? $this->get_site_language();
		$temperature = $options['temperature'] ?? 0.5;
		$title = $options['title'] ?? '';
		$prompt_template = $options['prompt_template'] ?? '';
		$model = sanitize_text_field( $options['model'] ?? '' );
		$language_instruction = $this->get_output_language_instruction( $language, 'excerpt' );
		
		// Use custom prompt if provided
		if ( ! empty( $prompt_template ) ) {
			$variables = array(
				'LANGUAGE' => $language,
				'TITLE'    => $title,
				'CONTENT'  => wp_trim_words( wp_strip_all_tags( $content ), 200 ),
				'TOPIC'    => $options['topic'] ?? '',
				'CONTEXT'  => $options['context'] ?? '',
			);
			$user_prompt = SmartyPress_AI_Prompt_Processor::process_prompt( $prompt_template, $variables );
			$system_prompt = sprintf(
				/* translators: %s: Language name */
				__( 'You are a professional content writer. Create concise and engaging summaries. Return ONLY plain text, no HTML tags, no Markdown formatting. Always wrap your response with ===CONTENT_START=== and ===CONTENT_END=== markers. Language: %s', 'smartypress-ai-engine' ),
				$language
			);
		} else {
			// Default prompt
			$system_prompt = sprintf(
				/* translators: %s: Language name */
				__( 'You are a professional content writer. Create concise and engaging summaries (max 150 characters). Return ONLY plain text, no HTML tags, no Markdown formatting. Always wrap your response with ===CONTENT_START=== and ===CONTENT_END=== markers. Language: %s', 'smartypress-ai-engine' ),
				$language
			);

			$user_prompt = sprintf(
				/* translators: %s: Content */
				__( 'Create a short summary of this content: %s Wrap your response with ===CONTENT_START=== and ===CONTENT_END=== markers.', 'smartypress-ai-engine' ),
				wp_strip_all_tags( $content )
			);
		}

		$system_prompt .= "\n\n" . $language_instruction;
		$user_prompt   .= "\n\nIMPORTANT LANGUAGE RULE: " . $language_instruction;

		// Add "generate different" instruction if current excerpt exists
		$current_excerpt = $options['current_excerpt'] ?? '';
		if ( ! empty( $current_excerpt ) ) {
			$user_prompt .= sprintf(
				"\n\nIMPORTANT: Do NOT reuse this existing excerpt: \"%s\". Write a completely different summary.",
				$current_excerpt
			);
		}

		$params = array(
			'system_prompt' => $this->add_cache_buster( $system_prompt ),
			'temperature'   => $temperature,
			'max_tokens'    => 100,
		);

		if ( ! empty( $model ) ) {
			$params['model'] = $model;
		}

		$result = $provider->generate_content( $user_prompt, $params );

		if ( $result['success'] ) {
			$result['excerpt'] = trim( $result['content'] );
			// Remove quotes if present
			$result['excerpt'] = trim( $result['excerpt'], '""\'"' );
			// Clean AI comments
			$result['excerpt'] = SmartyPress_AI_Prompt_Processor::clean_generated_content( $result['excerpt'] );
		}

		return $result;
	}

	/**
	 * Generate full post (title -> sections -> content -> excerpt)
	 *
	 * @param string $topic Topic/context for generation.
	 * @param array  $settings Settings array with prompts, language, post_type, number_of_sections, paragraphs_per_section, provider, model, temperature.
	 * @return array Response with post data or error.
	 */
	public function generate_full_post( $topic, $settings = array() ) {
		return $this->legacy_post_generation->generate_full_post(
			$this->generation_step_service,
			$topic,
			$settings,
			$this->get_site_language(),
			$this->settings['temperature'] ?? 0.8
		);
	}

	/**
	 * Get default editable plan structure.
	 *
	 * @return array
	 */
	protected function get_default_post_plan() {
		return array(
			'title'   => array(
				'value'       => '',
				'variants'    => array(),
				'note'        => '',
				'constraints' => array(),
			),
			'content' => array(
				'globalNote' => '',
				'keywords'   => array(),
				'sections'   => array(),
			),
			'excerpt' => array(
				'note' => '',
			),
			'seo'     => array(
				'keywords'              => array(),
				'metaDescriptionPrompt' => '',
			),
			'research' => array(
				'enabled'              => false,
				'searchQueries'        => array(),
				'competitors'          => array(),
				'keywordGaps'          => array(),
				'titlePatterns'        => array(),
				'contentOpportunities' => array(),
			),
		);
	}

	/**
	 * Generate an editable prompt plan from a topic.
	 *
	 * @param string $topic         Topic/context for generation.
	 * @param string $provider_name Provider name.
	 * @param array  $options       Generation options.
	 * @return array
	 */
	public function generate_post_plan( $topic, $provider_name = '', $options = array() ) {
		$provider = $this->get_provider( $provider_name );

		if ( ! $provider ) {
			return array(
				'success'    => false,
				'error'      => __( 'API key not configured. Go to SmartyPress AI > Settings and add your AI provider API key.', 'smartypress-ai-engine' ),
				'error_code' => 'api_key_missing',
			);
		}

		$language    = $options['language'] ?? $this->get_site_language();
		$post_type   = $options['post_type'] ?? 'post';
		$temperature = $options['temperature'] ?? $this->settings['temperature'] ?? 0.7;
		$max_tokens  = ! empty( $options['max_tokens'] ) ? absint( $options['max_tokens'] ) : 8000;
		$plan_prompt_template = $options['plan_prompt_template'] ?? '';
		$model       = sanitize_text_field( $options['model'] ?? '' );
		$plan_context = array(
			'provider'           => $provider_name,
			'model'              => $model,
			'topic'              => $topic,
			'context'            => $options['context'] ?? '',
			'language'           => $language,
			'post_type'          => $post_type,
			'plan_expert_mode'   => ! empty( $options['plan_expert_mode'] ),
			'seo_analysis_enabled' => false,
		);
		$seo_analysis_enabled = apply_filters( 'smartypress_ai_content_plan_seo_analysis_enabled', false, $provider_name, $model, $plan_context );
		$plan_context['seo_analysis_enabled'] = (bool) $seo_analysis_enabled;

		$schema_result = $this->generate_post_plan_schema(
			$provider,
			$plan_prompt_template,
			$plan_context,
			array(
				'language'    => $language,
				'temperature' => $temperature,
				'max_tokens'  => $max_tokens,
				'model'       => $model,
				'plan_expert_mode' => ! empty( $options['plan_expert_mode'] ),
			)
		);

		if ( empty( $schema_result['success'] ) ) {
			return $schema_result;
		}

		$plan_result = $this->generate_post_plan_from_schema(
			$provider,
			$schema_result['schema'],
			$plan_prompt_template,
			$plan_context,
			array(
				'language'    => $language,
				'temperature' => $temperature,
				'max_tokens'  => $max_tokens,
				'model'       => $model,
				'plan_expert_mode' => ! empty( $options['plan_expert_mode'] ),
			)
		);

		if ( empty( $plan_result['success'] ) ) {
			return $plan_result;
		}

		$plan = $plan_result['plan'];

		$normalized_plan = $this->normalize_post_plan( $plan );
		do_action( 'smartypress_ai_content_plan_research_generated', $normalized_plan, $plan_context );

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

	/**
	 * Generate a dynamic editable plan schema.
	 *
	 * @param object $provider             AI provider.
	 * @param string $plan_prompt_template User-facing plan instruction.
	 * @param array  $plan_context         Runtime context.
	 * @param array  $options              Request options.
	 * @return array
	 */
	protected function generate_post_plan_schema( $provider, $plan_prompt_template, $plan_context, $options = array() ) {
		return $this->plan_generation->generate_post_plan_schema(
			$provider,
			$plan_prompt_template,
			$plan_context,
			$options,
			$this->get_site_language(),
			function( $system_prompt ) {
				return $this->add_cache_buster( $system_prompt );
			}
		);
	}

	/**
	 * Fill a dynamic plan schema with concrete values.
	 *
	 * @param object $provider             AI provider.
	 * @param array  $schema               Normalized schema.
	 * @param string $plan_prompt_template User-facing plan instruction.
	 * @param array  $plan_context         Runtime context.
	 * @param array  $options              Request options.
	 * @return array
	 */
	protected function generate_post_plan_from_schema( $provider, $schema, $plan_prompt_template, $plan_context, $options = array() ) {
		return $this->plan_generation->generate_post_plan_from_schema(
			$provider,
			$schema,
			$plan_prompt_template,
			$plan_context,
			$options,
			$this->get_site_language(),
			function( $system_prompt ) {
				return $this->add_cache_buster( $system_prompt );
			}
		);
	}

	/**
	 * Process user-facing plan instruction.
	 *
	 * @param string $plan_prompt_template Prompt template.
	 * @param string $language             Language.
	 * @return string
	 */
	protected function get_processed_plan_instruction( $plan_prompt_template, $language ) {
		return $this->plan_generation->get_processed_plan_instruction( $plan_prompt_template, $language );
	}

	/**
	 * Improve a plan prompt template using an AI provider.
	 *
	 * @param string $prompt        Current prompt template.
	 * @param string $provider_name Provider name.
	 * @param array  $options       Generation options.
	 * @return array
	 */
	public function improve_plan_prompt_template( $prompt, $provider_name = '', $options = array() ) {
		$provider = $this->get_provider( $provider_name );

		if ( ! $provider && ! empty( $this->providers ) ) {
			$provider = reset( $this->providers );
		}

		if ( ! $provider ) {
			return array(
				'success'    => false,
				'error'      => __( 'API key not configured. Go to SmartyPress AI > Settings and add your AI provider API key.', 'smartypress-ai-engine' ),
				'error_code' => 'api_key_missing',
			);
		}

		$language    = $options['language'] ?? $this->get_site_language();
		$temperature = $options['temperature'] ?? 0.4;
		$max_tokens  = ! empty( $options['max_tokens'] ) ? absint( $options['max_tokens'] ) : 3000;

		$system_prompt = sprintf(
			/* translators: %s: Language name. */
			__( 'You are a prompt engineering assistant. Improve a reusable user-facing instruction in %s for generating a flexible editable content plan. Return only the improved prompt text, no commentary.', 'smartypress-ai-engine' ),
			$language
		);

		$user_prompt  = __( 'Improve the prompt below. It should describe audience, tone, depth, SEO focus, and what should be editable in the plan. Do not include JSON markers, required schema fields, or technical response-format instructions because the system adds them automatically.', 'smartypress-ai-engine' );
		$user_prompt .= "\n\nPROMPT_TEMPLATE:\n" . (string) $prompt;

		$params = array(
			'system_prompt' => $this->add_cache_buster( $system_prompt ),
			'temperature'   => $temperature,
			'max_tokens'    => $max_tokens,
		);

		if ( ! empty( $options['model'] ) ) {
			$params['model'] = $options['model'];
		}

		$result = $provider->generate_content( $user_prompt, $params );

		if ( empty( $result['success'] ) ) {
			return $result;
		}

		return array(
			'success' => true,
			'prompt'  => trim( SmartyPress_AI_Prompt_Processor::clean_generated_content( $result['content'] ?? '' ) ),
		);
	}

	/**
	 * Apply a user instruction to an existing plan.
	 *
	 * @param array  $plan          Current plan.
	 * @param string $instruction   User instruction.
	 * @param string $provider_name Provider name.
	 * @param array  $options       Generation options.
	 * @return array
	 */
	public function apply_post_plan_instruction( $plan, $instruction, $provider_name = '', $options = array() ) {
		$provider = $this->get_provider( $provider_name );

		if ( ! $provider ) {
			return array(
				'success'    => false,
				'error'      => __( 'API key not configured. Go to SmartyPress AI > Settings and add your AI provider API key.', 'smartypress-ai-engine' ),
				'error_code' => 'api_key_missing',
			);
		}

		$language    = $options['language'] ?? $this->get_site_language();
		$temperature = $options['temperature'] ?? $this->settings['temperature'] ?? 0.5;

		$system_prompt = sprintf(
			/* translators: %s: Language name. */
			__( 'You are an editorial planning assistant. Update the existing concrete content plan in %s. Preserve the same JSON format and editable field structure. Return ONLY valid JSON wrapped between ===PLAN_JSON_START=== and ===PLAN_JSON_END===.', 'smartypress-ai-engine' ),
			$language
		);

		$user_prompt = "CURRENT_PLAN:\n" . wp_json_encode( $this->normalize_post_plan( $plan ), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT );
		$user_prompt .= "\n\nUSER_CHANGE_REQUEST:\n" . $instruction;
		$user_prompt .= "\n\n" . __( 'Update the plan according to the request. Return the complete updated plan, not a diff, patch, or explanation. Do not generate the final article.', 'smartypress-ai-engine' );
		$user_prompt .= "\n" . __( 'Keep all editable values concrete and reader-facing. If the plan uses schema/data, preserve the schema and update only data/meta unless the user explicitly asks to change the structure.', 'smartypress-ai-engine' );
		$user_prompt .= "\n" . __( 'The response must contain only one JSON object with the same schema. Wrap it between ===PLAN_JSON_START=== and ===PLAN_JSON_END=== markers.', 'smartypress-ai-engine' );

		$params = array(
			'system_prompt' => $this->add_cache_buster( $system_prompt ),
			'temperature'   => $temperature,
			'max_tokens'    => 1600,
		);

		if ( ! empty( $options['model'] ) ) {
			$params['model'] = $options['model'];
		}

		$result = $provider->generate_content( $user_prompt, $params );

		if ( empty( $result['success'] ) ) {
			return $result;
		}

		$updated_plan = $this->parse_plan_json( $result['content'] ?? '' );

		if ( empty( $updated_plan ) ) {
			return array(
				'success'    => false,
				'error'      => __( 'Failed to parse the updated content plan.', 'smartypress-ai-engine' ),
				'error_code' => 'plan_parse_failed',
			);
		}

		return array(
			'success' => true,
			'plan'    => $this->normalize_post_plan( $updated_plan ),
		);
	}

	/**
	 * Convert dynamic plan fields to readable text for final draft generation.
	 *
	 * @param array $data   Dynamic data.
	 * @param array $fields Schema fields.
	 * @param int   $level  Nesting level.
	 * @return string
	 */
	protected function build_dynamic_plan_summary_from_fields( $data, $fields, $level = 0 ) {
		$lines = $this->plan_summary_builder->build_dynamic_plan_summary_from_fields(
			is_array( $fields ) ? $fields : array(),
			is_array( $data ) ? $data : array(),
			absint( $level )
		);

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

	/**
	 * Build readable summary for a dynamic plan.
	 *
	 * @param array $plan Dynamic plan.
	 * @return string
	 */
	protected function build_dynamic_plan_summary( $plan ) {
		$summary = $this->plan_summary_builder->build_dynamic_plan_summary( $plan );

		if ( ! empty( $plan['meta']['draftInstruction'] ) ) {
			$summary .= "\n\n" . __( 'Draft instruction:', 'smartypress-ai-engine' ) . "\n" . $plan['meta']['draftInstruction'];
		}

		return trim( $summary );
	}

	/**
	 * Find a probable title in dynamic plan data.
	 *
	 * @param array $data   Dynamic data.
	 * @param array $fields Schema fields.
	 * @return string
	 */
	protected function find_dynamic_plan_title( $data, $fields ) {
		return $this->plan_summary_builder->find_dynamic_plan_title(
			array(
				'data'   => is_array( $data ) ? $data : array(),
				'schema' => array(
					'fields' => is_array( $fields ) ? $fields : array(),
				),
			)
		);
	}

	/**
	 * Log the final draft request built from an approved plan.
	 *
	 * @param string $provider_name Provider name.
	 * @param array  $options       Request options.
	 * @param string $topic         Topic.
	 * @param array  $plan          Normalized plan.
	 * @param string $plan_type     Plan type.
	 * @param string $summary       Dynamic summary.
	 * @param string $user_prompt   Final user prompt sent to AI.
	 * @return void
	 */
	protected function log_draft_from_plan_request( $provider_name, $options, $topic, $plan, $plan_type, $summary, $user_prompt ) {
		if ( ! class_exists( 'SmartyPress_AI_Logger' ) ) {
			return;
		}

		SmartyPress_AI_Logger::log_module_event(
			'content_generator',
			'draft_from_plan_request',
			array(
				'provider'     => sanitize_key( $provider_name ),
				'model'        => sanitize_text_field( $options['model'] ?? '' ),
				'topic'        => sanitize_textarea_field( $topic ),
				'plan_type'    => $plan_type,
				'plan'         => $plan,
				'plan_summary' => $summary,
				'user_prompt'  => $user_prompt,
			),
			false
		);
	}

	/**
	 * Generate final post content from an approved plan.
	 *
	 * @param array  $plan          Approved plan.
	 * @param string $provider_name Provider name.
	 * @param array  $options       Generation options.
	 * @return array
	 */
	public function generate_post_from_plan( $plan, $provider_name = '', $options = array() ) {
		$provider = $this->get_provider( $provider_name );

		if ( ! $provider ) {
			return array(
				'success'    => false,
				'error'      => __( 'API key not configured. Go to SmartyPress AI > Settings and add your AI provider API key.', 'smartypress-ai-engine' ),
				'error_code' => 'api_key_missing',
			);
		}

		return $this->plan_draft_generation->generate_post_from_plan(
			$provider,
			$provider_name,
			$plan,
			$options,
			$options['language'] ?? $this->get_site_language(),
			$this->settings['temperature'] ?? 0.7,
			$this->settings['max_tokens'] ?? 0,
			function( $system_prompt ) {
				return $this->add_cache_buster( $system_prompt );
			},
			function( $provider_name, $options, $topic, $plan, $plan_type, $summary, $user_prompt ) {
				$this->log_draft_from_plan_request( $provider_name, $options, $topic, $plan, $plan_type, $summary, $user_prompt );
			}
		);
	}

	/**
	 * Generate a draft post from an approved plan and save plan metadata.
	 *
	 * @param array  $plan          Approved plan.
	 * @param string $provider_name Provider name.
	 * @param array  $options       Generation options.
	 * @return array
	 */
	public function generate_draft_from_plan( $plan, $provider_name = '', $options = array() ) {
		return $this->plan_draft_generation->generate_draft_from_plan(
			function( $plan, $provider_name, $options ) {
				return $this->generate_post_from_plan( $plan, $provider_name, $options );
			},
			function( $post_data ) {
				return $this->create_post( $post_data );
			},
			$plan,
			$provider_name,
			$options
		);
	}

	/**
	 * Parse generated plan JSON from marked or raw response.
	 *
	 * @param string $content AI response.
	 * @return array
	 */
	protected function parse_plan_json( $content, $context = array() ) {
		return $this->plan_parser->parse_plan_json( $content, $context );
	}

	/**
	 * Extract first balanced JSON object from AI response text.
	 *
	 * @param string $content AI response.
	 * @return string JSON object candidate.
	 */
	protected function extract_first_json_object( $content ) {
		return $this->plan_parser->extract_first_json_object( $content );
	}

	/**
	 * Log plan parsing failures without exposing full generated content.
	 *
	 * @param string $content AI response.
	 * @return void
	 */
	protected function log_plan_parse_failure( $content, $context = array() ) {
		$this->plan_parser->log_plan_parse_failure( $content, $context );
	}

	/**
	 * Check whether a plan uses the dynamic schema/data format.
	 *
	 * @param array $plan Plan data.
	 * @return bool
	 */
	protected function is_dynamic_post_plan( $plan ) {
		return $this->plan_normalizer->is_dynamic_post_plan( $plan );
	}

	/**
	 * Normalize dynamic plan schema.
	 *
	 * @param array $schema Raw schema.
	 * @return array
	 */
	protected function normalize_dynamic_plan_schema( $schema ) {
		return $this->plan_normalizer->normalize_dynamic_plan_schema( $schema );
	}

	/**
	 * Normalize a list of dynamic schema fields.
	 *
	 * @param array $fields Raw fields.
	 * @param int   $depth  Current nesting depth.
	 * @return array
	 */
	protected function normalize_dynamic_plan_fields( $fields, $depth = 0 ) {
		return $this->plan_normalizer->normalize_dynamic_plan_fields( $fields );
	}

	/**
	 * Normalize dynamic plan data according to schema.
	 *
	 * @param array $data   Raw data.
	 * @param array $fields Schema fields.
	 * @return array
	 */
	protected function normalize_dynamic_plan_data( $data, $fields ) {
		return $this->plan_normalizer->normalize_dynamic_plan_data( $data, $fields );
	}

	/**
	 * Sanitize one dynamic plan value.
	 *
	 * @param mixed $value Raw value.
	 * @param array $field Schema field.
	 * @return mixed
	 */
	protected function sanitize_dynamic_plan_value( $value, $field ) {
		return $this->plan_normalizer->sanitize_dynamic_plan_value( $value, $field );
	}

	/**
	 * Normalize a dynamic content plan.
	 *
	 * @param array $plan Raw plan.
	 * @return array
	 */
	protected function normalize_dynamic_post_plan( $plan ) {
		return $this->plan_normalizer->normalize_dynamic_post_plan( $plan );
	}

	/**
	 * Normalize a plan to the expected schema.
	 *
	 * @param array $plan Raw plan.
	 * @return array
	 */
	protected function normalize_post_plan( $plan ) {
		$defaults = $this->get_default_post_plan();
		$plan     = is_array( $plan ) ? $plan : array();

		if ( $this->is_dynamic_post_plan( $plan ) ) {
			return $this->normalize_dynamic_post_plan( $plan );
		}

		$plan     = wp_parse_args( $plan, $defaults );

		$plan['title'] = wp_parse_args( is_array( $plan['title'] ) ? $plan['title'] : array(), $defaults['title'] );
		$legacy_title_prompt = $plan['title']['prompt'] ?? '';
		$plan['title']['value'] = sanitize_text_field( $plan['title']['value'] ?? $legacy_title_prompt );
		$plan['title']['variants'] = $this->sanitize_string_list( $plan['title']['variants'] ?? array() );
		$plan['title']['note'] = sanitize_textarea_field( $plan['title']['note'] ?? $legacy_title_prompt );
		$plan['title']['constraints'] = $this->sanitize_string_list( $plan['title']['constraints'] );
		unset( $plan['title']['prompt'] );

		$plan['content'] = wp_parse_args( is_array( $plan['content'] ) ? $plan['content'] : array(), $defaults['content'] );
		$legacy_global_prompt = $plan['content']['globalPrompt'] ?? '';
		$plan['content']['globalNote'] = sanitize_textarea_field( $plan['content']['globalNote'] ?? $legacy_global_prompt );
		$plan['content']['keywords'] = $this->sanitize_string_list( $plan['content']['keywords'] );
		$plan['content']['sections'] = $this->sanitize_plan_sections( $plan['content']['sections'] );
		unset( $plan['content']['globalPrompt'] );

		$plan['excerpt'] = wp_parse_args( is_array( $plan['excerpt'] ) ? $plan['excerpt'] : array(), $defaults['excerpt'] );
		$plan['excerpt']['note'] = sanitize_textarea_field( $plan['excerpt']['note'] ?? ( $plan['excerpt']['prompt'] ?? '' ) );
		unset( $plan['excerpt']['prompt'] );

		$plan['seo'] = wp_parse_args( is_array( $plan['seo'] ) ? $plan['seo'] : array(), $defaults['seo'] );
		$plan['seo']['keywords'] = $this->sanitize_string_list( $plan['seo']['keywords'] );
		$plan['seo']['metaDescriptionPrompt'] = sanitize_textarea_field( $plan['seo']['metaDescriptionPrompt'] );
		$plan['research'] = $this->sanitize_plan_research( $plan['research'] ?? array(), $defaults['research'] );

		return $plan;
	}

	/**
	 * Sanitize content plan sections.
	 *
	 * @param array $sections Raw sections.
	 * @return array
	 */
	protected function sanitize_plan_sections( $sections ) {
		$sections = is_array( $sections ) ? $sections : array();
		$clean    = array();

		foreach ( $sections as $section ) {
			$section = is_array( $section ) ? $section : array();
			$legacy_section_prompt = $section['prompt'] ?? '';
			$clean[] = array(
				'heading'  => sanitize_text_field( $section['heading'] ?? '' ),
				'intent'   => sanitize_textarea_field( $section['intent'] ?? '' ),
				'note'     => sanitize_textarea_field( $section['note'] ?? $legacy_section_prompt ),
				'keywords' => $this->sanitize_string_list( $section['keywords'] ?? array() ),
			);
		}

		return $clean;
	}

	/**
	 * Sanitize optional competitor research data.
	 *
	 * @param array $research Raw research data.
	 * @param array $defaults Default research structure.
	 * @return array
	 */
	protected function sanitize_plan_research( $research, $defaults ) {
		$research = is_array( $research ) ? wp_parse_args( $research, $defaults ) : $defaults;

		$competitors = array();
		foreach ( array_slice( is_array( $research['competitors'] ?? null ) ? $research['competitors'] : array(), 0, 10 ) as $competitor ) {
			$competitor = is_array( $competitor ) ? $competitor : array();
			$competitors[] = array(
				'title'            => sanitize_text_field( $competitor['title'] ?? '' ),
				'url'              => esc_url_raw( $competitor['url'] ?? '' ),
				'observedKeywords' => array_slice( $this->sanitize_string_list( $competitor['observedKeywords'] ?? array() ), 0, 30 ),
				'headingPatterns'  => array_slice( $this->sanitize_string_list( $competitor['headingPatterns'] ?? array() ), 0, 30 ),
				'contentAngle'     => sanitize_textarea_field( $competitor['contentAngle'] ?? '' ),
			);
		}

		$has_research_data = ! empty( $research['searchQueries'] )
			|| ! empty( $competitors )
			|| ! empty( $research['keywordGaps'] )
			|| ! empty( $research['titlePatterns'] )
			|| ! empty( $research['contentOpportunities'] );

		return array(
			'enabled'              => ! empty( $research['enabled'] ) || $has_research_data,
			'searchQueries'        => array_slice( $this->sanitize_string_list( $research['searchQueries'] ?? array() ), 0, 30 ),
			'competitors'          => $competitors,
			'keywordGaps'          => array_slice( $this->sanitize_string_list( $research['keywordGaps'] ?? array() ), 0, 30 ),
			'titlePatterns'        => array_slice( $this->sanitize_string_list( $research['titlePatterns'] ?? array() ), 0, 30 ),
			'contentOpportunities' => array_slice( $this->sanitize_string_list( $research['contentOpportunities'] ?? array() ), 0, 30 ),
		);
	}

	/**
	 * Sanitize a list of strings.
	 *
	 * @param mixed $items Raw items.
	 * @return array
	 */
	protected function sanitize_string_list( $items ) {
		return $this->plan_normalizer->sanitize_string_list( $items );
	}

	/**
	 * Collect probable keywords from dynamic plan fields.
	 *
	 * @param array  $data   Dynamic data.
	 * @param array  $fields Schema fields.
	 * @param string $prefix Parent label.
	 * @return array
	 */
	protected function collect_dynamic_plan_keywords( $data, $fields, $prefix = '' ) {
		return $this->plan_summary_builder->collect_dynamic_plan_keywords( $data );
	}

	/**
	 * Collect all keywords from plan.
	 *
	 * @param array $plan Normalized plan.
	 * @return array
	 */
	protected function collect_plan_keywords( $plan ) {
		if ( $this->is_dynamic_post_plan( $plan ) ) {
			return $this->collect_dynamic_plan_keywords( $plan['data'], $plan['schema']['fields'] ?? array() );
		}

		$keywords = array_merge(
			$plan['content']['keywords'] ?? array(),
			$plan['seo']['keywords'] ?? array()
		);

		foreach ( $plan['content']['sections'] ?? array() as $section ) {
			$keywords = array_merge( $keywords, $section['keywords'] ?? array() );
		}

		return $this->sanitize_string_list( $keywords );
	}

	/**
	 * Parse final generated draft markers.
	 *
	 * @param string $content AI response.
	 * @return array
	 */
	protected function parse_generated_post_parts( $content ) {
		return $this->draft_parser->parse_generated_post_parts( $content );
	}

	/**
	 * Extract usable article content when the model ignored draft markers.
	 *
	 * @param string $content Generated response.
	 * @param string $title   Parsed or planned title.
	 * @return string
	 */
	protected function extract_unmarked_generated_content( $content, $title = '' ) {
		return $this->draft_parser->extract_unmarked_generated_content( $content, $title );
	}

	/**
	 * Normalize comma/newline separated keyword list.
	 *
	 * @param string|array $keywords Raw keywords.
	 * @return array
	 */
	protected function normalize_keyword_list( $keywords ) {
		return $this->sanitize_string_list( $keywords );
	}

	/**
	 * Convert Markdown to Gutenberg blocks format
	 *
	 * @param string $content Markdown content.
	 * @return string Gutenberg blocks format content.
	 */
	public function markdown_to_gutenberg_content( $content ) {
		return $this->markdown_converter->markdown_to_gutenberg_content( $content );
	}

	/**
	 * Convert HTML to Gutenberg blocks format
	 *
	 * @param string $html HTML content.
	 * @return string Gutenberg blocks format content.
	 */
	public function html_to_gutenberg_blocks( $html ) {
		return $this->markdown_converter->html_to_gutenberg_blocks( $html );
	}

	/**
	 * Create Gutenberg block with comment delimiters
	 *
	 * @param string $block_name Block name (e.g., 'core/paragraph').
	 * @param array  $attributes Block attributes.
	 * @param string $content Block content.
	 * @return string Gutenberg block format string.
	 */
	protected function create_gutenberg_block( $block_name, $attributes = array(), $content = '' ) {
		return $this->markdown_converter->create_gutenberg_block( $block_name, $attributes, $content );
	}

	/**
	 * Create WordPress post
	 *
	 * @param array $post_data Post data.
	 * @return array Response with post ID or error.
	 */
	public function create_post( $post_data ) {
		return $this->post_persistence->create_post( $post_data );
	}

	/**
	 * Generate and create post in one step
	 *
	 * @param string $topic Topic/context for generation.
	 * @param array  $settings Settings array.
	 * @return array Response with post data or error.
	 */
	public function generate_and_create_post( $topic, $settings = array() ) {
		// Generate full post data
		$result = $this->generate_full_post( $topic, $settings );
		
		if ( ! $result['success'] ) {
			return $result;
		}

		// Create post
		$post_type = $settings['post_type'] ?? 'post';
		$post_status = $settings['post_status'] ?? 'draft';
		
		$post_data = array(
			'post_title'    => $result['title'],
			'post_content'  => $result['content'],
			'post_excerpt'  => $result['excerpt'],
			'post_status'   => $post_status,
			'post_type'     => $post_type,
			'categories'    => $this->resolve_item_category_ids( $settings['item'] ?? array(), $post_type ),
			'product_meta'  => $settings['product_meta'] ?? array(),
			'seo_keywords'  => $result['seo_keywords'] ?? array(),
			'seo_metadata'  => $result['seo_metadata'] ?? array(),
		);
		$post_data = $this->post_type_extension_registry->prepare_post_data(
			$post_data,
			is_array( $settings['item'] ?? null ) ? $settings['item'] : array(),
			$settings
		);

		$create_result = $this->create_post( $post_data );

		if ( ! $create_result['success'] ) {
			return $create_result;
		}

		$this->post_type_extension_registry->apply_metadata(
			$create_result['post_id'],
			$post_data,
			is_array( $settings['item'] ?? null ) ? $settings['item'] : array(),
			$settings
		);

		return array(
			'success'   => true,
			'post_id'   => $create_result['post_id'],
			'title'     => $result['title'],
			'sections'  => $result['sections'],
			'content'   => $result['content'],
			'excerpt'   => $result['excerpt'],
			'edit_url'  => $create_result['edit_url'],
			'view_url'  => $create_result['view_url'],
			'seo_keywords' => $create_result['seo_metadata']['focus_keywords'] ?? array(),
		);
	}

	/**
	 * Resolve bulk item category names into WordPress category IDs.
	 *
	 * @param array  $item Bulk row metadata.
	 * @param string $post_type Target post type.
	 * @return array
	 */
	private function resolve_item_category_ids( $item, $post_type ) {
		$item      = is_array( $item ) ? $item : array();
		$post_type = sanitize_key( $post_type );
		$category  = sanitize_text_field( $item['category'] ?? '' );

		if ( '' === $category || 'post' !== $post_type || ! taxonomy_exists( 'category' ) ) {
			return array();
		}

		$names = preg_split( '/[,;]+/u', $category, -1, PREG_SPLIT_NO_EMPTY );
		$ids   = array();

		foreach ( (array) $names as $name ) {
			$name = trim( sanitize_text_field( $name ) );

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

			$term = term_exists( $name, 'category' );

			if ( 0 === $term || null === $term ) {
				$term = wp_insert_term( $name, 'category' );
			}

			if ( is_wp_error( $term ) ) {
				continue;
			}

			$ids[] = is_array( $term ) ? absint( $term['term_id'] ?? 0 ) : absint( $term );
		}

		return array_values( array_filter( array_unique( $ids ) ) );
	}
}

