<?php

/**
 * LolaCore � AbilitiesRegistry
 *
 * @package LolaCore
 * @license GPL-2.0-or-later
 */

/**
 * Abilities Registry
 *
 * @package LolaCore\Actions
 */

declare(strict_types=1);

namespace LolaCore\Actions;

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

/**
 * Registers LolaCore capabilities with the WordPress Abilities API.
 */
class AbilitiesRegistry {

	/**
	 * Hooks into the Abilities API initialization.
	 */
	public function register(): void {
		add_action( 'wp_abilities_api_categories_init', array( $this, 'registerCategory' ) );
		add_action( 'wp_abilities_api_init', array( $this, 'registerAbilities' ) );
	}

	/**
	 * Registers the LolaCore ability category.
	 */
	public function registerCategory(): void {
		wp_register_ability_category( 'lolacore', array(
			'label'       => __( 'LolaCore', 'lolacore' ),
			'description' => __( 'AI assistant abilities for site management, memory, and diagnostics.', 'lolacore' ),
		) );
	}

	/**
	 * Registers all LolaCore abilities with the WordPress Abilities API.
	 *
	 * Each ability includes:
	 * - input_schema: JSON Schema for input validation
	 * - output_schema: JSON Schema describing the return value structure
	 * - annotations (via meta): readonly, destructive, idempotent flags
	 */
	public function registerAbilities(): void {
		if ( ! function_exists( 'wp_register_ability' ) ) {
			return;
		}

		$this->registerGetSiteInfo();
		$this->registerGetMemoryContext();
		$this->registerAddMemoryFact();
		$this->registerGetActionHistory();
		$this->registerGetScanData();
		$this->registerChat();
		$this->registerExecuteAction();
		$this->registerAddSkill();

		// Bridge: register each chat action as an atomic MCP-accessible ability,
		// gated by the same disabled_domains toggle that controls the chat surface.
		$this->registerActionsAsAbilities();
	}

	/**
	 * Bridge — registers each entry of the ActionExecutor registry as an
	 * atomic WP Ability so MCP clients (Cursor, Antigravity, Claude Code) can
	 * discover and invoke them individually via execute-ability.
	 *
	 * The set is gated by the SAME disabled_domains config the chat surface uses
	 * (Settings::DOMAIN_MAP) — no separate MCP toggle, no asymmetric trust:
	 * what the chat can do, MCP can do. Protection lives in confirm flows + the
	 * IDE's own approval UI, not in a parallel list of exposures.
	 *
	 * Action_ids already registered as a higher-level ability (e.g. add_memory_fact,
	 * add_skill, request_full_toolset) are skipped to avoid duplicate registration.
	 */
	private function registerActionsAsAbilities(): void {
		// Read disabled domains from the SAME toggle the chat uses.
		$disabledDomainsJson = \LolaCore\Admin\Settings::getConfig( 'disabled_domains', '[]' );
		$disabledDomains     = json_decode( (string) $disabledDomainsJson, true );
		if ( ! is_array( $disabledDomains ) ) {
			$disabledDomains = array();
		}

		// Expand domain slugs → categories.
		$disabledCategories = array();
		foreach ( $disabledDomains as $domainSlug ) {
			if ( isset( \LolaCore\Admin\Settings::DOMAIN_MAP[ $domainSlug ] ) ) {
				$disabledCategories = array_merge( $disabledCategories, \LolaCore\Admin\Settings::DOMAIN_MAP[ $domainSlug ] );
			}
		}

		// Action_ids that already have a dedicated ability with custom shaping.
		$skipActionIds = array(
			'add_memory_fact',
			'add_skill',
			'request_full_toolset',
		);

		// Use the filtered registry so addon-contributed abilities (e.g. Elementor's
		// 92 abilities injected via `lolacore_available_abilities`) are exposed via
		// MCP too. Reading from ToolSchemaGenerator directly bypasses the filter and
		// silently drops every addon that hooks in through it.
		$abilities = $this->getRegisteredAbilities();

		foreach ( $abilities as $toolName => $tool ) {
			$category = $tool['category'] ?? 'uncategorized';

			if ( ! is_string( $toolName ) || $toolName === '' ) {
				continue;
			}

			if ( in_array( $category, $disabledCategories, true ) ) {
				continue;
			}

			// camelCase tool name → snake_case action_id.
			$actionId = strtolower( preg_replace( '/([a-z0-9])([A-Z])/', '$1_$2', $toolName ) );

			if ( in_array( $actionId, $skipActionIds, true ) ) {
				continue;
			}

			$abilityName = 'lolacore/' . str_replace( '_', '-', $actionId );

			// Don't shadow an already-registered ability (defensive — base abilities
			// use distinct names, but addons could collide). Use wp_has_ability() not
			// wp_get_ability(): the latter calls _doing_it_wrong() when the ability is
			// not yet registered, polluting the debug log on every request.
			if ( function_exists( 'wp_has_ability' ) && \wp_has_ability( $abilityName ) ) {
				continue;
			}

			$isWrite = $this->isWriteAction( $actionId );

			\wp_register_ability( $abilityName, array(
				'label'               => $this->humanizeActionId( $actionId ),
				'description'         => (string) ( $tool['description'] ?? '' ),
				'category'            => 'lolacore',
				'input_schema'        => $tool['parameters'] ?? array( 'type' => 'object', 'properties' => array() ),
				'output_schema'       => array(
					'type'       => 'object',
					'properties' => array(
						'action_id'             => array( 'type' => 'string' ),
						'mode'                  => array( 'type' => 'string', 'enum' => array( 'executed', 'preview', 'denied' ) ),
						'result'                => array( 'type' => 'object', 'description' => 'Action-specific result payload.' ),
						'message'               => array( 'type' => 'string' ),
						'requires_confirmation' => array( 'type' => 'boolean' ),
						'warnings'              => array( 'type' => 'array', 'items' => array( 'type' => 'string' ) ),
					),
					'required'   => array( 'action_id', 'mode', 'message' ),
				),
				'permission_callback' => array( $this, 'permissionCallback' ),
				'execute_callback'    => function ( array $input ) use ( $actionId, $abilityName ) {
					do_action( 'lolacore_ability_invoked', $abilityName, $input, 'rest' );
					$executor = new ActionExecutor();
					// MCP invocations execute directly: the IDE's tool-call approval
					// is the human gate, equivalent to the chat's confirm button.
					$response = $executor->execute( $actionId, $input, false, 'rest' );
					return apply_filters( 'lolacore_ability_output', $response, $abilityName );
				},
				'meta'                => $this->getMetaBlock( ! $isWrite, $isWrite, false ),
			) );
		}
	}

	/**
	 * Returns true when the action is registered as a write in ActionExecutor.
	 */
	private function isWriteAction( string $actionId ): bool {
		$executor = new ActionExecutor();
		$registry = $executor->getActionRegistry();
		return isset( $registry[ $actionId ] ) && ( $registry[ $actionId ]['type'] ?? 'read' ) === 'write';
	}

	/**
	 * snake_case → "Title Case" label.
	 */
	private function humanizeActionId( string $actionId ): string {
		return ucwords( str_replace( '_', ' ', $actionId ) );
	}

	/**
	 * Permission callback for all abilities.
	 *
	 * @return bool True if the user has the required capabilities.
	 */
	public function permissionCallback(): bool {
		return current_user_can( 'manage_options' );
	}

	/**
	 * Common meta block for LolaCore abilities.
	 *
	 * @param bool $readonly Whether the ability is read-only.
	 * @param bool $destructive Whether the ability is destructive.
	 * @param bool $idempotent Whether the ability is idempotent.
	 * @return array The meta array.
	 */
	private function getMetaBlock( bool $readonly, bool $destructive, bool $idempotent ): array {
		return array(
			'show_in_rest' => true,
			'mcp'          => array(
				'public' => true,
				'type'   => 'tool',
			),
			'annotations'  => array(
				'readonly'    => $readonly,
				'destructive' => $destructive,
				'idempotent'  => $idempotent,
			),
		);
	}

	private function registerGetSiteInfo(): void {
		wp_register_ability( 'lolacore/get-site-info', array(
			'label'               => __( 'Get Site Info', 'lolacore' ),
			'description'         => __( 'Get the complete state of the site.', 'lolacore' ),
			'category'            => 'lolacore',
			'input_schema'        => array( 'type' => 'object', 'properties' => array() ),
			'output_schema'       => array(
				'type'       => 'object',
				'properties' => array(
					'wp_version'     => array( 'type' => 'string', 'description' => 'WordPress version installed' ),
					'php_version'    => array( 'type' => 'string', 'description' => 'PHP version running' ),
					'ssl_active'     => array( 'type' => 'boolean', 'description' => 'Whether HTTPS is active' ),
					'active_theme'   => array( 'type' => 'string', 'description' => 'Name of the active theme' ),
					'active_plugins' => array(
						'type'        => 'array',
						'items'       => array(
							'type'       => 'object',
							'properties' => array(
								'slug'    => array( 'type' => 'string' ),
								'name'    => array( 'type' => 'string' ),
								'version' => array( 'type' => 'string' ),
							),
						),
						'description' => 'List of active plugins with versions',
					),
					'site_health'    => array( 'type' => 'string', 'enum' => array( 'good', 'recommended', 'critical' ) ),
					'db_size_mb'     => array( 'type' => 'number', 'description' => 'Database size in megabytes' ),
					'multisite'      => array( 'type' => 'boolean' ),
				),
				'required'   => array( 'wp_version', 'php_version', 'ssl_active', 'active_theme', 'active_plugins', 'site_health' ),
			),
			'permission_callback' => array( $this, 'permissionCallback' ),
			'execute_callback'    => array( $this, 'executeGetSiteInfo' ),
			'meta'                => $this->getMetaBlock( true, false, true ),
		) );
	}

	private function registerGetMemoryContext(): void {
		wp_register_ability( 'lolacore/get-memory-context', array(
			'label'               => __( 'Get Memory Context', 'lolacore' ),
			'description'         => __( 'Retrieve relevant memory context given a prompt.', 'lolacore' ),
			'category'            => 'lolacore',
			'input_schema'        => array(
				'type'       => 'object',
				'properties' => array(
					'prompt'      => array(
						'type'        => 'string',
						'description' => 'The user message or topic to retrieve relevant memory context for.',
						'minLength'   => 1,
					),
					'max_entries' => array(
						'type'        => 'integer',
						'description' => 'Maximum number of memory entries to return. Default: 10.',
						'default'     => 10,
						'minimum'     => 1,
						'maximum'     => 50,
					),
				),
				'required'   => array( 'prompt' ),
			),
			'output_schema'       => array(
				'type'       => 'object',
				'properties' => array(
					'entries'         => array(
						'type'  => 'array',
						'items' => array(
							'type'       => 'object',
							'properties' => array(
								'id'         => array( 'type' => 'integer' ),
								'fact_type'  => array( 'type' => 'string', 'description' => 'Category label as stored in DB (open set — addons may register new types).' ),
								'content'    => array( 'type' => 'string' ),
								'importance' => array( 'type' => 'integer', 'minimum' => 1, 'maximum' => 10 ),
								'source'     => array( 'type' => 'string', 'description' => 'Origin of the fact (open set — scan, conversation, explicit, action_result, addon-specific).' ),
								'created_at' => array( 'type' => 'string', 'format' => 'date-time' ),
							),
						),
					),
					'total_available' => array( 'type' => 'integer', 'description' => 'Total entries matching the prompt (before limit)' ),
				),
				'required'   => array( 'entries', 'total_available' ),
			),
			'permission_callback' => array( $this, 'permissionCallback' ),
			'execute_callback'    => array( $this, 'executeGetMemoryContext' ),
			'meta'                => $this->getMetaBlock( true, false, true ),
		) );
	}

	private function registerAddMemoryFact(): void {
		wp_register_ability( 'lolacore/add-memory-fact', array(
			'label'               => __( 'Add Memory Fact', 'lolacore' ),
			'description'         => __( 'Add an explicit fact to memory.', 'lolacore' ),
			'category'            => 'lolacore',
			'input_schema'        => array(
				'type'       => 'object',
				'properties' => array(
					'content'    => array(
						'type'        => 'string',
						'description' => 'The fact to store in memory. Must be a clear, atomic statement.',
						'minLength'   => 5,
						'maxLength'   => 500,
					),
					'fact_type'  => array(
						'type'        => 'string',
						'enum'        => array( 'user_preference', 'business_context', 'site_state', 'decision_history' ),
						'description' => 'Category of the fact being stored.',
					),
					'importance' => array(
						'type'        => 'integer',
						'description' => 'Importance score from 1 (trivial) to 10 (critical). Default: 5.',
						'minimum'     => 1,
						'maximum'     => 10,
						'default'     => 5,
					),
				),
				'required'   => array( 'content', 'fact_type' ),
			),
			'output_schema'       => array(
				'type'       => 'object',
				'properties' => array(
					'success'  => array( 'type' => 'boolean' ),
					'entry_id' => array( 'type' => 'integer', 'description' => 'ID of the created memory entry' ),
					'message'  => array( 'type' => 'string' ),
				),
				'required'   => array( 'success' ),
			),
			'permission_callback' => array( $this, 'permissionCallback' ),
			'execute_callback'    => array( $this, 'executeAddMemoryFact' ),
			'meta'                => $this->getMetaBlock( false, false, false ),
		) );
	}

	private function registerAddSkill(): void {
		wp_register_ability( 'lolacore/add-skill', array(
			'label'               => __( 'Add Skill', 'lolacore' ),
			'description'         => __( 'Stores a new behavioral skill in inactive state so the user can review the source before activating it.', 'lolacore' ),
			'category'            => 'lolacore',
			'input_schema'        => array(
				'type'       => 'object',
				'properties' => array(
					'skill_name' => array(
						'type'        => 'string',
						'description' => 'Kebab-case unique name for the skill.',
						'minLength'   => 2,
						'maxLength'   => 191,
					),
					'content'    => array(
						'type'        => 'string',
						'description' => 'Full skill markdown including --- YAML frontmatter --- and body.',
						'minLength'   => 10,
					),
				),
				'required'   => array( 'skill_name', 'content' ),
			),
			'output_schema'       => array(
				'type'       => 'object',
				'properties' => array(
					'success'    => array( 'type' => 'boolean' ),
					'skill_name' => array( 'type' => 'string' ),
					'id'         => array( 'type' => 'integer' ),
					'message'    => array( 'type' => 'string' ),
				),
				'required'   => array( 'success' ),
			),
			'permission_callback' => array( $this, 'permissionCallback' ),
			'execute_callback'    => array( $this, 'executeAddSkill' ),
			'meta'                => $this->getMetaBlock( false, false, false ),
		) );
	}

	private function registerGetActionHistory(): void {
		wp_register_ability( 'lolacore/get-action-history', array(
			'label'               => __( 'Get Action History', 'lolacore' ),
			'description'         => __( 'History of actions executed by Lola.', 'lolacore' ),
			'category'            => 'lolacore',
			'input_schema'        => array(
				'type'       => 'object',
				'properties' => array(
					'action_type' => array(
						'type'        => 'string',
						'description' => 'Filter by action type. Omit to return all types.',
						'enum'        => array( 'plugin_update', 'post_create', 'db_optimize', 'option_update', 'cache_purge' ),
					),
					'status'      => array(
						'type' => 'string',
						'enum' => array( 'success', 'failed', 'cancelled', 'overridden' ),
					),
					'limit'       => array(
						'type'    => 'integer',
						'default' => 20,
						'minimum' => 1,
						'maximum' => 100,
					),
				),
			),
			'output_schema'       => array(
				'type'       => 'object',
				'properties' => array(
					'actions' => array(
						'type'  => 'array',
						'items' => array(
							'type'       => 'object',
							'properties' => array(
								'id'             => array( 'type' => 'integer' ),
								'action_type'    => array( 'type' => 'string' ),
								'description'    => array( 'type' => 'string' ),
								'status'         => array( 'type' => 'string' ),
								'user_override'  => array( 'type' => 'boolean' ),
								'lola_objection' => array( 'type' => array( 'string', 'null' ), 'description' => 'Lola reasoning when user overrode recommendation. Null if no override.' ),
								'executed_at'    => array( 'type' => 'string', 'format' => 'date-time' ),
							),
						),
					),
					'total'   => array( 'type' => 'integer' ),
				),
				'required'   => array( 'actions', 'total' ),
			),
			'permission_callback' => array( $this, 'permissionCallback' ),
			'execute_callback'    => array( $this, 'executeGetActionHistory' ),
			'meta'                => $this->getMetaBlock( true, false, true ),
		) );
	}

	private function registerGetScanData(): void {
		wp_register_ability( 'lolacore/get-scan-data', array(
			'label'               => __( 'Get Scan Data', 'lolacore' ),
			'description'         => __( 'Data from the last algorithmic scan.', 'lolacore' ),
			'category'            => 'lolacore',
			'input_schema'        => array(
				'type'       => 'object',
				'properties' => array(
					'section' => array(
						'type'        => 'string',
						'description' => 'Return only a specific section of the scan. Omit for full scan data.',
						'enum'        => array( 'wordpress', 'plugins', 'theme', 'database', 'php', 'ssl', 'content', 'users', 'elementor', 'vulnerabilities' ),
					),
				),
			),
			'output_schema'       => array(
				'type'       => 'object',
				'properties' => array(
					'scan_data'  => array( 'type' => 'object', 'description' => 'Full or partial scan data as collected by ScanDataCollector' ),
					'scanned_at' => array( 'type' => 'string', 'format' => 'date-time', 'description' => 'When the last scan was performed' ),
					'age_hours'  => array( 'type' => 'number', 'description' => 'Hours since last scan' ),
				),
				'required'   => array( 'scan_data', 'scanned_at' ),
			),
			'permission_callback' => array( $this, 'permissionCallback' ),
			'execute_callback'    => array( $this, 'executeGetScanData' ),
			'meta'                => $this->getMetaBlock( true, false, true ),
		) );
	}

	private function registerChat(): void {
		wp_register_ability( 'lolacore/chat', array(
			'label'               => __( 'Chat with Lola', 'lolacore' ),
			'description'         => __( 'Send a message to Lola and receive a response with context.', 'lolacore' ),
			'category'            => 'lolacore',
			'input_schema'        => array(
				'type'       => 'object',
				'properties' => array(
					'message'    => array(
						'type'        => 'string',
						'description' => 'The message to send to Lola. Lola will respond with full memory and site context.',
						'minLength'   => 1,
						'maxLength'   => 4000,
					),
					'session_id' => array(
						'type'        => 'string',
						'description' => 'Optional UUID to continue an existing session. Omit to start a new session.',
						'pattern'     => '^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$',
					),
				),
				'required'   => array( 'message' ),
			),
			'output_schema'       => array(
				'type'       => 'object',
				'properties' => array(
					'response'         => array( 'type' => 'string', 'description' => 'Lola response text' ),
					'session_id'       => array( 'type' => 'string', 'description' => 'Session UUID (new or continued)' ),
					'facts_saved'      => array(
						'type'        => 'array',
						'items'       => array( 'type' => 'string' ),
						'description' => 'Facts extracted and saved to memory during this exchange',
					),
					'actions_proposed' => array(
						'type'        => 'array',
						'items'       => array(
							'type'       => 'object',
							'properties' => array(
								'action_id'             => array( 'type' => 'string' ),
								'description'           => array( 'type' => 'string' ),
								'requires_confirmation' => array( 'type' => 'boolean' ),
							),
						),
						'description' => 'Actions Lola proposes in her response (if any)',
					),
				),
				'required'   => array( 'response', 'session_id' ),
			),
			'permission_callback' => array( $this, 'permissionCallback' ),
			'execute_callback'    => array( $this, 'executeChat' ),
			'meta'                => $this->getMetaBlock( false, false, false ),
		) );
	}

	private function registerExecuteAction(): void {
		// Build the enum dynamically from ActionExecutor's registry
		$executor   = new ActionExecutor();
		$action_ids = array_keys( $executor->getActionRegistry() );

		wp_register_ability( 'lolacore/execute-action', array(
			'label'               => __( 'Execute Action', 'lolacore' ),
			'description'         => __( 'Dispatcher: execute any registered action of Lola.', 'lolacore' ),
			'category'            => 'lolacore',
			'input_schema'        => array(
				'type'       => 'object',
				'properties' => array(
					'action_id' => array(
						'type'        => 'string',
						'description' => 'The action to execute.',
						'enum'        => $action_ids,
					),
					'params'    => array(
						'type'        => 'object',
						'description' => 'Parameters specific to the action. See action documentation for required fields.',
					),
					'confirm'   => array(
						'type'        => 'boolean',
						'description' => 'For write actions: false (default) returns a dry-run preview. true executes the action. Read actions ignore this flag.',
						'default'     => false,
					),
				),
				'required'   => array( 'action_id' ),
			),
			'output_schema'       => array(
				'type'       => 'object',
				'properties' => array(
					'action_id'             => array( 'type' => 'string' ),
					'mode'                  => array( 'type' => 'string', 'enum' => array( 'executed', 'preview', 'denied' ) ),
					'result'                => array( 'type' => 'object', 'description' => 'Action result or preview data' ),
					'message'               => array( 'type' => 'string', 'description' => 'Human-readable summary' ),
					'requires_confirmation' => array(
						'type'        => 'boolean',
						'description' => 'True if this action requires confirm=true to execute',
					),
					'warnings'              => array(
						'type'        => 'array',
						'items'       => array( 'type' => 'string' ),
						'description' => 'Warnings or objections from Lola (e.g., no backup detected)',
					),
				),
				'required'   => array( 'action_id', 'mode', 'message' ),
			),
			'permission_callback' => array( $this, 'permissionCallback' ),
			'execute_callback'    => array( $this, 'executeExecuteAction' ),
			'meta'                => $this->getMetaBlock( false, true, false ),
		) );
	}

	// --- Execute Callbacks (Stubs) ---

	public function executeGetSiteInfo( array $input ) {
		do_action( 'lolacore_ability_invoked', 'lolacore/get-site-info', $input, 'rest' );
		$executor = new ActionExecutor();
		$result   = $executor->getSiteStatus();

		// Enrich active_plugins from integer count (LLM-friendly compact form) to the
		// array-of-objects shape declared in this ability's output_schema. The shared
		// SystemHandler returns a count for the legacy LLM tool surface; the ability
		// surface needs the structured form for downstream consumers.
		if ( ! function_exists( 'get_plugin_data' ) ) {
			require_once ABSPATH . 'wp-admin/includes/plugin.php';
		}
		$active_plugin_files = (array) get_option( 'active_plugins', array() );
		$plugins             = array();
		foreach ( $active_plugin_files as $plugin_file ) {
			$dir  = dirname( $plugin_file );
			$slug = ( '.' === $dir ) ? basename( $plugin_file, '.php' ) : $dir;
			$path = WP_PLUGIN_DIR . '/' . $plugin_file;
			$name = $slug;
			$ver  = '';
			if ( file_exists( $path ) ) {
				$data = get_plugin_data( $path, false, false );
				$name = $data['Name'] ?? $slug;
				$ver  = $data['Version'] ?? '';
			}
			$plugins[] = array(
				'slug'    => (string) $slug,
				'name'    => (string) $name,
				'version' => (string) $ver,
			);
		}
		$result['active_plugins'] = $plugins;

		// Enrich with persistent memory context for MCP agents.
		// The web chat agent gets memory auto-injected via ContextBuilder into the
		// system prompt on every turn. MCP agents are stateless — they don't get that
		// automatic injection. Since get-site-info is the first ability any MCP agent
		// calls, we piggyback on it to deliver the top-ranked memory facts.
		$contextBuilder = new \LolaCore\Memory\ContextBuilder();
		$memoryBlock    = $contextBuilder->buildContext();
		$sessionSummary = $contextBuilder->getLastSessionSummary();

		$result['memory_context'] = array(
			'facts'                => $memoryBlock,
			'last_session_summary' => $sessionSummary,
		);

		return apply_filters( 'lolacore_ability_output', $result, 'lolacore/get-site-info' );
	}

	public function executeGetMemoryContext( array $input ) {
		do_action( 'lolacore_ability_invoked', 'lolacore/get-memory-context', $input, 'rest' );

		$memory    = new \LolaCore\Memory\MemoryManager();
		$prompt    = $input['prompt'] ?? '';
		$max       = (int) ( $input['max_entries'] ?? 10 );
		$max       = max( 1, min( 50, $max ) );

		// If a prompt is given, search by content; otherwise return top-ranked facts
		$entries = ! empty( $prompt )
			? $memory->searchFacts( $prompt, $max )
			: $memory->getRelevantFacts( $max );

		$total = $memory->getActiveFactCount();

		// Shape entries to match the output_schema
		$formatted = array();
		foreach ( $entries as $entry ) {
			$formatted[] = array(
				'id'         => (int) $entry['id'],
				'fact_type'  => $entry['fact_type'] ?? 'site_state',
				'content'    => $entry['content'] ?? '',
				'importance' => (int) ( $entry['importance'] ?? 5 ),
				'source'     => $entry['source'] ?? 'scan',
				'created_at' => $entry['created_at'] ?? '',
			);
		}

		return apply_filters( 'lolacore_ability_output', array(
			'entries'         => $formatted,
			'total_available' => $total,
		), 'lolacore/get-memory-context' );
	}

	public function executeAddMemoryFact( array $input ) {
		do_action( 'lolacore_ability_invoked', 'lolacore/add-memory-fact', $input, 'rest' );

		$content   = $input['content'] ?? '';
		$fact_type = $input['fact_type'] ?? '';

		if ( empty( $content ) || empty( $fact_type ) ) {
			return new \WP_Error(
				'missing_fields',
				__( 'Both content and fact_type are required.', 'lolacore' ),
				array( 'status' => 400 )
			);
		}

		$importance = (int) ( $input['importance'] ?? 5 );
		$importance = max( 1, min( 10, $importance ) );

		$memory   = new \LolaCore\Memory\MemoryManager();
		$entry_id = $memory->saveFact(
			$fact_type,
			$content,
			$importance,
			'explicit'
		);

		return apply_filters( 'lolacore_ability_output', array(
			'success'  => $entry_id > 0,
			'entry_id' => $entry_id,
			'message'  => $entry_id > 0
				? __( 'Fact saved to memory.', 'lolacore' )
				: __( 'Failed to save fact.', 'lolacore' ),
		), 'lolacore/add-memory-fact' );
	}

	public function executeAddSkill( array $input ) {
		do_action( 'lolacore_ability_invoked', 'lolacore/add-skill', $input, 'rest' );

		try {
			$handler = new Handlers\SkillsHandler();
			$result  = $handler->handle( 'add_skill', $input, false );
		} catch ( \Throwable $e ) {
			return new \WP_Error(
				'add_skill_failed',
				$e->getMessage(),
				array( 'status' => 400 )
			);
		}

		return apply_filters( 'lolacore_ability_output', $result, 'lolacore/add-skill' );
	}

	public function executeGetActionHistory( array $input ) {
		do_action( 'lolacore_ability_invoked', 'lolacore/get-action-history', $input, 'rest' );
		$logger  = new ActionLogger();
		$filters = array();
		if ( isset( $input['action_type'] ) ) {
			$filters['action_type'] = $input['action_type'];
		}
		if ( isset( $input['status'] ) ) {
			$filters['status'] = $input['status'];
		}
		$limit  = $input['limit'] ?? 20;
		$result = $logger->getHistory( $filters, $limit );

		// Shape DB rows (MySQL returns strings) to the output_schema declared above.
		$shaped = array();
		foreach ( $result['actions'] ?? array() as $action ) {
			$shaped[] = array(
				'id'             => (int) ( $action['id'] ?? 0 ),
				'action_type'    => (string) ( $action['action_type'] ?? '' ),
				'description'    => (string) ( $action['description'] ?? '' ),
				'status'         => (string) ( $action['status'] ?? '' ),
				'user_override'  => (bool) ( $action['user_override'] ?? false ),
				'lola_objection' => isset( $action['lola_objection'] ) ? (string) $action['lola_objection'] : null,
				'executed_at'    => (string) ( $action['executed_at'] ?? '' ),
			);
		}

		return apply_filters( 'lolacore_ability_output', array(
			'actions' => $shaped,
			'total'   => (int) ( $result['total'] ?? 0 ),
		), 'lolacore/get-action-history' );
	}

	public function executeGetScanData( array $input ) {
		do_action( 'lolacore_ability_invoked', 'lolacore/get-scan-data', $input, 'rest' );

		$collector = new \LolaCore\Scanner\ScanDataCollector();
		$scanner   = new \LolaCore\Scanner\SiteScanner(
			$collector,
			new \LolaCore\Memory\MemoryManager()
		);

		$scan_data = $scanner->getLastScan();

		if ( null === $scan_data ) {
			// No scan has been run yet — return empty with a note
			return apply_filters( 'lolacore_ability_output', array(
				'scan_data'  => new \stdClass(),
				'scanned_at' => null,
				'age_hours'  => null,
				'message'    => __( 'No scan has been performed yet.', 'lolacore' ),
			), 'lolacore/get-scan-data' );
		}

		// Filter to a specific section if requested
		$section = $input['section'] ?? null;
		if ( null !== $section && isset( $scan_data[ $section ] ) ) {
			$scan_data = array( $section => $scan_data[ $section ] );
		}

		// Calculate age in hours from the scan timestamp
		$scanned_at = $scan_data['scanned_at'] ?? $scan_data['timestamp'] ?? gmdate( 'c' );
		$age_hours  = ( time() - strtotime( $scanned_at ) ) / 3600;

		return apply_filters( 'lolacore_ability_output', array(
			'scan_data'  => $scan_data,
			'scanned_at' => $scanned_at,
			'age_hours'  => round( $age_hours, 1 ),
		), 'lolacore/get-scan-data' );
	}

	public function executeChat( array $input ) {
		do_action( 'lolacore_ability_invoked', 'lolacore/chat', $input, 'rest' );

		// Chat requires SSE streaming and is designed for the admin UI.
		// For REST/MCP clients, return an informative error directing them
		// to use the execute-action ability or the admin chat interface.
		return new \WP_Error(
			'chat_not_available_via_rest',
			__( 'Chat with Lola requires the admin UI (SSE streaming). For programmatic access, use the lolacore/execute-action ability to run specific actions, or lolacore/get-memory-context to query Lola\'s knowledge.', 'lolacore' ),
			array( 'status' => 501 )
		);
	}

	public function executeExecuteAction( array $input ) {
		do_action( 'lolacore_ability_invoked', 'lolacore/execute-action', $input, 'rest' );
		$executor = new ActionExecutor();
		$action_id = $input['action_id'];
		$params    = $input['params'] ?? array();
		$confirm   = $input['confirm'] ?? false;
		
		// In REST, dryRun is TRUE if confirm is FALSE
		$dryRun = ! $confirm;
		
		$result = $executor->execute( $action_id, $params, $dryRun, 'rest' );
		return apply_filters( 'lolacore_ability_output', $result, 'lolacore/execute-action' );
	}

    /**
     * Returns all registered LLM tool schemas.
     */
    public function getRegisteredAbilities(): array {
        $schemaGen = new ToolSchemaGenerator($this);
        $tools = $schemaGen->getAllTools('anthropic');
        $result = [];
        foreach ($tools as $tool) {
            // Map 'input_schema' back to 'parameters' for WPAIClientAdapter
            $result[$tool['name']] = [
                'description' => $tool['description'],
                'category' => $tool['category'] ?? 'uncategorized',
                'keywords' => $tool['keywords'] ?? [],
                'parameters' => $tool['input_schema']
            ];
        }

        /**
         * Allow addon plugins to inject their abilities into the registry.
         *
         * This ensures that the Settings UI, the "View all abilities" list,
         * and the chat adapter all consume the same unified ability set.
         *
         * @since 1.1.1
         *
         * @param array $result Map of ability_name => schema.
         */
        $result = \apply_filters('lolacore_available_abilities', $result);

        return $result;
    }
}
