<?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';

	/** 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.
		self::persist_prompts_from_body( $body, $locale );

		return true;
	}

	/**
	 * 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 ): void {
		foreach ( range( 1, 7 ) as $n ) {
			$key    = sprintf( '%02d', $n );
			$prompt = isset( $body['agents'][ $key ] ) ? (string) $body['agents'][ $key ] : '';
			if ( $prompt !== '' ) {
				costudio_update_option( sprintf( 'costudio_agent_%02d', $n ), $prompt, false );
			}
		}

		costudio_update_option( self::VERSION_OPTION, (string) ( $body['prompts_version'] ?? '' ), false );
		costudio_update_option( self::LOCALE_OPTION,  $locale, false );
	}

	/**
	 * 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', '' ) );

		// Already current for this locale — nothing to do.
		if ( $has_prompts
			&& $local_locale === $locale
			&& '' !== $remote_version
			&& $remote_version === $local_version ) {
			return false;
		}

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

		return true;
	}

	/**
	 * 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 )
		) {
			return;
		}

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

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

		costudio_update_option( self::VERSION_OPTION, (string) $remote_version, false );
		costudio_update_option( self::LOCALE_OPTION,  $locale, false );
	}

	// ── 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 );
				return (string) ( $agents[ sprintf( '%02d', $agent ) ] ?? '' );
			}
		}

		$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 '';
		}

		return (string) ( $body['agents'][ sprintf( '%02d', $agent ) ] ?? '' );
	}

	/**
	 * 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 );
	}
}
