<?php
/**
 * Answyr Desk — AI suggestion service.
 *
 * The heart of the product: builds a grounded prompt (docs + codebase/release
 * context + the ticket thread) and asks the active provider for a DRAFT reply.
 * It never sends — a human reviews/edits and clicks Send.
 *
 * @package Answyr
 */

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

final class Answyr_Desk_Suggestion_Service {

	private Answyr_AI_Client $ai;
	private Answyr_Retrieval $retrieval;

	public function __construct( ?Answyr_AI_Client $ai = null, ?Answyr_Retrieval $retrieval = null ) {
		$this->ai        = $ai ?? new Answyr_AI_Client();
		$this->retrieval = $retrieval ?? new Answyr_Retrieval();
	}

	/**
	 * Produce a suggested reply for a ticket.
	 *
	 * @return array{draft:string,sources:array}
	 */
	public function suggest( int $ticket_id ): array {
		$ticket = Answyr_Desk_Store::get_ticket( $ticket_id );
		if ( ! $ticket ) {
			throw new \RuntimeException( esc_html__( 'Ticket not found.', 'answyr' ) );
		}

		$thread = Answyr_Desk_Store::thread( $ticket_id );
		$query  = $this->latest_customer_text( $ticket, $thread );

		$docs     = $this->retrieval->search( $query, 4 );
		$system   = $this->system_prompt( $this->retrieval->format_context( $docs ) );
		$messages = $this->thread_to_messages( $thread, $ticket );

		$draft = $this->ai->complete( $system, $messages );

		return array(
			'draft'   => $draft,
			'sources' => $this->retrieval->get_sources( $docs ),
		);
	}

	private function system_prompt( string $docs_ctx ): string {
		$signature = (string) Answyr_Settings::get( 'agent_signature', '' );

		$prompt = 'You are helping a human support agent answer a customer. Draft the reply the '
			. "agent will send, written as the support team speaking directly to the customer.\n\n"
			. "RULES:\n"
			. "- Write in the first person as the support team ('we'/'us') addressing the customer as "
			. "'you'. You ARE support — never tell the customer to contact support, reach out to "
			. 'support, or open a ticket. If something needs escalation or more detail, ask the customer '
			. "directly or state what the team will do next.\n"
			. '- Do not refer to yourself as an AI, bot, or assistant, and do not address the agent or '
			. "add notes to them — output only the message the customer will read.\n"
			. '- Ground every claim in the DOCUMENTATION below. If the answer isn\'t supported '
			. "there, say what you can and avoid inventing specifics you cannot confirm.\n"
			. "- Never promise a fix or feature the documentation does not confirm exists or is planned.\n"
			. "- Do not preface the reply with meta phrases like 'Based on the documentation' or "
			. "'According to the docs' — answer the customer directly.\n"
			. "- Be concise, warm, and specific. Reference relevant docs by writing their full URL.\n"
			. '- Write in plain prose for an email reply. Do NOT use Markdown — no **bold**, # headings, '
			. 'bullet or asterisk lists, backticks, or [text](url) links. Write any URLs in full. Use '
			. "blank lines between paragraphs and plain '1.' numbering for steps.\n"
			. "- Output only the reply body (a human will review and send it).\n\n"
			. "=== DOCUMENTATION ===\n{$docs_ctx}\n";

		if ( '' !== trim( $signature ) ) {
			$prompt .= "\nSign off as:\n{$signature}\n";
		}

		return apply_filters( 'answyr_desk_system_prompt', $prompt );
	}

	/**
	 * @param array<int,object> $thread
	 * @return array<int,array{role:string,content:string}>
	 */
	private function thread_to_messages( array $thread, object $ticket ): array {
		$messages = array();
		foreach ( $thread as $row ) {
			if ( 1 === (int) $row->is_draft ) {
				continue; // never feed prior unsent drafts back in
			}
			$role       = ( 'customer' === $row->author_type ) ? 'user' : 'assistant';
			$messages[] = array(
				'role'    => $role,
				'content' => (string) $row->body,
			);
		}
		if ( empty( $messages ) ) {
			$messages[] = array(
				'role'    => 'user',
				'content' => (string) $ticket->subject,
			);
		}
		// Surface the intake context (website, versions, setup) to the model so the
		// draft can account for the customer's environment. Prepend it to the first
		// customer turn.
		$context = $this->context_block( (int) $ticket->id );
		if ( '' !== $context ) {
			foreach ( $messages as $idx => $message ) {
				if ( 'user' === $message['role'] ) {
					$messages[ $idx ]['content'] = $context . "\n\n" . $message['content'];
					break;
				}
			}
		}
		return $messages;
	}

	/** A compact "Customer context: label — value" block, or '' when none was captured. */
	private function context_block( int $ticket_id ): string {
		$pairs = Answyr_Desk_Store::get_context( $ticket_id );
		if ( ! $pairs ) {
			return '';
		}
		$lines = array( __( 'Customer context:', 'answyr' ) );
		foreach ( $pairs as $pair ) {
			$lines[] = '- ' . $pair['label'] . ': ' . $pair['value'];
		}
		return implode( "\n", $lines );
	}

	/** @param array<int,object> $thread */
	private function latest_customer_text( object $ticket, array $thread ): string {
		for ( $i = count( $thread ) - 1; $i >= 0; $i-- ) {
			if ( 'customer' === $thread[ $i ]->author_type ) {
				return (string) $thread[ $i ]->body;
			}
		}
		return (string) $ticket->subject;
	}
}
