<?php

/**
 * RegisterAbilityAsMcpResource class for converting WordPress abilities to MCP resources.
 *
 * @package McpAdapter
 */

declare( strict_types=1 );

namespace Bluehost\Plugin\WP\MCP\Domain\Resources;

use Bluehost\Plugin\WP\MCP\Domain\Utils\McpAnnotationMapper;
use Bluehost\Plugin\WP\MCP\Domain\Utils\McpValidator;
use Bluehost\Plugin\WP\MCP\Infrastructure\ErrorHandling\Contracts\McpErrorHandlerInterface;
use Bluehost\Plugin\WP\McpSchema\Server\Resources\DTO\Resource as ResourceDto;
use WP_Error;

/**
 * Converts WordPress abilities to MCP Resource metadata.
 *
 * This class builds Resource DTOs for resources/list responses.
 * It extracts metadata only (uri, name, title, description, mimeType, size, icons, annotations).
 * Resource content (text/blob) is resolved separately at resources/read time.
 *
 * All MCP-specific ability meta should be under 'mcp' key:
 *
 * Required ability meta:
 * - 'mcp.uri' (string): The resource URI (RFC 3986 format)
 *
 * Optional ability meta:
 * - 'mcp.mimeType' (string): MIME type of the resource content
 * - 'mcp.size' (int): Size of resource content in bytes
 * - 'mcp.annotations' (array): MCP annotations (audience, priority, lastModified)
 * - 'mcp.icons' (array): Array of icon objects for UI display
 * - 'mcp._meta' (array): User-provided metadata to pass through
 *
 * Note: Top-level meta keys 'uri', 'mimeType', 'annotations' are deprecated as of 0.5.0.
 * They still work for backward compatibility but will trigger a `_doing_it_wrong` notice.
 * Use 'mcp.uri', 'mcp.mimeType', 'mcp.annotations' instead.
 *
 * @since 0.5.0
 */
class RegisterAbilityAsMcpResource {

	/**
	 * The WordPress ability instance.
	 *
	 * @var \WP_Ability
	 */
	private \WP_Ability $ability;

	/**
	 * Optional error handler for logging deprecation notices.
	 *
	 * @var \Bluehost\Plugin\WP\MCP\Infrastructure\ErrorHandling\Contracts\McpErrorHandlerInterface|null
	 */
	private ?McpErrorHandlerInterface $error_handler;

	/**
	 * Constructor.
	 *
	 * @param \WP_Ability $ability The ability.
	 * @param \Bluehost\Plugin\WP\MCP\Infrastructure\ErrorHandling\Contracts\McpErrorHandlerInterface|null $error_handler Optional error handler.
	 */
	private function __construct( \WP_Ability $ability, ?McpErrorHandlerInterface $error_handler = null ) {
		$this->ability       = $ability;
		$this->error_handler = $error_handler;
	}

	/**
	 * Make a new instance of the class.
	 *
	 * @param \WP_Ability $ability The ability.
	 * @param \Bluehost\Plugin\WP\MCP\Infrastructure\ErrorHandling\Contracts\McpErrorHandlerInterface|null $error_handler Optional error handler for logging.
	 *
	 * @return \Bluehost\Plugin\WP\McpSchema\Server\Resources\DTO\Resource|\WP_Error Returns Resource DTO or WP_Error if validation fails.
	 */
	public static function make( \WP_Ability $ability, ?McpErrorHandlerInterface $error_handler = null ) {
		$resource = new self( $ability, $error_handler );

		return $resource->get_resource();
	}

	/**
	 * Get the MCP resource instance.
	 *
	 * Resource schema validity is enforced by the php-mcp-schema DTO constructor.
	 *
	 * @return \Bluehost\Plugin\WP\McpSchema\Server\Resources\DTO\Resource|\WP_Error Returns the Resource DTO or WP_Error if validation fails.
	 */
	private function get_resource() {
		$data = $this->get_data();
		if ( is_wp_error( $data ) ) {
			return $data;
		}

		try {
			return ResourceDto::fromArray( $data );
		} catch ( \Throwable $e ) {
			return new WP_Error(
				'mcp_resource_schema_invalid',
				$e->getMessage()
			);
		}
	}

	/**
	 * Get the MCP resource data array.
	 *
	 * Builds metadata-only Resource data. Content (text/blob) is NOT included here;
	 * content is resolved at resources/read time by ResourcesHandler.
	 *
	 * @return array<string,mixed>|\WP_Error Resource data array or WP_Error if validation fails.
	 */
	private function get_data() {
		$built = $this->build_resource_data();
		if ( is_wp_error( $built ) ) {
			return $built;
		}

		return $built['resource_data'];
	}

	/**
	 * Build Resource DTO data and adapter metadata.
	 *
	 * @return array{resource_data: array<string, mixed>, adapter_meta: array<string, mixed>}|\WP_Error
	 * @since 0.5.0
	 *
	 */
	private function build_resource_data() {
		$uri = $this->get_uri();
		if ( is_wp_error( $uri ) ) {
			return $uri;
		}

		$ability_meta = $this->ability->get_meta();
		$mcp_meta     = $ability_meta['mcp'] ?? array();

		// Required fields.
		$resource_data = array(
			'name' => $this->resolve_resource_name(),
			'uri'  => $uri,
		);

		// Optional: title from ability label (human-readable display name).
		$label = trim( $this->ability->get_label() );
		if ( '' !== $label ) {
			$resource_data['title'] = $label;
		}

		// Optional: description.
		$description = trim( $this->ability->get_description() );
		if ( '' !== $description ) {
			$resource_data['description'] = $description;
		}

		// Optional: mimeType from ability meta (with validation).
		$mime_type = $this->get_mcp_meta( 'mimeType', 'string' );
		if ( null !== $mime_type ) {
			$mime_type = trim( $mime_type );
			if ( McpValidator::validate_mime_type( $mime_type ) ) {
				$resource_data['mimeType'] = $mime_type;
			}
		}

		// Optional: size from ability meta (bytes count for UI display).
		$size = $this->get_mcp_meta( 'size', 'int' );
		if ( null !== $size && $size > 0 ) {
			$resource_data['size'] = $size;
		}

		// Optional: annotations from ability meta (standardized location: mcp.annotations).
		$annotations = $this->get_mcp_meta( 'annotations', 'array' );
		if ( null !== $annotations ) {
			$mcp_annotations = McpAnnotationMapper::map( $annotations, 'resource' );
			if ( ! empty( $mcp_annotations ) ) {
				// Validate annotation values per MCP specification.
				$validation_errors = McpValidator::get_annotation_validation_errors( $mcp_annotations );
				if ( ! empty( $validation_errors ) ) {
					// Log the issue but don't fail registration - drop invalid annotations.
					$this->log_deprecation(
						self::class . '::get_data',
						sprintf(
						/* translators: 1: ability name, 2: validation errors */
							__( 'Invalid annotations for resource ability "%1$s" will be dropped: %2$s', 'mcp-adapter' ),
							$this->ability->get_name(),
							implode( '; ', $validation_errors )
						),
						array( 'validation_errors' => $validation_errors )
					);
				} else {
					$resource_data['annotations'] = $mcp_annotations;
				}
			}
		}

		// Optional: icons from mcp.icons (already in correct location).
		if ( ! empty( $mcp_meta['icons'] ) && is_array( $mcp_meta['icons'] ) ) {
			$icons_result = McpValidator::validate_icons_array( $mcp_meta['icons'] );
			if ( ! empty( $icons_result['valid'] ) ) {
				$resource_data['icons'] = $icons_result['valid'];
			}
		}

		// Build Resource `_meta`:
		// - Preserve user-provided `_meta` from ability.meta.mcp._meta.
		// - Adapter metadata is NEVER included in protocol DTO meta; it is returned separately in adapter_meta.
		$resource_meta = array();
		if ( ! empty( $mcp_meta['_meta'] ) && is_array( $mcp_meta['_meta'] ) ) {
			$resource_meta = $mcp_meta['_meta'];
		}
		if ( ! empty( $resource_meta ) ) {
			$resource_data['_meta'] = $resource_meta;
		}

		$adapter_meta = array(
			'ability' => $this->ability->get_name(),
		);

		return array(
			'resource_data' => $resource_data,
			'adapter_meta'  => $adapter_meta,
		);
	}

	/**
	 * Get the resource URI with validation.
	 *
	 * @return string|\WP_Error URI string or WP_Error if not found or invalid.
	 */
	private function get_uri() {
		$uri = $this->get_mcp_meta( 'uri', 'string' );

		if ( null === $uri ) {
			return new WP_Error(
				'resource_uri_not_found',
				sprintf(
				/* translators: %s: ability name */
					__( "Resource URI not found in ability meta for '%s'. URI must be provided at 'mcp.uri'.", 'mcp-adapter' ),
					$this->ability->get_name()
				)
			);
		}

		$uri = trim( $uri );

		// Validate URI format (RFC 3986).
		if ( ! McpValidator::validate_resource_uri( $uri ) ) {
			return new WP_Error(
				'resource_uri_invalid',
				sprintf(
				/* translators: 1: ability name, 2: invalid URI */
					__( "Invalid resource URI '%2\$s' for ability '%1\$s'. URI must be RFC 3986 compliant with a scheme.", 'mcp-adapter' ),
					$this->ability->get_name(),
					$uri
				)
			);
		}

		/**
		 * Filters the MCP resource URI derived from an ability.
		 *
		 * @since 0.5.0
		 *
		 * @param string $uri The validated resource URI.
		 * @param \WP_Ability $ability The source ability instance.
		 */
		$filtered_uri = apply_filters( 'mcp_adapter_resource_uri', $uri, $this->ability );

		// Validate post-filter.
		if ( ! is_string( $filtered_uri ) || ! McpValidator::validate_resource_uri( $filtered_uri ) ) {
			return new WP_Error(
				'mcp_resource_uri_filter_invalid',
				sprintf(
				/* translators: %s: invalid URI returned by filter */
					__( 'Filter returned invalid MCP resource URI: %s', 'mcp-adapter' ),
					is_string( $filtered_uri ) ? $filtered_uri : gettype( $filtered_uri )
				)
			);
		}

		return $filtered_uri;
	}

	/**
	 * Get a value from ability meta with standardized lookup.
	 *
	 * Looks in 'mcp' namespace first (preferred), then falls back to top-level (deprecated).
	 * Logs deprecation notice when using top-level location.
	 *
	 * @param string $key The key to look up.
	 * @param string $type Expected type: 'string', 'int', 'array'.
	 * @param mixed $default_value Default value if not found.
	 *
	 * @return mixed The value or default.
	 */
	private function get_mcp_meta( string $key, string $type = 'string', $default_value = null ) {
		$ability_meta = $this->ability->get_meta();
		$mcp_meta     = $ability_meta['mcp'] ?? array();

		// Preferred: Check mcp.{key} first.
		if ( isset( $mcp_meta[ $key ] ) ) {
			$value = $mcp_meta[ $key ];
			if ( $this->validate_type( $value, $type ) ) {
				return $value;
			}
		}

		// Deprecated fallback: Check top-level meta.{key}.
		if ( isset( $ability_meta[ $key ] ) ) {
			$value = $ability_meta[ $key ];
			if ( $this->validate_type( $value, $type ) ) {
				// Log deprecation notice.
				$this->log_deprecation(
					__METHOD__,
					sprintf(
					/* translators: 1: deprecated meta key, 2: new meta key path */
						__( 'Ability meta key "%1$s" is deprecated. Use "mcp.%1$s" instead.', 'mcp-adapter' ),
						$key
					),
					array( 'deprecated_key' => $key )
				);

				return $value;
			}
		}

		return $default_value;
	}

	/**
	 * Validate a value against expected type.
	 *
	 * @param mixed $value The value to validate.
	 * @param string $type Expected type.
	 *
	 * @return bool True if valid.
	 */
	private function validate_type( $value, string $type ): bool {
		switch ( $type ) {
			case 'string':
				return is_string( $value ) && '' !== trim( $value );
			case 'int':
				return is_int( $value ) && $value >= 0;
			case 'array':
				// Array must be non-empty AND have at least one non-null, non-empty value.
				// This prevents false positives when WordPress adds default empty annotations.
				if ( ! is_array( $value ) || empty( $value ) ) {
					return false;
				}
				// Check if any value in the array is actually meaningful (non-null, non-empty string).
				foreach ( $value as $item ) {
					if ( null !== $item && '' !== $item && array() !== $item ) {
						return true;
					}
				}

				return false;
			default:
				return false;
		}
	}

	/**
	 * Log a deprecation notice via both WordPress _doing_it_wrong and McpErrorHandler.
	 *
	 * This ensures deprecation notices are visible both as HTTP headers (WordPress REST API)
	 * and in debug.log (McpErrorHandler).
	 *
	 * @param string $method The method name where deprecation occurred.
	 * @param string $message The deprecation message.
	 * @param array $context Additional context for error handler.
	 *
	 * @return void
	 */
	private function log_deprecation( string $method, string $message, array $context = array() ): void {
		// WordPress standard deprecation notice (appears as X-WP-DoingItWrong header in REST API).
		_doing_it_wrong( esc_html( $method ), esc_html( $message ), '0.5.0' );

		// Also log via McpErrorHandler for debug.log visibility.
		if ( ! $this->error_handler ) {
			return;
		}

		$this->error_handler->log(
			$message,
			array_merge(
				array( 'ability' => $this->ability->get_name() ),
				$context
			),
			'warning'
		);
	}

	/**
	 * Resolve the MCP resource name from ability.
	 *
	 * Resource names have no charset restrictions (unlike Tool names).
	 *
	 * @return string The resolved resource name.
	 */
	private function resolve_resource_name(): string {
		$name = $this->ability->get_name();

		/**
		 * Filters the MCP resource name derived from an ability.
		 *
		 * Unlike tools, resource names have no charset restrictions.
		 *
		 * @since 0.5.0
		 *
		 * @param string $name The resource name.
		 * @param \WP_Ability $ability The source ability instance.
		 */
		$filtered_name = apply_filters( 'mcp_adapter_resource_name', $name, $this->ability );

		// Resource names have no charset restrictions, so just ensure it's a non-empty string.
		if ( is_string( $filtered_name ) && '' !== trim( $filtered_name ) ) {
			return $filtered_name;
		}

		// Fall back to original name if filter returns invalid value.
		return $name;
	}

	/**
	 * Build a clean Resource DTO and adapter metadata for internal wiring.
	 *
	 * This method returns a protocol-only Resource 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.
	 * @param \Bluehost\Plugin\WP\MCP\Infrastructure\ErrorHandling\Contracts\McpErrorHandlerInterface|null $error_handler Optional error handler.
	 *
	 * @return array{resource: \Bluehost\Plugin\WP\McpSchema\Server\Resources\DTO\Resource, adapter_meta: array<string, mixed>}|\WP_Error
	 * @since 0.5.0
	 *
	 */
	public static function build( \WP_Ability $ability, ?McpErrorHandlerInterface $error_handler = null ) {
		$resource = new self( $ability, $error_handler );
		$data     = $resource->build_resource_data();

		if ( is_wp_error( $data ) ) {
			return $data;
		}

		try {
			$resource_dto = ResourceDto::fromArray( $data['resource_data'] );
		} catch ( \Throwable $e ) {
			return new WP_Error(
				'mcp_resource_dto_creation_failed',
				sprintf(
				/* translators: %s: error message */
					__( 'Failed to create Resource 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 = McpResourceValidator::validate_resource_dto( $resource_dto );
			if ( is_wp_error( $validation_result ) ) {
				return $validation_result;
			}
		}

		return array(
			'resource'     => $resource_dto,
			'adapter_meta' => $data['adapter_meta'],
		);
	}
}
