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

/**
 * Token-saving article summaries passed between pipeline agents.
 */
final class Costudio_Pipeline_Summaries {

	private function __construct() {
	}

	/**
	 * Extracts headings, [IMG:] markers, metadata lines and the full FAQ section
	 * for the visual designer.
	 *
	 * @param string $discoverer_output Agent 01 raw output — source of `key_entities`. Optional.
	 * @param string $researcher_output Agent 03 raw output — source of `verified_facts`. Optional.
	 */
	public static function summarize_article_for_designer( string $text, array $inputs = array(), string $discoverer_output = '', string $researcher_output = '' ): string {
		$faq_block = '';
		if (
			preg_match(
				'/(^|\R)(##\s+(?:Perguntas\s+Frequentes|Preguntas\s+Frecuentes|Frequently\s+Asked\s+Questions|FAQ)\b[^\n]*)(.*?)(?=\R##\s|\z)/iu',
				$text,
				$faq_match
			)
		) {
			$faq_block = trim( $faq_match[2] . $faq_match[3] );
		}

		$lines     = explode( "\n", $text );
		$summary   = array();
		$intro_cnt = 0;

		foreach ( $lines as $line ) {
			$trimmed = trim( $line );
			if ( empty( $trimmed ) ) {
				continue;
			}

			if ( 0 === strpos( $trimmed, '#' ) ) {
				$summary[] = $trimmed;
				continue;
			}

			if ( false !== strpos( $trimmed, '[IMG:' ) ) {
				$summary[] = $trimmed;
				continue;
			}

			if ( preg_match( '/^(Título|Meta Description|Slug|SEO Title|Focus Keyword|URL Slug)\s*:/ui', $trimmed ) ) {
				$summary[] = $trimmed;
				continue;
			}

			if ( $intro_cnt < 3 ) {
				$summary[] = $trimmed;
				$intro_cnt++;
			}
		}

		$result = implode( "\n", $summary );

		if ( '' !== $faq_block ) {
			$result .= "\n\n=== FAQ INTEGRAL (use these exact Q&A pairs for the FAQPage schema) ===\n\n"
				. $faq_block;
		}

		$img_list = self::build_image_list_block( $text );
		if ( '' !== $img_list ) {
			$result .= "\n\n=== LISTA CANÓNICA DE IMAGENS INLINE (Obrigatório: um bloco de prompt por linha; o filename deve coincidir exatamente com o texto entre crases) ===\n\n"
				. $img_list;
		}

		$visual_context = self::build_visual_context_block( $text, $inputs, $discoverer_output, $researcher_output );
		if ( '' !== $visual_context ) {
			$result .= "\n\n" . $visual_context;
		}

		return $result;
	}

	/**
	 * Builds a compact, deterministic context packet for each image position.
	 * This prevents Agent 06 from inventing a generic scene without knowing the
	 * exact section and neighbouring argument that the image must support.
	 *
	 * Also carries the editorial-role machinery (Ciclo 3, Fatia 6): a heuristic
	 * `editorial_role` suggestion per asset, `key_entities` (from Agent 01) and
	 * `verified_facts` (from Agent 03) filtered to what's locally relevant, and
	 * `allows_data_visualization` — true ONLY when the role suggestion is
	 * evidence-data AND at least one verified fact actually matched locally.
	 * The model may confirm or override the role in its own VISUAL_PLAN
	 * response; Costudio_Visual_Plan enforces the rules for whichever role it
	 * actually claims, so this suggestion never has to be perfect.
	 */
	public static function build_visual_context_block( string $text, array $inputs = array(), string $discoverer_output = '', string $researcher_output = '' ): string {
		$lines         = preg_split( '/\R/', $text );
		$heading_stack = array();
		$paragraphs    = array();
		$markers       = array();
		$entity_pool   = self::extract_key_entities( $discoverer_output );
		$fact_pool     = self::extract_verified_facts( $researcher_output );

		foreach ( $lines as $line_number => $line ) {
			$trimmed = trim( (string) $line );
			if ( '' === $trimmed ) {
				continue;
			}

			if ( preg_match( '/^(#{2,6})\s+(.+)$/u', $trimmed, $heading_match ) ) {
				$level = strlen( $heading_match[1] );
				foreach ( array_keys( $heading_stack ) as $known_level ) {
					if ( $known_level >= $level ) {
						unset( $heading_stack[ $known_level ] );
					}
				}
				$heading_stack[ $level ] = self::context_text( $heading_match[2], 180 );
				ksort( $heading_stack );
				continue;
			}

			if ( preg_match_all( '/\[IMG:\s*([^\|\]]+)\s*\|\s*([^\]]+)\]/u', $trimmed, $image_matches, PREG_SET_ORDER ) ) {
				foreach ( $image_matches as $image_match ) {
					$markers[] = array(
						'line'         => (int) $line_number,
						'filename'     => self::context_text( $image_match[1], 120 ),
						'alt_text'     => self::context_text( $image_match[2], 220 ),
						'heading_path' => array_values( $heading_stack ),
					);
				}
				continue;
			}

			if ( 0 !== strpos( $trimmed, '[' ) && ! preg_match( '/^(Título|Title|Meta Description|Slug|SEO Title|Focus Keyword|URL Slug)\s*:/ui', $trimmed ) ) {
				$paragraphs[] = array(
					'line' => (int) $line_number,
					'text' => self::context_text( $trimmed, 420 ),
				);
			}
		}

		$cover_next = self::nearest_paragraph( $paragraphs, 0, 'after' );
		$cover_local_text = $cover_next;
		$cover_facts = self::relevant_matches( $fact_pool, $cover_local_text );
		$assets = array(
			array(
				'id'                        => 'cover',
				'role'                      => 'cover',
				'filename'                  => 'imagem1.webp',
				'heading_path'              => array(),
				'previous_text'             => '',
				'next_text'                 => $cover_next,
				'editorial_goal'            => 'Represent the article thesis without repeating an inline scene.',
				'editorial_role'            => 'cover',
				'key_entities'              => self::relevant_matches( $entity_pool, $cover_local_text ),
				'verified_facts'            => $cover_facts,
				'allows_data_visualization' => false, // The cover is never the evidence/data asset.
			),
		);

		foreach ( $markers as $marker_index => $marker ) {
			$previous_text = self::nearest_paragraph( $paragraphs, $marker['line'], 'before' );
			$next_text     = self::nearest_paragraph( $paragraphs, $marker['line'], 'after' );
			// alt_text is already an authored description of what the image
			// shows — a strong signal for the role suggestion (e.g. "...
			// interface" nudging toward product-ui) that heading/paragraph
			// text alone can miss.
			$local_text    = implode( ' ', array_merge( $marker['heading_path'], array( $previous_text, $next_text, $marker['alt_text'] ) ) );

			$matched_facts   = self::relevant_matches( $fact_pool, $local_text );
			$suggested_role  = self::suggest_editorial_role( $local_text );
			$allows_data_viz = 'evidence-data' === $suggested_role && array() !== $matched_facts;

			$assets[] = array(
				'id'                        => 'inline-' . ( $marker_index + 1 ),
				'role'                      => 'inline',
				'filename'                  => $marker['filename'],
				'alt_text'                  => $marker['alt_text'],
				'heading_path'              => $marker['heading_path'],
				'previous_text'             => $previous_text,
				'next_text'                 => $next_text,
				'editorial_goal'            => 'Clarify or extend the nearby argument; do not create decorative filler.',
				'editorial_role'            => $suggested_role,
				'key_entities'              => self::relevant_matches( $entity_pool, $local_text ),
				'verified_facts'            => $matched_facts,
				'allows_data_visualization' => $allows_data_viz,
			);
		}

		$context = array(
			'contract_version' => '1',
			'article'          => array(
				'keyword'        => self::context_text( (string) ( $inputs['keyword'] ?? '' ), 160 ),
				'editorial_angle' => self::context_text( (string) ( $inputs['angulo'] ?? '' ), 320 ),
				'reader_profile'  => self::context_text( (string) ( $inputs['perfil'] ?? '' ), 320 ),
				'article_type'    => self::context_text( (string) ( $inputs['article_type'] ?? '' ), 40 ),
			),
			'assets'           => $assets,
		);

		$json = function_exists( 'wp_json_encode' )
			? wp_json_encode( $context, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE )
			: json_encode( $context, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );

		return false === $json ? '' : "<VISUAL_CONTEXT>\n" . $json . "\n</VISUAL_CONTEXT>";
	}

	private static function nearest_paragraph( array $paragraphs, int $line, string $direction ): string {
		$candidates = array();
		foreach ( $paragraphs as $paragraph ) {
			if ( 'before' === $direction && $paragraph['line'] < $line ) {
				$candidates[] = $paragraph;
			} elseif ( 'after' === $direction && $paragraph['line'] > $line ) {
				$candidates[] = $paragraph;
			}
		}

		if ( array() === $candidates ) {
			return '';
		}

		$selected = 'before' === $direction ? end( $candidates ) : reset( $candidates );
		return (string) ( $selected['text'] ?? '' );
	}

	/**
	 * Structural match, not translated-header-text match — safe across the
	 * en_US/pt_BR/es_ES prompts. The LSI Entities table (Agent 01) always
	 * sits under a heading whose title contains "Entit" (Entity/Entities/
	 * Entidade/Entidades), and is a numbered markdown table whose first data
	 * column is the term itself.
	 */
	private static function extract_key_entities( string $discoverer_output ): array {
		if ( '' === trim( $discoverer_output ) ) {
			return array();
		}
		if ( ! preg_match( '/##[^\n]*Entit[^\n]*\n(.*?)(?:\n##|\n---|\z)/isu', $discoverer_output, $section ) ) {
			return array();
		}
		$entities = array();
		if ( preg_match_all( '/^\|\s*\d+\s*\|\s*([^|]+?)\s*\|/mu', $section[1], $rows ) ) {
			foreach ( $rows[1] as $term ) {
				$clean = self::context_text( $term, 80 );
				if ( '' !== $clean ) {
					$entities[] = $clean;
				}
			}
		}
		return array_values( array_unique( $entities ) );
	}

	/**
	 * Language-independent anchor: `{{ref:N}}` citation tokens are never
	 * translated (the established contract across all 3 locales — see the
	 * Researcher/Writer prompts), so matching on them to find Agent 03's
	 * validated-data table is safer than matching on translated header text.
	 * A row only counts as a "verified fact" when it actually carries a
	 * `{{ref:N}}` citation — no citation, no fact, never invented.
	 */
	private static function extract_verified_facts( string $researcher_output ): array {
		if ( '' === trim( $researcher_output ) ) {
			return array();
		}
		$facts = array();
		if ( preg_match_all( '/^\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|[^|]*\|\s*`?\{\{ref:\d+\}\}`?\s*\|/mu', $researcher_output, $rows, PREG_SET_ORDER ) ) {
			foreach ( $rows as $row ) {
				$claim = self::context_text( $row[1], 140 );
				$data  = self::context_text( $row[2], 160 );
				if ( '' !== $claim && '' !== $data ) {
					$facts[] = $claim . ': ' . $data;
				}
			}
		}
		return array_values( array_unique( $facts ) );
	}

	/**
	 * Filters a pool of entities/facts down to what's actually relevant to
	 * one asset's local text (heading path + neighbouring paragraphs) — the
	 * whole point of the context packet is NOT resending the whole article's
	 * pool to every single asset. No match, no field: empty is the safe
	 * degradation, never a fabricated placeholder.
	 */
	private static function relevant_matches( array $pool, string $local_text, int $limit = 5 ): array {
		$local_tokens = self::text_tokens( $local_text );
		if ( array() === $pool || array() === $local_tokens ) {
			return array();
		}
		$matches = array();
		foreach ( $pool as $item ) {
			if ( array() === array_intersect( self::text_tokens( (string) $item ), $local_tokens ) ) {
				continue;
			}
			$matches[] = $item;
			if ( count( $matches ) >= $limit ) {
				break;
			}
		}
		return $matches;
	}

	/** Crude significant-word tokenizer — no stopword list needed for this fuzzy relevance check. */
	private static function text_tokens( string $text ): array {
		$normalized = function_exists( 'remove_accents' ) ? remove_accents( $text ) : $text;
		$normalized = function_exists( 'mb_strtolower' ) ? mb_strtolower( (string) $normalized, 'UTF-8' ) : strtolower( (string) $normalized );
		$normalized = preg_replace( '/[^\p{L}\p{N}]+/u', ' ', (string) $normalized );
		$tokens     = preg_split( '/\s+/u', trim( (string) $normalized ), -1, PREG_SPLIT_NO_EMPTY ) ?: array();
		return array_values( array_unique( array_filter( $tokens, static function ( $token ) {
			return strlen( $token ) > 3;
		} ) ) );
	}

	/**
	 * Heuristic default only — the model may confirm or override this in its
	 * own VISUAL_PLAN `role` field. Costudio_Visual_Plan enforces the actual
	 * rules for whichever role the model ends up claiming, so this suggestion
	 * never has to be perfect, just a reasonable nudge away from defaulting
	 * every asset to the same generic role.
	 */
	private static function suggest_editorial_role( string $local_text ): string {
		$lower = function_exists( 'mb_strtolower' ) ? mb_strtolower( $local_text, 'UTF-8' ) : strtolower( $local_text );

		$has_number = (bool) preg_match( '/\b\d+(?:[.,]\d+)?\s?%|\b\d{2,}(?:[.,]\d+)?\b/u', $lower );
		if ( $has_number && preg_match( '/\b(study|studies|survey|report|research|data|statistics|estudo|pesquisa|relatorio|relat[oó]rio|dados|estat[ií]stica|estudio|investigaci[oó]n|estad[ií]stica)\b/u', $lower ) ) {
			return 'evidence-data';
		}

		if ( preg_match( '/\b(dashboard|interface|screen|app|plugin|software|painel|aplicativo|tela|programa|aplicaci[oó]n|pantalla)\b/u', $lower ) ) {
			return 'product-ui';
		}

		return 'explanatory';
	}

	private static function context_text( string $text, int $limit ): string {
		$text = trim( preg_replace( '/\s+/u', ' ', strip_tags( $text ) ) );
		if ( function_exists( 'mb_substr' ) ) {
			return mb_substr( $text, 0, $limit, 'UTF-8' );
		}
		return substr( $text, 0, $limit );
	}

	public static function build_image_list_block( string $text ): string {
		if (
			! preg_match_all(
				'/\[IMG:\s*([^\|\]]+)\s*\|\s*([^\]]+)\]/',
				$text,
				$matches,
				PREG_SET_ORDER
			)
		) {
			return '';
		}

		$lines = array();
		foreach ( $matches as $m ) {
			$fn  = trim( $m[1] );
			$alt = trim( $m[2] );
			if ( '' === $fn || '' === $alt ) {
				continue;
			}
			$lines[] = '- `' . $fn . '` — ' . $alt;
		}

		if ( array() === $lines ) {
			return '';
		}

		return "[IMAGE_LIST]\n" . implode( "\n", $lines ) . "\n[/IMAGE_LIST]";
	}

	/**
	 * Extracts title, meta, headings, intro and key stats for Agent 06.
	 */
	public static function summarize_article_for_social( string $text ): string {
		$lines      = explode( "\n", $text );
		$summary    = array();
		$intro_cnt  = 0;
		$faq_active = false;
		$faq_items  = 0;

		foreach ( $lines as $line ) {
			$trimmed = trim( $line );
			if ( empty( $trimmed ) ) {
				continue;
			}

			if ( preg_match( '/^(Título|Meta Description|Slug)\s*:/ui', $trimmed ) ) {
				$summary[] = $trimmed;
				continue;
			}

			if ( 0 === strpos( $trimmed, '## ' ) ) {
				$faq_active = ( false !== stripos( $trimmed, 'FAQ' ) || false !== stripos( $trimmed, 'Perguntas' ) );
				$summary[]  = $trimmed;
				continue;
			}

			if ( 0 === strpos( $trimmed, '### ' ) ) {
				$summary[] = $trimmed;
				continue;
			}

			if ( $faq_active && $faq_items < 5 && preg_match( '/^\d+[\.\)]/', $trimmed ) ) {
				$summary[] = $trimmed;
				$faq_items++;
				continue;
			}

			if ( $intro_cnt < 5 ) {
				$summary[] = $trimmed;
				$intro_cnt++;
			}
		}

		return implode( "\n", $summary );
	}

	/**
	 * Extracts PAA, gaps, search intent and LSI table from Agent 01.
	 */
	public static function summarize_discovery( string $text ): string {
		$lines         = explode( "\n", $text );
		$summary       = array();
		$in_section    = false;
		$skip_sections = array( 'top 5 concorrentes', 'concorrentes analisados' );

		foreach ( $lines as $line ) {
			$trimmed = trim( $line );

			if ( preg_match( '/^#{2,3}\s+/', $trimmed ) ) {
				$heading_lower = strtolower( $trimmed );
				$in_section    = true;
				foreach ( $skip_sections as $skip ) {
					if ( false !== strpos( $heading_lower, $skip ) ) {
						$in_section = false;
						break;
					}
				}
			}

			if ( $in_section || empty( $trimmed ) ) {
				$summary[] = $line;
			}
		}

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