<?php
if ( ! defined( 'ABSPATH' ) ) { exit; }
if ( class_exists( 'Truebe_Admin_Seeds' ) ) { return; }

class Truebe_Admin_Seeds {

	const MENU_SLUG              = 'truebe-review';
	const ACTION                 = 'truebe_seed_action';
	const SAVE_NONCE             = 'truebe_seed_save';
	const LOW_CONFIDENCE_THRESHOLD = 60;

	public static function init() {
		add_action( 'admin_menu', array( __CLASS__, 'register_page' ), 30 );
		add_action( 'admin_post_' . self::ACTION, array( __CLASS__, 'handle_action' ) );
		add_action( 'admin_post_truebe_audit_pdf', array( __CLASS__, 'export_audit_pdf' ) );
		add_action( 'wp_ajax_truebe_seed_save', array( __CLASS__, 'ajax_save' ) );
		add_action( 'wp_ajax_truebe_seed_create', array( __CLASS__, 'ajax_create' ) );
		add_action( 'wp_ajax_truebe_audit_fetch', array( __CLASS__, 'ajax_audit_fetch' ) );
		add_action( 'wp_ajax_truebe_gap_action', array( __CLASS__, 'ajax_gap_action' ) );
		add_action( 'wp_ajax_truebe_toggle_published', array( __CLASS__, 'ajax_toggle_published' ) );
		add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_assets' ) );
		add_action( 'admin_post_truebe_view_doc', array( __CLASS__, 'serve_doc' ) );
	}

	public static function ajax_create() {
		if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( 'forbidden', 403 ); }
		check_ajax_referer( 'truebe_create_nonce', 'nonce' );
		if ( ! class_exists( 'Truebe_Seeds' ) ) { wp_send_json_error( 'not_available', 400 ); }
		$business_name = get_option( 'truebe_business_name', __( 'you', 'truebe-fact-based-ai-chatbot' ) );
		/* translators: %s: business name, or "you" if none is set */
		$added_by = sprintf( __( 'Added by %s', 'truebe-fact-based-ai-chatbot' ), $business_name );
		$seed_id  = Truebe_Seeds::insert( array(
			'label'      => '',
			'thesis'     => '',
			'source_doc' => $added_by,
			'status'     => Truebe_Seeds::STATUS_PENDING,
		) );
if ( ! $seed_id ) { wp_send_json_error( 'insert_failed', 500 ); }
		wp_send_json_success( array( 'seed_id' => $seed_id ) );
	}

public static function ajax_audit_fetch() {
		if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( 'forbidden', 403 ); }
		check_ajax_referer( 'truebe_audit_nonce', 'nonce' );
		if ( ! class_exists( 'Truebe_Audit' ) ) {
			$audit_file = plugin_dir_path( __FILE__ ) . 'class-truebe-audit.php';
			if ( file_exists( $audit_file ) ) { require_once $audit_file; }
		}
		if ( ! class_exists( 'Truebe_Audit' ) ) { wp_send_json_error( 'not_available', 400 ); }
		global $wpdb;
		$table  = $wpdb->prefix . 'truebe_audit_log';
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- checking table existence; table name is a fixed prefix + string.
		$exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );
if ( $exists !== $table ) {
			Truebe_Audit::install();
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );
			if ( $exists !== $table ) {
				wp_send_json_success( array( 'rows' => array(), 'total' => 0 ) );
			}
		}
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- custom plugin table; table name is $wpdb->prefix plus a fixed string.
		$rows = $wpdb->get_results( "SELECT seed_id, question, action, approver, created_at FROM {$table} ORDER BY id DESC LIMIT 10000", ARRAY_A );		if ( ! is_array( $rows ) ) { $rows = array(); }
		$tz  = wp_timezone_string();
		$out = array();
		foreach ( $rows as $r ) {
			$out[] = array(
				'when'     => mysql2date( 'Y-m-d g:ia', $r['created_at'] ) . ' ' . $tz,
				'seed_id'  => ! empty( $r['seed_id'] ) ? (int) $r['seed_id'] : '',
				'question' => (string) $r['question'],
				'action'   => ucfirst( (string) $r['action'] ),
				'approver' => (string) $r['approver'],
			);
		}
		wp_send_json_success( array( 'rows' => $out, 'total' => count( $out ) ) );
	}

	public static function ajax_gap_action() {
		if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( 'forbidden', 403 ); }
		check_ajax_referer( 'truebe_gap_nonce', 'nonce' );
		$gid = isset( $_POST['gap_id'] ) ? absint( $_POST['gap_id'] ) : 0;
		$act = isset( $_POST['gap_act'] ) ? sanitize_key( $_POST['gap_act'] ) : '';
		$q   = isset( $_POST['gap_question'] ) ? sanitize_text_field( wp_unslash( $_POST['gap_question'] ) ) : '';
		if ( ! $gid || '' === $act ) { wp_send_json_error( 'bad_request', 400 ); }
if ( 'answer' === $act ) {
			$new_id = 0;
			if ( '' !== $q && class_exists( 'Truebe_Seeds' ) ) {
				$new_id = Truebe_Seeds::insert( array( 'label' => $q, 'thesis' => '', 'source_doc' => __( 'From a visitor question', 'truebe-fact-based-ai-chatbot' ), 'status' => Truebe_Seeds::STATUS_PENDING ) );
			}
			if ( class_exists( 'Truebe_Gaps' ) ) { Truebe_Gaps::set_status( $gid, Truebe_Gaps::STATUS_RESOLVED ); }
			wp_send_json_success( array( 'seed_id' => $new_id ) );
		}
		if ( 'dismiss' === $act ) {
			if ( class_exists( 'Truebe_Gaps' ) ) { Truebe_Gaps::set_status( $gid, Truebe_Gaps::STATUS_DISMISSED ); }
			wp_send_json_success();
		}
		wp_send_json_error( 'unknown_action', 400 );
	}

public static function export_audit_pdf() {
		if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Not allowed.', 'truebe-fact-based-ai-chatbot' ) ); }
		check_admin_referer( 'truebe_audit_pdf' );
		if ( ! class_exists( 'Truebe_Audit' ) ) {
			$audit_file = plugin_dir_path( __FILE__ ) . 'class-truebe-audit.php';
			if ( file_exists( $audit_file ) ) { require_once $audit_file; }
		}
		$rows = class_exists( 'Truebe_Audit' ) ? Truebe_Audit::get_all_for_export() : array();
		if ( ! is_array( $rows ) ) { $rows = array(); }
		$tz        = wp_timezone_string();
		$site      = get_bloginfo( 'name' );
		$exported  = mysql2date( 'Y-m-d g:ia', current_time( 'mysql' ) ) . ' ' . $tz;
		$total     = count( $rows );

		header( 'Content-Type: text/html; charset=utf-8' );
		wp_register_style( 'truebe-print', plugins_url( '../assets/print.css', __FILE__ ), array(), (string) filemtime( plugin_dir_path( __FILE__ ) . '../assets/print.css' ) );
		wp_enqueue_style( 'truebe-print' );
		echo '<!DOCTYPE html><html><head><meta charset="utf-8"><title>' . esc_html__( 'Audit Log', 'truebe-fact-based-ai-chatbot' ) . '</title>';
		wp_print_styles();
		echo '</head><body onload="window.print()">';
		echo '<h1>' . esc_html__( 'Audit Log', 'truebe-fact-based-ai-chatbot' ) . '</h1>';
		echo '<div class="meta">' . esc_html( $site ) . '</div>';
		/* translators: %s: export timestamp with timezone */
		echo '<div class="meta">' . esc_html( sprintf( __( 'Exported: %s', 'truebe-fact-based-ai-chatbot' ), $exported ) ) . '</div>';
		/* translators: %d: total number of audit records */
		echo '<div class="meta">' . esc_html( sprintf( _n( '%d record', '%d records', $total, 'truebe-fact-based-ai-chatbot' ), $total ) ) . '</div>';
		echo '<table><thead><tr>';
		echo '<th>' . esc_html__( 'Date / Time', 'truebe-fact-based-ai-chatbot' ) . '</th><th>' . esc_html__( 'ID', 'truebe-fact-based-ai-chatbot' ) . '</th><th>' . esc_html__( 'Question', 'truebe-fact-based-ai-chatbot' ) . '</th><th>' . esc_html__( 'Action', 'truebe-fact-based-ai-chatbot' ) . '</th><th>' . esc_html__( 'Reviewer', 'truebe-fact-based-ai-chatbot' ) . '</th>';
		echo '</tr></thead><tbody>';
		foreach ( $rows as $r ) {
			$when = mysql2date( 'Y-m-d g:ia', $r['created_at'] ) . ' ' . $tz;
			echo '<tr>';
			echo '<td style="white-space:nowrap;">' . esc_html( $when ) . '</td>';
			echo '<td>' . esc_html( ! empty( $r['seed_id'] ) ? (int) $r['seed_id'] : '' ) . '</td>';
			echo '<td>' . esc_html( (string) $r['question'] ) . '</td>';
			echo '<td>' . esc_html( ucfirst( (string) $r['action'] ) ) . '</td>';
			echo '<td>' . esc_html( (string) $r['approver'] ) . '</td>';
			echo '</tr>';
		}
		echo '</tbody></table>';
		/* translators: 1: total record count, 2: export timestamp */
		echo '<div class="foot">' . esc_html( sprintf( __( 'This report contains all %1$d audit records as of %2$s. Records are ordered most-recent first.', 'truebe-fact-based-ai-chatbot' ), $total, $exported ) ) . '</div>';
		echo '</body></html>';
		exit;
	}

public static function serve_doc() {
		if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Not allowed.', 'truebe-fact-based-ai-chatbot' ) ); }
		check_admin_referer( 'truebe_view_doc' );
		$name = isset( $_GET['doc'] ) ? basename( sanitize_file_name( wp_unslash( $_GET['doc'] ) ) ) : '';
		if ( '' === $name ) { wp_die( esc_html__( 'No document specified.', 'truebe-fact-based-ai-chatbot' ) ); }
		$dir  = wp_upload_dir();
		$path = trailingslashit( $dir['basedir'] ) . 'fact-library/' . $name;
		if ( ! file_exists( $path ) ) { wp_die( esc_html__( 'That document is no longer available.', 'truebe-fact-based-ai-chatbot' ) ); }
		header( 'Content-Type: application/pdf' );
		header( 'Content-Disposition: inline; filename="' . $name . '"' );
		header( 'Content-Length: ' . filesize( $path ) );
		readfile( $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile -- streaming a PDF to the browser; WP_Filesystem::get_contents() would buffer the entire file in memory. Path is gated by current_user_can( 'manage_options' ) and sanitized against traversal.
		exit;
	}

public static function enqueue_assets( $hook ) {
		if ( false === strpos( (string) $hook, self::MENU_SLUG ) ) { return; }
		wp_enqueue_style( 'truebe-admin', plugins_url( '../assets/admin.css', __FILE__ ), array(), (string) filemtime( plugin_dir_path( __FILE__ ) . '../assets/admin.css' ) );
		wp_add_inline_style( 'truebe-admin', '.truebe-edit:empty:before{content:attr(data-placeholder);color:#8c8f94;pointer-events:none;}.truebe-req{color:#b32d2e;font-size:11px;font-weight:600;margin-left:6px;display:none;}.truebe-req.is-on{display:inline;}details.truebe-approve-caret>summary{list-style:none;}details.truebe-approve-caret>summary::-webkit-details-marker{display:none;}details.truebe-approve-caret>summary::marker{content:"";}.truebe-approve-all-item:hover{background:#f0f6f4;}' );
		$active_tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only tab state used only to decide which assets to enqueue.
		if ( 'chat' === $active_tab && class_exists( 'Truebe_Admin_Chat' ) ) {
			Truebe_Admin_Chat::enqueue_assets();
		}
		wp_enqueue_script( 'truebe-seeds', plugins_url( '../assets/seeds.js', __FILE__ ), array(), (string) filemtime( plugin_dir_path( __FILE__ ) . '../assets/seeds.js' ), true );	
		wp_localize_script(
			'truebe-seeds',
			'truebeSeeds',
			array(
				'ajax'         => esc_url_raw( admin_url( 'admin-ajax.php' ) ),
				'adminPost'    => esc_url_raw( admin_url( 'admin-post.php' ) ),
				'pageUrl'      => esc_url_raw( add_query_arg( array( 'page' => self::MENU_SLUG ), admin_url( 'admin.php' ) ) ),
				'actionName'   => self::ACTION,
				'actionNonce'  => wp_create_nonce( self::ACTION ),
				'saveNonce'    => wp_create_nonce( self::SAVE_NONCE ),
				'gapNonce'     => wp_create_nonce( 'truebe_gap_nonce' ),
				'auditNonce'    => wp_create_nonce( 'truebe_audit_nonce' ),
				'approveLabel'  => __( 'Approve', 'truebe-fact-based-ai-chatbot' ),
				'confirmDelete' => __( 'Are you sure you want to delete this question and answer?', 'truebe-fact-based-ai-chatbot' ),
			)
		);
	}

	public static function register_page() {
		add_submenu_page( 'truebe', __( 'Fact Library', 'truebe-fact-based-ai-chatbot' ), __( 'Fact Library', 'truebe-fact-based-ai-chatbot' ), 'manage_options', self::MENU_SLUG, array( __CLASS__, 'render' ) );
	}

	public static function handle_action() {
		if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Not allowed.', 'truebe-fact-based-ai-chatbot' ) ); }
		check_admin_referer( self::ACTION );
		$id     = isset( $_POST['seed_id'] ) ? absint( $_POST['seed_id'] ) : 0;
		$index  = isset( $_POST['i'] ) ? max( 0, (int) $_POST['i'] ) : 0;
		$action = isset( $_POST['truebe_action'] ) ? sanitize_key( $_POST['truebe_action'] ) : '';
		$args   = array( 'page' => self::MENU_SLUG, 'i' => $index );
switch ( $action ) {
		case 'approve':
		$seed = self::find_pending( $id );
				// Already approved: this click un-approves, flip back to pending, no reviewer needed.
if ( $seed && isset( $seed['status'] ) && Truebe_Seeds::STATUS_READY === $seed['status'] ) {
					Truebe_Seeds::set_status( $id, Truebe_Seeds::STATUS_PENDING, 0 );
					global $wpdb;
					// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom plugin table.
					$wpdb->update( Truebe_Seeds::table(), array( 'published' => 0 ), array( 'id' => $id ), array( '%d' ), array( '%d' ) );
					if ( class_exists( 'Truebe_Audit' ) ) {
						Truebe_Audit::log( $id, $seed['label'], 'unapprove', '' );
					}
					$args['msg'] = 'unapproved';
					$args['sid'] = $id;
					break;
				}
				if ( $seed ) {
					if ( '' === trim( (string) $seed['label'] ) || '' === trim( (string) $seed['thesis'] ) ) {
						self::set_notice( 'incomplete' );
						$args['msg'] = 'incomplete';
						break;
					}
					$reviewer_id = isset( $_POST['truebe_reviewer'] ) ? absint( $_POST['truebe_reviewer'] ) : 0;
					if ( ! $reviewer_id ) {
						self::set_notice( 'no_reviewer' );
						$args['msg'] = 'no_reviewer';
						break;
					}
					$confidence            = self::calculate_confidence( $seed );
					$low_confidence_force  = ! empty( $_POST['truebe_low_confidence_force'] );
					if ( $confidence < self::LOW_CONFIDENCE_THRESHOLD && ! $low_confidence_force ) {
						self::set_notice( 'low_confidence', $confidence . '|' . $id );
						$args['msg'] = 'low_confidence';
						$args['sid'] = $id;
						break;
					}
$force = ! empty( $_POST['truebe_force'] );
					$check = class_exists( 'Truebe_Validator' ) ? Truebe_Validator::check( $seed ) : array( 'clear' => true );
					if ( ! empty( $check['clear'] ) || $force ) {
					$publish_default = ( '1' === get_option( 'truebe_publish_default', '1' ) ) ? 1 : 0;
					Truebe_Seeds::set_status( $id, Truebe_Seeds::STATUS_READY, $reviewer_id );
					global $wpdb;
					// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom plugin table.
					$wpdb->update( Truebe_Seeds::table(), array( 'confidence_tier' => (int) $confidence, 'published' => $publish_default ), array( 'id' => $id ), array( '%d', '%d' ), array( '%d' ) );
						if ( class_exists( 'Truebe_Audit' ) ) {
							$user          = get_user_by( 'ID', $reviewer_id );
							$reviewer_name = $user ? $user->display_name : __( 'Unknown', 'truebe-fact-based-ai-chatbot' );
							Truebe_Audit::log( $id, $seed['label'], ( $force && empty( $check['clear'] ) ) ? 'approve-override' : 'approve', $reviewer_name );
						}
						update_option( 'truebe_last_reviewer', $reviewer_id );
						$args['msg'] = 'published';
						$args['sid'] = $id;
					} else {
						self::set_notice( 'collision', ( isset( $check['conflict_label'] ) ? $check['conflict_label'] : '' ) . '|' . $id );
						$args['msg'] = 'collision';
					}
				}
				break;
				case 'create':
				/* translators: %s: business name, or "you" if none is set */
				$create_source = sprintf( __( 'Added by %s', 'truebe-fact-based-ai-chatbot' ), get_option( 'truebe_business_name', __( 'you', 'truebe-fact-based-ai-chatbot' ) ) );
				$new_id = Truebe_Seeds::insert( array(
					'label'      => '',
					'thesis'     => '',
					'source_doc' => $create_source,
					'status'     => Truebe_Seeds::STATUS_PENDING,
				) );
				$args['msg'] = 'created';
				if ( $new_id ) { $args['sid'] = $new_id; }
				break;

case 'approve_all':
				$reviewer_id = isset( $_POST['truebe_reviewer'] ) ? absint( $_POST['truebe_reviewer'] ) : 0;
				if ( ! $reviewer_id ) {
					self::set_notice( 'no_reviewer' );
					$args['msg'] = 'no_reviewer';
					break;
				}
				$approved        = 0;
				$skipped         = 0;
				$publish_default = ( '1' === get_option( 'truebe_publish_default', '1' ) ) ? 1 : 0;
				$user            = get_user_by( 'ID', $reviewer_id );
				$reviewer_name   = $user ? $user->display_name : __( 'Unknown', 'truebe-fact-based-ai-chatbot' );
				global $wpdb;
				foreach ( self::pending() as $seed ) {
					if ( Truebe_Seeds::STATUS_PENDING !== $seed['status'] ) { continue; }
					$has_label  = '' !== trim( (string) $seed['label'] );
					$has_thesis = '' !== trim( (string) $seed['thesis'] );
					if ( ! $has_label && ! $has_thesis ) { continue; }
					if ( ! $has_label || ! $has_thesis ) { $skipped++; continue; }
					$check = class_exists( 'Truebe_Validator' ) ? Truebe_Validator::check( $seed ) : array( 'clear' => true );
					if ( empty( $check['clear'] ) ) { $skipped++; continue; }
					$sid_i      = (int) $seed['id'];
					$confidence = self::calculate_confidence( $seed );
					Truebe_Seeds::set_status( $sid_i, Truebe_Seeds::STATUS_READY, $reviewer_id );
					// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom plugin table.
					$wpdb->update( Truebe_Seeds::table(), array( 'confidence_tier' => (int) $confidence, 'published' => $publish_default ), array( 'id' => $sid_i ), array( '%d', '%d' ), array( '%d' ) );
					if ( class_exists( 'Truebe_Audit' ) ) {
						Truebe_Audit::log( $sid_i, $seed['label'], 'bulk-approve', $reviewer_name );
					}
					$approved++;
				}
				update_option( 'truebe_last_reviewer', $reviewer_id );
				self::set_notice( 'bulk_approved', $approved . '|' . $skipped );
				$args['msg'] = 'bulk_approved';
				$args['i']   = 0;
				break;
			case 'delete':
				if ( $id ) {				

					$seed = self::find_pending( $id );
					if ( class_exists( 'Truebe_Audit' ) ) { Truebe_Audit::log( $id, $seed ? $seed['label'] : '', 'delete' ); }
					self::delete_seed( $id );
					$args['msg'] = 'deleted';
				}
				break;
			case 'answer_gap':
				$gid = isset( $_POST['gap_id'] ) ? absint( $_POST['gap_id'] ) : 0;
				$gq  = isset( $_POST['gap_question'] ) ? sanitize_text_field( wp_unslash( $_POST['gap_question'] ) ) : '';
				if ( $gid && '' !== $gq ) {
					Truebe_Seeds::insert( array( 'label' => $gq, 'thesis' => '', 'source_doc' => __( 'From a visitor question', 'truebe-fact-based-ai-chatbot' ), 'status' => Truebe_Seeds::STATUS_PENDING ) );
					if ( class_exists( 'Truebe_Gaps' ) ) { Truebe_Gaps::set_status( $gid, Truebe_Gaps::STATUS_RESOLVED ); }
					self::set_notice( 'gap_added' );
					$args['i']   = 0;
					$args['msg'] = 'gap_added';
				}
				break;
			case 'dismiss_gap':
				$gid = isset( $_POST['gap_id'] ) ? absint( $_POST['gap_id'] ) : 0;
				if ( $gid && class_exists( 'Truebe_Gaps' ) ) { Truebe_Gaps::set_status( $gid, Truebe_Gaps::STATUS_DISMISSED ); }
				$args['tab'] = 'unanswered';
				break;
		}
		wp_safe_redirect( add_query_arg( $args, admin_url( 'admin.php' ) ) );
		exit;
	}

public static function ajax_save() {
		if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( 'forbidden', 403 ); }
		check_ajax_referer( self::SAVE_NONCE, 'nonce' );
		$id    = isset( $_POST['id'] ) ? absint( $_POST['id'] ) : 0;
		$field = isset( $_POST['field'] ) ? sanitize_key( $_POST['field'] ) : '';

		// Handoff save: stores a label + URL under the answer without demoting the
		// seed to Pending, since a handoff edit is not an answer change.
if ( 'handoff' === $field ) {
			if ( ! $id ) { wp_send_json_error( 'bad_request', 400 ); }
			$label  = isset( $_POST['handoff_label'] ) ? sanitize_text_field( wp_unslash( $_POST['handoff_label'] ) ) : '';
			$url    = isset( $_POST['handoff_url'] ) ? esc_url_raw( wp_unslash( $_POST['handoff_url'] ) ) : '';
			$stored = ( '' !== $label || '' !== $url ) ? wp_json_encode( array( 'label' => $label, 'url' => $url ) ) : null;
			global $wpdb;
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom plugin table; $wpdb->update() is the supported API and there is no object cache for per-row admin edits.
			$wpdb->update( Truebe_Seeds::table(), array( 'handoff' => $stored ), array( 'id' => $id ), array( '%s' ), array( '%d' ) );
			wp_send_json_success();
		}

		$value = isset( $_POST['value'] ) ? wp_kses_post( wp_unslash( $_POST['value'] ) ) : '';
		$column = ( 'question' === $field ) ? 'label' : ( ( 'answer' === $field ) ? 'thesis' : '' );
		if ( ! $id || '' === $column ) { wp_send_json_error( 'bad_request', 400 ); }
		$existing  = self::find_pending( $id );
		$was_ready = $existing && isset( $existing['status'] ) && Truebe_Seeds::STATUS_READY === $existing['status'];
		global $wpdb;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom plugin table; $wpdb->update() is the supported API and there is no object cache for per-row admin edits.
		$wpdb->update( Truebe_Seeds::table(), array( $column => $value, 'status' => Truebe_Seeds::STATUS_PENDING, 'reviewer_id' => null, 'updated_at' => current_time( 'mysql' ) ), array( 'id' => $id ), array( '%s', '%s', '%d', '%s' ), array( '%d' ) );
		if ( $was_ready && class_exists( 'Truebe_Audit' ) ) {
			$q = ( 'label' === $column ) ? $value : ( isset( $existing['label'] ) ? $existing['label'] : '' );
			Truebe_Audit::log( $id, $q, 'edit' );
		}
		wp_send_json_success();
	}

	public static function ajax_toggle_published() {
		if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( 'forbidden', 403 ); }
		check_ajax_referer( 'truebe_publish_nonce', 'nonce' );
		$id  = isset( $_POST['seed_id'] ) ? absint( $_POST['seed_id'] ) : 0;
		$pub = ! empty( $_POST['published'] ) ? 1 : 0;
		if ( ! $id || ! class_exists( 'Truebe_Seeds' ) || ! method_exists( 'Truebe_Seeds', 'set_published' ) ) { wp_send_json_error( 'bad_request', 400 ); }
		Truebe_Seeds::set_published( $id, $pub );
		wp_send_json_success( array( 'published' => $pub ) );
	}

	private static function find_pending( $id ) {
		foreach ( self::pending() as $seed ) { if ( (int) $seed['id'] === (int) $id ) { return $seed; } }
		return null;
	}

	private static function pending() {
		return class_exists( 'Truebe_Seeds' ) ? Truebe_Seeds::get_all() : array();
	}

	private static function delete_seed( $id ) {
		global $wpdb;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom plugin table; $wpdb->delete() is the supported API and there is no object cache for this row.
		$wpdb->delete( Truebe_Seeds::table(), array( 'id' => absint( $id ) ), array( '%d' ) );
	}

	private static function get_reviewer_name( $reviewer_id ) {
		if ( empty( $reviewer_id ) ) { return ''; }
		$user = get_user_by( 'ID', (int) $reviewer_id );
		return $user ? $user->display_name : __( 'Unknown', 'truebe-fact-based-ai-chatbot' );
	}

private static function calculate_confidence( $seed ) {
		$label  = isset( $seed['label'] ) ? trim( (string) $seed['label'] ) : '';
		$thesis = isset( $seed['thesis'] ) ? trim( (string) $seed['thesis'] ) : '';
		if ( '' === $label || '' === $thesis ) { return 0; }
		$score = 25;
		if ( ! empty( $seed['context_main'] ) ) { $score += 30; }
		if ( ! empty( $seed['source_page'] ) ) { $score += 15; }
		if ( ! empty( $seed['citations'] ) && is_array( $seed['citations'] ) ) { $score += 15; }
		if ( ! empty( $seed['keywords'] ) && is_array( $seed['keywords'] ) && count( $seed['keywords'] ) >= 3 ) { $score += 15; }
		return min( 100, $score );
	}

	/**
	 * Remove empty placeholder seeds (no question, no answer) once real seeds exist.
	 * The blank "Added by you" card is only a UI affordance for an empty library; it
	 * must never linger alongside real content.
	 */
	private static function prune_blank_placeholders() {
		$all = self::pending();
		if ( count( $all ) < 2 ) { return; }
		foreach ( $all as $seed ) {
			$label  = isset( $seed['label'] ) ? trim( (string) $seed['label'] ) : '';
			$thesis = isset( $seed['thesis'] ) ? trim( (string) $seed['thesis'] ) : '';
			if ( '' === $label && '' === $thesis && Truebe_Seeds::STATUS_PENDING === $seed['status'] ) {
				self::delete_seed( (int) $seed['id'] );
			}
		}
	}

public static function render() {
		if ( ! current_user_can( 'manage_options' ) ) { return; }

		$post = esc_url( admin_url( 'admin-post.php' ) );
		echo '<div class="wrap truebe-settings">';

		$base  = admin_url( 'admin.php' );
		$flash = isset( $_GET['msg'] ) ? sanitize_key( $_GET['msg'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$tab   = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'answers'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended

		echo '<div style="margin-bottom:16px;">';
		echo '<h1 style="margin:0 0 4px;display:inline-flex;align-items:center;gap:8px;">' . esc_html__( 'Fact Library', 'truebe-fact-based-ai-chatbot' );
		echo '<span class="truebe-tooltip-wrap" tabindex="0" style="font-weight:400;">';
		echo '<span class="dashicons dashicons-info-outline truebe-info-icon" aria-hidden="true"></span>';
		echo '<span class="truebe-tooltip">' . esc_html__( 'Every question and answer is stored in your WordPress database in the /uploads folder. Your data never leaves your server.', 'truebe-fact-based-ai-chatbot' ) . '</span>';
		echo '</span>';
		if ( 'deleted' === $flash ) {
			echo '<span class="truebe-flash" id="truebe-flash" style="font-weight:400;"><span class="truebe-flash-pill">' . esc_html__( 'Question and its answer were deleted.', 'truebe-fact-based-ai-chatbot' ) . '</span></span>';
		}
		echo '</h1>';
		echo '<p style="color:#646970;font-size:13px;margin:0;">' . esc_html__( 'Review and approve each answer so they can be used by your chatbot.', 'truebe-fact-based-ai-chatbot' ) . '</p>';
		echo '</div>';

		// Tabs.
// Tabs.
		$gap_count   = class_exists( 'Truebe_Gaps' ) ? Truebe_Gaps::count_open() : 0;
		$answers_url = add_query_arg( array( 'page' => self::MENU_SLUG ), $base );
		$gaps_url    = add_query_arg( array( 'page' => self::MENU_SLUG, 'tab' => 'unanswered' ), $base );
		$chat_url    = add_query_arg( array( 'page' => self::MENU_SLUG, 'tab' => 'chat' ), $base );
		echo '<div class="truebe-tabs">';
		$pending_count = class_exists( 'Truebe_Seeds' ) ? count( array_filter( self::pending(), function( $s ) { return $s['status'] === Truebe_Seeds::STATUS_PENDING; } ) ) : 0;
		echo '<a href="' . esc_url( $answers_url ) . '" class="truebe-tab' . ( 'answers' === $tab ? ' is-active' : '' ) . '">' . esc_html__( 'Answers', 'truebe-fact-based-ai-chatbot' );
		if ( $pending_count > 0 ) { echo '<span class="truebe-tab-badge">' . esc_html( $pending_count ) . '</span>'; }
		echo '</a>';
		echo '<a href="' . esc_url( $gaps_url ) . '" class="truebe-tab' . ( 'unanswered' === $tab ? ' is-active' : '' ) . '">' . esc_html__( 'Unanswered', 'truebe-fact-based-ai-chatbot' );
		if ( $gap_count > 0 ) { echo '<span class="truebe-tab-badge">' . esc_html( $gap_count ) . '</span>'; }
		echo '</a>';
		echo '<a href="' . esc_url( $chat_url ) . '" class="truebe-tab' . ( 'chat' === $tab ? ' is-active' : '' ) . '">' . esc_html__( 'Chat', 'truebe-fact-based-ai-chatbot' ) . '</a>';
		$reports_url = add_query_arg( array( 'page' => self::MENU_SLUG, 'tab' => 'reports' ), $base );
		echo '<a href="' . esc_url( $reports_url ) . '" class="truebe-tab' . ( 'reports' === $tab ? ' is-active' : '' ) . '">' . esc_html__( 'Reports', 'truebe-fact-based-ai-chatbot' ) . '</a>';
		echo '</div>';

		self::notice();

		if ( 'unanswered' === $tab ) {
			self::render_gaps( $post );
			echo '</div>';
			return;
		}

		if ( 'chat' === $tab ) {
			if ( class_exists( 'Truebe_Admin_Chat' ) ) { Truebe_Admin_Chat::render_body(); }
			echo '</div>';
			return;
		}

		if ( 'reports' === $tab ) {
			self::render_reports();
			echo '</div>';
			return;
		}

		// Blank seeds now persist until the user deletes them with the trash icon,
		// so a newly created card survives pagination and page reloads. But once
		// real seeds exist, any leftover blank placeholder from an earlier empty
		// state must be swept so it never sits ahead of real content.
		self::prune_blank_placeholders();
		$fdoc    = '';
		$pending = array_values( self::pending() );
		$total   = count( $pending );
		$index   = isset( $_GET['i'] ) ? max( 0, (int) $_GET['i'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$sid     = isset( $_GET['sid'] ) ? absint( $_GET['sid'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		if ( $sid ) {
			foreach ( $pending as $k => $s ) {
				if ( (int) $s['id'] === $sid ) { $index = $k; break; }
			}
		}
		if ( $index >= $total ) { $index = $total - 1; }

		if ( 0 === $total ) {
			/* translators: %s: business name, or "you" if none is set */
			$added_by = sprintf( __( 'Added by %s', 'truebe-fact-based-ai-chatbot' ), get_option( 'truebe_business_name', __( 'you', 'truebe-fact-based-ai-chatbot' ) ) );
			Truebe_Seeds::insert( array(
				'label'      => '',
				'thesis'     => '',
				'source_doc' => $added_by,
				'status'     => Truebe_Seeds::STATUS_PENDING,
			) );
			$pending = array_values( self::pending() );
			$total   = count( $pending );
			if ( $total > 0 ) {
				self::render_card( $pending[0], 0, $post, $total );
				echo '</div>';
				return;
			}
			echo '<p>' . esc_html__( 'Nothing to review. Click + to add an answer, or upload a document.', 'truebe-fact-based-ai-chatbot' ) . '</p></div>';
			return;
		}

		self::render_card( $pending[ $index ], $index, $post, $total, $fdoc );
		self::render_audit_modal();
		echo '</div>';
	}

	private static function render_reports() {
		global $wpdb;
		$table = $wpdb->prefix . 'truebe_query_log';
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- checking table existence; fixed prefix + string.
		$exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );
		if ( $exists !== $table ) {
			if ( class_exists( 'Truebe_Query_Log' ) && method_exists( 'Truebe_Query_Log', 'install' ) ) {
				Truebe_Query_Log::install();
			}
		}

		$since = gmdate( 'Y-m-d H:i:s', current_time( 'timestamp' ) - 30 * DAY_IN_SECONDS );

		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- custom plugin table; prefix is core-provided.
		$asked = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE created_at >= %s", $since ) );
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
		$answered = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE created_at >= %s AND status = %s", $since, 'answered' ) );
		$deflected = max( 0, $asked - $answered );
		$rate      = $asked > 0 ? round( ( $answered / $asked ) * 100 ) : 0;

		// Top unanswered topics (last 30 days).
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
		$top = $wpdb->get_results( $wpdb->prepare(
			"SELECT question, COUNT(*) AS hits FROM {$table} WHERE created_at >= %s AND status = %s GROUP BY question ORDER BY hits DESC LIMIT 10", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			$since,
			'gap'
		), ARRAY_A );
		if ( ! is_array( $top ) ) { $top = array(); }

		// Weekly trend, last 6 weeks.
		$weeks = array();
		for ( $w = 5; $w >= 0; $w-- ) {
			$start = current_time( 'timestamp' ) - ( $w + 1 ) * WEEK_IN_SECONDS;
			$end   = current_time( 'timestamp' ) - $w * WEEK_IN_SECONDS;
			$s     = gmdate( 'Y-m-d H:i:s', $start );
			$e     = gmdate( 'Y-m-d H:i:s', $end );
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
			$c = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE created_at >= %s AND created_at < %s", $s, $e ) );
			$weeks[] = array( 'label' => gmdate( 'M j', $start ), 'count' => $c );
		}
		$max_week = 0;
		foreach ( $weeks as $wk ) { if ( $wk['count'] > $max_week ) { $max_week = $wk['count']; } }

		if ( 0 === $asked ) {
			echo '<div style="background:#fff;border:1px solid #dcdcde;border-radius:10px;padding:28px 32px;color:#646970;font-size:14px;">' . esc_html__( "No questions yet. Once your chatbot is live and visitors start asking, you'll see how many questions came in, how many were answered from your library, and what people asked that you haven't covered.", 'truebe-fact-based-ai-chatbot' ) . '</div>';
			return;
		}

		echo '<p style="color:#646970;font-size:13px;margin:0 0 12px;">' . esc_html__( 'Last 30 days.', 'truebe-fact-based-ai-chatbot' ) . '</p>';

		// Stat cards.
		echo '<div style="display:flex;gap:12px;margin-bottom:20px;flex-wrap:wrap;">';
		$cards = array(
			array( __( 'Questions asked', 'truebe-fact-based-ai-chatbot' ), $asked, '#1d2327' ),
			array( __( 'Answered from library', 'truebe-fact-based-ai-chatbot' ), $answered . '  (' . $rate . '%)', '#0f6e56' ),
			array( __( 'Sent to a human', 'truebe-fact-based-ai-chatbot' ), $deflected, '#92400e' ),
		);
		foreach ( $cards as $c ) {
			echo '<div style="flex:1;min-width:180px;background:#fff;border:1px solid #dcdcde;border-radius:10px;padding:18px 20px;">';
			echo '<div style="font-size:12px;color:#646970;margin-bottom:6px;">' . esc_html( $c[0] ) . '</div>';
			echo '<div style="font-size:24px;font-weight:600;color:' . esc_attr( $c[2] ) . ';">' . esc_html( $c[1] ) . '</div>';
			echo '</div>';
		}
		echo '</div>';

		// Trend.
		echo '<div style="background:#fff;border:1px solid #dcdcde;border-radius:10px;padding:20px 24px;margin-bottom:20px;">';
		echo '<div style="font-size:14px;font-weight:600;color:#000;margin-bottom:16px;">' . esc_html__( 'Questions per week', 'truebe-fact-based-ai-chatbot' ) . '</div>';
		echo '<div style="display:flex;align-items:flex-end;gap:12px;height:120px;">';
		foreach ( $weeks as $wk ) {
			$h = $max_week > 0 ? max( 4, (int) round( ( $wk['count'] / $max_week ) * 100 ) ) : 4;
			echo '<div style="flex:1;display:flex;flex-direction:column;align-items:center;gap:6px;">';
			echo '<div style="font-size:11px;color:#646970;">' . esc_html( $wk['count'] ) . '</div>';
			echo '<div style="width:100%;background:#0f6e56;border-radius:4px 4px 0 0;height:' . esc_attr( $h ) . 'px;"></div>';
			echo '<div style="font-size:11px;color:#8a8a8a;">' . esc_html( $wk['label'] ) . '</div>';
			echo '</div>';
		}
		echo '</div>';
		echo '</div>';

		// Top unanswered.
		echo '<div style="background:#fff;border:1px solid #dcdcde;border-radius:10px;padding:20px 24px;">';
		echo '<div style="font-size:14px;font-weight:600;color:#000;margin-bottom:12px;">' . esc_html__( 'Top unanswered questions', 'truebe-fact-based-ai-chatbot' ) . '</div>';
		if ( empty( $top ) ) {
			echo '<div style="font-size:13px;color:#646970;">' . esc_html__( 'Nothing unanswered in this period. Every question matched an answer.', 'truebe-fact-based-ai-chatbot' ) . '</div>';
		} else {
			foreach ( $top as $row ) {
				echo '<div style="display:flex;justify-content:space-between;gap:16px;padding:8px 0;border-bottom:1px solid #f0f0f1;font-size:13px;">';
				echo '<span style="color:#1d2327;">' . esc_html( (string) $row['question'] ) . '</span>';
				/* translators: %d: number of times asked */
				echo '<span style="color:#646970;white-space:nowrap;">' . esc_html( sprintf( _n( '%d time', '%d times', (int) $row['hits'], 'truebe-fact-based-ai-chatbot' ), (int) $row['hits'] ) ) . '</span>';
				echo '</div>';
			}
		}
		echo '</div>';
	}

	private static function render_audit_modal() {
		$pdf_url = wp_nonce_url( add_query_arg( array( 'action' => 'truebe_audit_pdf' ), admin_url( 'admin-post.php' ) ), 'truebe_audit_pdf' );
		echo '<div id="truebe-audit-overlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:100000;align-items:center;justify-content:center;">';
		echo '<div style="background:#fff;border-radius:10px;width:min(900px,92vw);max-height:85vh;display:flex;flex-direction:column;overflow:hidden;">';
		echo '<div style="display:flex;align-items:center;justify-content:space-between;padding:18px 24px;border-bottom:1px solid #dcdcde;">';
		echo '<h2 style="margin:0;font-size:18px;font-weight:600;">' . esc_html__( 'Audit Log', 'truebe-fact-based-ai-chatbot' ) . '</h2>';
		echo '<div style="display:flex;align-items:center;gap:12px;">';
		echo '<a href="' . esc_url( $pdf_url ) . '" target="_blank" title="' . esc_attr__( 'Export as PDF', 'truebe-fact-based-ai-chatbot' ) . '" style="text-decoration:none;color:#0f6e56;display:inline-flex;align-items:center;gap:6px;font-size:13px;font-weight:500;margin-right:20px;"><span class="dashicons dashicons-media-document" style="font-size:18px;width:18px;height:18px;"></span>' . esc_html__( 'PDF', 'truebe-fact-based-ai-chatbot' ) . '</a>';
		echo '<button type="button" id="truebe-audit-close" style="border:none;background:none;cursor:pointer;font-size:22px;color:#646970;line-height:1;padding:0;">&times;</button>';
		echo '</div>';
		echo '</div>';
		echo '<div id="truebe-audit-body" style="overflow-y:auto;padding:0;">';
		echo '<div id="truebe-audit-loading" style="padding:40px;text-align:center;color:#646970;">' . esc_html__( 'Loading…', 'truebe-fact-based-ai-chatbot' ) . '</div>';
		echo '<table id="truebe-audit-table" style="width:100%;border-collapse:collapse;font-size:13px;display:none;">';
		echo '<thead><tr style="background:#f6f7f7;text-align:left;">';
		echo '<th style="padding:10px 16px;font-weight:600;border-bottom:1px solid #dcdcde;">' . esc_html__( 'Date / Time', 'truebe-fact-based-ai-chatbot' ) . '</th>';
		echo '<th style="padding:10px 16px;font-weight:600;border-bottom:1px solid #dcdcde;">' . esc_html__( 'ID', 'truebe-fact-based-ai-chatbot' ) . '</th>';
		echo '<th style="padding:10px 16px;font-weight:600;border-bottom:1px solid #dcdcde;">' . esc_html__( 'Question', 'truebe-fact-based-ai-chatbot' ) . '</th>';
		echo '<th style="padding:10px 16px;font-weight:600;border-bottom:1px solid #dcdcde;">' . esc_html__( 'Action', 'truebe-fact-based-ai-chatbot' ) . '</th>';
		echo '<th style="padding:10px 16px;font-weight:600;border-bottom:1px solid #dcdcde;">' . esc_html__( 'Reviewer', 'truebe-fact-based-ai-chatbot' ) . '</th>';
		echo '</tr></thead><tbody id="truebe-audit-rows"></tbody>';
		echo '</table>';
		echo '</div>';
		echo '<div style="padding:14px 24px;border-top:1px solid #dcdcde;">';
		echo '<span id="truebe-audit-count" style="font-size:12px;color:#646970;"></span>';
		echo '</div>';
		echo '</div>';
		echo '</div>';
	}

private static function render_gaps( $post ) {
		$gaps = class_exists( 'Truebe_Gaps' ) ? Truebe_Gaps::get_open( 100 ) : array();
		if ( empty( $gaps ) ) {
			echo '<div style="background:#fff;border:1px solid #dcdcde;border-radius:10px;padding:28px 32px;color:#646970;font-size:14px;">' . esc_html__( "No unanswered questions yet. When a visitor asks something your chatbot can't answer, it shows up here.", 'truebe-fact-based-ai-chatbot' ) . '</div>';
			return;
		}

		foreach ( $gaps as $gap ) {
			$gid  = (int) $gap['id'];
			$q    = (string) $gap['question'];
			$hits = (int) $gap['hits'];
			$seen = ! empty( $gap['last_seen'] ) ? mysql2date( 'M j, Y g:ia', $gap['last_seen'] ) : '';
			echo '<div class="truebe-gap-row">';
			echo '<div class="truebe-gap-q">' . esc_html( $q ) . '</div>';
			if ( $seen ) {
				/* translators: 1: number of times asked, 2: last-asked date */
				$meta = sprintf( _n( 'Asked %1$d time · last asked %2$s', 'Asked %1$d times · last asked %2$s', $hits, 'truebe-fact-based-ai-chatbot' ), $hits, $seen );
			} else {
				/* translators: %d: number of times asked */
				$meta = sprintf( _n( 'Asked %d time', 'Asked %d times', $hits, 'truebe-fact-based-ai-chatbot' ), $hits );
			}
			echo '<div class="truebe-gap-meta">' . esc_html( $meta ) . '</div>';
			echo '<div class="truebe-gap-actions">';
			echo '<button type="button" class="truebe-btn truebe-gap-btn" data-gap-act="answer" data-gap-id="' . esc_attr( $gid ) . '" data-gap-q="' . esc_attr( $q ) . '">' . esc_html__( 'Answer', 'truebe-fact-based-ai-chatbot' ) . '</button>';
			echo '<button type="button" class="truebe-btn truebe-btn-ghost truebe-gap-btn" data-gap-act="dismiss" data-gap-id="' . esc_attr( $gid ) . '">' . esc_html__( 'Delete', 'truebe-fact-based-ai-chatbot' ) . '</button>';
			echo '</div>';
			echo '</div>';
		}
	}

private static function notice_key() {
		return 'truebe_notice_' . get_current_user_id();
	}

	private static function set_notice( $msg, $extra = '' ) {
		set_transient( self::notice_key(), array( 'msg' => sanitize_key( $msg ), 'extra' => sanitize_text_field( (string) $extra ) ), 60 );
	}

	private static function get_notice() {
		$data = get_transient( self::notice_key() );
		if ( ! is_array( $data ) ) { return array( 'msg' => '', 'extra' => '' ); }
		delete_transient( self::notice_key() );
		return array(
			'msg'   => isset( $data['msg'] ) ? sanitize_key( $data['msg'] ) : '',
			'extra' => isset( $data['extra'] ) ? sanitize_text_field( (string) $data['extra'] ) : '',
		);
	}

	private static function notice() {
		$notice = self::get_notice();
		$msg    = $notice['msg'];
		if ( '' === $msg ) { return; }
		if ( 'gap_added' === $msg ) {
			echo '<div class="notice notice-success is-dismissible" style="margin:0 0 10px;"><p>' . esc_html__( 'Added to your answers. Write the answer below and approve it.', 'truebe-fact-based-ai-chatbot' ) . '</p></div>';
			return;
		}
		if ( 'bulk_approved' === $msg ) {
			$parts    = array_map( 'intval', explode( '|', $notice['extra'] ) );
			$approved = isset( $parts[0] ) ? $parts[0] : 0;
			$skipped  = isset( $parts[1] ) ? $parts[1] : 0;
			/* translators: %d: number of answers approved */
			$text = sprintf( _n( 'Approved %d answer.', 'Approved %d answers.', $approved, 'truebe-fact-based-ai-chatbot' ), $approved );
			if ( $skipped > 0 ) {
				/* translators: %d: number of answers skipped */
				$text .= ' ' . sprintf( _n( '%d still needs a quick look below — it didn\'t pass automatically.', '%d still need a quick look below — they didn\'t pass automatically.', $skipped, 'truebe-fact-based-ai-chatbot' ), $skipped );
			}
			echo '<div class="notice notice-success is-dismissible" style="margin:0 0 10px;"><p>' . esc_html( $text ) . '</p></div>';
			return;
		}
		if ( 'no_reviewer' === $msg ) {
			echo '<div class="notice notice-warning is-dismissible" style="margin:0 0 10px;"><p>' . esc_html__( 'Select a reviewer before approving.', 'truebe-fact-based-ai-chatbot' ) . '</p></div>';
			return;
		}
		if ( 'incomplete' === $msg ) {
			echo '<div class="notice notice-warning is-dismissible" style="margin:0 0 10px;"><p>' . esc_html__( 'Both a question and an answer are required before approving.', 'truebe-fact-based-ai-chatbot' ) . '</p></div>';
			return;
		}
if ( 'collision' === $msg ) {
			$parts       = explode( '|', $notice['extra'] );
			$c           = isset( $parts[0] ) ? $parts[0] : '';
			$conflict_id = isset( $parts[1] ) ? absint( $parts[1] ) : 0;
			$idx         = isset( $_GET['i'] ) ? max( 0, (int) $_GET['i'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only pagination index, used only to preserve card position after a delete.
			self::render_collision_box( $c, $conflict_id, $idx );
			return;
		}
		if ( 'low_confidence' === $msg ) {
			$parts = explode( '|', $notice['extra'] );
			$conf  = isset( $parts[0] ) ? absint( $parts[0] ) : 0;
			$sid   = isset( $parts[1] ) ? absint( $parts[1] ) : 0;
			self::render_low_confidence_box( $conf, $sid );
			return;
		}
	}
	private static function render_collision_box( $conflict_label, $seed_id, $index = 0 ) {
		/* translators: %s: the conflicting published answer's question text */
		$text = ( '' !== $conflict_label ) ? sprintf( __( 'Very similar to: "%s"', 'truebe-fact-based-ai-chatbot' ), $conflict_label ) : __( 'Very similar to another published answer.', 'truebe-fact-based-ai-chatbot' );
		echo '<div class="notice notice-warning is-dismissible" style="margin:0 0 10px;display:flex;align-items:center;justify-content:space-between;gap:16px;">';
		echo '<p style="margin:0;">' . esc_html( $text ) . '</p>';
		echo '<span class="truebe-collision-actions">';
		echo '<button type="button" id="truebe-approve-anyway" class="truebe-collision-btn truebe-collision-keep">' . esc_html__( 'Keep', 'truebe-fact-based-ai-chatbot' ) . '</button>';
		echo '<button type="button" id="truebe-collision-delete" data-seed-id="' . esc_attr( $seed_id ) . '" data-index="' . esc_attr( $index ) . '" class="truebe-collision-btn truebe-collision-delete">' . esc_html__( 'Delete', 'truebe-fact-based-ai-chatbot' ) . '</button>';
		echo '</span>';
		echo '</div>';
	}

	/**
	 * Shown when an approve attempt is blocked for scoring below LOW_CONFIDENCE_THRESHOLD.
	 * "Approve anyway" resubmits the existing #truebe-approve-form with an override flag.
	 */
	private static function render_low_confidence_box( $confidence, $seed_id ) {
		/* translators: 1: the confidence percentage that fell below the threshold, 2: the required threshold percentage */
		$text = sprintf( __( 'This answer scored %1$d%%, below the %2$d%% threshold. Check the source before approving.', 'truebe-fact-based-ai-chatbot' ), $confidence, self::LOW_CONFIDENCE_THRESHOLD );
		echo '<div class="notice notice-warning is-dismissible" style="margin:0 0 10px;display:flex;align-items:center;justify-content:space-between;gap:16px;">';
		echo '<p style="margin:0;">' . esc_html( $text ) . '</p>';
		echo '<button type="button" id="truebe-approve-low-confidence" data-seed-id="' . esc_attr( $seed_id ) . '" class="truebe-collision-btn truebe-collision-delete">' . esc_html__( 'Approve anyway', 'truebe-fact-based-ai-chatbot' ) . '</button>';
		echo '</div>';
	}

		private static function render_card( $seed, $index, $post, $total = 1, $fdoc = '' ) {
		$id       = (int) $seed['id'];
		$is_ready = ( isset( $seed['status'] ) && Truebe_Seeds::STATUS_READY === $seed['status'] );
		$pending_count = 0;
		foreach ( self::pending() as $ps ) {
			if ( Truebe_Seeds::STATUS_PENDING === $ps['status'] && ( '' !== trim( (string) $ps['label'] ) || '' !== trim( (string) $ps['thesis'] ) ) ) { $pending_count++; }
		}
$base     = admin_url( 'admin.php' );
		$prev     = max( 0, $index - 1 );
		$next     = min( $total - 1, $index + 1 );
		$navargs  = array( 'page' => self::MENU_SLUG );
		if ( '' !== $fdoc ) { $navargs['fdoc'] = $fdoc; }

		if ( ! $is_ready && class_exists( 'Truebe_Validator' ) ) {
			$has_content = '' !== trim( (string) $seed['label'] ) || '' !== trim( (string) $seed['thesis'] );
			if ( $has_content ) {
				$check = Truebe_Validator::check( $seed );
			if ( empty( $check['clear'] ) ) {
					self::render_collision_box( isset( $check['conflict_label'] ) ? $check['conflict_label'] : '', $id, $index );
				}
			}
		}

		echo '<div style="background:#fff;border:1px solid #dcdcde;border-radius:10px;padding:28px 32px;">';
		// Header row: Question label, status pill (centered), delete + add + reviewer (right side).
		echo '<div style="display:flex;align-items:center;margin-bottom:12px;gap:12px;">';
		echo '<span style="font-size:14px;font-weight:600;color:#000;">' . esc_html__( 'Question', 'truebe-fact-based-ai-chatbot' ) . '</span>';
		echo '<span style="flex:1;display:inline-flex;align-items:center;justify-content:center;gap:8px;">';

		if ( $is_ready ) {
			$when        = ! empty( $seed['updated_at'] ) ? mysql2date( 'l, F j \| g:ia', $seed['updated_at'] ) : '';
			$reviewer_id = ! empty( $seed['reviewer_id'] ) ? (int) $seed['reviewer_id'] : 0;
			$reviewer    = self::get_reviewer_name( $reviewer_id );
			echo '<span style="background:#d3ebe8; color:#0f6e56; padding:4px 7px; border-radius:20px; font-size:12px; font-weight:500; display:inline-flex; align-items:center; gap:8px;">';
			echo '<span>&#10003; ' . esc_html__( 'Published', 'truebe-fact-based-ai-chatbot' ) . ( $when ? ' ' . esc_html( $when ) : '' ) . '</span>';
			if ( $reviewer ) {
				echo '<span style="background:#fff; color:#0f6e56; padding:3px 7px; border-radius:20px; font-weight:500; font-size:12px;">' . esc_html( $reviewer ) . '</span>';
			}
			echo '</span>';
			} else {
						echo '<span style="background:#fef3c7;color:#92400e;padding:6px 12px;border-radius:20px;font-size:13px;font-weight:500;">' . esc_html__( 'Pending', 'truebe-fact-based-ai-chatbot' ) . '</span>';
					}
if ( $is_ready ) {
echo '<button type="button" id="truebe-audit-open" title="' . esc_attr__( 'View audit log', 'truebe-fact-based-ai-chatbot' ) . '" style="border:none;background:none;cursor:pointer;color:#0f6e56;display:inline-flex;align-items:center;padding:2px;line-height:1;"><span class="dashicons dashicons-backup" style="font-size:20px;width:20px;height:20px;"></span></button>';
}
					echo '<span class="truebe-saved" style="font-size:11px;color:#8a8a8a;display:none;align-items:center;gap:4px;">&#10003; ' . esc_html__( 'Saved', 'truebe-fact-based-ai-chatbot' ) . '</span>';
					echo '</span>';

					// Right side: reviewer dropdown (pending only), add, delete.
					echo '<div style="display:flex;align-items:center;gap:8px;">';
			if ( ! $is_ready ) {
			$last_reviewer = (int) get_option( 'truebe_last_reviewer', 0 );
			echo '<select name="truebe_reviewer" form="truebe-approve-form" style="height:36px;width:auto;border:1px solid #e6e6e8;border-radius:6px;font-size:13px;padding:0 28px 0 10px;">';
			echo '<option value="">' . esc_html__( 'Reviewer', 'truebe-fact-based-ai-chatbot' ) . '</option>';
			foreach ( get_users( array( 'fields' => array( 'ID', 'display_name' ) ) ) as $u ) {
				echo '<option value="' . esc_attr( (int) $u->ID ) . '"' . selected( $last_reviewer, (int) $u->ID, false ) . '>' . esc_html( $u->display_name ) . '</option>';
			}
			echo '</select>';
		}

		echo '<button type="button" class="truebe-add-btn" data-action="truebe-create" title="' . esc_attr__( 'Add answer', 'truebe-fact-based-ai-chatbot' ) . '">+</button>';

		echo '<form method="post" action="' . esc_url( $post ) . '" class="truebe-delete-form" style="margin:0;display:inline-flex;">';
		wp_nonce_field( self::ACTION );
		echo '<input type="hidden" name="action" value="' . esc_attr( self::ACTION ) . '">';
		echo '<input type="hidden" name="truebe_action" value="delete">';
		echo '<input type="hidden" name="seed_id" value="' . esc_attr( $id ) . '">';
		echo '<input type="hidden" name="i" value="' . esc_attr( $index ) . '">';
		echo '<button title="' . esc_attr__( 'Delete', 'truebe-fact-based-ai-chatbot' ) . '" style="width:28px;height:28px;border:none;background:none;color:#c3c4c7;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;padding:0;line-height:1;">&#128465;</button>';
		echo '</form>';
		echo '</div>';
		echo '</div>';

echo '<div class="truebe-edit" data-id="' . esc_attr( $id ) . '" data-field="question" data-placeholder="' . esc_attr__( 'Type the question a visitor would ask…', 'truebe-fact-based-ai-chatbot' ) . '" contenteditable="true" style="font-size:16px;font-weight:500;margin-bottom:20px;padding:10px 12px;border:1px solid #dcdcde;border-radius:6px;outline:none;">' . esc_html( $seed['label'] ) . '</div>';

		// Answer label + confidence pill.
		echo '<div style="display:flex;align-items:center;gap:12px;margin-bottom:6px;">';
		echo '<span style="font-size:14px;font-weight:600;color:#000;">' . esc_html__( 'Answer', 'truebe-fact-based-ai-chatbot' ) . '</span>';
		$confidence = ! empty( $seed['confidence_tier'] ) ? (int) $seed['confidence_tier'] : self::calculate_confidence( $seed );
		if ( $confidence >= 0 ) {
			$label_ok  = ( '' !== trim( (string) ( $seed['label'] ?? '' ) ) );
			$thesis_ok = ( '' !== trim( (string) ( $seed['thesis'] ?? '' ) ) );
			if ( $label_ok && $thesis_ok ) {
				$qa_state = 'green'; $qa_pct = 100; $qa_reason = __( 'Both the question and answer are filled in.', 'truebe-fact-based-ai-chatbot' );
			} elseif ( $label_ok || $thesis_ok ) {
				$qa_state = 'yellow'; $qa_pct = 50; $qa_reason = __( 'Only one of the question or answer is filled in.', 'truebe-fact-based-ai-chatbot' );
			} else {
				$qa_state = 'red'; $qa_pct = 0; $qa_reason = __( 'Neither the question nor the answer is filled in yet.', 'truebe-fact-based-ai-chatbot' );
			}

			$has_passage    = ! empty( $seed['context_main'] );
			$passage_state  = $has_passage ? 'green' : 'red';
			$passage_pct    = $has_passage ? 100 : 0;
			$passage_reason = $has_passage
				? __( 'A matching passage from the source document is attached.', 'truebe-fact-based-ai-chatbot' )
				: __( 'No matching passage was found in the source document, so this answer cannot be traced back to its origin.', 'truebe-fact-based-ai-chatbot' );

			$has_page    = ! empty( $seed['source_page'] );
			$page_state  = $has_page ? 'green' : 'red';
			$page_pct    = $has_page ? 100 : 0;
			$page_reason = $has_page
				? __( 'A page number is recorded for this answer.', 'truebe-fact-based-ai-chatbot' )
				: __( 'No page number is recorded, so a reviewer cannot jump straight to the source.', 'truebe-fact-based-ai-chatbot' );

			$has_citation    = ( ! empty( $seed['citations'] ) && is_array( $seed['citations'] ) );
			$citation_state  = $has_citation ? 'green' : 'red';
			$citation_pct    = $has_citation ? 100 : 0;
			$citation_reason = $has_citation
				? __( 'A direct citation from the source is attached.', 'truebe-fact-based-ai-chatbot' )
				: __( 'No citation is attached, so the exact source sentence is unknown.', 'truebe-fact-based-ai-chatbot' );

			$kw_count = ( ! empty( $seed['keywords'] ) && is_array( $seed['keywords'] ) ) ? count( $seed['keywords'] ) : 0;
			if ( $kw_count >= 3 ) {
				$kw_state = 'green'; $kw_pct = 100;
				/* translators: %d: number of keywords attached */
				$kw_reason = sprintf( __( '%d keywords are attached, enough to reliably match visitor questions.', 'truebe-fact-based-ai-chatbot' ), $kw_count );
			} elseif ( $kw_count >= 1 ) {
				$kw_state = 'yellow'; $kw_pct = 50;
				/* translators: %d: number of keywords attached */
				$kw_reason = sprintf( _n( 'Only %d keyword attached; at least 3 is recommended for reliable matching.', 'Only %d keywords attached; at least 3 is recommended for reliable matching.', $kw_count, 'truebe-fact-based-ai-chatbot' ), $kw_count );
			} else {
				$kw_state = 'red'; $kw_pct = 0;
				$kw_reason = __( 'No keywords are attached, so this answer may be harder to match to visitor questions.', 'truebe-fact-based-ai-chatbot' );
			}

			$breakdown = array(
				array( 'label' => __( 'Complete Q&A', 'truebe-fact-based-ai-chatbot' ), 'weight' => '25%', 'state' => $qa_state, 'pct' => $qa_pct, 'reason' => $qa_reason ),
				array( 'label' => __( 'Source passage', 'truebe-fact-based-ai-chatbot' ), 'weight' => '30%', 'state' => $passage_state, 'pct' => $passage_pct, 'reason' => $passage_reason ),
				array( 'label' => __( 'Page locator', 'truebe-fact-based-ai-chatbot' ), 'weight' => '15%', 'state' => $page_state, 'pct' => $page_pct, 'reason' => $page_reason ),
				array( 'label' => __( 'Citation', 'truebe-fact-based-ai-chatbot' ), 'weight' => '15%', 'state' => $citation_state, 'pct' => $citation_pct, 'reason' => $citation_reason ),
				array( 'label' => __( 'Keyword coverage', 'truebe-fact-based-ai-chatbot' ), 'weight' => '15%', 'state' => $kw_state, 'pct' => $kw_pct, 'reason' => $kw_reason ),
			);
			echo '<span class="truebe-confidence-score" data-breakdown="' . esc_attr( wp_json_encode( $breakdown ) ) . '" style="background:#d3ebe8; padding:2px 4px; border-radius: 3px; font-size:10px; font-weight:500; color:#0f6e56; cursor:pointer; position:relative;">' . esc_html( $confidence ) . '%</span>';
			}
		echo '</div>';
		
		echo '<div class="truebe-edit" data-id="' . esc_attr( $id ) . '" data-field="answer" contenteditable="true" style="background:#f6f7f7;border-radius:6px;padding:12px 14px;font-size:15px;line-height:1.5;margin-bottom:0;outline:none;">' . esc_html( $seed['thesis'] ) . '</div>';

		self::render_source( $seed );

// Button: optional labeled link shown under the answer, for the private step the library can't answer.
		$handoff       = ( ! empty( $seed['handoff'] ) && is_array( $seed['handoff'] ) ) ? $seed['handoff'] : array();
		$handoff_label = isset( $handoff['label'] ) ? (string) $handoff['label'] : '';
		$handoff_url   = isset( $handoff['url'] ) ? (string) $handoff['url'] : '';
		$handoff_open  = ( '' !== $handoff_label || '' !== $handoff_url ) ? ' open' : '';
		echo '<details class="truebe-handoff-details" style="margin-top:20px;"' . esc_html( $handoff_open ) . '>';
		echo '<summary style="cursor:pointer;font-size:14px;font-weight:600;color:#000;list-style:none;display:flex;align-items:center;gap:6px;">';
		echo '<span style="color:#0f6e56;">+</span> ' . esc_html__( 'Button', 'truebe-fact-based-ai-chatbot' );
		echo '</summary>';
		echo '<p style="font-size:12px;font-weight:400;color:#8a8a8a;margin:6px 0 0;">' . esc_html__( 'Optional. Shows a button under this answer, e.g. book a consult, log in, call us.', 'truebe-fact-based-ai-chatbot' ) . '</p>';
		echo '<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-top:8px;">';
		echo '<input type="text" class="truebe-handoff-label" data-seed-id="' . esc_attr( $id ) . '" value="' . esc_attr( $handoff_label ) . '" placeholder="' . esc_attr__( 'Button text', 'truebe-fact-based-ai-chatbot' ) . '" style="flex:1;min-width:160px;border:1px solid #dcdcde;border-radius:6px;padding:9px 12px;font-size:14px;outline:none;">';
		echo '<input type="url" class="truebe-handoff-url" data-seed-id="' . esc_attr( $id ) . '" value="' . esc_attr( $handoff_url ) . '" placeholder="https://..." style="flex:2;min-width:220px;border:1px solid #dcdcde;border-radius:6px;padding:9px 12px;font-size:14px;outline:none;">';
		echo '<span class="truebe-handoff-saved" style="font-size:11px;color:#8a8a8a;display:none;align-items:center;gap:4px;">&#10003; ' . esc_html__( 'Saved', 'truebe-fact-based-ai-chatbot' ) . '</span>';
		echo '</div>';
		echo '</details>';

		echo '</div>';

		// Bottom row, outside the card: approve button (left, pending only); nav (right).
		echo '<div style="margin-top:16px;display:flex;align-items:center;justify-content:space-between;gap:12px;">';

		echo '<div style="display:flex;align-items:center;gap:16px;">';
		echo '<div style="display:flex;align-items:center;gap:16px;">';
		echo '<form method="post" action="' . esc_url( $post ) . '" style="margin:0;" id="truebe-approve-form">';
		wp_nonce_field( self::ACTION );
		echo '<input type="hidden" name="action" value="' . esc_attr( self::ACTION ) . '">';
		echo '<input type="hidden" name="truebe_action" value="approve">';
		echo '<input type="hidden" name="seed_id" value="' . esc_attr( $id ) . '">';
		echo '<input type="hidden" name="i" value="' . esc_attr( $index ) . '">';
		echo '<span class="truebe-approve-split" style="position:relative;display:inline-flex;align-items:center;">';
		echo '<button type="button" class="truebe-btn truebe-approve' . ( $is_ready ? ' is-approved' : '' ) . '" id="truebe-approve" data-seed-id="' . esc_attr( $id ) . '" data-was-ready="' . ( $is_ready ? '1' : '0' ) . '" title="' . esc_attr( $is_ready ? __( 'Click to un-approve', 'truebe-fact-based-ai-chatbot' ) : __( 'Approve this answer', 'truebe-fact-based-ai-chatbot' ) ) . '" style="text-align:left;padding-right:44px;">';

		if ( $is_ready ) {
			echo '&#10003; ' . esc_html__( 'Approved', 'truebe-fact-based-ai-chatbot' );
		} else {
			echo esc_html__( 'Approve', 'truebe-fact-based-ai-chatbot' );
		}
		echo '</button>';
		if ( ! $is_ready && $pending_count >= 2 ) {
			echo '<span class="truebe-approve-caret-toggle" title="' . esc_attr__( 'More approve options', 'truebe-fact-based-ai-chatbot' ) . '" style="position:absolute;right:0;top:0;bottom:0;display:inline-flex;align-items:center;padding:0 12px;cursor:pointer;font-size:22px;color:#fff;border-left:1px solid rgba(255,255,255,0.3);">&#9662;</span>';
			echo '<div class="truebe-approve-menu" style="display:none;position:absolute;top:calc(100% + 6px);left:0;z-index:50;background:#fff;border:1px solid #dcdcde;border-radius:8px;box-shadow:0 6px 20px rgba(0,0,0,0.12);padding:6px;white-space:nowrap;">';
			/* translators: %d: number of answers that will be approved */
			echo '<button type="submit" name="truebe_action" value="approve_all" class="truebe-approve-all-item" onmouseover="this.style.background=\'#f0f6f4\'" onmouseout="this.style.background=\'none\'" style="text-align:left;background:none;border:none;padding:8px 10px;font-size:13px;color:#1d2327;cursor:pointer;border-radius:6px;">' . esc_html( sprintf( _n( 'Approve all %d', 'Approve all %d', $pending_count, 'truebe-fact-based-ai-chatbot' ), $pending_count ) ) . '</button>';
			echo '</div>';
		}
		echo '</span>';
		echo '</form>';

		if ( $is_ready ) {
			$is_pub = ! isset( $seed['published'] ) || (int) $seed['published'] === 1;
			echo '<label style="display:inline-flex;align-items:center;gap:8px;font-size:13px;color:#646970;cursor:pointer;">';
			echo '<span class="truebe-ap-switch">';
			echo '<input type="checkbox" id="truebe-publish-toggle" data-seed-id="' . esc_attr( $id ) . '" data-nonce="' . esc_attr( wp_create_nonce( 'truebe_publish_nonce' ) ) . '"' . checked( $is_pub, true, false ) . ' />';
			echo '<span class="truebe-ap-track"><span class="truebe-ap-knob"></span></span>';
			echo '</span>';
			echo '<span>' . esc_html__( 'Publish', 'truebe-fact-based-ai-chatbot' ) . '</span>';
			echo '</label>';
		}
		echo '</div>';
		echo '</div>';

echo '<div style="display:flex;align-items:center;gap:10px;">';
		echo '<a href="' . esc_url( add_query_arg( array_merge( $navargs, array( 'i' => $prev ) ), $base ) ) . '" class="truebe-nav-btn">&lsaquo;</a>';
		echo '<span style="font-size:13px;color:#646970;min-width:48px;text-align:center;" data-pagination-total>' . esc_html( ( $index + 1 ) . ' ' . __( 'of', 'truebe-fact-based-ai-chatbot' ) . ' ' . $total ) . '</span>';
		$next_class = ( $is_ready && $next !== $index ) ? 'truebe-nav-btn truebe-nav-next-active' : 'truebe-nav-btn';
		echo '<a href="' . esc_url( add_query_arg( array_merge( $navargs, array( 'i' => $next ) ), $base ) ) . '" class="' . esc_attr( $next_class ) . '">&rsaquo;</a>';
		echo '</div>';
		echo '</div>';
	}

	private static function render_source( $seed ) {
		$main = isset( $seed['context_main'] ) ? (string) $seed['context_main'] : '';

		if ( '' === trim( $main ) ) {
			$doc = isset( $seed['source_doc'] ) ? (string) $seed['source_doc'] : '';
			if ( '' !== $doc ) {
				echo '<div style="margin-top:20px;">';
				echo '<div style="font-size:14px;font-weight:600;color:#000;margin-bottom:6px;">' . esc_html__( 'Source', 'truebe-fact-based-ai-chatbot' ) . '</div>';
				echo '<div style="font-size:12px;color:#8a8a8a;">' . esc_html( $doc ) . '</div>';
				echo '</div>';
			}
			return;
		}

		$doc      = isset( $seed['source_doc'] ) ? (string) $seed['source_doc'] : '';
		$page_num = ! empty( $seed['source_page'] ) ? (int) $seed['source_page'] : 0;
		$section  = isset( $seed['source_section'] ) ? (string) $seed['source_section'] : '';
		$heading  = isset( $seed['source_heading'] ) ? (string) $seed['source_heading'] : '';

		$doc_link = '';
		if ( '' !== $doc ) {
			$url      = wp_nonce_url( add_query_arg( array( 'action' => 'truebe_view_doc', 'doc' => rawurlencode( $doc ) ), admin_url( 'admin-post.php' ) ), 'truebe_view_doc' );
			$doc_link = '<a href="' . esc_url( $url ) . '" target="_blank" rel="noopener" style="color:#0f6e56;text-decoration:none;">' . esc_html( $doc ) . '</a>';
		}

		$tail_parts = array_filter( array( $section, $heading ) );
		$tail_html  = $tail_parts ? '  ›  ' . esc_html( implode( '  ›  ', $tail_parts ) ) : '';

		$page_link = '';
		if ( $page_num > 0 ) {
			$page_url  = wp_nonce_url( add_query_arg( array( 'action' => 'truebe_view_doc', 'doc' => rawurlencode( $doc ) ), admin_url( 'admin-post.php' ) ), 'truebe_view_doc' ) . '#page=' . $page_num;
			$page_link = '  ›  <a href="' . esc_url( $page_url ) . '" target="_blank" rel="noopener" style="color:#0f6e56;text-decoration:none;">page ' . esc_html( $page_num ) . '</a>';
		}

		$cite   = ! empty( $seed['citations'][0]['text'] ) ? (string) $seed['citations'][0]['text'] : '';
		$before = isset( $seed['context_before'] ) ? (string) $seed['context_before'] : '';
		$after  = isset( $seed['context_after'] ) ? (string) $seed['context_after'] : '';

		echo '<div style="margin-top:20px;">';
		echo '<div style="font-size:14px;font-weight:600;color:#000;margin-bottom:6px;">' . esc_html__( 'Source', 'truebe-fact-based-ai-chatbot' ) . '</div>';
		if ( '' !== $doc_link || '' !== $tail_html || '' !== $page_link ) {
			echo '<div style="font-size:12px;color:#646970;margin-bottom:6px;">' . $doc_link . $tail_html . $page_link . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		}
		echo '<div style="border:1px solid #dcdcde;border-radius:6px;background:#fff;resize:vertical;overflow:auto;height:90px;min-height:80px;padding:12px 14px;font-size:13px;line-height:1.7;">';
		if ( '' !== trim( $before ) ) { echo '<p style="color:#8a8a8a;margin:0 0 10px;">' . esc_html( $before ) . '</p>'; }
		echo '<p style="margin:0;"><span style="background:#e1f5ee;color:#0f6e56;border-radius:3px;padding:1px 4px;">' . esc_html( $main ) . '</span></p>';
		if ( '' !== trim( $after ) ) { echo '<p style="color:#8a8a8a;margin:10px 0 0;">' . esc_html( $after ) . '</p>'; }
		echo '</div>';
		echo '</div>';
	}

	private static function highlight( $paragraph, $needle ) {
		$paragraph = (string) $paragraph;
		$needle    = trim( (string) $needle );
		if ( '' === $needle ) { return esc_html( $paragraph ); }

		$pos = stripos( $paragraph, $needle );
		$len = strlen( $needle );

		if ( false === $pos ) {
			$found = self::find_normalized( $paragraph, $needle );
			if ( null === $found ) { return esc_html( $paragraph ); }
			$pos = $found['pos'];
			$len = $found['len'];
		}

		$before = substr( $paragraph, 0, $pos );
		$match  = substr( $paragraph, $pos, $len );
		$after  = substr( $paragraph, $pos + $len );
		return esc_html( $before )
			. '<span style="background:#e1f5ee;color:#0f6e56;border-radius:3px;padding:1px 4px;">' . esc_html( $match ) . '</span>'
			. esc_html( $after );
	}

	private static function find_normalized( $haystack, $needle ) {
		$norm     = '';
		$map      = array();
		$prev_ws  = false;
		$h_len    = strlen( $haystack );
		for ( $i = 0; $i < $h_len; $i++ ) {
			$ch = $haystack[ $i ];
			if ( ctype_space( $ch ) ) {
				if ( $prev_ws ) { continue; }
				$norm   .= ' ';
				$map[]   = $i;
				$prev_ws = true;
			} else {
				$norm   .= strtolower( $ch );
				$map[]   = $i;
				$prev_ws = false;
			}
		}

		$needle_norm = strtolower( trim( preg_replace( '/\s+/', ' ', $needle ) ) );
		if ( '' === $needle_norm ) { return null; }

		$np = strpos( $norm, $needle_norm );
		if ( false === $np ) { return null; }

		$start_byte = $map[ $np ];
		$end_index  = $np + strlen( $needle_norm ) - 1;
		$end_byte   = isset( $map[ $end_index ] ) ? $map[ $end_index ] : $start_byte;

		return array(
			'pos' => $start_byte,
			'len' => ( $end_byte - $start_byte ) + 1,
		);
	}
}

Truebe_Admin_Seeds::init();