<?php
/**
 * NeccoSpeak — Agent UI: プレビュー タブ（本番 ai-agent-embed v1.5.1.1 の
 * 「プレビュー」タブの transcribe）。
 *
 * マークアップは本番 includes/admin-agent-settings.php
 * ai_agent_embed_render_tab_preview()（3175-3208）と同一構成:
 * ランチャーボタン（展開前）のプレビュー枠 (#ai-agent-launcher-preview) と、
 * ウィジェット（展開後）のプレビュー枠 (#ai-agent-panel-preview) ＋
 * 「プレビューに反映される項目:」リスト。
 *
 * 描画方式だけ本番と異なる:
 *   - ランチャー: サーバ側で neccospeak_frame_launcher_config() をマップした
 *     設定 JSON を <script type="application/json"
 *     id="neccospeak-agent-ui-preview-config"> に埋め込み、
 *     assets/agent-ui/js/agent-ui.js が DOMContentLoaded 時にローカルで
 *     静的描画する（fetch/AJAX なし・外部接続なし・LLM 呼び出しなし）。
 *   - ウィジェット（展開後）: 本番の手描き DOM モック (renderPanelPreview)
 *     ではなく、NS がローカル同梱している実ウィジェット文書
 *     (includes/widget-frame.php の neccospeak_frame_html()) そのものを
 *     iframe srcdoc で描画する。iframe は sandbox="allow-same-origin"
 *     （allow-scripts なし）にしてスクリプト実行・storage・親アクセスを
 *     遮断しつつ、CSS/画像/アイコンフォント等のサブリソースを同一オリジン
 *     として読み込ませ、実 CSS レンダリングさせる（opaque origin だと
 *     フォントだけ CORS 必須で豆腐化する — iframe ヘルパーの docblock 参照）。実フロントで
 *     クライアント JS が担っている表示 2 点（クイックアクションチップの
 *     agent--hidden 解除・#cta-panel への CTA 描画）はサーバ側で srcdoc に
 *     注入する（下記ヘルパー群）。
 *
 * @package NeccoSpeak
 */

defined( 'ABSPATH' ) || exit;

/**
 * プレビュー描画用の設定 JSON（agent-ui.js が読む）を構築する。
 *
 * ウィジェット（展開後）が実ウィジェット文書の iframe srcdoc 描画に
 * 置き換わったため、埋め込む JSON はランチャー静的描画に必要な launcher
 * のみに縮小されている。値は includes/widget-frame.php の既存ビルダー
 * neccospeak_frame_launcher_config()（バリデーション/enum/clamp 済み）の
 * 戻り値そのまま — 実フロントの widget-loader.js が受け取る
 * window.NECCOSPEAK_LAUNCHER と同一形状。
 *
 * @param array $opts neccospeak_editing_options() 相当。
 * @return array {launcher:array}
 */
function neccospeak_agent_ui_preview_config( $opts ) {
	$opts = is_array( $opts ) ? $opts : array();
	return array(
		'launcher' => function_exists( 'neccospeak_frame_launcher_config' ) ? neccospeak_frame_launcher_config( $opts ) : array(),
	);
}

/**
 * #cta-panel（実ウィジェット文書内の空 div）へ注入する CTA マークアップを
 * 組み立てる。実フロントで同じ div を埋める
 * assets/widget-frame/js/openai-realtime-agent/widget-app-persistent.js の
 * renderWelcomeCtas()（~3734-）が生成する DOM を一字一句同じクラス構成
 * （agent__cta-card agent__welcome-cta / agent__cta-card-icon /
 * agent__cta-card-body / agent__cta-card-text / agent__cta-card-sub /
 * agent__cta-card-arrow、secondary は agent__welcome-cta-row /
 * agent__welcome-cta-btn）で再現する。表示条件も同関数と同一:
 * primary.enabled かつ text 非空、または secondary が 1 件以上あるときのみ
 * 描画し、どちらも無ければ '' を返す（= 空 div のまま、実挙動と同じ）。
 * ボタンは非インタラクティブ前提（sandbox で JS は動かない）だが
 * type="button" のまま出力する。
 *
 * @param array $cta neccospeak_frame_cta_primary_secondary() の戻り値
 *                   {primary:array, secondary:array<int,array>}。
 * @return string #cta-panel の中身 HTML。表示するものが無ければ ''。
 */
function neccospeak_agent_ui_preview_cta_panel_html( $cta ) {
	$cta       = is_array( $cta ) ? $cta : array();
	$primary   = ( isset( $cta['primary'] ) && is_array( $cta['primary'] ) ) ? $cta['primary'] : array();
	$secondary = ( isset( $cta['secondary'] ) && is_array( $cta['secondary'] ) ) ? $cta['secondary'] : array();

	// renderWelcomeCtas() の hasPrimary / hasSecondary 判定と同一。
	$primary_text  = isset( $primary['text'] ) ? (string) $primary['text'] : '';
	$has_primary   = ! empty( $primary['enabled'] ) && '' !== $primary_text;
	$has_secondary = ! empty( $secondary );
	if ( ! $has_primary && ! $has_secondary ) {
		return '';
	}

	$html = '';

	if ( $has_primary ) {
		$icon        = isset( $primary['icon'] ) ? (string) $primary['icon'] : '';
		$subtitle    = isset( $primary['subtitle'] ) ? (string) $primary['subtitle'] : '';
		$url         = isset( $primary['url'] ) ? (string) $primary['url'] : '';
		$action_type = isset( $primary['action_type'] ) ? (string) $primary['action_type'] : '';
		// renderWelcomeCtas() は voice / url ありのとき card.style.cursor =
		// "pointer" を立てる — クリックハンドラは付けない（sandbox で不動作）。
		$clickable = ( 'voice' === $action_type ) || '' !== $url;

		$html .= '<div class="agent__cta-card agent__welcome-cta"' . ( $clickable ? ' style="cursor:pointer"' : '' ) . '>';
		if ( '' !== $icon ) {
			$html .= '<span class="agent__cta-card-icon"><i class="bi bi-' . esc_attr( $icon ) . '"></i></span>';
		}
		$html .= '<div class="agent__cta-card-body">';
		$html .= '<div class="agent__cta-card-text">' . esc_html( $primary_text ) . '</div>';
		if ( '' !== $subtitle ) {
			$html .= '<div class="agent__cta-card-sub">' . esc_html( $subtitle ) . '</div>';
		}
		$html .= '</div>';
		$html .= '<span class="agent__cta-card-arrow"><i class="bi bi-chevron-right"></i></span>';
		$html .= '</div>';
	}

	if ( $has_secondary ) {
		$html .= '<div class="agent__welcome-cta-row">';
		foreach ( $secondary as $item ) {
			if ( ! is_array( $item ) ) {
				continue;
			}
			$text = isset( $item['text'] ) ? (string) $item['text'] : '';
			if ( '' === $text ) {
				continue; // renderWelcomeCtas() の if (!s.text) continue; と同一。
			}
			$icon  = isset( $item['icon'] ) ? (string) $item['icon'] : '';
			$html .= '<button class="agent__welcome-cta-btn" type="button">';
			if ( '' !== $icon ) {
				// btn.innerHTML = `<i ...></i> ` + <span> — </i> 後の半角空白も同じ。
				$html .= '<i class="bi bi-' . esc_attr( $icon ) . '"></i> <span>' . esc_html( $text ) . '</span>';
			} else {
				$html .= esc_html( $text );
			}
			$html .= '</button>';
		}
		$html .= '</div>';
	}

	return $html;
}

/**
 * 実ウィジェット文書 HTML（neccospeak_frame_html() の戻り値）へ、実フロントで
 * クライアント JS が担っている表示 2 点をサーバ側で注入する（文字列 in/out の
 * 純関数 — テストから直接呼べる）。
 *
 * 注入は includes/agent-ui-tabs/tab-widget.php で確立済みのマーカー方式:
 * 文書内に必ず 1 度だけ現れる一意文字列を探し、見つからなければ黙って
 * スキップして素の文書を返す（並行作業で widget-frame.php 側のマークアップが
 * 変わってもプレビューを壊さない）。
 *
 *   a. チップ表示: #quick-actions は agent--hidden 付きでサーバ描画済み
 *      （中身のチップ DOM は既にある; 実フロントは updateQuickActions() が
 *      表示を解除する）。</head> 直前に <style> を注入し、
 *      .agent__quick-actions の実 display（assets/widget-frame/
 *      widget-frame.css 612-617: display:grid）へ上書きする。
 *      セレクタ #quick-actions.agent--hidden は .agent--hidden
 *      { display:none !important; } より高詳細度なので !important 同士でも勝つ。
 *   b. CTA パネル: 空 div <div class="agent__cta-panel" id="cta-panel"></div>
 *      を $cta_inner で埋めた div に置換する（$cta_inner が '' なら
 *      実挙動どおり空のまま触らない）。
 *
 * @param string $cta_inner neccospeak_agent_ui_preview_cta_panel_html() の戻り値。
 * @param string $html      実ウィジェット文書 HTML。
 * @return string 注入済み HTML。
 */
function neccospeak_agent_ui_preview_inject_overrides( $html, $cta_inner ) {
	$html      = (string) $html;
	$cta_inner = (string) $cta_inner;

	$head_marker = '</head>';
	$head_pos    = strpos( $html, $head_marker );
	if ( false !== $head_pos ) {
		$style = '<style id="neccospeak-preview-overrides">#quick-actions.agent--hidden{display:grid !important;}</style>';
		$html  = substr_replace( $html, $style . $head_marker, $head_pos, strlen( $head_marker ) );
	}

	if ( '' !== $cta_inner ) {
		$cta_marker = '<div class="agent__cta-panel" id="cta-panel"></div>';
		$cta_pos    = strpos( $html, $cta_marker );
		if ( false !== $cta_pos ) {
			$html = substr_replace(
				$html,
				'<div class="agent__cta-panel" id="cta-panel">' . $cta_inner . '</div>',
				$cta_pos,
				strlen( $cta_marker )
			);
		}
	}

	return $html;
}

/**
 * ウィジェット（展開後）プレビュー iframe の srcdoc 用文書を組み立てる:
 * 実ウィジェット文書 (neccospeak_frame_html()) ＋ 表示 2 点の注入。
 *
 * nonce は includes/widget-frame.php の neccospeak_frame_maybe_render() と
 * 同じ方式 (base64_encode( random_bytes( 16 ) )) — 文書内の 3 つの
 * <script nonce="…"> タグの形式を実配信と揃えるためだけのもので、iframe は
 * sandbox に allow-scripts を含めないためスクリプトは一切実行されない。
 *
 * @param array $opts neccospeak_editing_options() 相当（ドラフト反映済み）。
 * @return string srcdoc に入れる完全な HTML 文書。neccospeak_frame_html() が
 *                無い部分チェックアウトでは ''（空文書として劣化）。
 */
function neccospeak_agent_ui_preview_frame_srcdoc( $opts ) {
	if ( ! function_exists( 'neccospeak_frame_html' ) ) {
		return '';
	}
	$opts = is_array( $opts ) ? $opts : array();

	$nonce = base64_encode( random_bytes( 16 ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- same CSP/script-nonce construction as neccospeak_frame_maybe_render(); never decoded back into meaningful data.

	$html      = neccospeak_frame_html( $opts, $nonce );
	$cta       = function_exists( 'neccospeak_frame_cta_primary_secondary' ) ? neccospeak_frame_cta_primary_secondary( $opts ) : array();
	$cta_inner = neccospeak_agent_ui_preview_cta_panel_html( $cta );

	return neccospeak_agent_ui_preview_inject_overrides( $html, $cta_inner );
}

/**
 * ウィジェット（展開後）プレビューの <iframe> タグを組み立てる。
 *
 * sandbox="allow-same-origin"（allow-scripts なし）: スクリプトは一切実行
 * されない（storage・親アクセス・フォーム送信も遮断されたまま）が、文書の
 * origin をサイト自身として扱わせる。値なしの完全サンドボックス（opaque
 * origin）にしないのは、@font-face のフォント取得だけは仕様上 CORS モードで
 * 行われるため、opaque origin だと bootstrap-icons のアイコンフォントが
 * ブロックされて全アイコンが豆腐になる（BASIC 認証の有無に関わらず、
 * ACAO ヘッダを返さない一般のサイトで再現する）ため。実機 (test-hp) の
 * サンドボックス probe で確認済み (2026-08-07)。
 * srcdoc は esc_attr() で属性エスケープする。
 *
 * @param array $opts neccospeak_editing_options() 相当。
 * @return string <iframe> タグの HTML。
 */
function neccospeak_agent_ui_preview_iframe_html( $opts ) {
	return '<iframe'
		. ' title="' . esc_attr__( 'Widget (expanded) preview', 'neccospeak' ) . '"'
		. ' sandbox="allow-same-origin"'
		. ' style="width:100%;height:580px;border:0;display:block"'
		. ' srcdoc="' . esc_attr( neccospeak_agent_ui_preview_frame_srcdoc( $opts ) ) . '"'
		. '></iframe>';
}

/**
 * プレビュー タブ本体（本番 3175-3208 の transcribe＋ローカル設定 JSON）。
 *
 * @param array $opts neccospeak_editing_options() 相当。
 * @return void
 */
function neccospeak_agent_ui_tab_preview( $opts ) {
	$opts = is_array( $opts ) ? $opts : array();
	?>
	<div class="ai-agent-section" id="ai-agent-preview">
		<h2><?php echo esc_html__( 'Widget preview', 'neccospeak' ); ?></h2>
		<p class="description"><?php echo esc_html__( 'A locally rendered preview of the draft design settings (no external connection, no LLM calls). After clicking "Save draft", reload this page to see the changes. Do the final check on the live site.', 'neccospeak' ); ?></p>

		<!-- ランチャーボタン プレビュー -->
		<h3 style="margin-top:24px;"><?php esc_html_e( 'Launcher button (collapsed)', 'neccospeak' ); ?></h3>
		<p class="description"><?php esc_html_e( 'Preview of the launcher button shown on your actual pages.', 'neccospeak' ); ?></p>
		<div id="ai-agent-launcher-preview" style="position:relative; height:160px; background:#f8f9fa; border:1px solid #dcdcde; border-radius:8px; margin-top:12px; overflow:hidden;">
			<div id="ai-agent-launcher-preview-inner" style="position:absolute; bottom:24px; right:24px;">
				<div id="ai-agent-launcher-preview-spinner" style="display:flex; align-items:center; justify-content:center; width:48px; height:48px; border-radius:9999px; background:#0D9488; box-shadow:0 4px 16px rgba(13,148,136,0.35);">
					<span style="width:18px; height:18px; border:2px solid rgba(255,255,255,0.35); border-top-color:#fff; border-radius:50%; animation:ai-agent-spin 0.7s linear infinite; display:block;"></span>
				</div>
			</div>
		</div>
		<style>@keyframes ai-agent-spin{to{transform:rotate(360deg);}}</style>

		<!-- ウィジェット本体（展開後）プレビュー — 実ウィジェット文書の iframe srcdoc 描画 -->
		<h3 style="margin-top:32px;"><?php esc_html_e( 'Widget (expanded)', 'neccospeak' ); ?></h3>
		<p class="description"><?php esc_html_e( 'Renders the home screen (header / welcome / quick actions) locally. No LLM is called, so no API tokens are consumed.', 'neccospeak' ); ?></p>
		<div id="ai-agent-panel-preview-wrap" style="margin-top:12px; display:flex; gap:16px; align-items:flex-start; flex-wrap:wrap;">
			<div id="ai-agent-panel-preview" style="width:400px; min-height:580px; border:1px solid #dcdcde; border-radius:12px; background:#fff; overflow:hidden; box-shadow:0 4px 16px rgba(0,0,0,0.08); display:flex; flex-direction:column;"><?php echo neccospeak_agent_ui_preview_iframe_html( $opts ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- neccospeak_agent_ui_preview_iframe_html() escapes every dynamic value at its own point of output (title via esc_attr__, srcdoc via esc_attr); this line is markup assembly, not raw unescaped input. ?></div>
			<div style="flex:1; min-width:240px;">
				<p class="description" style="margin:0 0 8px;"><?php esc_html_e( 'Items reflected in the preview:', 'neccospeak' ); ?></p>
				<ul class="description" style="margin:0; padding-left:18px;">
					<li><?php esc_html_e( 'Primary color / header title', 'neccospeak' ); ?></li>
					<li><?php esc_html_e( 'Welcome icon, title, and subtitle', 'neccospeak' ); ?></li>
					<li><?php esc_html_e( 'Capabilities list', 'neccospeak' ); ?></li>
					<li><?php esc_html_e( 'Avatar image / watermark image', 'neccospeak' ); ?></li>
					<li><?php esc_html_e( 'Input placeholder', 'neccospeak' ); ?></li>
				</ul>
				<p class="description" style="margin:12px 0 0;"><?php esc_html_e( 'Panel opening animation, sending/receiving messages, the voice UI, and navigation behavior are not reproduced. Check them on the live site.', 'neccospeak' ); ?></p>
			</div>
		</div>

		<script type="application/json" id="neccospeak-agent-ui-preview-config"><?php echo wp_json_encode( neccospeak_agent_ui_preview_config( $opts ), JSON_HEX_TAG | JSON_HEX_AMP ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON_HEX_TAG encodes every angle bracket as a <-style escape, so the payload can never close the script element; agent-ui.js parses it with JSON.parse. ?></script>
	</div>
	<?php
}
