<?php

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

/**
 * Bridge between MCP protocol and Niwa's admin autonomy runtime.
 *
 * Responsibilities:
 * 1. Convert tool definitions from OpenAI format (build_function_tool output) to MCP format.
 * 2. Prefer the modern founder chat/runtime executor for runtime/control tools.
 * 3. Keep the legacy Admin_Autonomy_Tools preview -> validate -> execute pipeline as fallback.
 * 4. Classify tools as read-only vs. write for MCP permission checks.
 */
class Niwa_AI_MCP_Tools_Bridge {

	/**
	 * The Niwa_AI_Admin_Autonomy_Tools instance.
	 *
	 * @var Niwa_AI_Admin_Autonomy_Tools
	 */
	private $tools;

	/**
	 * The Niwa_AI_Admin_Autonomy_Chat runtime instance.
	 *
	 * @var Niwa_AI_Admin_Autonomy_Chat|null
	 */
	private $runtime;

	/**
	 * Cached runtime tool-name map.
	 *
	 * @var array|null
	 */
	private $runtime_tool_names = null;

	/**
	 * Cached legacy tool-name map.
	 *
	 * @var array|null
	 */
	private $legacy_tool_names = null;

	/**
	 * Read-only tool prefixes — these skip preview/validate and go straight to execute.
	 *
	 * @var array
	 */
	private static $read_only_prefixes = array(
		'search_',
		'list_',
		'get_',
		'woo_report_',
		'cache_settings_get',
		'cache_presets_list',
		'elementor_list_',
		'elementor_get_',
		'elementor_find_',
		'wp_search_',
		'wp_get_',
	);

	/**
	 * Explicit read-only tool names (no prefix match needed).
	 *
	 * @var array
	 */
	private static $read_only_explicit = array(
		'get_wp_settings',
		'get_site_info',
		'get_current_user',
		'get_media_file',
		'get_media_details',
		'list_custom_post_types',
		'cache_settings_get',
		'cache_presets_list',
		'search_stock_images',
		'scan_site_for_linking',
	);

	/**
	 * Constructor.
	 *
	 * @param Niwa_AI_Admin_Autonomy_Tools      $tools   The legacy tools facade instance.
	 * @param Niwa_AI_Admin_Autonomy_Chat|null $runtime The modern founder runtime.
	 */
	public function __construct( $tools, $runtime = null ) {
		$this->tools   = $tools;
		$this->runtime = $runtime;

		if ( ! $this->tools && ! $this->runtime ) {
			error_log( '[Niwa MCP Bridge] WARNING: Tools facade and founder runtime are null — admin autonomy may have failed to boot.' );
		}
	}

	/**
	 * Determine whether a tool is read-only (safe to execute without preview/validate).
	 *
	 * @param string $name Tool name.
	 * @return bool True if the tool is read-only.
	 */
	public function is_read_only_tool( $name ) {
		$name = (string) $name;
		if ( 'code_read' === $name ) {
			$name = 'file_read';
		}
		if ( $this->runtime_has_tool( $name ) && $this->runtime && method_exists( $this->runtime, 'is_mcp_runtime_write_tool' ) ) {
			return ! $this->runtime->is_mcp_runtime_write_tool( $name );
		}

		if ( in_array( $name, self::$read_only_explicit, true ) ) {
			return true;
		}

		foreach ( self::$read_only_prefixes as $prefix ) {
			if ( 0 === strpos( $name, $prefix ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Convert an OpenAI function tool format to MCP tool schema format.
	 *
	 * OpenAI: { type: 'function', function: { name, description, parameters: { type, properties, required } } }
	 * MCP:    { name, description, inputSchema: { type, properties, required } }
	 *
	 * @param array $openai_tool Tool definition in OpenAI format.
	 * @return array Tool definition in MCP format.
	 */
	private function convert_tool_to_mcp( $openai_tool ) {
		$func   = isset( $openai_tool['function'] ) ? $openai_tool['function'] : array();
		$name   = isset( $func['name'] ) ? (string) $func['name'] : '';
		$desc   = isset( $func['description'] ) ? (string) $func['description'] : '';
		$params = isset( $func['parameters'] ) && is_array( $func['parameters'] )
			? $func['parameters']
			: array( 'type' => 'object', 'properties' => array() );

		// Ensure 'type' is always present in inputSchema.
		if ( ! isset( $params['type'] ) ) {
			$params['type'] = 'object';
		}

		$params = $this->normalize_schema_for_mcp( $params );

		return array(
			'name'        => $name,
			'description' => $desc,
			'inputSchema' => $params,
		);
	}

	/**
	 * Normalize JSON Schema arrays so empty properties encode as JSON objects.
	 *
	 * @param mixed $schema Schema node.
	 * @return mixed Normalized schema node.
	 */
	private function normalize_schema_for_mcp( $schema ) {
		if ( is_object( $schema ) || ! is_array( $schema ) ) {
			return $schema;
		}

		foreach ( $schema as $key => $value ) {
			if ( 'properties' === (string) $key && is_array( $value ) && empty( $value ) ) {
				$schema[ $key ] = new \stdClass();
				continue;
			}

			$schema[ $key ] = $this->normalize_schema_for_mcp( $value );
		}

		return $schema;
	}

	/**
	 * Get all tools in MCP format.
	 *
	 * @return array List of MCP-formatted tool definitions.
	 */
	public function get_mcp_tool_list() {
		$openai_tools = array_merge(
			$this->get_runtime_tool_definitions(),
			$this->get_legacy_tool_definitions()
		);
		if ( empty( $openai_tools ) ) {
			error_log( '[Niwa MCP Bridge] get_mcp_tool_list: no runtime or legacy tool definitions are available.' );
			return array();
		}

		$mcp_tools = array();
		$seen      = array();

		foreach ( $openai_tools as $tool ) {
			$converted = $this->convert_tool_to_mcp( $tool );
			$name      = isset( $converted['name'] ) ? (string) $converted['name'] : '';
			if ( '' === $name || isset( $seen[ $name ] ) ) {
				continue;
			}

			$seen[ $name ] = true;
			$mcp_tools[]   = $converted;
		}

		return $mcp_tools;
	}

	/**
	 * Fetch modern founder runtime tool definitions.
	 *
	 * @return array
	 */
	private function get_runtime_tool_definitions() {
		if ( ! $this->runtime || ! method_exists( $this->runtime, 'get_mcp_runtime_tool_definitions' ) ) {
			return array();
		}

		try {
			$tools = $this->runtime->get_mcp_runtime_tool_definitions(
				$this->build_mcp_runtime_context(),
				'main',
				array(
					'allow_writes' => true,
					'mcp'          => true,
				)
			);
		} catch ( \Throwable $e ) {
			error_log( '[Niwa MCP Bridge] get_mcp_runtime_tool_definitions() threw: ' . $e->getMessage() );
			return array();
		}

		if ( ! is_array( $tools ) ) {
			error_log( '[Niwa MCP Bridge] get_mcp_runtime_tool_definitions() returned non-array: ' . gettype( $tools ) );
			return array();
		}

		return $tools;
	}

	/**
	 * Fetch legacy admin tool definitions.
	 *
	 * @return array
	 */
	private function get_legacy_tool_definitions() {
		if ( ! $this->tools || ! method_exists( $this->tools, 'get_tool_definitions' ) ) {
			return array();
		}

		try {
			$tools = $this->tools->get_tool_definitions();
		} catch ( \Throwable $e ) {
			error_log( '[Niwa MCP Bridge] get_tool_definitions() threw: ' . $e->getMessage() );
			return array();
		}

		if ( ! is_array( $tools ) ) {
			error_log( '[Niwa MCP Bridge] get_tool_definitions() returned non-array: ' . gettype( $tools ) );
			return array();
		}

		return $tools;
	}

	/**
	 * Build the default founder-runtime context for external MCP calls.
	 *
	 * @param string $tool_name Tool name.
	 * @return array
	 */
	private function build_mcp_runtime_context( $tool_name = '' ) {
		$user_id = function_exists( 'get_current_user_id' ) ? intval( get_current_user_id() ) : 0;
		$login   = '';
		if ( function_exists( 'wp_get_current_user' ) ) {
			$user = wp_get_current_user();
			if ( is_object( $user ) && isset( $user->user_login ) ) {
				$login = (string) $user->user_login;
			}
		}

		$context = array(
			'session_id'      => 'mcp-' . ( $user_id > 0 ? (string) $user_id : 'anonymous' ),
			'user_role'       => 'admin',
			'channel'         => 'mcp',
			'source'          => 'mcp',
			'admin_user_id'   => $user_id,
			'current_prompt'  => '' !== (string) $tool_name ? 'MCP tool call: ' . (string) $tool_name : 'MCP external tool call.',
		);

		if ( '' !== $login ) {
			$context['admin_user_login'] = $login;
		}

		return $context;
	}

	/**
	 * Determine whether the modern runtime advertises a tool.
	 *
	 * @param string $name Tool name.
	 * @return bool
	 */
	private function runtime_has_tool( $name ) {
		if ( null === $this->runtime_tool_names ) {
			$this->runtime_tool_names = $this->build_tool_name_map( $this->get_runtime_tool_definitions() );
		}

		return isset( $this->runtime_tool_names[ (string) $name ] );
	}

	/**
	 * Determine whether the legacy admin facade advertises a tool.
	 *
	 * @param string $name Tool name.
	 * @return bool
	 */
	private function legacy_has_tool( $name ) {
		if ( null === $this->legacy_tool_names ) {
			$this->legacy_tool_names = $this->build_tool_name_map( $this->get_legacy_tool_definitions() );
		}

		return isset( $this->legacy_tool_names[ (string) $name ] );
	}

	/**
	 * Build a tool-name lookup from OpenAI function-tool definitions.
	 *
	 * @param array $tools Tool definitions.
	 * @return array
	 */
	private function build_tool_name_map( $tools ) {
		$map = array();
		foreach ( (array) $tools as $tool ) {
			$name = isset( $tool['function']['name'] ) ? (string) $tool['function']['name'] : '';
			if ( '' !== $name ) {
				$map[ $name ] = true;
			}
		}

		return $map;
	}

	/**
	 * Execute a tool call via the Niwa admin autonomy tools pipeline.
	 *
	 * Read-only tools: build minimal action and execute directly.
	 * Write tools: preview -> validate -> execute pipeline.
	 *
	 * @param string $name      Tool name.
	 * @param array  $arguments Tool arguments.
	 * @return array MCP-formatted content result.
	 */
	public function call_tool( $name, $arguments ) {
		$name      = (string) $name;
		$arguments = is_array( $arguments ) ? $arguments : array();
		if ( 'code_read' === $name ) {
			$name = 'file_read';
		}

		if ( ! $this->tools && ! $this->runtime ) {
			return $this->format_mcp_error( 'Tools system is not available. Admin autonomy failed to boot.' );
		}

		try {
			if ( $this->runtime_has_tool( $name ) && $this->runtime && method_exists( $this->runtime, 'execute_mcp_runtime_tool_call' ) ) {
				$runtime_result = $this->runtime->execute_mcp_runtime_tool_call(
					$name,
					$arguments,
					$this->build_mcp_runtime_context( $name ),
					array(
						'allow_writes' => true,
						'mcp'          => true,
					)
				);

				if ( ! ( is_array( $runtime_result ) && ! empty( $runtime_result['mcp_runtime_unsupported'] ) && $this->legacy_has_tool( $name ) ) ) {
					return $this->format_mcp_result( $runtime_result );
				}

			}

			if ( ! $this->tools ) {
				return $this->format_mcp_error( 'Legacy tools facade is not available for tool: ' . $name );
			}

			if ( $this->is_read_only_tool( $name ) ) {
				return $this->execute_read_only( $name, $arguments );
			}

			return $this->execute_write( $name, $arguments );
		} catch ( \Throwable $e ) {
			error_log( '[Niwa MCP Bridge] call_tool EXCEPTION: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine() );
			return $this->format_mcp_error( 'Tool execution failed: ' . $e->getMessage() );
		}
	}

	/**
	 * Execute a read-only tool directly (no preview/validate step).
	 *
	 * @param string $name      Tool name.
	 * @param array  $arguments Tool arguments.
	 * @return array MCP-formatted result.
	 */
	private function execute_read_only( $name, $arguments ) {
		$action = array(
			'type' => $name,
			'args' => $arguments,
		);

		$result = $this->tools->execute_action( $action );

		return $this->format_mcp_result( $result );
	}

	/**
	 * Execute a write tool through the full preview -> validate -> execute pipeline.
	 *
	 * @param string $name      Tool name.
	 * @param array  $arguments Tool arguments.
	 * @return array MCP-formatted result.
	 */
	private function execute_write( $name, $arguments ) {
		// Step 1: Preview.
		$preview = $this->tools->preview_tool_call( $name, $arguments );
		if ( empty( $preview['success'] ) ) {
			$error_msg = isset( $preview['error'] )
				? (string) $preview['error']
				: 'Preview failed for tool: ' . $name;
			return $this->format_mcp_error( $error_msg );
		}

		// Extract action from preview.
		$action = isset( $preview['action'] ) && is_array( $preview['action'] )
			? $preview['action']
			: null;

		if ( ! $action ) {
			return $this->format_mcp_error( 'No action returned from preview for tool: ' . $name );
		}

		// Step 2: Validate.
		$validation = $this->tools->validate_action( $action );
		if ( empty( $validation['success'] ) ) {
			$error_msg = isset( $validation['error'] )
				? (string) $validation['error']
				: 'Validation failed for tool: ' . $name;
			return $this->format_mcp_error( $error_msg );
		}

		// Step 3: Execute.
		$result = $this->tools->execute_action( $action );
		if ( is_array( $result ) && ( ! empty( $result['success'] ) || ! empty( $result['ok'] ) ) ) {
			Niwa_AI_External_Page_Cache::refresh_seraphinite_after_change( $name );
		}

		return $this->format_mcp_result( $result );
	}

	/**
	 * Format an execution result into MCP content response.
	 *
	 * @param array $result Raw result from the tools pipeline.
	 * @return array MCP-formatted response with content and isError flag.
	 */
	private function format_mcp_result( $result ) {
		$result   = is_array( $result ) ? $result : array(
			'success' => true,
			'value'   => $result,
		);
		$is_error = empty( $result['success'] ) && empty( $result['ok'] );
		$text     = '';

		if ( $is_error ) {
			$text = isset( $result['error'] ) ? (string) $result['error'] : 'Unknown error';
		} else {
			// Remove internal fields, return clean data.
			$clean = $result;
			unset( $clean['success'], $clean['ok'], $clean['rollback_entries'], $clean['executed_actions'], $clean['mcp_runtime_unsupported'] );

			$text = wp_json_encode( $clean, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT );
			if ( false === $text ) {
				$text = isset( $result['summary'] ) ? (string) $result['summary'] : 'Operation completed.';
			}
		}

		return array(
			'content' => array(
				array(
					'type' => 'text',
					'text' => $text,
				),
			),
			'isError' => $is_error,
		);
	}

	/**
	 * Format an error message into MCP content response.
	 *
	 * @param string $message Error description.
	 * @return array MCP-formatted error response.
	 */
	private function format_mcp_error( $message ) {
		return array(
			'content' => array(
				array(
					'type' => 'text',
					'text' => (string) $message,
				),
			),
			'isError' => true,
		);
	}
}
