<?php
/**
 * Ability Loader Trait
 *
 * @package zip-ai
 */

namespace ZipAI\MCP\Classes\Abilities;

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

use ZipAI\MCP\Classes\Abilities\Abstract_Ability;

/**
 * Trait Ability_Loader
 */
trait Ability_Loader {

	public function check_ability_format( $ability ) {
		$id = $ability->get_id();
		$canonical_actions = array(
			// CRUD.
			'list',
			'get',
			'create',
			'update',
			'delete',
			// State.
			'activate',
			'deactivate',
			'restore',
			// Lifecycle.
			'install',
			'uninstall',
			'upload',
			// Data.
			'import',
			'export',
			'flush',
			'replace',
			// Operations.
			'check',
			'clean',
			'run',
			'edit',
			'read',
			'search',
			'scan',
		);
		$pattern = '/^[a-z0-9-]+\/(' . implode( '|', $canonical_actions ) . ')-[a-z0-9-]+$/';

		if ( ! preg_match( $pattern, $id ) ) {
			// Check if we are in export mode or debug mode.
			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				trigger_error(
					sprintf(
						'Invalid Ability ID format: "%s" in class %s. Expected format: "{namespace}/{action}-{resource}" (e.g., "zipai/update-post_format", "zipai/list-block-patterns"). Action must be one of: %s.',
						$id,
						get_class( $ability ),
						implode( ', ', $canonical_actions )
					),
					E_USER_WARNING
				);
			}
			return false;
		}
		return true;
	}

	/**
	 * Abilities skipped at registration. Hidden from MCP discovery entirely
	 * (LLM cannot see them, internal callers cannot use them either). Use the
	 * `internal` visibility flag in an ability's get_meta() to keep it
	 * registered for backend/server-only callers while hiding from the LLM.
	 *
	 * ExecuteRestRequest stays registered (internal callers like
	 * PageDeliveryService, ParallelPageBuilderService still need it); it is
	 * hidden from the LLM via meta visibility on the Laravel side instead.
	 *
	 * Currently empty — ExecuteAjaxAction and SearchEndpoints were removed
	 * outright (unused by the LLM and every internal caller).
	 */
	protected static $disabled_abilities = array();

	/**
	 * Build a basename index for disabled abilities.
	 *
	 * Accepts either `ClassName` or `Vendor\Ns\ClassName` entries and
	 * normalizes both to `ClassName` for the file-basename match used by
	 * directory scanning.
	 *
	 * @return array<string, true>
	 */
	protected function get_disabled_abilities_index() {
		$index = array();

		foreach ( self::$disabled_abilities as $ability_class ) {
			if ( ! is_string( $ability_class ) || '' === $ability_class ) {
				continue;
			}

			$normalized_class_name = ltrim( $ability_class, '\\' );
			$last_separator        = strrpos( $normalized_class_name, '\\' );
			$basename              = false === $last_separator
				? $normalized_class_name
				: substr( $normalized_class_name, $last_separator + 1 );

			if ( '' !== $basename ) {
				$index[ $basename ] = true;
			}
		}

		return $index;
	}

	protected function load_abilities_from_dir( $directory, $namespace ) {
		// Normalize directory path.
		$directory = trailingslashit( $directory );
		$disabled_abilities_index = $this->get_disabled_abilities_index();

		// Check if directory exists.
		if ( ! is_dir( $directory ) ) {
			return;
		}

		// Use RecursiveDirectoryIterator for recursive scanning.
		$directory_iterator = new \RecursiveDirectoryIterator( $directory, \RecursiveDirectoryIterator::SKIP_DOTS );
		$iterator           = new \RecursiveIteratorIterator( $directory_iterator );

		foreach ( $iterator as $file_info ) {
			if ( $file_info->isDir() || 'php' !== $file_info->getExtension() ) {
				continue;
			}

			$file_path  = $file_info->getPathname();
			$class_name = $file_info->getBasename( '.php' );

			// Skip index and handler files.
			if ( 'index' === $class_name || 'handler' === $class_name ) {
				continue;
			}

			if ( isset( $disabled_abilities_index[ $class_name ] ) ) {
				continue;
			}

			// Calculate class namespace based on relative path.
			$relative_path = str_replace( array( $directory, '.php' ), '', $file_path );
			$path_parts    = explode( DIRECTORY_SEPARATOR, $relative_path );
			array_pop( $path_parts ); // Remove class name from parts.

			// Capitalize each path part to match PSR-4 namespace convention.
			$path_parts = array_map( 'ucfirst', $path_parts );

			$sub_namespace = ! empty( $path_parts ) ? '\\' . implode( '\\', $path_parts ) : '';
			$full_class_name = $namespace . $sub_namespace . '\\' . $class_name;

			// Check if class exists.
			if ( class_exists( $full_class_name ) ) {
				// Instantiate the ability.
				$ability = new $full_class_name();

				// Check if it's an instance of Abstract_Ability.
				if ( $ability instanceof Abstract_Ability ) {
					// Collect meta data.
					$meta = array(
						'tool_type'    => $ability->get_tool_type(),
						'examples'     => $ability->get_examples(),
						'api_endpoint' => $ability->get_api_endpoint(),
					);

					// Add boost screens if defined.
					if ( method_exists( $ability, 'get_boost_screens' ) ) {
						$boost_screens = $ability->get_boost_screens();
						if ( ! empty( $boost_screens ) ) {
							$meta['boost_screens'] = $boost_screens;
						}
					}

					// Add resource identifier if defined.
					if ( method_exists( $ability, 'get_resource' ) ) {
						$resource = $ability->get_resource();
						if ( ! empty( $resource ) ) {
							$meta['resource'] = $resource;
						}
					}

					// Read-only sub-action allowlist for multiplexed abilities.
					// Forwarded through meta → MCP tools/list → brain so the
					// writes-require-approval gate can treat safe sub-actions
					// (e.g. `action:"list"` on an otherwise-destructive tool)
					// as reads. Empty for single-purpose abilities.
					if ( method_exists( $ability, 'get_read_only_actions' ) ) {
						$read_only_actions = $ability->get_read_only_actions();
						if ( is_array( $read_only_actions ) && ! empty( $read_only_actions ) ) {
							$meta['read_only_actions'] = array_values( $read_only_actions );
						}
					}

					// Merge with any class-specific meta.
					if ( method_exists( $ability, 'get_meta_data' ) ) {
						$meta = array_merge( $meta, $ability->get_meta_data() );
					}

					// Add constraints if the method exists.
					if ( method_exists( $ability, 'get_constraints' ) ) {
						$meta['constraints'] = $ability->get_constraints();
					}

					// Validate ID format.
					$this->check_ability_format( $ability );

					// Register directly with wp_register_ability (no lazy loading).
					wp_register_ability(
						$ability->get_id(),
						array(
							'label'               => $ability->get_label(),
							'description'         => $ability->get_description(),
							'category'            => $ability->get_category(),
							'input_schema'        => $ability->get_final_input_schema(),
							'execute_callback'    => array( $ability, 'handle_execute' ),
							'permission_callback' => array( $ability, 'check_permission' ),
							'meta'                => $meta,
						)
					);
				}
			}
		}
	}
}
