<?php
/**
 * Costudio_Agent_Seeder
 *
 * Busca os prompts dos 7 agentes na contentosapp-api e os persiste
 * nas options `costudio_agent_01` … `costudio_agent_07`.
 *
 * Chamado uma vez na ativação do plugin e, nas cargas seguintes, apenas
 * quando a API reportar uma versão mais nova que a gravada localmente.
 * No ContentOS Auto atual, essas options são cache/admin display e fallback.
 * A execução padrão do ContentOS Auto usa o endpoint remoto `/ai/agent/generate`;
 * BYOK usa prompts locais ou prompts completos de execução baixados sem licença paga.
 *
 * Options usadas:
 *   costudio_prompts_version  → versão atual gravada (ex: 2025-04-09-001)
 *   costudio_prompts_locale   → locale do último fetch
 *
 * @package Contentosapp_Studio
 * @since   1.1.0
 */

defined( 'ABSPATH' ) || exit;

class Costudio_Agent_Seeder {

	/** URL base da contentosapp-api (sempre no servidor da ContentOS). */
	const API_BASE = COSTUDIO_API_URL . '/wp-json/contentosapp-api/v1';

	/** Option que guarda a versão dos prompts atualmente no DB. */
	const VERSION_OPTION = 'costudio_prompts_version';

	/** Option que guarda o locale do último fetch. */
	const LOCALE_OPTION = 'costudio_prompts_locale';

	/** Verified provenance for prompts persisted in the local runtime cache. */
	const PROVENANCE_OPTION = 'costudio_prompts_provenance';

	/** Provenance of the last ephemeral BYOK execution prompt fetched. */
	const EXECUTION_PROVENANCE_OPTION = 'costudio_prompt_execution_provenance';

	/** Timeout da requisição HTTP em segundos. */
	const HTTP_TIMEOUT = 15;

	/** Transient que bloqueia novas tentativas após falha de conexão. */
	const COOLDOWN_TRANSIENT = 'costudio_prompts_fetch_cooldown';

	/** Horas de espera após falha antes de tentar novamente. */
	const COOLDOWN_HOURS = 6;

	/**
	 * Flag option that, when truthy, makes maybe_fetch() a no-op so the prompts
	 * seeded from the local Vault by Costudio_DB::cleanup_legacy_agent_prompts_v20()
	 * are not overwritten by stale or unreachable CAP API responses.
	 *
	 * Production releases that ship with prompts already on the CAP API can
	 * delete this option to re-enable normal fetch behaviour.
	 */
	const LOCAL_VAULT_LOCK_OPTION = 'costudio_prompts_lock_vault';

	/**
	 * Ponto de entrada principal.
	 *
	 * - Se nenhum prompt está no DB → faz o fetch imediatamente.
	 * - Se o locale mudou → re-fetch.
	 * - Se já existe versão local → checa versão remota antes de baixar.
	 *
	 * @param string $locale  Locale desejado (pt_BR | en_US | es_ES).
	 * @param bool   $force   Se true, ignora versão local e sempre re-fetcha.
	 */
	public static function maybe_fetch( string $locale, bool $force = false ): void {
		// Não roda durante requisições AJAX/REST para não bloquear o pipeline.
		// Exceção: chamadas explícitas/forçadas (Sync prompts ou fallback BYOK)
		// precisam poder reparar uma instalação sem prompts locais.
		if ( ! $force && (
			( defined( 'DOING_AJAX' ) && DOING_AJAX )
			|| ( defined( 'REST_REQUEST' ) && REST_REQUEST )
		) ) {
			return;
		}

		// Local vault is the canonical source for prompts in this release —
		// see Costudio_DB::cleanup_legacy_agent_prompts_v20(). Bail out
		// before any HTTP work so CAP API drift can't overwrite us.
		if ( ! $force && (bool) costudio_get_option( self::LOCAL_VAULT_LOCK_OPTION, false ) ) {
			return;
		}

		// Atalho de loopback: se a constante CAP_API_SERVER estiver definida,
		// este WordPress é o próprio servidor da contentosapp-api. Lemos os
		// prompts diretamente do DB local — sem nenhuma requisição HTTP.
		if ( defined( 'CAP_API_SERVER' ) ) {
			self::seed_from_cap_prompts( $locale );
			return;
		}

		$stored_version = (string) costudio_get_option( self::VERSION_OPTION, '' );
		$stored_locale  = (string) costudio_get_option( self::LOCALE_OPTION, '' );

		// Detecta se algum prompt está realmente gravado no DB.
		$has_prompts = ! empty( costudio_get_option( 'costudio_agent_01', '' ) );

		// Se já tem prompts e está em cooldown (falha recente), não tenta de novo.
		// Isso evita travar o admin a cada page load em servidores que bloqueiam cURL.
		if ( $has_prompts && ! $force && get_transient( self::COOLDOWN_TRANSIENT ) ) {
			return;
		}

		// Re-fetch se: forçado, sem prompts, locale mudou, ou sem versão.
		$needs_fetch = $force
			|| ! $has_prompts
			|| $stored_locale !== $locale
			|| empty( $stored_version );

		if ( ! $needs_fetch ) {
			// Prompts existem e locale não mudou — só verifica se há versão nova.
			// Também entra em cooldown se a verificação falhar.
			$needs_fetch = self::has_newer_version( $locale, $stored_version );
		}

		if ( $needs_fetch ) {
			self::fetch_and_store( $locale );
		}
	}

	/**
	 * Faz o fetch completo e grava os prompts no DB.
	 * Retorna true em sucesso, false em falha (fallback mínimo aplicado se sem prompts).
	 *
	 * @param string $locale
	 * @return bool
	 */
	public static function fetch_and_store( string $locale ): bool {
		$response = self::request_agent_prompts( $locale );

		if ( is_wp_error( $response ) ) {
			// Falha de conexão (timeout, DNS, etc.) — ativa cooldown para não
			// repetir a tentativa a cada carregamento de página admin.
			self::log_sync_failure( 'request_error', [
				'locale' => $locale,
				'error'  => $response->get_error_message(),
			] );
			self::set_cooldown();
			return false;
		}

		$code = wp_remote_retrieve_response_code( $response );
		if ( $code !== 200 ) {
			self::log_sync_failure( 'http_error', [
				'locale' => $locale,
				'status' => $code,
				'body'   => self::response_snippet( wp_remote_retrieve_body( $response ) ),
			] );
			self::set_cooldown();
			return false;
		}

		$body = json_decode( wp_remote_retrieve_body( $response ), true );
		if ( ! is_array( $body )
			|| empty( $body['agents'] )
			|| ! is_array( $body['agents'] ) ) {
			self::log_sync_failure( 'invalid_body', [
				'locale' => $locale,
				'status' => $code,
				'body'   => self::response_snippet( wp_remote_retrieve_body( $response ) ),
			] );
			return false;
		}

		// Grava os agentes + metadados do fetch.
		return self::persist_prompts_from_body( $body, $locale );
	}

	/**
	 * Persist a decoded prompt payload into the local option cache.
	 *
	 * Shared by fetch_and_store() and sync_if_newer_for_run() so both paths
	 * write the agents and version/locale metadata identically. Only non-empty
	 * agent prompts overwrite the cache — an empty field never wipes a good one.
	 *
	 * @param array  $body   Decoded response body (expects ['agents'], ['prompts_version']).
	 * @param string $locale Locale being stored.
	 */
	private static function persist_prompts_from_body( array $body, string $locale ): bool {
		$version  = (string) ( $body['prompts_version'] ?? '' );
		$incoming = [];
		foreach ( range( 1, 7 ) as $n ) {
			$key              = sprintf( '%02d', $n );
			$incoming[ $key ] = isset( $body['agents'][ $key ] ) ? (string) $body['agents'][ $key ] : '';
		}

		$remote_manifest = isset( $body['prompts_manifest'] ) && is_array( $body['prompts_manifest'] )
			? $body['prompts_manifest']
			: [];
		if ( $remote_manifest && ! self::manifest_matches( $incoming, $version, $locale, $remote_manifest ) ) {
			self::log_sync_failure( 'manifest_mismatch_before_write', [
				'locale'  => $locale,
				'version' => $version,
			] );
			return false;
		}

		foreach ( range( 1, 7 ) as $n ) {
			$key    = sprintf( '%02d', $n );
			$prompt = $incoming[ $key ];
			if ( $prompt !== '' ) {
				costudio_update_option( sprintf( 'costudio_agent_%02d', $n ), $prompt, false );
			}
		}

		costudio_update_option( self::VERSION_OPTION, $version, false );
		costudio_update_option( self::LOCALE_OPTION,  $locale, false );

		$runtime          = self::runtime_prompt_set();
		$runtime_manifest = self::build_prompt_manifest( $runtime, $version, $locale );
		$attested         = $remote_manifest
			&& hash_equals(
				(string) ( $remote_manifest['manifest_hash'] ?? '' ),
				(string) $runtime_manifest['manifest_hash']
			);

		costudio_update_option( self::PROVENANCE_OPTION, [
			'attested'      => (bool) $attested,
			'source'        => $remote_manifest ? 'cap_manifest' : 'legacy_unattested',
			'version'       => $version,
			'locale'        => self::normalize_locale_for_manifest( $locale ),
			'manifest_hash' => (string) $runtime_manifest['manifest_hash'],
			'agents'        => $runtime_manifest['agents'],
			'verified_at'   => gmdate( 'Y-m-d H:i:s' ),
		], false );

		if ( $remote_manifest && ! $attested ) {
			self::log_sync_failure( 'runtime_manifest_mismatch', [
				'locale'  => $locale,
				'version' => $version,
			] );
			return false;
		}

		return true;
	}

	/**
	 * Lightweight freshness check for the START of a BYOK production run.
	 *
	 * In BYOK the pipeline executes the locally cached prompt (costudio_agent_NN).
	 * A canonical prompt edited on the ContentOS server otherwise only reaches the
	 * install through the background cron, which can lag, sit in cooldown, or be
	 * disabled by the vault lock — so a generation started right after an edit can
	 * run stale prompts. This does ONE request, compares the server's
	 * prompts_version against the cached one, and overwrites the cache only when
	 * the server is genuinely newer.
	 *
	 * Deliberately different from maybe_fetch():
	 *   - runs inside the AJAX/REST/cron pipeline context (no early return);
	 *   - ignores the cooldown AND the vault lock (this is an explicit, guarded,
	 *     BYOK-only freshness pull — the pipeline calls it at most once per
	 *     production via a per-production transient);
	 *   - any connection/parse failure is a SILENT no-op so generation always
	 *     proceeds with whatever is in the local cache (never blocks a run).
	 *
	 * @param string $locale Content locale (pt_BR | en_US | es_ES).
	 * @return bool True when the local cache was updated to a newer version.
	 */
	public static function sync_if_newer_for_run( string $locale ): bool {
		// Loopback: this WordPress IS the CAP server — read prompts directly, no HTTP.
		if ( defined( 'CAP_API_SERVER' ) ) {
			self::seed_from_cap_prompts( $locale );
			return true;
		}

		$response = self::request_agent_prompts( $locale );
		if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 ) {
			return false; // Silent: keep local cache, never block the run.
		}

		$body = json_decode( wp_remote_retrieve_body( $response ), true );
		if ( ! is_array( $body ) || empty( $body['agents'] ) || ! is_array( $body['agents'] ) ) {
			return false;
		}

		$remote_version = (string) ( $body['prompts_version'] ?? '' );
		$local_version  = (string) costudio_get_option( self::VERSION_OPTION, '' );
		$local_locale   = (string) costudio_get_option( self::LOCALE_OPTION, '' );
		$has_prompts    = ! empty( costudio_get_option( 'costudio_agent_01', '' ) );

		// A matching version string is not enough: the runtime prompt bytes must
		// also be attested. This closes the stale/partially-written cache case in
		// which a deploy kept the same version but the local options diverged.
		if ( $has_prompts
			&& $local_locale === $locale
			&& '' !== $remote_version
			&& $remote_version === $local_version
			&& self::runtime_prompts_are_attested( $locale )
			&& ( empty( $body['prompts_manifest'] )
				|| hash_equals(
					(string) ( $body['prompts_manifest']['manifest_hash'] ?? '' ),
					(string) ( self::runtime_prompt_provenance()['manifest_hash'] ?? '' )
				)
			)
		) {
			return false;
		}

		return self::persist_prompts_from_body( $body, $locale );
	}

	/**
	 * Force a prompt sync from the canonical ContentOS API source.
	 *
	 * Kept under the historical method name because the admin UI already calls
	 * `costudio_sync_vault`; in the current architecture the "vault" is the
	 * remote CAP prompt store, not local .txt files shipped with the plugin.
	 */
	public static function force_sync_from_vault( ?string $locale = null ): bool {
		$locale = $locale ?: (
			class_exists( 'Costudio_Admin' )
				? Costudio_Admin::get_default_content_language()
				: 'pt_BR'
		);

		delete_transient( self::COOLDOWN_TRANSIENT );

		return self::fetch_and_store( $locale );
	}

	// ── Cooldown ──────────────────────────────────────────────────────────────

	/**
	 * Ativa um cooldown após falha de conexão.
	 * Durante o cooldown, maybe_fetch() retorna imediatamente (se há prompts),
	 * evitando que cada carregamento de página admin gere um timeout de rede.
	 */
	private static function set_cooldown(): void {
		set_transient( self::COOLDOWN_TRANSIENT, 1, self::COOLDOWN_HOURS * HOUR_IN_SECONDS );
	}

	// ── Loopback shortcut ─────────────────────────────────────────────────────

	/**
	 * Quando o Contentosapp Studio está instalado no próprio servidor da contentosapp-api,
	 * lê os prompts diretamente via CAP_Prompts (sem HTTP).
	 *
	 * Só atualiza uma option se o conteúdo remoto for diferente do local,
	 * para evitar writes desnecessários no DB a cada carregamento de página.
	 *
	 * @param string $locale
	 */
	private static function seed_from_cap_prompts( string $locale ): void {
		// Carrega CAP_Prompts explicitamente — não depende do autoloader ter sido
		// disparado antes desta chamada.
		if ( ! class_exists( 'CAP_Prompts' ) ) {
			if ( defined( 'CAP_DIR' ) && file_exists( CAP_DIR . 'includes/class-cap-prompts.php' ) ) {
				require_once CAP_DIR . 'includes/class-cap-prompts.php';
			}
		}

		if ( ! class_exists( 'CAP_Prompts' ) ) {
			return;
		}

		if ( ! CAP_Prompts::has_prompts() ) {
			return;
		}

		$remote_version = CAP_Prompts::get_version();
		$local_version  = (string) costudio_get_option( self::VERSION_OPTION, '' );
		$local_locale   = (string) costudio_get_option( self::LOCALE_OPTION, '' );
		$has_prompts    = ! empty( costudio_get_option( 'costudio_agent_01', '' ) );

		// Nada mudou — pula.
		if ( $has_prompts
			&& $local_locale === $locale
			&& $local_version === $remote_version
			&& ! empty( $local_version )
			&& self::runtime_prompts_are_attested( $locale )
		) {
			return;
		}

		$agents = CAP_Prompts::get( $locale );

		self::persist_prompts_from_body( [
			'prompts_version'  => (string) $remote_version,
			'agents'           => $agents,
			'prompts_manifest' => CAP_Prompts::build_manifest( $agents, (string) $remote_version, $locale ),
		], $locale );
	}

	// ── Helpers privados ──────────────────────────────────────────────────────

	/**
	 * Faz uma requisição leve à API apenas para comparar a versão.
	 * Usa o mesmo endpoint — a resposta é pequena quando os prompts já existem.
	 */
	private static function has_newer_version( string $locale, string $local_version ): bool {
		$response = self::request_agent_prompts( $locale );

		if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 ) {
			// Não conseguiu verificar — ativa cooldown e mantém prompts atuais.
			self::log_sync_failure( 'version_check_failed', [
				'locale' => $locale,
				'status' => is_wp_error( $response ) ? 0 : wp_remote_retrieve_response_code( $response ),
				'error'  => is_wp_error( $response ) ? $response->get_error_message() : '',
				'body'   => is_wp_error( $response ) ? '' : self::response_snippet( wp_remote_retrieve_body( $response ) ),
			] );
			self::set_cooldown();
			return false;
		}

		$body = json_decode( wp_remote_retrieve_body( $response ), true );
		$remote_version = (string) ( $body['prompts_version'] ?? '' );

		return $remote_version !== '' && $remote_version !== $local_version;
	}

	/**
	 * Fetch prompt payload with the same WAF strategy as runtime AI calls:
	 * admin-ajax first, REST only as fallback.
	 *
	 * @param  string $locale
	 * @param  array  $extra_params  Parâmetros adicionais (ex: ['purpose' => 'execution']).
	 * @return array|WP_Error
	 */
	private static function request_agent_prompts( string $locale, array $extra_params = [] ) {
		$params = array_merge( [
			'locale'         => $locale,
			'plugin_version' => COSTUDIO_VERSION,
			'site_url'       => home_url(),
		], $extra_params );

		$body         = http_build_query( $params, '', '&' );
		$ajax_headers = self::auth_headers( $body );
		$ajax_headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';

		$ajax = wp_remote_post(
			costudio_api_ajax_url( 'cap_api_agent_prompts' ),
			[
				'timeout'    => self::HTTP_TIMEOUT,
				'sslverify'  => function_exists( 'costudio_sslverify' ) ? costudio_sslverify( 'agent_prompts_ajax' ) : true,
				'headers'    => $ajax_headers,
				'body'       => $body,
				'user-agent' => self::browser_user_agent(),
			]
		);

		if ( ! is_wp_error( $ajax ) && self::looks_like_json_response( $ajax ) ) {
			return $ajax;
		}

		$rest_url = add_query_arg( $params, self::API_BASE . '/agent-prompts' );

		return wp_remote_get(
			$rest_url,
			[
				'timeout'    => self::HTTP_TIMEOUT,
				'sslverify'  => function_exists( 'costudio_sslverify' ) ? costudio_sslverify( 'agent_prompts_rest' ) : true,
				'headers'    => self::auth_headers( '' ),
				'user-agent' => self::browser_user_agent(),
			]
		);
	}

	/**
	 * Busca o prompt completo de um agente específico para execução BYOK.
	 *
	 * Diferente do fetch normal (que pode armazenar teasers legados no DB),
	 * este método usa purpose=execution para receber o texto real do servidor
	 * sem persistir nada no DB — o prompt existe apenas durante a execução.
	 *
	 * Atalho loopback: se CAP_API_SERVER estiver definido (servidor próprio),
	 * lê diretamente via CAP_Prompts sem HTTP.
	 *
	 * @param  int    $agent  Número do agente (1-7).
	 * @param  string $locale Locale desejado (pt_BR | en_US | es_ES).
	 * @return string         Texto completo do prompt, ou '' em caso de falha.
	 */
	public static function fetch_single_for_execution( int $agent, string $locale ): string {
		// Atalho loopback: este WordPress é o próprio servidor da contentosapp-api.
		if ( defined( 'CAP_API_SERVER' ) ) {
			if ( ! class_exists( 'CAP_Prompts' ) && defined( 'CAP_DIR' ) ) {
				$path = CAP_DIR . 'includes/class-cap-prompts.php';
				if ( file_exists( $path ) ) {
					require_once $path;
				}
			}
			if ( class_exists( 'CAP_Prompts' ) ) {
				$agents = CAP_Prompts::get( $locale );
				$prompt = (string) ( $agents[ sprintf( '%02d', $agent ) ] ?? '' );
				self::record_execution_provenance(
					$agent,
					$prompt,
					CAP_Prompts::get_manifest( $locale )
				);
				return $prompt;
			}
		}

		$response = self::request_agent_prompts( $locale, [ 'purpose' => 'execution' ] );

		if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 ) {
			return '';
		}

		$body = json_decode( wp_remote_retrieve_body( $response ), true );
		if ( ! is_array( $body ) || empty( $body['agents'] ) ) {
			return '';
		}

		$prompt = (string) ( $body['agents'][ sprintf( '%02d', $agent ) ] ?? '' );
		if ( ! empty( $body['prompts_manifest'] ) && is_array( $body['prompts_manifest'] ) ) {
			self::record_execution_provenance( $agent, $prompt, $body['prompts_manifest'] );
		}

		return $prompt;
	}

	/**
	 * Build the same deterministic manifest produced by the ContentOS API.
	 * Public so benchmark/test tooling can prove the runtime prompt identity.
	 */
	public static function build_prompt_manifest( array $agents, string $version, string $locale ): array {
		$agent_manifest = [];
		$manifest_parts = [];
		foreach ( range( 1, 7 ) as $n ) {
			$key    = sprintf( '%02d', $n );
			$prompt = self::canonicalize_prompt_for_hash( (string) ( $agents[ $key ] ?? '' ) );
			$hash   = hash( 'sha256', $prompt );
			$agent_manifest[ $key ] = [ 'hash' => $hash, 'bytes' => strlen( $prompt ) ];
			$manifest_parts[]       = $key . ':' . $hash . ':' . strlen( $prompt );
		}

		$locale = self::normalize_locale_for_manifest( $locale );
		$root   = implode( "\n", [
			'version:' . $version,
			'locale:' . $locale,
			implode( "\n", $manifest_parts ),
		] );

		return [
			'algorithm'     => 'sha256',
			'version'       => $version,
			'locale'        => $locale,
			'agents'        => $agent_manifest,
			'manifest_hash' => hash( 'sha256', $root ),
		];
	}

	public static function runtime_prompt_provenance(): array {
		$value = costudio_get_option( self::PROVENANCE_OPTION, [] );
		return is_array( $value ) ? $value : [];
	}

	public static function runtime_prompts_are_attested( string $locale = '' ): bool {
		$provenance = self::runtime_prompt_provenance();
		if ( empty( $provenance['attested'] ) ) {
			return false;
		}

		$locale  = $locale ?: (string) costudio_get_option( self::LOCALE_OPTION, '' );
		$version = (string) costudio_get_option( self::VERSION_OPTION, '' );
		$current = self::build_prompt_manifest( self::runtime_prompt_set(), $version, $locale );

		return hash_equals(
			(string) ( $provenance['manifest_hash'] ?? '' ),
			(string) $current['manifest_hash']
		);
	}

	private static function manifest_matches( array $agents, string $version, string $locale, array $manifest ): bool {
		if ( 'sha256' !== (string) ( $manifest['algorithm'] ?? '' ) ) {
			return false;
		}
		$local = self::build_prompt_manifest( $agents, $version, $locale );

		return '' !== (string) ( $manifest['manifest_hash'] ?? '' )
			&& hash_equals( (string) $manifest['manifest_hash'], (string) $local['manifest_hash'] );
	}

	/**
	 * Confirm (or flag) that the prompt about to execute is byte-identical to
	 * what the manifest attests. This is Gate 0-B's execution-time check —
	 * the one that matters most, because it runs right before the prompt is
	 * actually used to generate content.
	 *
	 * Deliberately a WARN, not a block: a mismatch is logged (visible in the
	 * Technical Logs panel via Costudio_Logger, unlike costudio_log_debug()
	 * which is silent unless WP_DEBUG-style diagnostics are opted in) and
	 * execution proceeds. Hard-blocking on divergence is a possible future
	 * step once this signal has been observed in practice — not applied yet.
	 */
	private static function record_execution_provenance( int $agent, string $prompt, array $manifest ): void {
		$key      = sprintf( '%02d', $agent );
		$expected = (string) ( $manifest['agents'][ $key ]['hash'] ?? '' );
		$actual   = hash( 'sha256', self::canonicalize_prompt_for_hash( $prompt ) );

		if ( '' === $prompt || '' === $expected ) {
			return; // Nothing to compare against yet — not itself a mismatch.
		}

		if ( ! hash_equals( $expected, $actual ) ) {
			if ( class_exists( 'Costudio_Logger' ) ) {
				Costudio_Logger::warn(
					'prompt.provenance_mismatch',
					[
						'agent'          => $key,
						'locale'         => (string) ( $manifest['locale'] ?? '' ),
						'manifest_version' => (string) ( $manifest['version'] ?? '' ),
						// Hashes only, never the prompt body — same rule the manifest itself follows.
						'expected_hash'  => $expected,
						'runtime_hash'   => $actual,
					],
					null,
					$agent
				);
			}
			return;
		}

		costudio_update_option( self::EXECUTION_PROVENANCE_OPTION, [
			'attested'      => true,
			'agent'         => $key,
			'prompt_hash'   => $actual,
			'manifest_hash' => (string) ( $manifest['manifest_hash'] ?? '' ),
			'version'       => (string) ( $manifest['version'] ?? '' ),
			'locale'        => (string) ( $manifest['locale'] ?? '' ),
			'verified_at'   => gmdate( 'Y-m-d H:i:s' ),
		], false );
	}

	private static function runtime_prompt_set(): array {
		$agents = [];
		foreach ( range( 1, 7 ) as $n ) {
			$agents[ sprintf( '%02d', $n ) ] = (string) costudio_get_option(
				sprintf( 'costudio_agent_%02d', $n ),
				''
			);
		}
		return $agents;
	}

	private static function canonicalize_prompt_for_hash( string $prompt ): string {
		return str_replace( [ "\r\n", "\r" ], "\n", $prompt );
	}

	private static function normalize_locale_for_manifest( string $locale ): string {
		if ( 0 === stripos( $locale, 'pt' ) ) {
			return 'pt_BR';
		}
		if ( 0 === stripos( $locale, 'es' ) ) {
			return 'es_ES';
		}
		return 'en_US';
	}

	/**
	 * Build auth headers for prompt sync. Paid service installs send a bearer
	 * key; Starter/BYOK installs authenticate by site_url in the request body.
	 */
	private static function auth_headers( string $signed_body = '' ): array {
		$base = rtrim( COSTUDIO_API_URL, '/' );

		$headers = [
			'Accept'           => 'application/json, */*;q=0.8',
			'Accept-Language'  => 'en-US,en;q=0.9,pt-BR;q=0.8,pt;q=0.7',
			'Origin'           => $base,
			'Referer'          => $base . '/wp-admin/',
			'X-Requested-With' => 'XMLHttpRequest',
		];

		if ( ! class_exists( 'Costudio_Standalone_License' ) ) {
			return $headers;
		}

		$key = (string) get_option( Costudio_Standalone_License::OPT_KEY, '' );
		if ( '' === $key ) {
			return $headers;
		}

		$headers['Authorization'] = 'Bearer ' . $key;

		if ( class_exists( 'Costudio_Standalone_Crypto' ) ) {
			$secret                     = Costudio_Standalone_License::get_signing_secret( $key );
			$headers['X-FOS-Signature'] = Costudio_Standalone_Crypto::sign_payload( $signed_body, $secret );
		}

		return $headers;
	}

	private static function looks_like_json_response( $response ): bool {
		$raw  = ltrim( (string) wp_remote_retrieve_body( $response ) );
		$code = (int) wp_remote_retrieve_response_code( $response );

		if ( $code <= 0 || '' === $raw || ( '{' !== $raw[0] && '[' !== $raw[0] ) ) {
			return false;
		}

		return is_array( json_decode( $raw, true ) );
	}

	private static function browser_user_agent(): string {
		if ( class_exists( 'Costudio_Standalone_License' ) && defined( 'Costudio_Standalone_License::BROWSER_UA' ) ) {
			return Costudio_Standalone_License::BROWSER_UA;
		}

		return 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
	}

	/**
	 * Keep prompt-sync failures diagnosable without exposing bearer tokens.
	 */
	private static function log_sync_failure( string $reason, array $context = [] ): void {
		$context = array_merge(
			[
				'reason'       => $reason,
				'api_base'     => self::API_BASE,
				'has_service_key' => class_exists( 'Costudio_Standalone_License' )
					&& '' !== (string) get_option( Costudio_Standalone_License::OPT_KEY, '' ),
			],
			$context
		);

		if ( function_exists( 'costudio_log_debug' ) ) {
			costudio_log_debug( 'prompts.sync_failed', $context );
		}
	}

	/**
	 * Small redacted response sample for troubleshooting failed syncs.
	 */
	private static function response_snippet( string $body ): string {
		$body = wp_strip_all_tags( $body );
		$body = preg_replace( '/(FOS2\\.)[A-Za-z0-9_\\-.]+/', '$1[redacted]', $body );
		$body = preg_replace( '/(Bearer\\s+)[A-Za-z0-9_\\-.]+/i', '$1[redacted]', (string) $body );

		return mb_substr( trim( (string) $body ), 0, 300 );
	}
}
