<?php

namespace Antimanual\Mcp;

use Antimanual\Encryption;
use Antimanual\Tools\ToolRegistry;

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

/**
 * Model Context Protocol (MCP) server.
 *
 * Exposes the Antimanual Tool Registry to external AI agents (Claude, Claude
 * Code, ChatGPT, …) over a JSON-RPC 2.0 endpoint using the streamable HTTP
 * transport. Requests authenticate with a bearer token generated in the admin;
 * the token's access level (read / write) decides whether write tools are
 * offered, and tool calls run as the token owner so capability checks apply.
 *
 * Endpoint: POST {site}/wp-json/antimanual/v1/mcp
 *
 * @package Antimanual
 */
class McpServer {

	/** Option key for MCP server settings. */
	const OPTION = 'antimanual_mcp';

	/** MCP protocol version this server speaks. */
	const PROTOCOL_VERSION = '2025-06-18';

	/**
	 * Register the MCP REST route.
	 *
	 * @param string $namespace REST namespace.
	 */
	public function register_routes( string $namespace ) {
		register_rest_route(
			$namespace,
			'/mcp',
			[
				'methods'             => 'POST',
				'callback'            => [ $this, 'handle' ],
				'permission_callback' => [ $this, 'authenticate' ],
			]
		);
	}

	/**
	 * Get MCP settings merged with defaults.
	 *
	 * @return array{enabled:bool,access_level:string,token:string,owner_id:int,tools:array<string,bool>}
	 */
	public static function get_settings(): array {
		$saved = get_option( self::OPTION, [] );
		$saved = is_array( $saved ) ? $saved : [];

		return wp_parse_args(
			$saved,
			[
				'enabled'       => false,
				'access_level'  => 'read',
				'token'         => '',
				'token_created' => 0,
				'owner_id'      => 0,
				'tools'         => [],
			]
		);
	}

	/**
	 * Whether a tool is enabled for MCP exposure (enabled unless explicitly off).
	 *
	 * @param string $name Tool name.
	 * @param array  $settings Settings array.
	 * @return bool
	 */
	private static function is_tool_enabled( string $name, array $settings ): bool {
		$tools = $settings['tools'] ?? [];
		return ! array_key_exists( $name, $tools ) || ! empty( $tools[ $name ] );
	}

	/**
	 * Verify a raw bearer token against the stored (encrypted) token.
	 *
	 * @param string $raw Bearer token from the request.
	 * @return bool
	 */
	public static function verify_token( string $raw ): bool {
		$settings = self::get_settings();

		if ( empty( $settings['token'] ) || '' === $raw ) {
			return false;
		}

		$stored = Encryption::decrypt( $settings['token'] );

		if ( ! is_string( $stored ) || '' === $stored ) {
			return false;
		}

		return hash_equals( $stored, $raw );
	}

	/**
	 * REST permission callback: require the MCP server to be enabled and a valid
	 * bearer token. Returns WP_Error (401/403) to short-circuit otherwise.
	 *
	 * @param \WP_REST_Request $request Request.
	 * @return bool|\WP_Error
	 */
	public function authenticate( $request ) {
		$settings = self::get_settings();

		if ( empty( $settings['enabled'] ) ) {
			return new \WP_Error( 'mcp_disabled', __( 'The MCP server is disabled.', 'antimanual' ), [ 'status' => 403 ] );
		}

		$header = (string) $request->get_header( 'authorization' );

		if ( 0 === stripos( $header, 'bearer ' ) ) {
			$token = trim( substr( $header, 7 ) );
		} else {
			$token = '';
		}

		if ( ! self::verify_token( $token ) ) {
			return new \WP_Error( 'mcp_unauthorized', __( 'Invalid MCP token.', 'antimanual' ), [ 'status' => 401 ] );
		}

		return true;
	}

	/**
	 * Handle a JSON-RPC 2.0 request and dispatch the MCP method.
	 *
	 * @param \WP_REST_Request $request Request.
	 * @return \WP_REST_Response
	 */
	public function handle( $request ) {
		$settings = self::get_settings();

		// Run subsequent tool calls as the token owner so capability checks and
		// authored content attribute correctly.
		if ( ! empty( $settings['owner_id'] ) ) {
			wp_set_current_user( (int) $settings['owner_id'] );
		}

		$body   = json_decode( $request->get_body(), true );
		$body   = is_array( $body ) ? $body : [];
		$id     = $body['id'] ?? null;
		$method = isset( $body['method'] ) ? (string) $body['method'] : '';
		$params = isset( $body['params'] ) && is_array( $body['params'] ) ? $body['params'] : [];

		// Notifications (no id) get an empty 200 with no body.
		if ( null === $id && 0 === strpos( $method, 'notifications/' ) ) {
			return new \WP_REST_Response( null, 202 );
		}

		switch ( $method ) {
			case 'initialize':
				$result = $this->result_initialize();
				break;
			case 'ping':
				$result = (object) [];
				break;
			case 'tools/list':
				$result = $this->result_tools_list( $settings );
				break;
			case 'tools/call':
				return $this->result_tools_call( $id, $params, $settings );
			default:
				return $this->rpc_error( $id, -32601, __( 'Method not found.', 'antimanual' ) );
		}

		return $this->rpc_result( $id, $result );
	}

	/**
	 * Build the `initialize` result.
	 *
	 * @return array
	 */
	private function result_initialize(): array {
		return [
			'protocolVersion' => self::PROTOCOL_VERSION,
			'capabilities'    => [ 'tools' => [ 'listChanged' => false ] ],
			'serverInfo'      => [
				'name'    => 'Antimanual',
				'version' => defined( 'ANTIMANUAL_VERSION' ) ? ANTIMANUAL_VERSION : '1.0.0',
			],
		];
	}

	/**
	 * Build the `tools/list` result, filtered by access level and enabled set.
	 *
	 * @param array $settings Settings.
	 * @return array
	 */
	private function result_tools_list( array $settings ): array {
		$write = 'write' === ( $settings['access_level'] ?? 'read' );

		$available = ToolRegistry::available(
			[
				'source' => 'mcp',
				'public' => false,
				'write'  => $write,
			]
		);

		$tools = [];
		foreach ( $available as $name => $tool ) {
			if ( ! self::is_tool_enabled( $name, $settings ) ) {
				continue;
			}

			$tools[] = [
				'name'        => $name,
				'description' => $tool['description'],
				'inputSchema' => ToolRegistry::to_schema( $tool ),
			];
		}

		return [ 'tools' => $tools ];
	}

	/**
	 * Execute a tool call and build the MCP `tools/call` response.
	 *
	 * @param mixed $id       JSON-RPC id.
	 * @param array $params   Params ({ name, arguments }).
	 * @param array $settings Settings.
	 * @return \WP_REST_Response
	 */
	private function result_tools_call( $id, array $params, array $settings ) {
		$name = isset( $params['name'] ) ? (string) $params['name'] : '';
		$args = isset( $params['arguments'] ) && is_array( $params['arguments'] ) ? $params['arguments'] : [];

		if ( '' === $name || ! self::is_tool_enabled( $name, $settings ) ) {
			return $this->rpc_error( $id, -32602, __( 'Tool not available.', 'antimanual' ) );
		}

		$result = ToolRegistry::execute(
			$name,
			$args,
			[
				'source' => 'mcp',
				'public' => false,
				'write'  => 'write' === ( $settings['access_level'] ?? 'read' ),
			]
		);

		if ( is_wp_error( $result ) ) {
			return $this->rpc_result(
				$id,
				[
					'content' => [ [ 'type' => 'text', 'text' => $result->get_error_message() ] ],
					'isError' => true,
				]
			);
		}

		return $this->rpc_result(
			$id,
			[
				'content' => [
					[
						'type' => 'text',
						'text' => wp_json_encode( $result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ),
					],
				],
			]
		);
	}

	/**
	 * Build a JSON-RPC success response.
	 *
	 * @param mixed $id     Request id.
	 * @param mixed $result Result payload.
	 * @return \WP_REST_Response
	 */
	private function rpc_result( $id, $result ) {
		return new \WP_REST_Response(
			[
				'jsonrpc' => '2.0',
				'id'      => $id,
				'result'  => $result,
			],
			200
		);
	}

	/**
	 * Build a JSON-RPC error response.
	 *
	 * @param mixed  $id      Request id.
	 * @param int    $code    JSON-RPC error code.
	 * @param string $message Error message.
	 * @return \WP_REST_Response
	 */
	private function rpc_error( $id, int $code, string $message ) {
		return new \WP_REST_Response(
			[
				'jsonrpc' => '2.0',
				'id'      => $id,
				'error'   => [ 'code' => $code, 'message' => $message ],
			],
			200
		);
	}
}
