<?php
/**
 * SEO fix prompt builder.
 *
 * @package SmartyPress_AI_Engine
 * @since 1.0.0
 */

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

/**
 * Builds provider-agnostic prompts for SEO fixes.
 */
class SmartyPress_AI_SEO_Fix_Prompt_Builder {

	/**
	 * Build the system prompt.
	 *
	 * @param string $language Output language.
	 * @return string
	 */
	public function build_system_prompt( $language ) {
		return sprintf(
			/* translators: %s: language name. */
			__( 'You are a senior SEO editor. Return ONLY valid JSON, without Markdown fences or explanations. Preserve the article meaning, factual claims, language, HTML/block structure, and author style. Improve SEO naturally without keyword stuffing. Do not invent external links, do not create images, and do not bypass paid SEO plugin features. Language: %s', 'smartypress-ai-engine' ),
			$language
		);
	}

	/**
	 * Build the full-context SEO diagnosis prompt.
	 *
	 * @param array $payload Normalized request payload.
	 * @return string
	 */
	public function build_analysis_prompt( $payload ) {
		return wp_json_encode(
			array(
				'task'            => 'Analyze the complete article and produce an SEO diagnosis/strategy. Do not rewrite content or generate fixes yet.',
				'response_schema' => $this->get_analysis_schema_contract(),
				'rules'           => array(
					'Read the full article context before choosing keywords.',
					'Choose primary_keyword as a natural search query phrase in its dictionary/base form, not as a random inflected occurrence from the article.',
					'primary_keyword must contain at least 2 words and at most 3 words.',
					'Single-word primary_keyword is invalid. Do not return broad topic words as primary_keyword.',
					'For inflected languages, prefer nominative/base phrase forms that users would search for, for example "ландшафтный дизайн", not "ландшафтному дизайну участка".',
					'The selected phrase must be strongly grounded in the article topic, title, headings, or repeated terms. Do not invent unrelated keywords.',
					'If only inflected forms appear in the body, you may normalize the phrase to a natural base search form when it clearly refers to the same topic.',
					'Identify phrases that are already overused so later fixes do not repeat them unnecessarily.',
					'Detect whether the article already has a real table of contents, including custom HTML, anchor lists, plugin blocks, or a heading-linked navigation block.',
					'Map the article structure: intro block, first useful heading, heading candidates, keyword density candidates, and the best TOC insertion target.',
					'Map failed SEO tests to actionable strategies with target Gutenberg block IDs whenever possible.',
					'If the article has no real table of contents and has at least 2-3 meaningful H2/H3 headings, add contentHasTOC to recommendations as an optional_fix. Do not add it to fix_strategy unless it was already present in input.failed_tests.',
					'Recommendations are optional user choices, not failed SEO tests.',
					'Use context_scope local only when a small fragment is enough. Use expanded or full when global meaning, density, or overuse matters.',
					'Put tests that cannot be safely fixed automatically into manual_only.',
				),
				'input'           => $payload,
			),
			JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT
		);
	}

	/**
	 * Build a strict keyword-selection prompt when full analysis fails keyword validation.
	 *
	 * @param array $payload Keyword selection context.
	 * @return string
	 */
	public function build_keyword_selection_prompt( $payload ) {
		return wp_json_encode(
			array(
				'task'            => 'Select a valid primary SEO keyword phrase from the article. Return only keyword fields.',
				'response_schema' => array(
					'primary_keyword'    => 'string, natural search query phrase in base/dictionary form, at least 2 words and at most 3 words',
					'secondary_keywords' => array( 'string, natural search query phrase, 2-3 words' ),
					'reason'             => 'string, short explanation of why primary_keyword was selected',
				),
				'validation_context' => array(
					'rejected_primary_keyword' => sanitize_text_field( $payload['rejected_primary_keyword'] ?? '' ),
					'failure_reason'           => sanitize_text_field( $payload['failure_reason'] ?? 'missing_or_invalid_primary_keyword' ),
					'required_rule'            => 'primary_keyword must be a natural search query phrase in base/dictionary form with at least 2 words and at most 3 words. Single-word keywords are invalid.',
				),
				'rules'           => array(
					'Read input.context.post.content_plain_text, title, headings, and SEO metadata before choosing a phrase.',
					'primary_keyword must contain at least 2 words and at most 3 words.',
					'Single-word primary_keyword is invalid.',
					'Do not return broad topic words, generic categories, or invented phrases.',
					'For inflected languages, choose the natural base/nominative search form, for example "ландшафтный дизайн", not an inflected body occurrence like "ландшафтному дизайну участка".',
					'The phrase must be semantically grounded in the article topic and reusable naturally in SEO title, meta description, headings, and body text.',
					'If only inflected forms appear in the body, normalize to the base search phrase when it clearly refers to the same topic.',
					'secondary_keywords must also be natural 2-3 word search phrases grounded in the article.',
					'If there are several candidates, choose the most central phrase for search intent and metadata.',
				),
				'input'           => $payload,
			),
			JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT
		);
	}

	/**
	 * Build retry prompt after keyword-selection validation fails.
	 *
	 * @param array $payload Retry payload.
	 * @return string
	 */
	public function build_keyword_selection_retry_prompt( $payload ) {
		$payload['failure_reason'] = 'previous_keyword_selection_failed_validation';

		return $this->build_keyword_selection_prompt( $payload );
	}

	/**
	 * Build the metadata fix prompt.
	 *
	 * @param array $payload Context and analysis payload.
	 * @return string
	 */
	public function build_meta_prompt( $payload ) {
		$primary_keyword = sanitize_text_field( $payload['effective_primary_keyword'] ?? $payload['analysis']['primary_keyword'] ?? '' );

		return wp_json_encode(
			array(
				'task'            => 'Generate SEO metadata fixes using the article analysis. Return only metadata fields and no content operations.',
				'response_schema' => array(
					'seo_title'            => 'string, 45-58 chars, exact primary keyword verbatim',
					'meta_description'     => 'string, 140-155 chars, exact primary keyword verbatim',
					'slug'                 => 'string, slug only',
					'post_title'           => 'string, optional',
					'focus_keywords'       => array( 'primary keyword first, then up to 4 secondary keywords' ),
					'fixed_tests'          => array( 'string' ),
					'skipped_tests'        => array( array( 'id' => 'string', 'reason' => 'string' ) ),
					'warnings'             => array( 'string' ),
				),
				'critical_keyword_constraint' => array(
					'primary_keyword' => $primary_keyword,
					'must_appear_verbatim_in' => array( 'seo_title', 'meta_description', 'focus_keywords[0]' ),
					'rule'            => 'The output must contain primary_keyword exactly as provided, character by character. Do not change word forms, endings, spelling, spacing, word order, or any character in the keyword.',
				),
				'strict_limits'   => array(
					'seo_title_chars'        => '45-58 characters. REPHRASE to fit, NEVER truncate mechanically. Include the exact primary focus keyword verbatim.',
					'meta_description_chars' => '140-155 characters. REPHRASE into a complete sentence, NEVER truncate. Include the exact primary focus keyword verbatim.',
					'slug_chars'             => 'Return ONLY the slug portion. Keep concise but do not mechanically cut important words. WordPress will normalize and transliterate server-side.',
				),
				'rules'           => array(
					'Use effective_primary_keyword as the first focus keyword.',
					'Do not invent keywords absent from the article.',
					'The generated SEO title and meta description must contain a 100% exact occurrence of primary_keyword.',
					'Do not change word forms, endings, spelling, spacing, word order, or any character in primary_keyword.',
					'If the sentence sounds awkward, rewrite the surrounding sentence around the unchanged primary_keyword.',
					'Do not return content_operations in this step.',
					'Do not return skipped_tests for content, link, media, Table of Contents, heading, density, or readability tests. This metadata step is responsible only for SEO title, description, slug, post title, and focus keywords.',
					'If input.context.failed_tests contains titleHasNumber, include one natural number in seo_title without making it clickbait.',
				),
				'input'           => $payload,
			),
			JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT
		);
	}

	/**
	 * Build a metadata retry prompt after exact keyword validation fails.
	 *
	 * @param array $payload Retry context.
	 * @return string
	 */
	public function build_meta_retry_prompt( $payload ) {
		$primary_keyword = sanitize_text_field( $payload['primary_keyword'] ?? $payload['effective_primary_keyword'] ?? $payload['analysis']['primary_keyword'] ?? '' );

		return wp_json_encode(
			array(
				'task'            => 'Fix SEO metadata after validation failed. Return only valid JSON with the same metadata fields.',
				'response_schema' => array(
					'seo_title'        => 'string, 45-58 chars',
					'meta_description' => 'string, 140-155 chars',
					'slug'             => 'string, slug only',
					'post_title'       => 'string, optional',
					'focus_keywords'   => array( 'primary keyword first, then up to 4 secondary keywords' ),
					'fixed_tests'      => array( 'string' ),
					'skipped_tests'    => array( array( 'id' => 'string', 'reason' => 'string' ) ),
					'warnings'         => array( 'string' ),
				),
				'validation_failure' => array(
					'failed_fields'   => array_values( array_filter( array_map( 'sanitize_key', (array) ( $payload['failed_fields'] ?? array() ) ) ) ),
					'reason'          => 'missing_exact_primary_keyword',
					'primary_keyword' => $primary_keyword,
					'required_rule'   => 'The output must contain primary_keyword exactly as provided, character by character. Do not change word forms, endings, spelling, spacing, word order, or any character in the keyword.',
					'field_notes'     => 'Each failed field used a missing, changed, inflected, or paraphrased keyword form. Replace it with the exact primary_keyword substring.',
				),
				'rules'           => array(
					'The generated result failed validation because it did not contain primary_keyword as an exact substring.',
					'You must include primary_keyword exactly as provided, character by character.',
					'For every failed field, the exact primary_keyword must appear as a literal substring in that field.',
					'Do not use an inflected, translated, transliterated, shortened, pluralized, singularized, or paraphrased variant instead of primary_keyword.',
					'Do not change word forms, endings, spelling, spacing, punctuation inside the keyword, word order, or any character in the keyword.',
					'The output must contain a 100% exact occurrence of primary_keyword.',
					'If the sentence sounds awkward, rewrite the surrounding sentence around the unchanged primary_keyword.',
					'Return only valid JSON.',
				),
				'input'           => array(
					'context'           => $payload['context'] ?? array(),
					'analysis'          => $payload['analysis'] ?? array(),
					'previous_response' => $payload['previous_response'] ?? array(),
				),
			),
			JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT
		);
	}

	/**
	 * Build a focused content strategy prompt.
	 *
	 * @param SmartyPress_AI_SEO_Strategy_Interface $strategy Strategy.
	 * @param array                                 $payload  Strategy payload.
	 * @return string
	 */
	public function build_strategy_prompt( $strategy, $payload ) {
		$primary_keyword     = sanitize_text_field( $payload['primary_keyword'] ?? '' );
		$strategy_rules      = method_exists( $strategy, 'get_strategy_rules' ) ? $strategy->get_strategy_rules() : array();
		$critical_constraint = '';

		if ( '' !== $primary_keyword ) {
			$critical_constraint = sprintf(
				'The html output must contain primary_keyword exactly as provided: "%s". Do not change word forms, endings, spelling, spacing, punctuation inside the keyword, word order, or any character in the keyword. The output must contain a 100%% exact occurrence of primary_keyword. If the sentence sounds awkward, rewrite the surrounding sentence around the unchanged primary_keyword so it reads naturally. For inflected languages, the keyword should already be a natural search phrase; place it in a grammatical sentence, heading, or title instead of forcing it into an incompatible phrase.',
				$primary_keyword
			);
		}

		return wp_json_encode(
			array(
				'task'            => 'Generate a minimal content operation for one SEO assessment only.',
				'strategy'        => array(
					'testId'        => $strategy->get_test_id(),
					'reason'        => $strategy->get_reason(),
					'label'         => $strategy->get_label(),
					'context_scope' => $strategy->get_context_scope(),
				),
				'response_schema' => array(
					'content_operations' => array(
						array(
							'type'          => 'replace|insert_before|insert_after',
							'reason'        => $strategy->get_reason(),
							'testId'        => $strategy->get_test_id(),
							'label'         => $strategy->get_label(),
							'blockClientId' => 'string',
							'html'          => 'string',
							'plainSummary'  => 'string',
						),
					),
					'fixed_tests'        => array( 'string' ),
					'skipped_tests'      => array( array( 'id' => 'string', 'reason' => 'string' ) ),
					'warnings'           => array( 'string' ),
				),
				'critical_constraint' => $critical_constraint,
				'rules'               => array_values( array_filter( array_map( 'trim', array_map( 'strval', (array) $strategy_rules ) ) ) ),
				'input'               => $payload,
			),
			JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT
		);
	}

	/**
	 * Get a schema contract description for the analysis prompt.
	 *
	 * @return array
	 */
	private function get_analysis_schema_contract() {
		return array(
			'topic'            => 'string',
			'search_intent'    => 'informational|commercial|transactional|navigational',
			'content_summary'  => 'string',
			'primary_keyword'  => '2-3 words, natural base/dictionary search phrase grounded in the article topic',
			'secondary_keywords' => array( 'string' ),
			'keyword_usage'    => array(
				'overused_phrases' => array( 'string' ),
				'safe_to_add'      => array( 'string' ),
				'avoid_repeating'  => array( 'string' ),
			),
			'content_map'      => array(
				'intro_block_id'          => 'string',
				'first_heading_block_id'  => 'string',
				'heading_candidates'      => array( 'blockClientId' ),
				'density_candidate_blocks' => array( 'blockClientId' ),
					'toc_available'           => 'boolean, true if the article already has a meaningful table of contents in any language or markup style',
					'toc_reason'              => 'string, short explanation of why TOC is present or absent',
					'toc_target_block_id'      => 'string, best heading blockClientId for TOC insertion when toc_available is false',
			),
			'fix_strategy'     => array(
				array(
					'testId'        => 'string',
					'reason'        => 'string',
					'label'         => 'string',
					'context_scope' => 'local|expanded|full',
					'target_blocks' => array( 'blockClientId' ),
				),
			),
			'recommendations'  => array(
				array(
					'testId'        => 'contentHasTOC',
					'type'          => 'optional_fix',
					'reason'        => 'toc_recommended',
					'label'         => 'Add a Table of Contents block',
					'context_scope' => 'local|expanded|full',
					'target_blocks' => array( 'blockClientId' ),
				),
			),
			'manual_only'      => array(
				array(
					'testId' => 'string',
					'reason' => 'string',
				),
			),
		);
	}

	/**
	 * Build the user prompt.
	 *
	 * @param array $payload Normalized request payload.
	 * @return string
	 */
	public function build_user_prompt( $payload ) {
		$contract = array(
			'seo_title'          => 'string, rephrased to 45-58 chars, contains exact primary keyword verbatim',
			'meta_description'   => 'string, rephrased to 140-155 chars, contains exact primary keyword verbatim',
			'slug'               => 'string, slug only (no domain/protocol), 5-65 chars, transliterated latin with hyphens',
			'post_title'         => 'string',
			'focus_keywords'     => array( 'first element = primary keyword (2-3 words, most frequent phrase), then up to 4 secondary keywords' ),
			'content_operations' => array(
				array(
					'type'          => 'replace|insert_before|insert_after',
					'blockClientId' => 'string',
					'html'          => 'string',
					'plainSummary'  => 'string',
				),
			),
			'internal_link_operations' => array(
				array(
					'blockClientId' => 'string',
					'url'           => 'string',
					'anchor'        => 'string',
					'plainSummary'  => 'string',
				),
			),
			'image_alt_updates'  => array(
				array(
					'blockClientId' => 'string',
					'alt'           => 'string',
				),
			),
			'fixed_tests'        => array( 'string' ),
			'skipped_tests'      => array(
				array(
					'id'     => 'string',
					'reason' => 'string',
				),
			),
			'warnings'           => array( 'string' ),
		);

		return wp_json_encode(
			array(
				'task'            => 'Generate safe SEO improvements for the current WordPress editor draft.',
				'response_schema' => $contract,
			'strict_limits'   => array(
				'seo_title_chars'        => '45-58 characters. REPHRASE to fit, NEVER truncate mechanically. Include the exact primary focus keyword verbatim. Do not append site name or separator variables like %sep% or %sitename%.',
				'meta_description_chars' => '140-155 characters. REPHRASE into a complete compelling sentence, NEVER truncate. Include the exact primary focus keyword verbatim at least once.',
				'slug_chars'             => 'Return ONLY the slug portion (no domain, no protocol, no path prefix). Keep it concise but do not mechanically cut important words. Lowercase latin words separated by hyphens. Include the focus keyword meaning. WordPress will transliterate the slug server-side via Cyr To Lat plugin, so provide a reasonable transliteration yourself; the server will normalize it.',
				'keyword_density'        => 'Aim for 1.0-1.5 percent in visible content without unnatural repetition.',
				'primary_keyword_words'  => '2-3 words maximum. Choose a natural search phrase in base/dictionary form, grounded in the article topic and repeated terms.',
				'secondary_keywords'     => 'Next 4 natural multi-word search phrases (2+ words each) grounded in the article text.',
				'keyword_exact_match'    => 'The primary focus keyword MUST appear VERBATIM (exact character-by-character match) in both seo_title and meta_description. Do NOT inflect, decline, or conjugate any word of the keyword phrase. Restructure the sentence so the exact keyword form fits grammatically.',
			),
			'rules'           => array(
				'Keep the same language as the post unless language explicitly says otherwise.',
				'To choose focus_keywords: analyze input.post.content_plain_text, title, headings, and SEO metadata. Use natural nominative/base search phrases. The top phrase (2-3 words) becomes the primary keyword (first element). The next 4 phrases become secondary keywords. Do NOT choose a random inflected occurrence when a natural search form is clear from the topic.',
				'CRITICAL: the primary focus keyword must appear VERBATIM (identical characters, no inflection) in seo_title and meta_description. For languages with inflection, do NOT change word endings. Restructure the sentence so the exact keyword form fits grammatically.',
				'NEVER truncate seo_title or meta_description mechanically. Always REPHRASE to fit the character limit while preserving the primary focus keyword and meaning.',
				'For slug: return ONLY the slug string (e.g. "remont-kozhanykh-sapog"), NOT the full URL. Transliterate non-latin characters to latin. The server uses WordPress sanitize_title + Cyr To Lat plugin to produce the final slug, so the exact transliteration table may differ from yours — that is expected. A transliterated slug that conveys the focus keyword meaning is CORRECT — do NOT report it as an error.',
				'For keywordInPermalink test: if the existing slug already contains a reasonable transliteration of the focus keyword, consider this test PASSED. Add it to fixed_tests, do NOT include it in skipped_tests or warnings.',
				'For keywordIn10Percent test: insert the exact primary keyword (in nominative/base form, without inflection) into the first paragraph via a content_operations entry (type "replace" on the first text block). Restructure the opening sentence to include the keyword naturally. Do NOT skip this test.',
				'For contentHasTOC test: if the article has H2 or H3 headings, return a content_operations entry with type "insert_before", reason "toc", testId "contentHasTOC", and blockClientId set to the first heading block. Do NOT generate final TOC HTML; the Rank Math provider will render the native Rank Math Table of Contents block. Use html as a small placeholder div only.',
				'Target natural keyword density around 1-1.5 percent; never repeat the keyword mechanically.',
				'For keyword density, add the focus keyword to the intro, one body paragraph, and one subheading only if it reads naturally.',
				'For heading tests, update one existing H2/H3 instead of adding unnecessary headings.',
				'For internal link tests, only use URLs from input.post.internal_link_candidates and return internal_link_operations.',
				'For external link tests, do not invent URLs; return clear warnings unless trusted external URLs already exist in content.',
				'For Content AI and uniqueness tests, add skipped_tests entries instead of trying to fake plugin state.',
				'For image alt updates, only update existing image blocks present in the input.',
				'For content edits, prefer small operations over rewriting the whole article.',
			),
				'input'           => $payload,
			),
			JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT
		);
	}
}
