<?php
defined( 'ABSPATH' ) || exit;

/**
 * Builds prompt interpolation maps and article-type context for the pipeline.
 */
final class Costudio_Pipeline_Input_Mapper {

	private function __construct() {
	}

	public static function interpolate( string $template, array $inputs, float $default_calibration ): string {
		$map = self::map_inputs_for_secure_execution( $inputs, $default_calibration );
		return str_replace( array_keys( $map ), array_values( $map ), $template );
	}

	/**
	 * Prepares the replacement map for prompt variables.
	 * Shared between local interpolation and secure remote execution.
	 */
	public static function map_inputs_for_secure_execution( array $inputs, float $default_calibration ): array {
		$extensao = $inputs['extensao'] ?? '2000-2600';
		$min_max  = self::parse_extensao( $extensao, $default_calibration );

		$cts        = costudio_settings();
		// Prefer per-production snapshot (set at creation time) over live global
		// settings so that async pipeline steps see a consistent image mode even
		// if the user changes settings while the production is running.
		$image_qty  = isset( $inputs['costudio_image_quantity'] )
			? intval( $inputs['costudio_image_quantity'] )
			: ( $cts ? intval( $cts->get( 'costudio_image_quantity', 2 ) ) : 2 );
		$image_ai_mode = (string) ( $inputs['execution_ai_mode'] ?? $inputs['ai_mode'] ?? ( $cts ? $cts->get( 'ai_mode', '' ) : '' ) );
		$image_qty  = class_exists( 'Costudio_Standalone_Settings' )
			? Costudio_Standalone_Settings::clamp_image_quantity( $image_qty, $image_ai_mode )
			: max( 1, min( 3, $image_qty ) );
		$image_mode = isset( $inputs['costudio_image_mode'] )
			? (string) $inputs['costudio_image_mode']
			: ( $cts ? (string) $cts->get( 'costudio_image_mode', 'generate' ) : 'generate' );

		if ( 'none' === $image_mode ) {
			$image_qty  = 0;
			$inline_qty = 0;
		} elseif ( 'placeholder' === $image_mode ) {
			// In suggest-placement mode the Writer decides autonomously how many
			// inline markers to insert — $image_qty still controls the cover (1).
			$inline_qty = 'auto';
		} else {
			$inline_qty = max( 0, $image_qty - 1 );
		}

		$base_color = ! empty( $inputs['site_color'] ) ? $inputs['site_color'] : self::get_nicho_color( $inputs['nicho'] ?? '', 'main' );
		$soft_color = ! empty( $inputs['site_color'] ) ? self::get_soft_color( $base_color ) : self::get_nicho_color( $inputs['nicho'] ?? '', 'soft' );

		return array(
			'[NICHE]'               => $inputs['categoria'] ?? ( $inputs['nicho'] ?? '' ),
			'[SITE_NAME]'           => $inputs['nicho'] ?? '',
			'[KEYWORD]'             => $inputs['keyword'] ?? '',
			'[LANGUAGE]'            => $inputs['lang_name'] ?? 'English (US)',
			'[CONTEXT]'             => self::build_input_context( $inputs ),
			'[LENGTH]'              => $extensao,
			'[LENGTH_MAX]'          => (string) $min_max['max_prompt'],
			'[LENGTH_TARGET]'       => (string) $min_max['alvo_prompt'],
			'[INTRO_WORDS]'         => (string) (int) round( $min_max['alvo_prompt'] * 0.10 ),
			'[MAX_SECTIONS]'        => (string) max( 4, (int) round( $min_max['alvo'] / 450 ) ),
			'[MAX_PARAGRAPHS]'      => $min_max['alvo'] >= 2500 ? '3' : '2',
			'[ANGLE]'               => $inputs['angulo'] ?? '',
			'[READER_PROFILE]'      => $inputs['perfil'] ?? '',
			'[ARTICLE_TYPE]'        => Costudio_Article_Types::normalize( $inputs['article_type'] ?? '' ),
			'[PLATFORMS]'           => 'Instagram, Facebook, Pinterest, GBP',
			'[TOTAL_IMAGES]'        => (string) $image_qty,
			'[TOTAL_IMAGES_INLINE]' => (string) $inline_qty,
			'[NICHE_COLOR]'         => $base_color,
			'[NICHE_COLOR_SOFT]'    => $soft_color,
			'[YEAR]'                => gmdate( 'Y' ),
			'[MONTH/YEAR]'          => gmdate( 'm/Y' ),
			// Authorship and publication metadata — populated for any article
			// type, but only consumed by the news pipeline (byline + schema).
			// Other types get empty strings, which their prompts ignore.
			'[AUTHOR_NAME]'         => self::resolve_author_name( $inputs ),
			'[AUTHOR_URL]'          => self::resolve_author_url( $inputs ),
			'[PUBLISHER_NAME]'      => self::resolve_publisher_name( $inputs ),
			'[PUBLISHER_LOGO_URL]'  => self::resolve_publisher_logo_url(),
			'[ISO_DATETIME]'        => self::resolve_iso_datetime(),
			'[SITE_TIMEZONE]'       => self::resolve_site_timezone(),
		);
	}

	/**
	 * Resolve the human display name of the author WP_User. Falls back to an
	 * empty string when the input is missing or the user no longer exists —
	 * the news validator blocks invalid IDs upstream, so an empty value here
	 * means "not a news pipeline".
	 */
	private static function resolve_author_name( array $inputs ): string {
		$author_id = (int) ( $inputs['news_author_id'] ?? 0 );
		if ( $author_id <= 0 ) {
			return '';
		}
		$user = function_exists( 'get_userdata' ) ? get_userdata( $author_id ) : null;
		return $user ? (string) $user->display_name : '';
	}

	/**
	 * Resolve the public author archive URL (the WP /author/{slug}/ page).
	 * Used by the news schema (author.url) and by the visible byline link.
	 */
	private static function resolve_author_url( array $inputs ): string {
		$author_id = (int) ( $inputs['news_author_id'] ?? 0 );
		if ( $author_id <= 0 || ! function_exists( 'get_author_posts_url' ) ) {
			return '';
		}
		return (string) get_author_posts_url( $author_id );
	}

	/**
	 * Publisher name shown in schema.org Publisher block. Prefers the niche
	 * label entered for the production (matches what readers see as the
	 * site brand) and falls back to the WordPress site name.
	 */
	private static function resolve_publisher_name( array $inputs ): string {
		$niche = trim( (string) ( $inputs['nicho'] ?? '' ) );
		if ( '' !== $niche ) {
			return $niche;
		}
		return function_exists( 'get_bloginfo' ) ? (string) get_bloginfo( 'name' ) : '';
	}

	/**
	 * Publisher logo URL for schema.org Publisher.logo.url. WordPress' Site
	 * Icon (configured in Customizer) is the canonical source; returns empty
	 * string when no icon is configured so the schema can omit the property
	 * rather than ship a broken URL.
	 */
	private static function resolve_publisher_logo_url(): string {
		if ( function_exists( 'get_site_icon_url' ) ) {
			$url = (string) get_site_icon_url( 512 );
			if ( '' !== $url ) {
				return $url;
			}
		}
		return '';
	}

	/**
	 * Current datetime in ISO 8601 with the site's configured timezone.
	 * Format example: 2026-05-21T14:32:00-03:00. This is what the news
	 * prompt injects as "publication moment context" for the agent; the
	 * actual datePublished sent to the schema is re-resolved by the
	 * Publisher at publication time (so scheduled drafts get the right
	 * datetime, not the prompt-generation moment).
	 */
	private static function resolve_iso_datetime(): string {
		if ( function_exists( 'wp_date' ) ) {
			return (string) wp_date( 'c' );
		}
		return gmdate( 'c' );
	}

	private static function resolve_site_timezone(): string {
		if ( function_exists( 'wp_timezone_string' ) ) {
			return (string) wp_timezone_string();
		}
		return (string) date_default_timezone_get();
	}

	/**
	 * Lightens a hex color to be used as a soft background.
	 */
	public static function get_soft_color( string $hex ): string {
		$hex = ltrim( $hex, '#' );
		if ( 3 === strlen( $hex ) ) {
			$hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
		}
		$r = hexdec( substr( $hex, 0, 2 ) );
		$g = hexdec( substr( $hex, 2, 2 ) );
		$b = hexdec( substr( $hex, 4, 2 ) );
		$r = (int) round( $r * 0.10 + 255 * 0.90 );
		$g = (int) round( $g * 0.10 + 255 * 0.90 );
		$b = (int) round( $b * 0.10 + 255 * 0.90 );
		return sprintf( '#%02x%02x%02x', $r, $g, $b );
	}

	/**
	 * Returns a hex color pair (main/soft) for the UI based on the content niche.
	 */
	public static function get_nicho_color( string $nicho, string $type = 'main' ): string {
		$matched = array(
			'main' => '#3b82f6',
			'soft' => '#eff6ff',
		);
		return $matched[ $type ];
	}

	/**
	 * Build the universal article-type context block.
	 */
	public static function build_type_context( array $inputs ): string {
		$type = Costudio_Article_Types::normalize( $inputs['article_type'] ?? '' );

		switch ( $type ) {
			case Costudio_Article_Types::PILLAR:
				$base_color = ! empty( $inputs['site_color'] ) ? $inputs['site_color'] : self::get_nicho_color( $inputs['nicho'] ?? '', 'main' );
				$soft_color = self::get_soft_color( $base_color );

				$block  = "## ARTICLE TYPE: PILLAR\n\n";
				$block .= "**Mission**: Produce the DEFINITIVE reference article on this topic for the reader's market and language. Not an overview — the deepest, most authoritative treatment of the subject available in this niche.\n\n";
				$block .= "### Structural requirements (all agents must respect)\n";
				$block .= "- **Minimum 5 H2 development sections** (excluding Introduction, TLDR, FAQ, and Conclusion).\n";
				$block .= "- **TLDR block — MANDATORY**: 134–167 words in `<!-- wp:html --><div style=\"background:{$soft_color};border-left:4px solid {$base_color};padding:16px 20px;border-radius:6px;margin-bottom:24px\">`. This range is the statistical sweet spot for AI Overview citation (Ahrefs/Backlinko March 2026). A ~80-word TLDR is outdated — do NOT produce one.\n";
				$block .= "- **Introduction**: approximately 10% of the target word count (1–2 paragraphs).\n";
				$block .= "- **FAQ — MANDATORY**: 4–8 questions drawn from real PAA / People Also Ask data. Vary the count — never exactly 5 (a fixed count signals AI authorship).\n";
				$block .= "- **Conclusion**: approximately 120 words (1 paragraph).\n";
				$block .= "- **Minimum 2 citations** from high-authority sources: academic institutions, regulatory bodies, government databases, or peer-reviewed studies.\n\n";
				$block .= "### Tone and positioning\n";
				$block .= "- Position this article as the SINGLE definitive resource on the topic.\n";
				$block .= "- Depth over breadth: develop each section with evidence, examples, data, and original analysis — not surface summaries.\n";
				$block .= "- Include **at least 2 original assertions** (Information Gain) not found in the top-10 competing results. This is the dominant SEO quality signal since Core Update March 2026.\n";
				$block .= "- State facts clearly and attribute them. Do NOT hedge to the point of vagueness.\n";
				return rtrim( $block );

			case Costudio_Article_Types::COMPARISON:
				$base_color          = ! empty( $inputs['site_color'] ) ? $inputs['site_color'] : self::get_nicho_color( $inputs['nicho'] ?? '', 'main' );
				$comparison_items    = $inputs['comparison_items'] ?? array();
				$comparison_criteria = $inputs['comparison_criteria'] ?? '';
				$total_items         = count( $comparison_items );

				$block  = "## ARTICLE TYPE: COMPARISON (Bottom-of-Funnel Product Review)\n\n";
				$block .= "**Mission**: Help a purchase-ready reader make a confident buying decision. Target the buyer queries for this product category (e.g. \"best [product]\", \"which [product] to buy\", \"top [product]\"). Tone: impartial, data-driven, consultative — NEVER salesy.\n\n";
				$block .= "**Language**: this structure is written in English for clarity only. Every reader-facing string it asks you to produce — section headings, table headers, badges, button text, verdicts, alt text — MUST be written in the article's output language (the OUTPUT LANGUAGE rule injected above is authoritative). The tokens `[FAVORITOS_BOX]`, `[COMPRAR: …]`, `[PROS]`, `[CONTRAS]` and `[IMG: …]` are literal system markers — keep the marker names EXACTLY as written, never translate them.\n\n";

				// ── Products list ───────────────────────────────────────────────
				$block .= "### Products / Items Being Compared ({$total_items} total)\n\n";
				foreach ( $comparison_items as $idx => $item ) {
					$label  = trim( $item['label'] ?? '' );
					$url    = trim( $item['url'] ?? '' );
					$num    = $idx + 1;
					$block .= "**{$num}.** {$label}";
					if ( ! empty( $url ) ) {
						$block .= " — affiliate URL: {$url}";
					}
					$block .= "\n";
				}
				$block .= "\n";

				$block .= "**Comparison criteria**: " . ( ! empty( $comparison_criteria )
					? $comparison_criteria
					: 'define the most relevant criteria for this product category and reader (e.g. price, performance, ease of use, durability, warranty).'
				) . "\n\n";

				// ── Mandatory structure ─────────────────────────────────────────
				$block .= "---\n\n";
				$block .= "## MANDATORY ARTICLE STRUCTURE (follow in exact order)\n\n";

				$block .= "### 1. Metadata — PLAIN TEXT, first 3 lines, no Markdown\n";
				$block .= "```\nTitle: [H1 title targeting \"best [product]\" or \"top [product]\"]\nMeta Description: [150–160 chars, include main keyword + benefit]\nSlug: [slug]\n```\n\n";

				$block .= "### 2. Introduction (~150 words, 1–2 paragraphs)\n";
				$block .= "Hook: frame the pain of choosing wrong (e.g. \"picking the wrong model can cost you dearly\"). Establish why this comparison is trustworthy. Do NOT name individual products yet. Close with: announce that the article compares [N] options and explains how to choose.\n\n";

				$block .= "### 3. \"Our Picks\" box\n";
				$block .= "Write this marker on its own line — the CMS replaces it with a styled highlight box showing the top products (the box title is rendered by the CMS in the output language):\n\n";
				$block .= "[FAVORITOS_BOX]\n\n";

				$block .= "### 4. Quick Comparison Table (Markdown)\n";
				$block .= "Columns: **Product** | **Highlight** | **Price Range** | **Best For** (write the header labels in the output language). One row per product, {$total_items} rows total. Use ONLY the listed products — NEVER add unlisted items.\n\n";

				// ── Per-product section instructions ────────────────────────────
				$block .= "### 5. Individual Product Sections (one H2 per product, in the listed order)\n\n";
				$block .= "Repeat the following sub-structure for EACH of the {$total_items} products:\n\n";
				$block .= "```\n";
				$block .= "## [Product Name]\n\n";
				$block .= "_[Badge]_\n\n";
				$block .= "[IMG: product-N.webp | Alt: concise visual description of the product]\n\n";
				$block .= "[COMPRAR: URL | Button text]\n\n";
				$block .= "[Evaluative paragraph — 100–140 words. Explain what the product is, who it suits best, its standout feature, and one honest limitation. Write naturally in the output language — no bullet-point prose.]\n\n";
				$block .= "[PROS]\n";
				$block .= "- Specific advantage 1\n";
				$block .= "- Specific advantage 2\n";
				$block .= "- Specific advantage 3\n";
				$block .= "[/PROS]\n\n";
				$block .= "[CONTRAS]\n";
				$block .= "- Specific disadvantage 1\n";
				$block .= "- Specific disadvantage 2\n";
				$block .= "[/CONTRAS]\n";
				$block .= "```\n\n";

				// ── Badge rules ─────────────────────────────────────────────────
				$block .= "**Badge rules** — assign exactly ONE distinct badge per product. Use these concepts, written in the output language:\n";
				$block .= "⭐ Best Value · 🏆 Editor's Choice · 💡 Best for Beginners · 🔥 Most Complete · 💰 Best Premium · ✅ Most Versatile · 🎯 Most Specialized · 🌟 Customer Favorite\n";
				$block .= "No two products may share the same badge. Base each badge on your analysis — it must be defensible by the product's features or positioning.\n\n";

				// ── Marker rules ────────────────────────────────────────────────
				$block .= "**[COMPRAR: URL | Button text] rules:**\n";
				$block .= "- URL: use the affiliate URL from the product list above. If the product has no URL, omit the marker entirely.\n";
				$block .= "- Button text (reader-facing — write in the output language): \"Buy on Amazon\" if the URL contains `amazon.com` or `amzn.to`; \"View on Mercado Livre\" if `mercadolivre.com`; otherwise a neutral \"View offer\".\n";
				$block .= "- The CMS automatically applies rel=\"nofollow noopener sponsored\" — you do not need to write it.\n\n";

				$block .= "**[IMG: product-N.webp | Alt] rules:**\n";
				$block .= "- N is the sequential product number: product-1.webp for the first product, product-2.webp for the second, etc.\n";
				$block .= "- Do NOT use imagem1.webp (reserved for the cover image). Start at product-1.webp.\n";
				$block .= "- Alt text (reader-facing — write in the output language): describe the product visually and concisely.\n";
				$block .= "- The CMS renders these as editorial image placeholders for manual replacement.\n\n";

				$block .= "**[PROS] / [CONTRAS] rules:**\n";
				$block .= "- 3–5 bullet points each. Every bullet must be specific and factual — no filler (\"good design\", \"nice quality\").\n";
				$block .= "- Write concrete buyer-relevant points: specs, real-world performance, warranty, compatibility, common complaints.\n";
				$block .= "- The CMS converts these markers to styled green/red boxes automatically.\n\n";

				// ── Remaining sections ───────────────────────────────────────────
				$block .= "### 6. Buying Guide (2–3 H2 sections)\n";
				$block .= "Educate the reader on what criteria matter for this product category — independently of which product they choose. Examples (in the output language): \"How to Choose the Right Power Rating\", \"Safety Certifications You Should Require\", \"Price vs. Durability: Finding the Balance\". Do NOT push toward a specific product here.\n\n";

				$block .= "### 7. Final Verdict\n";
				$block .= "One concise paragraph per reader profile (e.g. \"If you want the best value → [Product A]. If you need advanced features → [Product B].\"). Back every recommendation with a reason from the analysis. No vague superlatives.\n\n";

				$block .= "### 8. FAQ (4–6 questions)\n";
				$block .= "Real pre-purchase questions a buyer would type into a search engine. Avoid generic SEO filler. Vary the count — never exactly 5.\n\n";

				$block .= "### 9. Conclusion (~100 words, 1 paragraph)\n";
				$block .= "Summarise the key decision signals. Close with a neutral call to action (e.g. \"compare current prices before deciding\"). Do NOT name a single \"winner\" here — the Final Verdict already did that.\n\n";

				// ── Hard rules ───────────────────────────────────────────────────
				$block .= "---\n\n";
				$block .= "### Hard Rules\n";
				$block .= "- ONLY the {$total_items} listed products may appear in Sections 3–5 — NEVER invent or add unlisted items.\n";
				$block .= "- Every spec, price range, or feature claim must be supportable by research. No fabricated data.\n";
				$block .= "- Avoid superlatives unless anchored to a specific data point.\n";
				$block .= "- Do NOT write a `## References` section — the CMS manages bibliographic data.\n";
				$block .= "- Do NOT use the Writer's standard article-structure rules — this override replaces them entirely.\n";

				return rtrim( $block );

			case Costudio_Article_Types::NEWS:
				// News pipeline context block — injected at the top of every
				// agent's message when article_type === news. The block is
				// intentionally written in English: it instructs agents about
				// the universal news principles (synthesis, DNA absorption,
				// language adaptation) and lets each agent's own prompt handle
				// the language-specific section rules. The output of the
				// pipeline itself is produced in [LANGUAGE], not English.
				$author_name    = self::resolve_author_name( $inputs );
				$author_url     = self::resolve_author_url( $inputs );
				$publisher_name = self::resolve_publisher_name( $inputs );
				$publisher_logo = self::resolve_publisher_logo_url();
				$iso_datetime   = self::resolve_iso_datetime();
				$site_timezone  = self::resolve_site_timezone();
				$language       = (string) ( $inputs['lang_name'] ?? 'English (US)' );

				$block  = "## ARTICLE TYPE: NEWS\n\n";
				$block .= "**Mission**: produce ORIGINAL news content in {$language}, synthesised from the multiple inspiration sources listed below. Sources are INSPIRATION — never material to copy.\n\n";

				$block .= "### Multi-source synthesis principle\n";
				$block .= "- Read EVERY source URL provided (sources may be in any language — read them anyway).\n";
				$block .= "- Extract facts, attributed quotes (with name + role + institution), data points, context, next steps, and divergences between sources.\n";
				$block .= "- Identify the **EDITORIAL DNA** of the sources: tone (factual / explanatory / analytical / urgent / conversational), average paragraph length, attribution patterns, presence of sublead, presence of explainer boxes.\n";
				$block .= "- Build your OWN narrative. NEVER copy a source verbatim. NEVER mirror a single source's angle. Synthesise.\n";
				$block .= "- The Writer will MIRROR the editorial DNA the Researcher identifies, adapted to the output language — this is how the news adopts the right voice for its market without the pipeline hard-coding regional styles.\n\n";

				$block .= "### Output language\n";
				$block .= "{$language}. Translate facts and absorb tone from sources of any language. Do NOT preserve original-language phrases in the body. Idioms and figures of speech belong to the output language, not the source language.\n\n";

				$block .= "### Universal news structure (applies to every market)\n";
				$block .= "1. Sublead / standfirst (optional — include when the DNA shows sources commonly use one): one sentence ~150 chars, contextualises without repeating the title.\n";
				$block .= "2. Lede (1-2 paragraphs): the main fact with a factual hook (number, concrete action, relevant change). NEVER use narrative hooks (scene, anecdote) — that belongs to features/columns, not news.\n";
				$block .= "3. Context (2-3 paragraphs): background, why it matters now.\n";
				$block .= "4. Detail (2-4 paragraphs): how it happened, attributed quotes, specific data with sources.\n";
				$block .= "5. Reaction / Next steps (1-2 paragraphs): official position, expected outcomes, what comes next.\n";
				$block .= "6. \"Explainer\" box (optional H2 — include when DNA shows sources use it AND the topic is technically dense): 3-5 bullets explaining the topic for newcomers.\n";
				$block .= "7. Closing (1 short paragraph): hook to next development OR final contextualisation.\n\n";

				$block .= "### Hard rules for every agent\n";
				$block .= "- NO FAQ section (FAQ belongs to pillar/satellite/comparison, never news).\n";
				$block .= "- NO `## References` section (the CMS assembles the bibliography automatically).\n";
				$block .= "- NEVER invent quotes, names, numbers, dates, or institutions. Use only what the Researcher extracted from the sources.\n";
				$block .= "- Quotes must be attributed in the body: name + role + institution.\n";
				$block .= "- Critical numerical data must mention its source in the same sentence or paragraph.\n\n";

				$block .= "### Authorship metadata (for byline + schema — DO NOT alter)\n";
				$block .= "- Author name: {$author_name}\n";
				$block .= "- Author URL: {$author_url}\n";
				$block .= "- Publisher: {$publisher_name}\n";
				if ( '' !== $publisher_logo ) {
					$block .= "- Publisher logo: {$publisher_logo}\n";
				}
				$block .= "- Publication datetime: {$iso_datetime}\n";
				$block .= "- Timezone: {$site_timezone}\n\n";

				$block .= "### Inspiration sources\n";
				$news_sources = $inputs['news_sources'] ?? array();
				$has_source   = false;
				foreach ( $news_sources as $source ) {
					$url = $source['url'] ?? '';
					if ( ! empty( $url ) ) {
						$block    .= "- {$url}\n";
						$has_source = true;
					}
				}
				if ( ! $has_source ) {
					$block .= "(No sources provided — validator should have blocked this; emit a clear error if you see this line.)\n";
				}

				return rtrim( $block );

			case Costudio_Article_Types::UPDATE:
				$update_post_title   = $inputs['update_post_title'] ?? '';
				$update_post_url     = $inputs['update_post_url'] ?? '';
				$update_instructions = $inputs['update_instructions'] ?? '';
				$update_post_content = $inputs['update_post_content'] ?? '';

				// Split the original article into H2-delimited sections and
				// render them as a numbered Markdown listing. The agent edits
				// per-section using the [SECTION N] protocol; sections it
				// doesn't touch are preserved byte-for-byte by the publisher
				// at rebuild time. See Costudio_Update_Sectioner.
				$sections    = Costudio_Update_Sectioner::split( $update_post_content );
				$sections_md = Costudio_Update_Sectioner::render_for_prompt( $sections );
				$total       = count( $sections );

				// This block is injected at the TOP of the agent's message,
				// before the standard Writer prompt. Because the Writer prompt
				// is heavily biased toward "write a full SEO article" (with
				// detailed sections for metadata/intro/TLDR/FAQ/conclusion),
				// the UPDATE override has to be loud enough to dominate the
				// agent's attention — otherwise it falls back to its default
				// writer identity and rewrites the article from scratch.
				$block  = "## ⚠️ ABSOLUTE OVERRIDE — UPDATE MODE ACTIVE ⚠️\n\n";
				$block .= "**This is an UPDATE task, NOT a writing task. Most of the prompt below describes how to write a NEW article — IGNORE those rules. Your task is completely different.**\n\n";
				$block .= "### ❌ THINGS YOU MUST NOT DO\n";
				$block .= "- ❌ DO NOT write a `Título:` / `Meta Description:` / `Slug:` header\n";
				$block .= "- ❌ DO NOT write an introduction, TLDR block, FAQ, or conclusion\n";
				$block .= "- ❌ DO NOT add `[IMG:...]` markers\n";
				$block .= "- ❌ DO NOT cite new sources or write a `## Referências` section\n";
				$block .= "- ❌ DO NOT write a complete article body in Markdown\n";
				$block .= "- ❌ DO NOT follow the `## ESTRUTURA OBRIGATÓRIA DO ARTIGO` rules\n";
				$block .= "- ❌ DO NOT follow the `## OUTPUT` rules for pillar/satellite/comparison/news\n";
				$block .= "- ❌ DO NOT emit a `# H1` heading ANYWHERE — WordPress renders the post title as `<h1>` of the page automatically. An `# H1` in the body creates a double-title.\n";
				$block .= "- ❌ DO NOT repeat, paraphrase, or restate the post title (\"" . $update_post_title . "\") as a heading inside the body. That includes appending \" em 2026\" / \" in 2026\" / \" — Updated\" to the title and emitting it as `## ...`. The title is shown above the article — never duplicate it.\n";
				$block .= "- ❌ DO NOT put a heading on top of SECTION 0 (the intro). Section 0 is just paragraphs that come BEFORE the first H2 — when you EDIT section 0, the first line of your replacement MUST be a paragraph, not a `## ...` line.\n\n";
				$block .= "### ✅ WHAT YOU MUST DO\n";
				$block .= "- ✅ List EVERY section below in order (sections 0 to " . max( 0, $total - 1 ) . ") with exactly ONE action each: KEEP, EDIT, or DELETE\n";
				$block .= "- ✅ Read the USER UPDATE INSTRUCTIONS below and interpret them SEMANTICALLY — a single instruction usually touches multiple sections (e.g. \"update the year to 2026\" applies to EVERY section that mentions a year, dated stat, or temporal reference)\n";
				$block .= "- ✅ Sections marked `[BLOCO PRESERVADO — não editar diretamente]` MUST receive KEEP (FAQ / schema / custom widgets that break if rewritten)\n";
				$block .= "- ✅ In EDIT, write the new full section in Markdown on the line after the EDIT keyword, starting with `## Heading`\n";
				$block .= "- ✅ In EDIT, keep image URLs and internal links IDENTICAL to the originals\n";
				$block .= "- ✅ Add new sections via `[INSERT AFTER SECTION N]` when the update instructions ask for them\n\n";
				$block .= "### 🎯 MINIMUM ACTIVITY RULE\n";
				$block .= "If the USER UPDATE INSTRUCTIONS below contain ANY actionable request, AT LEAST ONE section MUST receive a non-KEEP action (EDIT / DELETE / INSERT). Returning all KEEPs is only acceptable when the instructions are empty, explicitly say \"do not change anything\", or have zero overlap with any section's content. Defaulting to all-KEEP because the instructions don't name a specific section number is the WRONG behaviour — interpret them semantically.\n\n";
				$block .= "### 🧭 DECISION PROTOCOL (apply per section, in order)\n";
				$block .= "1. Re-read the USER UPDATE INSTRUCTIONS.\n";
				$block .= "2. Extract the SCOPE of each instruction:\n";
				$block .= "   - \"update the year / dates / stats\" → EVERY section mentioning a year, date, or dated statistic\n";
				$block .= "   - \"add a part about X\" → emit `[INSERT AFTER SECTION N]` at the most logical position\n";
				$block .= "   - \"remove the part about Y\" → DELETE the matching section(s)\n";
				$block .= "   - \"modernise / improve / refresh\" → EDIT the sections that most benefit (intro, conclusion, dense or outdated ones)\n";
				$block .= "   - \"fix a specific section\" → EDIT only that section\n";
				$block .= "3. For each section, ask: \"Does this section contain content that benefits from any instruction above?\" If YES → EDIT (or DELETE / INSERT as appropriate). If NO → KEEP.\n";
				$block .= "4. Before submitting, count your non-KEEP actions. If the instructions were actionable and the count is zero, GO BACK and revise — you missed the implicit scope.\n\n";
				$block .= "### 📚 WORKED EXAMPLES\n\n";
				$block .= "**Example A — Instructions: \"Update the year to 2026 and refresh anything important.\"**\n";
				$block .= "```\n";
				$block .= "[SECTION 0]\n";
				$block .= "EDIT\n";
				$block .= "## (Intro that mentions \"in 2025\" — change to \"in 2026\", refresh framing, keep tone)\n\n";
				$block .= "[SECTION 1]\n";
				$block .= "KEEP   ← timeless conceptual discussion, no dates\n\n";
				$block .= "[SECTION 2]\n";
				$block .= "EDIT\n";
				$block .= "## (Section with \"2024 study showed\" — contextualise data for 2026 or cite the original year)\n\n";
				$block .= "[SECTION 3]\n";
				$block .= "KEEP\n";
				$block .= "```\n\n";
				$block .= "**Example B — Instructions: \"Add a section about generative AI after the introduction.\"**\n";
				$block .= "```\n";
				$block .= "[SECTION 0]\n";
				$block .= "KEEP\n\n";
				$block .= "[INSERT AFTER SECTION 0]\n";
				$block .= "## How Generative AI Reshapes This Topic\n\n";
				$block .= "(Full new section in Markdown…)\n\n";
				$block .= "[SECTION 1]\n";
				$block .= "KEEP\n";
				$block .= "```\n\n";
				$block .= "**Example C — Instructions: \"Remove the crypto part and modernise the conclusion.\"**\n";
				$block .= "```\n";
				$block .= "[SECTION 4]   (← the one about crypto)\n";
				$block .= "DELETE\n\n";
				$block .= "[SECTION 7]   (← the conclusion)\n";
				$block .= "EDIT\n";
				$block .= "## (Rewritten conclusion, modern tone, no AI-style closers)\n";
				$block .= "```\n\n";
				$block .= "### ARTICLE BEING UPDATED\n";
				$block .= sprintf( "Title: %s\nURL: %s\n\n", $update_post_title, $update_post_url );
				$block .= "### USER UPDATE INSTRUCTIONS\n";
				$block .= $update_instructions . "\n\n";
				$block .= "### SECTIONS ({$total} TOTAL)\n\n";
				$block .= $sections_md . "\n";
				$block .= "### ⛔ ANTI-ECHO RULE (CRITICAL)\n\n";
				$block .= "The USER UPDATE INSTRUCTIONS tell you WHAT to change. **You must actually make the change and write the new text.** For each EDIT section, write the paragraphs that will appear on the published page — NOT editorial notes, NOT instructions to yourself, NOT a summary of what should change.\n\n";
				$block .= "- ❌ ERRADO — ecoar instrução: `(Update dated statistic from University of Illinois study...)`\n";
				$block .= "- ❌ ERRADO — nota interna: `(Modernise this section for 2026 relevance)`\n";
				$block .= "- ✅ CORRETO — prosa real: `## Truque 1: Técnica Pomodoro\n\nPesquisas da Universidade de Illinois (2024) confirmam que...`\n\n";
				$block .= "If you cannot rewrite a section with confidence, use KEEP — never publish an editorial note as article content.\n\n";
				$block .= "### REQUIRED RESPONSE FORMAT\n\n";
				$block .= "Emit ONLY the protocol below. NO title line. NO metadata. NO commentary. NO ``` fences.\n\n";
				$block .= "```\n";
				$block .= "[SECTION 0]\n";
				$block .= "KEEP\n\n";
				$block .= "[SECTION 1]\n";
				$block .= "EDIT\n";
				$block .= "## Section Heading\n\n";
				$block .= "Full Markdown content for the section…\n\n";
				$block .= "[SECTION 2]\n";
				$block .= "DELETE\n\n";
				$block .= "[INSERT AFTER SECTION 2]\n";
				$block .= "## New Section\n\n";
				$block .= "Full Markdown for a brand-new section…\n\n";
				$block .= "[SECTION 3]\n";
				$block .= "KEEP\n";
				$block .= "```\n\n";
				$block .= "Now produce your response for sections 0 through " . max( 0, $total - 1 ) . " in order. Apply the DECISION PROTOCOL and the MINIMUM ACTIVITY RULE above — interpret the instructions semantically across all sections, not just the ones the user names by number.";
				return $block;

			case Costudio_Article_Types::SATELLITE:
			default:
				$base_color  = ! empty( $inputs['site_color'] ) ? $inputs['site_color'] : self::get_nicho_color( $inputs['nicho'] ?? '', 'main' );
				$soft_color  = self::get_soft_color( $base_color );
				$pilar_url   = $inputs['pilar_url'] ?? '';
				$pilar_title = $inputs['pilar_title'] ?? '';

				$block  = "## ARTICLE TYPE: SATELLITE\n\n";
				$block .= "**Mission**: Focused support article on ONE specific subtopic. Do NOT expand into adjacent areas — that is the Pillar's job.\n\n";
				$block .= "### Structural requirements (all agents must respect)\n";
				$block .= "- **Maximum 4 H2 development sections** (excluding Introduction, TLDR, FAQ, and Conclusion).\n";
				$block .= "- **TLDR block — MANDATORY**: 134–167 words in `<!-- wp:html --><div style=\"background:{$soft_color};border-left:4px solid {$base_color};padding:16px 20px;border-radius:6px;margin-bottom:24px\">`. Same AI Overview sweet spot as pillar — do NOT skip.\n";
				$block .= "- **Introduction**: approximately 10% of the target word count (1–2 paragraphs).\n";
				$block .= "- **FAQ — MANDATORY**: 4–6 questions. Vary the count — never exactly 5.\n";
				$block .= "- **Conclusion**: approximately 120 words (1 paragraph).\n\n";
				$block .= "### Internal linking requirement\n";
				if ( ! empty( $pilar_url ) ) {
					$block .= "**MANDATORY**: Insert an organic contextual link to the related Pillar Article inside a body paragraph — NOT only in a references block.\n";
					$block .= "- Pillar: [{$pilar_title}]({$pilar_url})\n\n";
				} else {
					$block .= "No pillar URL provided. Identify a relevant internal link opportunity organically if appropriate.\n\n";
				}
				$block .= "### Tone and positioning\n";
				$block .= "- Focused and actionable: the reader has ONE specific question — answer it completely.\n";
				$block .= "- Do NOT attempt topic breadth (scope creep dilutes the satellite's semantic signal).\n";
				$block .= "- Include at least 1 original assertion (Information Gain) not found in competing top-10 results.\n";
				return rtrim( $block );
		}
	}

	public static function parse_extensao( ?string $extensao, float $default_calibration ): array {
		$min      = 2000;
		$max      = 2600;
		$extensao = $extensao ?? '2000-2600';

		if ( preg_match( '/^\s*(\d+)\s*[-–]\s*(\d+)\s*$/', trim( $extensao ), $m ) ) {
			$min = max( 500, (int) $m[1] );
			$max = max( $min, (int) $m[2] );
		}
		$calibration = self::get_word_count_calibration( $default_calibration );
		$alvo        = (int) round( ( $min + $max ) / 2 );
		$alvo_prompt = (int) round( $alvo * $calibration );
		$max_prompt  = (int) round( $max * $calibration );
		return array(
			'min'         => $min,
			'max'         => $max,
			'alvo'        => $alvo,
			'alvo_prompt' => $alvo_prompt,
			'max_prompt'  => $max_prompt,
			'calibration' => $calibration,
		);
	}

	public static function get_word_count_calibration( float $default_calibration ): float {
		$value = (float) apply_filters( 'costudio_word_count_calibration', $default_calibration );
		return max( 0.50, min( 1.00, $value ) );
	}

	public static function count_words( string $text ): int {
		$clean = wp_strip_all_tags($text );
		$clean = preg_replace( '/[\*\_\#\[\]\(\)\>\`\~\|]/', ' ', $clean );
		$clean = preg_replace( '/\s+/', ' ', trim( $clean ) );
		return empty( $clean ) ? 0 : count( preg_split( '/\s+/u', $clean ) );
	}

	public static function build_input_context( array $inputs ): string {
		// Labels are kept in neutral English so they do not act as a language
		// signal that biases the model away from the configured output language.
		// (The values themselves are whatever the user entered.)
		$parts = array();
		if ( ! empty( $inputs['article_type'] ) ) {
			$type     = Costudio_Article_Types::normalize( $inputs['article_type'] );
			$labels   = Costudio_Article_Types::get_labels();
			$parts[] = 'Article Type: ' . ( $labels[ $type ] ?? $type );
		}
		if ( ! empty( $inputs['angulo'] ) ) {
			$parts[] = "Angle: {$inputs['angulo']}";
		}
		if ( ! empty( $inputs['perfil'] ) ) {
			$parts[] = "Reader Profile: {$inputs['perfil']}";
		}
		if ( ! empty( $inputs['notas_adicionais'] ) ) {
			$parts[] = "Notes: {$inputs['notas_adicionais']}";
		}
		return implode( "\n", $parts );
	}
}
