<?php

/**
 * RegisterAbilityAsMcpPrompt class for converting WordPress abilities to MCP prompts.
 *
 * @package McpAdapter
 */
declare (strict_types=1);
namespace PYS_PRO_GLOBAL\WP\MCP\Domain\Prompts;

use PYS_PRO_GLOBAL\WP\MCP\Domain\Utils\McpNameSanitizer;
use PYS_PRO_GLOBAL\WP\MCP\Domain\Utils\McpValidator;
use PYS_PRO_GLOBAL\WP\MCP\Domain\Utils\SchemaTransformer;
use PYS_PRO_GLOBAL\WP\McpSchema\Server\Prompts\DTO\Prompt as PromptDto;
use PYS_PRO_GLOBAL\WP\McpSchema\Server\Prompts\DTO\PromptArgument;
use WP_Error;
/**
 * Converts WordPress abilities to MCP prompts according to the specification.
 *
 * This class extracts prompt data from ability properties and converts the JSON Schema
 * input_schema to MCP prompt arguments format.
 *
 * Schema Handling:
 * - Object schemas with properties: Each property becomes a PromptArgument
 * - Flattened schemas (type: string, number, etc.): Wrapped as single argument named "input"
 * - Empty/null schemas: No arguments
 * - Complex schemas (oneOf/anyOf): Treated as no arguments (documented limitation)
 *
 * Example ability registration:
 * wp_register_ability(
 *     'prompts/code-review',
 *     array(
 *         'label' => 'Code Review Prompt',
 *         'description' => 'Generate code review prompt',
 *         'input_schema' => array(
 *             'type' => 'object',
 *             'properties' => array(
 *                 'code' => array('type' => 'string', 'description' => 'Code to review'),
 *             ),
 *             'required' => array('code'),
 *         ),
 *         'meta' => array(
 *             'mcp' => array('public' => true, 'type' => 'prompt'),
 *             'annotations' => array(...)
 *         )
 *     )
 * );
 *
 * @since 0.5.0
 */
class RegisterAbilityAsMcpPrompt
{
    /**
     * The WordPress ability instance.
     *
     * @var \WP_Ability
     */
    private \WP_Ability $ability;
    /**
     * Tracks whether input_schema was transformed from flattened to object format.
     *
     * @since 0.5.0
     *
     * @var bool
     */
    private bool $schema_was_transformed = \false;
    /**
     * The wrapper property name used when transforming flattened schemas.
     *
     * @since 0.5.0
     *
     * @var string|null
     */
    private ?string $schema_wrapper_property = null;
    /**
     * Tracks the source of prompt arguments.
     *
     * Possible values:
     * - 'explicit': Arguments came from ability.meta.mcp.arguments
     * - 'schema': Arguments were auto-converted from ability.input_schema
     * - null: No arguments present
     *
     * @since 0.5.0
     *
     * @var string|null
     */
    private ?string $arguments_source = null;
    /**
     * Constructor.
     *
     * @param \WP_Ability $ability The ability.
     */
    private function __construct(\WP_Ability $ability)
    {
        $this->ability = $ability;
    }
    /**
     * Make a new instance of the class.
     *
     * @param \WP_Ability $ability The ability.
     *
     * @return \WP\McpSchema\Server\Prompts\DTO\Prompt|\WP_Error Returns Prompt DTO or WP_Error if validation fails.
     */
    public static function make(\WP_Ability $ability)
    {
        $prompt = new self($ability);
        return $prompt->get_prompt();
    }
    /**
     * Get the MCP prompt instance.
     *
     * @return \WP\McpSchema\Server\Prompts\DTO\Prompt|\WP_Error Prompt DTO or WP_Error if validation fails.
     * @since 0.5.0
     *
     */
    private function get_prompt()
    {
        $built = $this->build_prompt_data();
        // Propagate WP_Error from argument validation.
        if (is_wp_error($built)) {
            return $built;
        }
        try {
            return PromptDto::fromArray($built['prompt_data']);
        } catch (\Throwable $e) {
            return new WP_Error('mcp_prompt_schema_invalid', $e->getMessage());
        }
    }
    /**
     * Build Prompt DTO data and adapter metadata.
     *
     * @return array{prompt_data: array<string, mixed>, adapter_meta: array<string, mixed>}|\WP_Error
     * @since 0.5.0
     *
     */
    private function build_prompt_data()
    {
        $data = $this->get_data();
        // Propagate WP_Error from argument validation.
        if (is_wp_error($data)) {
            return $data;
        }
        // Get ability meta for icons and user _meta extraction.
        $ability_meta = $this->ability->get_meta();
        $mcp_meta = $ability_meta['mcp'] ?? array();
        // Map icons from ability.meta.mcp.icons if present.
        // Uses same pattern as tools/resources for consistency.
        if (!empty($mcp_meta['icons']) && is_array($mcp_meta['icons'])) {
            $icons_result = McpValidator::validate_icons_array($mcp_meta['icons']);
            if (!empty($icons_result['valid'])) {
                $data['icons'] = $icons_result['valid'];
            }
        }
        // Build adapter metadata, tracking transformation when it occurred.
        $adapter_meta = array('ability' => $this->ability->get_name());
        // Track arguments source when arguments are present.
        if (null !== $this->arguments_source) {
            $adapter_meta['arguments_source'] = $this->arguments_source;
        }
        // Record transformation metadata when schema was wrapped (matches tool behavior).
        // Only relevant when arguments_source is 'schema'.
        if ($this->schema_was_transformed && 'schema' === $this->arguments_source) {
            $adapter_meta['input_schema_transformed'] = \true;
            $adapter_meta['input_schema_wrapper'] = $this->schema_wrapper_property;
        }
        // Preserve user-provided _meta from ability.meta.mcp._meta.
        $prompt_meta = array();
        if (!empty($mcp_meta['_meta']) && is_array($mcp_meta['_meta'])) {
            $prompt_meta = $mcp_meta['_meta'];
        }
        if (!empty($prompt_meta)) {
            $data['_meta'] = $prompt_meta;
        }
        return array('prompt_data' => $data, 'adapter_meta' => $adapter_meta);
    }
    /**
     * Get the MCP prompt data array.
     *
     * Per MCP 2025-11-25 specification, Prompt objects do NOT support annotations at the
     * template level. Annotations are only supported on content blocks inside prompt messages
     * (messages[].content.annotations).
     *
     * Arguments Resolution:
     * 1. If `ability.meta.mcp.arguments` is defined and non-empty, use it directly (explicit override)
     * 2. Otherwise, auto-convert from `ability.input_schema`
     *
     * This follows the `mcp.*` override pattern used elsewhere (mcp.uri, mcp.icons, mcp.annotations).
     *
     * @return array<string,mixed>|\WP_Error Prompt data array, or WP_Error if explicit arguments are invalid.
     * @since 0.5.0
     *
     */
    private function get_data()
    {
        $prompt_name = $this->resolve_prompt_name();
        if (is_wp_error($prompt_name)) {
            return $prompt_name;
        }
        $prompt_data = array('name' => $prompt_name);
        // Add optional title from ability label.
        $label = trim($this->ability->get_label());
        if (!empty($label)) {
            $prompt_data['title'] = $label;
        }
        // Add optional description.
        $description = trim($this->ability->get_description());
        if (!empty($description)) {
            $prompt_data['description'] = $description;
        }
        // Check for explicit mcp.arguments override first.
        $explicit_arguments = $this->get_explicit_arguments();
        if (is_array($explicit_arguments) && !empty($explicit_arguments)) {
            $arguments = $this->convert_explicit_arguments($explicit_arguments);
            if (is_wp_error($arguments)) {
                return $arguments;
            }
            if (!empty($arguments)) {
                $prompt_data['arguments'] = $arguments;
                $this->arguments_source = 'explicit';
            }
            return $prompt_data;
        }
        // Fall back to auto-converting from input_schema.
        $input_schema = $this->ability->get_input_schema();
        if (!empty($input_schema)) {
            // Use SchemaTransformer to handle flattened schemas (consistent with tool behavior).
            $transform = SchemaTransformer::transform_to_object_schema($input_schema);
            // Track transformation state for _meta.
            $this->schema_was_transformed = $transform['was_transformed'];
            $this->schema_wrapper_property = $transform['wrapper_property'];
            $arguments = $this->convert_input_schema_to_arguments($transform['schema']);
            if (!empty($arguments)) {
                $prompt_data['arguments'] = $arguments;
                $this->arguments_source = 'schema';
            }
        }
        return $prompt_data;
    }
    /**
     * Get explicit arguments from ability meta.mcp.arguments.
     *
     * @return list<array<string,mixed>>|null Explicit arguments array or null if not defined.
     * @since 0.5.0
     *
     */
    private function get_explicit_arguments(): ?array
    {
        $meta = $this->ability->get_meta();
        if (!isset($meta['mcp']) || !is_array($meta['mcp'])) {
            return null;
        }
        $mcp = $meta['mcp'];
        if (!isset($mcp['arguments']) || !is_array($mcp['arguments'])) {
            return null;
        }
        return array_values($mcp['arguments']);
    }
    /**
     * Convert and validate explicit arguments from ability.meta.mcp.arguments.
     *
     * Per MCP 2025-11-25 specification, PromptArgument has:
     * - name (string, required): Argument identifier
     * - title (string, optional): Human-readable display name
     * - description (string, optional): Human-readable description
     * - required (boolean, optional): Whether the argument must be provided
     *
     * @param list<array<string,mixed>> $explicit_arguments User-defined arguments array.
     *
     * @return list<\WP\McpSchema\Server\Prompts\DTO\PromptArgument>|\WP_Error PromptArgument DTOs or WP_Error.
     * @since 0.5.0
     *
     */
    private function convert_explicit_arguments(array $explicit_arguments)
    {
        $arguments = array();
        foreach ($explicit_arguments as $index => $arg) {
            if (!is_array($arg)) {
                return new WP_Error('mcp_prompt_invalid_argument', sprintf(
                    /* translators: 1: argument index, 2: ability name */
                    __('Argument at index %1$d must be an array for ability "%2$s".', 'mcp-adapter'),
                    $index,
                    $this->ability->get_name()
                ));
            }
            // Validate required 'name' field.
            if (!isset($arg['name']) || !is_string($arg['name']) || '' === trim($arg['name'])) {
                return new WP_Error('mcp_prompt_argument_missing_name', sprintf(
                    /* translators: 1: argument index, 2: ability name */
                    __('Argument at index %1$d is missing required "name" field for ability "%2$s".', 'mcp-adapter'),
                    $index,
                    $this->ability->get_name()
                ));
            }
            $argument_data = array('name' => trim($arg['name']));
            // Map optional 'title' field.
            if (isset($arg['title']) && is_string($arg['title']) && '' !== trim($arg['title'])) {
                $argument_data['title'] = trim($arg['title']);
            }
            // Map optional 'description' field.
            if (isset($arg['description']) && is_string($arg['description']) && '' !== trim($arg['description'])) {
                $argument_data['description'] = trim($arg['description']);
            }
            // Map optional 'required' field (only emit when true, per existing pattern).
            if (isset($arg['required']) && \true === $arg['required']) {
                $argument_data['required'] = \true;
            }
            $arguments[] = PromptArgument::fromArray($argument_data);
        }
        return $arguments;
    }
    /**
     * Convert JSON Schema input_schema to MCP prompt arguments format.
     *
     * Converts from WordPress Abilities JSON Schema format:
     * {
     *   "type": "object",
     *   "properties": {
     *     "topic": {"type": "string", "title": "Topic", "description": "..."},
     *     "tone": {"type": "string", "description": "..."}
     *   },
     *   "required": ["topic"]
     * }
     *
     * To MCP prompt arguments format:
     * [
     *   {"name": "topic", "title": "Topic", "description": "...", "required": true},
     *   {"name": "tone", "description": "..."}
     * ]
     *
     * Note: `required` is only emitted when true; optional arguments omit the field entirely.
     *
     * @param array<string,mixed> $input_schema The JSON Schema from ability.
     *
     * @return list<\WP\McpSchema\Server\Prompts\DTO\PromptArgument> Argument DTO list.
     * @since 0.5.0
     *
     */
    private function convert_input_schema_to_arguments(array $input_schema): array
    {
        $arguments = array();
        // Ensure we have properties to convert.
        if (empty($input_schema['properties']) || !is_array($input_schema['properties'])) {
            return $arguments;
        }
        // Get the list of required properties.
        $required_fields = array();
        if (isset($input_schema['required']) && is_array($input_schema['required'])) {
            $required_fields = $input_schema['required'];
        }
        // Convert each property to an MCP argument.
        foreach ($input_schema['properties'] as $property_name => $property_schema) {
            if (!is_array($property_schema)) {
                continue;
            }
            $is_required = in_array($property_name, $required_fields, \true);
            $argument_data = array('name' => $property_name);
            // Map JSON Schema title to PromptArgument.title when present.
            if (!empty($property_schema['title']) && is_string($property_schema['title'])) {
                $argument_data['title'] = $property_schema['title'];
            }
            // Map JSON Schema description to PromptArgument.description when present.
            if (!empty($property_schema['description']) && is_string($property_schema['description'])) {
                $argument_data['description'] = $property_schema['description'];
            }
            // Only emit required when true; omit for optional arguments.
            if ($is_required) {
                $argument_data['required'] = \true;
            }
            $arguments[] = PromptArgument::fromArray($argument_data);
        }
        return $arguments;
    }
    /**
     * Resolve the MCP prompt name from ability.
     *
     * Sanitizes the ability name to MCP-valid format, applies filter, and validates result.
     *
     * @since 0.5.0
     *
     * @return string|\WP_Error Valid prompt name or error.
     */
    private function resolve_prompt_name()
    {
        // Sanitize ability name to MCP-valid format.
        $sanitized_name = McpNameSanitizer::sanitize_name($this->ability->get_name());
        if (is_wp_error($sanitized_name)) {
            return $sanitized_name;
        }
        /**
         * Filters the MCP prompt name derived from an ability.
         *
         * @since 0.5.0
         *
         * @param string      $name    The sanitized prompt name.
         * @param \WP_Ability $ability The source ability instance.
         */
        $filtered_name = apply_filters('mcp_adapter_prompt_name', $sanitized_name, $this->ability);
        // Validate post-filter (in case filter broke it).
        if (!is_string($filtered_name) || !McpValidator::validate_name($filtered_name)) {
            return new WP_Error('mcp_prompt_name_filter_invalid', sprintf(
                /* translators: %s: invalid prompt name returned by filter */
                __('Filter returned invalid MCP prompt name: %s', 'mcp-adapter'),
                is_string($filtered_name) ? $filtered_name : gettype($filtered_name)
            ));
        }
        return $filtered_name;
    }
    /**
     * Build a clean Prompt DTO and adapter metadata for internal wiring.
     *
     * This method returns a protocol-only Prompt DTO and provides the adapter metadata
     * separately. This keeps the DTO stable across MCP spec changes and avoids coupling internal execution
     * wiring to protocol surfaces.
     *
     * @param \WP_Ability $ability The ability.
     *
     * @return array{prompt: \WP\McpSchema\Server\Prompts\DTO\Prompt, adapter_meta: array<string, mixed>}|\WP_Error
     * @since 0.5.0
     *
     */
    public static function build(\WP_Ability $ability)
    {
        $prompt = new self($ability);
        $data = $prompt->build_prompt_data();
        if (is_wp_error($data)) {
            return $data;
        }
        try {
            $prompt_dto = PromptDto::fromArray($data['prompt_data']);
        } catch (\Throwable $e) {
            return new WP_Error('mcp_prompt_dto_creation_failed', sprintf(
                /* translators: %s: error message */
                __('Failed to create Prompt DTO for ability %1$s: %2$s', 'mcp-adapter'),
                $ability->get_name(),
                $e->getMessage()
            ), array('exception' => $e));
        }
        // Optional deep validation if enabled.
        $mcp_validation_enabled = apply_filters('mcp_adapter_validation_enabled', \false);
        if ($mcp_validation_enabled) {
            $validation_result = McpPromptValidator::validate_prompt_dto($prompt_dto);
            if (is_wp_error($validation_result)) {
                return $validation_result;
            }
        }
        return array('prompt' => $prompt_dto, 'adapter_meta' => $data['adapter_meta']);
    }
}
