<?php
/**
 * Answyr Chat — REST endpoint + escalation.
 *
 * Answers come from the active provider (Anthropic/OpenAI), grounded in docs
 * (Retrieval) AND the codebase knowledge pack. Plain JSON request/response —
 * NOT SSE: streaming raw cURL from a REST callback proved fragile under
 * php-fpm (a non-flushable output buffer could spin a worker and hang the
 * whole site). The answer returns in one shot; the widget shows a spinner.
 * Anything the chat can't resolve escalates into a Desk ticket.
 *
 *   POST /answyr/v1/chat            (public) → { answer, sources }
 *   POST /answyr/v1/chat/escalate   (public) → creates a Desk ticket
 *
 * @package Answyr
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

final class Answyr_Chat_Endpoint {

	private const NS = 'answyr/v1';

	public function register(): void {
		add_action( 'rest_api_init', array( $this, 'routes' ) );
	}

	public function routes(): void {
		register_rest_route(
			self::NS,
			'/chat',
			array(
				'methods'             => 'POST',
				'callback'            => array( $this, 'handle' ),
				'permission_callback' => '__return_true',
				'args'                => array(
					'question'   => array(
						'required'          => true,
						'type'              => 'string',
						'sanitize_callback' => 'sanitize_text_field',
						'validate_callback' => static function ( $v ) {
							return '' !== trim( (string) $v ) && mb_strlen( (string) $v ) <= 1000;
						},
					),
					'history'    => array(
						'required' => false,
						'type'     => 'array',
						'default'  => array(),
					),
					'session_id' => array(
						'required'          => false,
						'type'              => 'string',
						'sanitize_callback' => 'sanitize_text_field',
						'default'           => '',
					),
				),
			)
		);

		register_rest_route(
			self::NS,
			'/chat/escalate',
			array(
				'methods'             => 'POST',
				'callback'            => array( $this, 'escalate' ),
				'permission_callback' => '__return_true',
			)
		);
	}

	public function handle( WP_REST_Request $request ) {
		if ( ! $this->nonce_ok( $request ) ) {
			return new WP_REST_Response( array( 'error' => __( 'Security check failed. Please refresh and try again.', 'answyr' ) ), 403 );
		}

		// Optional gate: the endpoint calls the paid AI provider on the site's key,
		// so the owner can restrict it to logged-in visitors.
		if ( Answyr_Settings::chat_requires_login() && ! is_user_logged_in() ) {
			return new WP_REST_Response(
				array( 'error' => __( 'Please log in to use chat.', 'answyr' ) ),
				401
			);
		}

		$key = Answyr_Rate_Limiter::client_key( 'chat' );
		if ( ! Answyr_Rate_Limiter::allow( $key, 30, HOUR_IN_SECONDS ) ) {
			return new WP_REST_Response(
				array( 'error' => __( "You've reached the query limit. Please try again later.", 'answyr' ) ),
				429
			);
		}

		// Site-wide daily ceiling across all visitors — a hard cap on AI spend that
		// per-IP throttling can't provide (IP rotation defeats it). 0 = unlimited.
		$cap = Answyr_Settings::chat_daily_cap();
		if ( $cap > 0 && ! Answyr_Rate_Limiter::allow( 'chat_global_' . gmdate( 'Y-m-d' ), $cap, DAY_IN_SECONDS ) ) {
			return new WP_REST_Response(
				array( 'error' => __( 'Chat is temporarily unavailable. Please try again later.', 'answyr' ) ),
				429
			);
		}

		$question = (string) $request->get_param( 'question' );
		$history  = $this->sanitize_history( (array) ( $request->get_param( 'history' ) ?? array() ) );

		$retrieval = new Answyr_Retrieval();
		$docs      = $retrieval->search( $question, 4 );
		$sources   = $retrieval->get_sources( $docs );
		$system    = $this->system_prompt( $retrieval->format_context( $docs ) );

		$messages   = $history;
		$messages[] = array(
			'role'    => 'user',
			'content' => $question,
		);

		try {
			$answer = ( new Answyr_AI_Client() )->complete( $system, $messages );
		} catch ( \Throwable $e ) {
			return new WP_REST_Response( array( 'error' => $e->getMessage() ), 500 );
		}

		// Pro: record the answer for analytics + the documentation-gap report. The
		// number of grounding sources found is the gap signal (0 = KB couldn't answer).
		if ( answyr_is_pro() && class_exists( 'Answyr_Chat_Store' ) ) {
			Answyr_Chat_Store::log_answer(
				(string) $request->get_param( 'session_id' ),
				$question,
				$answer,
				count( $sources ),
				is_user_logged_in() ? get_current_user_id() : null
			);
		}

		return new WP_REST_Response(
			array(
				'answer'  => $answer,
				'sources' => $sources,
			),
			200
		);
	}

	/** Chat → Desk escalation: create a ticket pre-loaded with the transcript. */
	public function escalate( WP_REST_Request $request ) {
		if ( ! $this->nonce_ok( $request ) ) {
			return new WP_REST_Response( array( 'error' => __( 'Security check failed. Please refresh and try again.', 'answyr' ) ), 403 );
		}

		// Escalation opens a Desk ticket — only allow it when the Desk is enabled.
		if ( ! Answyr_Settings::module_enabled( 'desk' ) ) {
			return new WP_REST_Response( array( 'error' => __( 'Ticketing is not available.', 'answyr' ) ), 403 );
		}

		$key = Answyr_Rate_Limiter::client_key( 'chat_escalate' );
		if ( ! Answyr_Rate_Limiter::allow( $key, 10, HOUR_IN_SECONDS ) ) {
			return new WP_REST_Response( array( 'error' => __( 'Too many requests.', 'answyr' ) ), 429 );
		}

		$email      = sanitize_email( (string) $request->get_param( 'email' ) );
		$name       = sanitize_text_field( (string) $request->get_param( 'name' ) );
		$message    = trim( wp_strip_all_tags( (string) $request->get_param( 'message' ) ) );
		$transcript = $this->format_transcript( (array) ( $request->get_param( 'history' ) ?? array() ) );

		// Logged-in customers: tie the ticket to their account so it appears in their portal.
		if ( is_user_logged_in() ) {
			$user  = wp_get_current_user();
			$email = $user->user_email;
			$name  = $user->display_name;
		}

		if ( ! is_email( $email ) ) {
			return new WP_REST_Response( array( 'error' => __( 'A valid email is required.', 'answyr' ) ), 400 );
		}
		// A description is optional only when a chat transcript is attached.
		if ( '' === $message && '' === $transcript ) {
			return new WP_REST_Response( array( 'error' => __( 'Please describe your issue.', 'answyr' ) ), 400 );
		}

		$store = ANSWYR_PATH . 'inc/modules/desk/class-desk-store.php';
		if ( ! class_exists( 'Answyr_Desk_Store' ) && is_readable( $store ) ) {
			require_once $store;
		}
		if ( ! class_exists( 'Answyr_Desk_Store' ) ) {
			return new WP_REST_Response( array( 'error' => __( 'Desk module unavailable.', 'answyr' ) ), 503 );
		}

		$subject = '' !== $message ? wp_trim_words( $message, 10, '…' ) : __( 'Chat conversation', 'answyr' );

		$ticket_id = Answyr_Desk_Store::create_ticket(
			array(
				'subject'        => $subject,
				'customer_email' => $email,
				'customer_name'  => $name,
				'channel'        => 'chat',
			)
		);

		$parts = array();
		if ( '' !== $message ) {
			$parts[] = $message;
		}
		if ( '' !== $transcript ) {
			$parts[] = "— chat transcript —\n" . $transcript;
		}
		$body = implode( "\n\n", $parts );
		Answyr_Desk_Store::add_message( $ticket_id, 'customer', $body );

		// Pro: flag this conversation as escalated so deflection metrics + the
		// gap report reflect that the chat couldn't resolve it.
		if ( answyr_is_pro() && class_exists( 'Answyr_Chat_Store' ) ) {
			Answyr_Chat_Store::mark_escalated( (string) $request->get_param( 'session_id' ) );
		}

		do_action( 'answyr_desk_ticket_created', $ticket_id );

		return new WP_REST_Response(
			array(
				'ok'        => true,
				'ticket_id' => $ticket_id,
			),
			201
		);
	}

	// ── Helpers ─────────────────────────────────────────────────────────────────

	/**
	 * Soft CSRF guard for the public chat endpoints. The widget sends a wp_rest
	 * nonce; when one is present it must be valid, which blocks forged cross-site
	 * posts. A missing nonce is tolerated — full-page caches can serve the widget
	 * with a stale/absent token — so the per-IP rate limiter and daily cap remain
	 * the primary abuse guards.
	 */
	private function nonce_ok( WP_REST_Request $request ): bool {
		$nonce = (string) $request->get_header( 'X-WP-Nonce' );
		return '' === $nonce || false !== wp_verify_nonce( $nonce, 'wp_rest' );
	}

	private function system_prompt( string $docs_ctx ): string {
		$prompt = 'You are a documentation assistant for a software product. Answer using ONLY '
			. "the documentation below. If it isn't covered, say so plainly and invite the "
			. 'customer to open a support ticket so a person can help. Never tell the customer '
			. 'to email, call, or visit an outside company, website, or address — the only '
			. 'next step you offer is creating a support ticket right here. Cite source page '
			. 'titles. Be concise; use numbered steps for procedures. Do not preface answers '
			. "with meta phrases like 'Based on the documentation' or 'According to the docs' "
			. "— just answer directly.\n\n"
			. "=== DOCUMENTATION ===\n{$docs_ctx}\n";
		return apply_filters( 'answyr_chat_system_prompt', $prompt );
	}

	/** @param array<int,mixed> $history */
	private function sanitize_history( array $history ): array {
		$clean = array();
		foreach ( $history as $turn ) {
			if ( ! is_array( $turn ) ) {
				continue;
			}
			$role    = sanitize_text_field( $turn['role'] ?? '' );
			$content = sanitize_textarea_field( $turn['content'] ?? '' );
			if ( ! in_array( $role, array( 'user', 'assistant' ), true ) || '' === $content ) {
				continue;
			}
			if ( mb_strlen( $content ) > 2000 ) {
				$content = mb_substr( $content, 0, 2000 );
			}
			$clean[] = array(
				'role'    => $role,
				'content' => $content,
			);
		}
		return array_slice( $clean, -6 );
	}

	/** @param array<int,mixed> $history */
	private function format_transcript( array $history ): string {
		$lines = array();
		foreach ( $this->sanitize_history( $history ) as $turn ) {
			$who     = 'user' === $turn['role'] ? __( 'Customer', 'answyr' ) : __( 'Assistant', 'answyr' );
			$lines[] = $who . ': ' . $turn['content'];
		}
		return implode( "\n", $lines );
	}
}
