<?php
/**
 * Lightweight orchestration layer wrapping the existing chat flow.
 *
 * The harness intentionally does NOT replace the 5000-line streaming chat
 * pipeline — it brackets it. Per turn it:
 *   1. Retrieves builder-side RAG context (top-K) and tracks the sources.
 *   2. Merges builder retrieval with the parent Antimanual KB context.
 *   3. After generation, runs guardrails on the produced HTML/CSS.
 *   4. Records a structured turn log for observability.
 *
 * This keeps the chat-api integration footprint tiny while giving us a
 * single seam to grow into a fuller agent loop later.
 *
 * @package Antimanual_Builder
 * @since   1.6.0
 */

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

class AMBLD_Agent_Harness {

	/** @var AMBLD_Agent_Turn_Logger */
	private $logger;

	/** @var array<int,array> */
	private $sources = array();

	public function __construct() {
		$this->logger = new AMBLD_Agent_Turn_Logger();
	}

	public function set_provider( $provider, $model ) {
		$this->logger->set_provider( $provider, $model );
	}

	/**
	 * Attach the post id that owns this chat turn. Read by the accept /
	 * regenerate feedback path so it can find the matching turn record.
	 */
	public function set_post_id( $post_id ) {
		$this->logger->set_post_id( $post_id );
	}

	/**
	 * Build the merged KB+RAG context fragment to inject into the system prompt.
	 *
	 * @param string $query                User query (already concatenated with page title / selection).
	 * @param string $parent_kb_context    Output of AMBLD_Antimanual_Knowledge_Base::build_context_for_query().
	 * @return string
	 */
	public function build_context( $query, $parent_kb_context = '' ) {
		$query = trim( (string) $query );

		// Lazy seed bootstrap. When `ambld_seed_pending` is still set on a
		// chat turn, try to satisfy it inline:
		//
		// 1. Pre-baked shipped vectors first — no API cost, ~50ms for a
		//    typical corpus. This is the fast path and works even when
		//    the parent embedder isn't configured (we only need to READ
		//    files + INSERT rows). So the very first chat turn benefits
		//    from RAG, no waiting for cron.
		//
		// 2. If shipped vectors don't fully cover (exotic provider, edited
		//    seed bodies, missing build artifacts), schedule the cron
		//    fallback to API-embed the rest. The current turn doesn't
		//    benefit from API-embed work; the user gets full coverage on
		//    the next turn.
		//
		// Skipping this work entirely on cold turns isn't an option — that
		// was the gap the user flagged: the cron may not have fired yet,
		// and we'd hand the model an empty context just because of timing.
		if ( class_exists( 'AMBLD_Rag_Indexer' )
			&& 1 === (int) get_option( AMBLD_Rag_Indexer::OPT_SEED_PENDING, 0 )
		) {
			$indexer    = new AMBLD_Rag_Indexer();
			$result     = $indexer->seed_corpus();
			$seed_files = class_exists( 'AMBLD_Rag_Seed_Loader' ) ? count( AMBLD_Rag_Seed_Loader::load_all() ) : 0;

			if ( is_array( $result ) ) {
				$covered = (int) $result['installed_from_vectors'] + (int) $result['embedded'] + (int) $result['skipped'];
				if ( $covered >= $seed_files ) {
					delete_option( AMBLD_Rag_Indexer::OPT_SEED_PENDING );
					delete_option( 'ambld_seed_retry_count' );
					delete_option( 'ambld_seed_last_error' );
					update_option( 'ambld_seed_completed_at', current_time( 'mysql', true ), false );
				} elseif ( AMBLD_Rag_Indexer::can_embed() && ! wp_next_scheduled( AMBLD_Rag_Indexer::CRON_HOOK ) ) {
					AMBLD_Rag_Indexer::schedule_seed_embed( 5 );
				}
			}
		}

		$builder_hits = array();
		if ( '' !== $query && class_exists( 'AMBLD_Rag_Retriever' ) ) {
			error_log( sprintf(
				'[AMB-RAG] harness.build_context: turn START | query_chars=%d | parent_kb_chars=%d',
				strlen( $query ),
				strlen( (string) $parent_kb_context )
			) );
			$builder_hits = AMBLD_Rag_Retriever::retrieve(
				$query,
				// Include shipped seed_* types alongside user-indexed sources.
				// The retriever's source_type filter is whitelisted, so the
				// seed corpus is otherwise invisible to chat.
				array(
					'template',
					'component',
					'shortcode',
					'doc',
					'seed_archetype',
					'seed_pattern',
					'seed_convention',
					'seed_copy',
					'seed_doc',
				),
				6
			);
		} elseif ( '' === $query ) {
			error_log( '[AMB-RAG] harness.build_context: SKIP retrieval — empty query' );
		}

		if ( ! empty( $builder_hits ) ) {
			$this->sources = array_merge(
				$this->sources,
				AMBLD_Rag_Retriever::summarize_sources( $builder_hits )
			);
			$this->logger->add_sources( $this->sources );
		}

		$builder_block = AMBLD_Rag_Retriever::format_for_prompt( $builder_hits );
		$pieces        = array();
		if ( '' !== trim( (string) $parent_kb_context ) ) {
			$pieces[] = trim( (string) $parent_kb_context );
		}
		if ( '' !== trim( $builder_block ) ) {
			$pieces[] = trim( $builder_block );
		}
		$merged = implode( "\n\n", $pieces );

		// Final marker — single line per chat turn. Confirms the corpus
		// actually informed the system prompt and shows the final injection
		// size so you can correlate against AI cost / quality.
		error_log( sprintf(
			'[AMB-RAG] harness.build_context: turn END | rag_hits=%d | parent_kb_chars=%d | builder_rag_chars=%d | merged_chars=%d',
			count( $builder_hits ),
			strlen( (string) $parent_kb_context ),
			strlen( $builder_block ),
			strlen( $merged )
		) );

		return $merged;
	}

	/**
	 * Sources accumulated this turn — for the SSE `sources_used` event.
	 */
	public function get_sources() {
		return $this->sources;
	}

	/**
	 * Last retrieval's diagnostic record. Forwards
	 * `AMBLD_Rag_Retriever::last_diagnostic()` so callers don't have to
	 * know which class owns the data. Used by chat_stream() to ALWAYS
	 * emit a `sources_used` event (even with 0 hits) so the browser
	 * console shows the FACT that retrieval ran and what happened.
	 *
	 * The most common silent failure mode in production: stored vectors
	 * are at one embedding dim, the live query is at another (e.g. a
	 * provider switch from OpenAI 1536 to Gemini 3072), and every row
	 * skips via dim-mismatch. Without this diagnostic the user just
	 * sees a quiet chat panel with no "Sources used" pills and assumes
	 * RAG is broken.
	 */
	public function get_diagnostic() {
		if ( ! class_exists( 'AMBLD_Rag_Retriever' ) ) {
			return array( 'skip_reason' => 'retriever_unavailable' );
		}
		return AMBLD_Rag_Retriever::last_diagnostic();
	}

	public function record_tool_call( $name, $summary = '' ) {
		$this->logger->add_tool_call( $name, $summary );
	}

	/**
	 * Validate generated HTML/CSS. Soft mode warns and lets it through;
	 * strict mode flags the result so the caller can refuse it.
	 */
	public function guard( $html, $css ) {
		$result = AMBLD_Agent_Guardrails::check( $html, $css );
		$this->logger->add_guardrail_result( $result );
		return $result;
	}

	public function finish( $reason ) {
		$this->logger->finish( $reason );
	}
}
