<?php
/**
 * Truebe chatbot: visitor Q&A widget with multi-turn, fallback, audio.
 * Reads the saved appearance options and passes them to the front end.
 */

if ( ! defined( 'ABSPATH' ) ) { exit; }
if ( class_exists( 'Truebe_Chatbot' ) ) { return; }

class Truebe_Chatbot {

	public static function init() {
		add_shortcode( 'truebe_chat', array( __CLASS__, 'render_shortcode' ) );
		add_action( 'wp_ajax_truebe_chat_query', array( __CLASS__, 'ajax_query' ) );
		add_action( 'wp_ajax_nopriv_truebe_chat_query', array( __CLASS__, 'ajax_query' ) );
	}

	/**
	 * Default public greeting: "Ask me about [business name]."
	 * Falls back to the site title when no business name is saved (or it is
	 * the placeholder "you"), then to a generic line if both are empty.
	 * Public so the Appearance preview can show the same default.
	 */
	public static function default_greeting() {
		$name = self::business_name();
		if ( '' !== $name ) {
			/* translators: %s: business or site name */
			return sprintf( __( 'Ask me about %s.', 'truebe-fact-based-ai-chatbot' ), $name );
		}
		return __( 'Ask me a question.', 'truebe-fact-based-ai-chatbot' );
	}

	/**
	 * Business name for visitor-facing copy: saved business name, falling
	 * back to the site title when empty or still the placeholder "you".
	 */
	private static function business_name() {
		$name = trim( (string) get_option( 'truebe_business_name', '' ) );
		if ( '' === $name || __( 'you', 'truebe-fact-based-ai-chatbot' ) === $name ) {
			$name = trim( (string) get_bloginfo( 'name' ) );
		}
		return $name;
	}

	/**
	 * Human offramp shown when no relevant suggestions could be found.
	 * Appends the site owner's saved contact email/phone when set, so the
	 * visitor has a real way to reach the business, not just a dead sentence.
	 */
	private static function no_match_offramp() {
		$name = self::business_name();
		$base = ( '' === $name )
			? __( 'In the meantime, feel free to reach out to us directly.', 'truebe-fact-based-ai-chatbot' )
			/* translators: %s: business or site name */
			: sprintf( __( 'In the meantime, feel free to reach out to %s directly.', 'truebe-fact-based-ai-chatbot' ), $name );

		$email = sanitize_email( get_option( 'truebe_contact_email', '' ) );
		$phone = trim( (string) get_option( 'truebe_contact_phone', '' ) );

		$contacts = array();
		if ( '' !== $email ) { $contacts[] = $email; }
		if ( '' !== $phone ) { $contacts[] = $phone; }

		if ( empty( $contacts ) ) {
			return $base;
		}

		return $base . ' ' . implode( ' ' . __( 'or', 'truebe-fact-based-ai-chatbot' ) . ' ', $contacts );
	}

	private static function theme() {
		$header = get_option( 'truebe_chat_header', '' );
		if ( '' === $header ) { $header = get_bloginfo( 'name' ); }
		$greeting = get_option( 'truebe_chat_greeting', '' );
		if ( '' === $greeting ) { $greeting = self::default_greeting(); }
		$accent = get_option( 'truebe_chat_accent', '' );        if ( '' === $accent ) { $accent = '#0f6e56'; }
		$bot    = get_option( 'truebe_chat_bot_color', '' );      if ( '' === $bot ) { $bot = '#f6f7f7'; }
		$fb     = get_option( 'truebe_chat_fallback_color', '' ); if ( '' === $fb ) { $fb = '#fff9e6'; }
		$wkey   = get_option( 'truebe_chat_width', 'standard' );
		$wmap   = array( 'compact' => 300, 'standard' => 360, 'wide' => 420 );
		$width  = isset( $wmap[ $wkey ] ) ? $wmap[ $wkey ] : 360;

		return array(
			'header'      => $header,
			'greeting'    => $greeting,
			'accent'      => $accent,
			'bot'         => $bot,
			'fallback'    => $fb,
			'width'       => $width,
			'audio'       => ( '1' === (string) get_option( 'truebe_chat_audio', '0' ) ),
			'suggestions' => ( '1' === (string) get_option( 'truebe_chat_suggestions', '1' ) ),
			'noMatchText' => self::no_match_offramp(),
		);
	}

	private static function enqueue_assets() {
		wp_enqueue_script( 'truebe-chatbot', plugin_dir_url( __FILE__ ) . '../assets/chatbot.js', array(), '1.2', true );
		wp_enqueue_style( 'truebe-chatbot', plugin_dir_url( __FILE__ ) . '../assets/admin.css', array(), '1.1' );
		wp_localize_script( 'truebe-chatbot', 'truebeChatbot', array(
			'ajax_url' => admin_url( 'admin-ajax.php' ),
			'nonce'    => wp_create_nonce( 'truebe_chat' ),
			'theme'    => self::theme(),
		) );
	}
	
	public static function render_shortcode() {
		ob_start();
		self::enqueue_assets();
		echo '<div id="truebe-chatbot-widget"></div>';
		if ( ! class_exists( 'Truebe_Plan' ) || ! Truebe_Plan::is_paid() ) {
			echo '<div style="text-align:center;margin-top:0 !important;"><a href="https://www.truebe.io/?utm_source=badge&utm_medium=widget" target="_blank" rel="noopener" style="font-size:10px;color:#cccccc;text-decoration:none;">' . esc_html__( 'Powered by Truebe', 'truebe-fact-based-ai-chatbot' ) . '</a></div>';
		}
		return ob_get_clean();
	}

public static function ajax_query() {
		check_ajax_referer( 'truebe_chat' );
		$query = isset( $_POST['query'] ) ? sanitize_text_field( wp_unslash( $_POST['query'] ) ) : '';
		if ( '' === $query ) {
			wp_send_json_error( 'empty_query' );
		}
		$is_internal = isset( $_POST['context'] ) && 'internal' === sanitize_text_field( wp_unslash( $_POST['context'] ) );
		$doc_filter  = ( $is_internal && isset( $_POST['doc'] ) ) ? sanitize_text_field( wp_unslash( $_POST['doc'] ) ) : '';
		// Internal research chat requires a logged-in admin.
		if ( $is_internal && ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( 'forbidden', 403 );
		}
		wp_send_json_success( self::match_query( $query, $is_internal, $doc_filter ) );
	}

private static function match_query( $query, $is_internal = false, $doc_filter = '' ) {
		if ( ! class_exists( 'Truebe_Matcher' ) ) {
			if ( ! $is_internal && class_exists( 'Truebe_Query_Log' ) ) {
				Truebe_Query_Log::record( $query, Truebe_Query_Log::STATUS_GAP );
			}
			return self::fallback( $query );
		}

		$result = Truebe_Matcher::match( $query, $doc_filter );

		if ( null === $result ) {
			if ( ! $is_internal && class_exists( 'Truebe_Query_Log' ) ) {
				Truebe_Query_Log::record( $query, Truebe_Query_Log::STATUS_GAP );
			}
			return self::fallback( $query );
		}

if ( in_array( $result['status'], array( 'match', 'followup' ), true ) && ! empty( $result['seed'] ) ) {
			if ( class_exists( 'Truebe_Gaps' ) && method_exists( 'Truebe_Gaps', 'resolve_by_question' ) ) {
				Truebe_Gaps::resolve_by_question( $query );
			}
			if ( class_exists( 'Truebe_Review_Ask' ) && ! $is_internal ) {
				Truebe_Review_Ask::count_answer();
			}
			if ( ! $is_internal && class_exists( 'Truebe_Query_Log' ) ) {
				$matched_seed = $result['seed'];
				Truebe_Query_Log::record(
					$query,
					Truebe_Query_Log::STATUS_ANSWERED,
					isset( $matched_seed['id'] ) ? (int) $matched_seed['id'] : 0,
					isset( $matched_seed['label'] ) ? (string) $matched_seed['label'] : ''
				);
			}
			return self::answer( $result, $is_internal );
		}

		if ( ! $is_internal && class_exists( 'Truebe_Query_Log' ) ) {
			Truebe_Query_Log::record( $query, Truebe_Query_Log::STATUS_GAP );
		}
		return self::fallback( $query, isset( $result['suggestions'] ) ? $result['suggestions'] : array() );
	}

	private static function answer( $result, $is_internal = false ) {
		$seed   = $result['seed'];
		$thesis = isset( $seed['thesis'] ) ? (string) $seed['thesis'] : '';

$out = array(
			'type'   => 'answer',
			'label'  => isset( $seed['label'] ) ? (string) $seed['label'] : '',
			'thesis' => $thesis,
			'source' => self::source_label( $seed ),
			'time'   => current_time( 'g:i A' ),
		);

		// A matched seed may carry a handoff: a labeled link routing the visitor
		// to a private step (login, booking, phone) the fact library can't answer.
		if ( ! empty( $seed['handoff'] ) && is_array( $seed['handoff'] )
			&& ! empty( $seed['handoff']['label'] ) && ! empty( $seed['handoff']['url'] ) ) {
			$out['handoff'] = array(
				'label' => (string) $seed['handoff']['label'],
				'url'   => (string) $seed['handoff']['url'],
			);
		}

		if ( $is_internal ) {
			$out['source_detail'] = array(
				'doc'     => (string) ( $seed['source_doc'] ?? '' ),
				'page'    => ! empty( $seed['source_page'] ) ? (int) $seed['source_page'] : 0,
				'section' => (string) ( $seed['source_section'] ?? '' ),
				'heading' => (string) ( $seed['source_heading'] ?? '' ),
				'quote'   => (string) ( $seed['context_main'] ?? '' ),
			);
		}

		return $out;
	}

	private static function source_label( $seed ) {
		if ( empty( $seed['citations'] ) || ! is_array( $seed['citations'] ) ) {
			return '';
		}
		$first = reset( $seed['citations'] );
		if ( is_string( $first ) ) {
			return $first;
		}
		if ( is_array( $first ) ) {
			if ( ! empty( $first['source'] ) ) { return (string) $first['source']; }
			if ( ! empty( $first['url'] ) )    { return (string) $first['url']; }
			if ( ! empty( $first['text'] ) )   { return (string) $first['text']; }
		}
		return '';
	}

	private static function fallback( $query = '', $suggestions = array() ) {
		if ( '' !== trim( (string) $query ) && class_exists( 'Truebe_Gaps' ) && method_exists( 'Truebe_Gaps', 'log' ) ) {
			Truebe_Gaps::log( $query );
		}
		if ( empty( $suggestions ) ) {
			$suggestions = self::nearest_neighbor( $query );
		}
		$custom = trim( (string) get_option( 'truebe_fallback_message', '' ) );
		return array(
			'type'        => 'fallback',
			'message'     => ( '' !== $custom ) ? $custom : __( "We don't have an answer for that yet. Your question has been logged so we can answer it next time.", 'truebe-fact-based-ai-chatbot' ),
			'custom'      => ( '' !== $custom ),
			'suggestions' => $suggestions,
			'time'        => current_time( 'g:i A' ),
		);
	}

	/** Common words that carry no topical signal and should not drive relevance matching. */
	private static function stopwords() {
		return array(
			'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
			'do', 'does', 'did', 'what', 'when', 'where', 'why', 'how', 'who',
			'which', 'you', 'your', 'i', 'my', 'me', 'we', 'our', 'it', 'its',
			'for', 'to', 'of', 'in', 'on', 'at', 'and', 'or', 'but', 'with',
			'about', 'can', 'could', 'will', 'would', 'should', 'have', 'has',
			'had', 'if', 'this', 'that', 'there', 'their',
		);
	}

	/** Lowercase, strip punctuation, split into significant words (3+ chars, not a stopword). */
	private static function significant_words( $text ) {
		$text  = strtolower( (string) $text );
		$text  = preg_replace( '/[^a-z0-9\s]/', ' ', $text );
		$words = preg_split( '/\s+/', trim( $text ) );
		$stop  = self::stopwords();
		return array_values( array_filter( $words, function( $w ) use ( $stop ) {
			return strlen( $w ) >= 3 && ! in_array( $w, $stop, true );
		} ) );
	}

	/**
	 * Suggest published seeds related to the failed query by keyword overlap,
	 * instead of just the most recently published ones. Returns nothing when
	 * no seed shares meaningful words with the query, since an unrelated
	 * suggestion is worse than no suggestion (and can leak unrelated topics).
	 */
	private static function nearest_neighbor( $query ) {
		$query_words = self::significant_words( $query );
		if ( empty( $query_words ) ) {
			return array();
		}

		global $wpdb;
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- {$wpdb->prefix} is core-provided, not user input; SQL identifiers cannot be passed as prepare() parameters. Custom plugin table, no core API or object cache.
		$results = $wpdb->get_results( $wpdb->prepare(
			"SELECT id, label FROM {$wpdb->prefix}truebe_seeds WHERE status = %s ORDER BY id DESC LIMIT 200",
			'ready'
		) );
		if ( empty( $results ) ) {
			return array();
		}

		$scored = array();
		foreach ( $results as $r ) {
			$seed_words = self::significant_words( $r->label );
			if ( empty( $seed_words ) ) {
				continue;
			}
			$overlap = count( array_intersect( $query_words, $seed_words ) );
			if ( $overlap > 0 ) {
				$scored[] = array( 'id' => (int) $r->id, 'label' => $r->label, 'score' => $overlap );
			}
		}
		if ( empty( $scored ) ) {
			return array();
		}

		usort( $scored, function( $a, $b ) { return $b['score'] <=> $a['score']; } );
		$scored = array_slice( $scored, 0, 3 );
		return array_map( function( $r ) {
			return array( 'id' => $r['id'], 'label' => $r['label'] );
		}, $scored );
	}
}

Truebe_Chatbot::init();