<?php

namespace Antimanual\Api;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use Antimanual\AIProvider;
use Antimanual\UsageTracker;

/**
 * Inline AI Copilot ("Magic Wand") endpoint.
 *
 * Transforms a selected snippet of editor text according to a chosen action
 * (improve, fix grammar, shorten, lengthen, translate, custom, …). Powers the
 * block-editor format toolbar button. Returns transformed text only — never
 * touches the post directly, so the editor stays the source of truth.
 */
class CopilotController {

	/**
	 * Actions the copilot understands, mapped to model instructions. The text is
	 * appended after a shared system preamble that enforces output hygiene.
	 *
	 * @var array<string, string>
	 */
	private const ACTIONS = [
		'improve'      => 'Improve the writing quality: make it clearer, more engaging, and well-structured, while preserving the original meaning and language.',
		'fix_grammar'  => 'Correct any spelling, grammar, and punctuation mistakes. Do not change the meaning, tone, or language.',
		'shorten'      => 'Make the text more concise and to the point, removing redundancy while keeping the key information and language.',
		'lengthen'     => 'Expand the text with more detail, explanation, and supporting context, keeping the same tone and language.',
		'simplify'     => 'Rewrite the text so it is simpler and easier to read for a general audience, keeping the same language.',
		'professional' => 'Rewrite the text in a polished, professional tone, keeping the same language and meaning.',
		'translate'    => 'Translate the text into the target language specified by the user. Output only the translation.',
		'custom'       => 'Apply the user-provided instruction to the text.',
	];

	/**
	 * Register REST routes.
	 *
	 * @param string $namespace REST namespace.
	 */
	public function register_routes( string $namespace ) {
		register_rest_route(
			$namespace,
			'/copilot',
			[
				'methods'             => 'POST',
				'callback'            => [ $this, 'transform' ],
				'permission_callback' => fn() => current_user_can( 'edit_posts' ),
				'timeout'             => 120,
			]
		);
	}

	/**
	 * Transform a snippet of text.
	 *
	 * @param \WP_REST_Request $request REST request.
	 * @return \WP_REST_Response|\WP_Error
	 */
	public function transform( $request ) {
		// Per-role monthly spend governance (admins are exempt by default).
		$limit_error = atml_usage_limit_guard();
		if ( is_wp_error( $limit_error ) ) {
			return $limit_error;
		}

		if ( ! AIProvider::has_api_key() ) {
			return new \WP_Error(
				'api_key_missing',
				sprintf(
					/* translators: %s: provider name */
					__( '%s API key is not configured. Add one in Antimanual → Configuration.', 'antimanual' ),
					AIProvider::get_display_name()
				),
				[ 'status' => 400 ]
			);
		}

		$payload = json_decode( $request->get_body(), true );
		$payload = is_array( $payload ) ? $payload : [];

		$action = sanitize_key( (string) ( $payload['action'] ?? '' ) );
		$text   = (string) ( $payload['text'] ?? '' );

		// Selection may contain inline HTML (e.g. <strong>, <a>) from RichText.
		$text = wp_kses_post( $text );

		if ( '' === trim( wp_strip_all_tags( $text ) ) ) {
			return new \WP_Error( 'empty_selection', __( 'Select some text first.', 'antimanual' ), [ 'status' => 400 ] );
		}

		if ( ! isset( self::ACTIONS[ $action ] ) ) {
			return new \WP_Error( 'invalid_action', __( 'Unknown copilot action.', 'antimanual' ), [ 'status' => 400 ] );
		}

		// Guard against runaway requests — the copilot is for selections, not
		// whole documents (which Bulk Rewrite handles).
		if ( mb_strlen( $text ) > 8000 ) {
			return new \WP_Error(
				'selection_too_long',
				__( 'Selection is too long. Select a smaller passage, or use Bulk Rewrite for whole posts.', 'antimanual' ),
				[ 'status' => 400 ]
			);
		}

		$instruction = self::ACTIONS[ $action ];

		if ( 'custom' === $action ) {
			$custom = sanitize_textarea_field( (string) ( $payload['prompt'] ?? '' ) );
			if ( '' === $custom ) {
				return new \WP_Error( 'missing_prompt', __( 'Enter an instruction for the custom action.', 'antimanual' ), [ 'status' => 400 ] );
			}
			$instruction .= "\n\nUser instruction: " . $custom;
		}

		if ( 'translate' === $action ) {
			$language = sanitize_text_field( (string) ( $payload['language'] ?? '' ) );
			if ( '' === $language ) {
				return new \WP_Error( 'missing_language', __( 'Choose a target language.', 'antimanual' ), [ 'status' => 400 ] );
			}
			$instruction .= "\n\nTarget language: " . $language;
		}

		$system = "You are an inline writing assistant embedded in the WordPress block editor.\n"
			. "Apply the requested transformation to the user's selected text.\n"
			. "Rules:\n"
			. "1. Return ONLY the transformed text — no preamble, no explanation, no quotes, no code fences.\n"
			. "2. Preserve any inline HTML tags (such as <strong>, <em>, <a>) that appear in the selection.\n"
			. "3. Do not add block-level wrappers or Gutenberg block comments.\n"
			. "4. Keep the original language unless explicitly asked to translate.\n\n"
			. 'Transformation: ' . $instruction;

		$messages = [
			[ 'role' => 'system', 'content' => $system ],
			[ 'role' => 'user', 'content' => $text ],
		];

		$reply = AIProvider::get_reply( $messages );

		if ( ! is_string( $reply ) ) {
			return new \WP_Error(
				'copilot_failed',
				is_array( $reply ) && isset( $reply['error'] ) ? $reply['error'] : __( 'The AI request failed. Please try again.', 'antimanual' ),
				[ 'status' => 502 ]
			);
		}

		$result = $this->clean_inline_reply( $reply );

		if ( '' === trim( wp_strip_all_tags( $result ) ) ) {
			return new \WP_Error( 'empty_result', __( 'The AI returned an empty result. Please try again.', 'antimanual' ), [ 'status' => 502 ] );
		}

		// Record usage for the Usage & Cost report and per-role limits.
		UsageTracker::record( 'copilot', $system . "\n" . $text, $result );

		return rest_ensure_response(
			[
				'success' => true,
				'data'    => [
					'text' => $result,
				],
			]
		);
	}

	/**
	 * Strip wrappers the model may add despite instructions: code fences,
	 * surrounding quotes, and disallowed HTML. Keeps safe inline markup.
	 *
	 * @param string $reply Raw model reply.
	 * @return string
	 */
	private function clean_inline_reply( string $reply ): string {
		$reply = trim( $reply );

		// Remove ``` fences if the model wrapped the answer.
		$reply = preg_replace( '/^```[a-zA-Z]*\s*|\s*```$/m', '', $reply );
		$reply = trim( (string) $reply );

		// Strip a single pair of wrapping quotes.
		if ( strlen( $reply ) >= 2 ) {
			$first = $reply[0];
			$last  = substr( $reply, -1 );
			if ( ( '"' === $first && '"' === $last ) || ( "'" === $first && "'" === $last ) ) {
				$reply = substr( $reply, 1, -1 );
			}
		}

		// Allow only safe inline tags; drop block-level markup.
		$allowed = [
			'strong' => [],
			'b'      => [],
			'em'     => [],
			'i'      => [],
			'u'      => [],
			'a'      => [ 'href' => [], 'title' => [], 'target' => [], 'rel' => [] ],
			'code'   => [],
			'mark'   => [],
			'sub'    => [],
			'sup'    => [],
			'br'     => [],
		];

		return trim( wp_kses( $reply, $allowed ) );
	}
}
