<?php
/**
 * Abstract Ability Class
 *
 * Base class for all Astra abilities.
 *
 * @package Astra
 * @subpackage Abilities
 * @since 4.12.6
 */

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

/**
 * Abstract Class Astra_Abstract_Ability
 */
abstract class Astra_Abstract_Ability {
	/**
	 * Ability ID (e.g. 'astra/get-font-body').
	 *
	 * @var string
	 */
	protected $id = '';

	/**
	 * Ability Category.
	 *
	 * @var string
	 */
	protected $category = 'astra';

	/**
	 * Ability Label.
	 *
	 * @var string
	 */
	protected $label = '';

	/**
	 * Ability Description.
	 *
	 * @var string
	 */
	protected $description = '';

	/**
	 * Required capability for this ability.
	 *
	 * @var string
	 */
	protected $capability = 'edit_theme_options';

	/**
	 * Ability Meta Data.
	 *
	 * @var array
	 */
	protected $meta = array();

	/**
	 * Tool version.
	 *
	 * @var string
	 */
	protected $version = '1.0.0';

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->configure();
	}

	/**
	 * Configure the ability (set ID, label, description, etc.).
	 *
	 * @return void
	 */
	abstract public function configure();

	/**
	 * Get the input schema for the ability.
	 *
	 * @return array
	 */
	abstract public function get_input_schema();

	/**
	 * Execute the ability.
	 *
	 * @param array $args Input arguments.
	 * @return array Result array.
	 */
	abstract public function execute( $args );

	/**
	 * Get the final input schema.
	 *
	 * @return array
	 */
	public function get_final_input_schema() {
		$schema = $this->get_input_schema();

		if ( ! isset( $schema['properties'] ) ) {
			$schema['properties'] = array();
		}

		return $schema;
	}

	/**
	 * Handle execution with error handling.
	 *
	 * @param array $args Input arguments.
	 * @return array Result array.
	 */
	public function handle_execute( $args ) {
		try {
			return $this->execute( $args );
		} catch ( Exception $e ) {
			/* translators: %s: error message */
			return Astra_Abilities_Response::error( sprintf( __( 'An unexpected error occurred: %s', 'astra' ), $e->getMessage() ) );
		} catch ( Error $e ) {
			/* translators: %s: error message */
			return Astra_Abilities_Response::error( sprintf( __( 'A system error occurred: %s', 'astra' ), $e->getMessage() ) );
		}
	}

	/**
	 * Get the output schema for the ability.
	 *
	 * Override in child classes to define the data properties
	 * returned in the success response.
	 *
	 * @return array
	 */
	public function get_output_schema() {
		return array();
	}

	/**
	 * Build a standardized output schema wrapping data properties
	 * in the Astra_Abilities_Response::success() format.
	 *
	 * @param array $data_properties Properties for the 'data' key.
	 * @return array Full output schema.
	 */
	protected function build_output_schema( $data_properties ) {
		return array(
			'type'       => 'object',
			'required'   => array( 'success', 'message' ),
			'properties' => array(
				'success' => array(
					'type'        => 'boolean',
					'description' => 'Whether the operation succeeded.',
				),
				'message' => array(
					'type'        => 'string',
					'description' => 'Human-readable result message.',
				),
				'data'    => array(
					'type'       => 'object',
					'properties' => $data_properties,
				),
			),
		);
	}

	/**
	 * Get usage examples.
	 *
	 * @return array
	 */
	public function get_examples() {
		return array();
	}

	/**
	 * Get tool type (read, write, list).
	 *
	 * @return string
	 */
	public function get_tool_type() {
		return 'read';
	}

	/**
	 * Get whether to show this ability in the REST API.
	 *
	 * @return bool
	 */
	public function get_show_in_rest() {
		/**
		 * Filter whether to show this ability in the REST API.
		 *
		 * @param bool   $show_in_rest     Whether to show in REST API. Default true.
		 * @param string $ability_id       The ability ID (e.g. 'astra/get-font-body').
		 * @param self   $ability_instance The ability instance.
		 * @since 4.12.6
		 */
		/** @psalm-suppress TooManyArguments -- WordPress apply_filters accepts variadic args for filter callbacks. */
		return apply_filters( 'astra_ability_show_in_rest', true, $this->id, $this );
	}

	/**
	 * Get MCP annotations based on tool type.
	 *
	 * Returns semantic hints for the MCP Adapter describing the ability's
	 * behavioral characteristics. Override in child classes for custom behavior.
	 *
	 * @return array{readonly: bool, destructive: bool, idempotent: bool}
	 */
	public function get_annotations() {
		$tool_type = $this->get_tool_type();

		// Read and list tools are readonly, write tools are not.
		return array(
			'readonly'    => 'write' !== $tool_type,
			'destructive' => false,
			'idempotent'  => true,
		);
	}

	/**
	 * Get MCP meta configuration for this ability.
	 *
	 * Returns the MCP Adapter metadata including public visibility
	 * and MCP type. The public flag is filterable via 'astra_ability_mcp_public'.
	 *
	 * @return array{public: bool, type: string}
	 */
	public function get_mcp() {
		/**
		 * Filter whether an Astra ability is publicly exposed via MCP.
		 *
		 * @since 4.12.6
		 *
		 * @param bool   $is_public        Whether the ability is public for MCP. Default true.
		 * @param string $ability_id       The ability ID (e.g. 'astra/get-font-body').
		 * @param self   $ability_instance The ability instance.
		 */
		/** @psalm-suppress TooManyArguments -- WordPress apply_filters accepts variadic args for filter callbacks. */
		$is_public = apply_filters( 'astra_ability_mcp_public', true, $this->id, $this );

		return array(
			'public' => (bool) $is_public,
			'type'   => 'tool',
		);
	}

	/**
	 * Check permissions.
	 *
	 * @param WP_REST_Request $request REST Request.
	 * @return bool|WP_Error
	 */
	public function check_permission( $request ) {
		return current_user_can( $this->capability );
	}

	/**
	 * Get the ability ID.
	 *
	 * @return string
	 */
	public function get_id() {
		return $this->id;
	}

	/**
	 * Get the ability label.
	 *
	 * @return string
	 */
	public function get_label() {
		return $this->label;
	}

	/**
	 * Get the ability description.
	 *
	 * @return string
	 */
	public function get_description() {
		return $this->description;
	}

	/**
	 * Get the category.
	 *
	 * @return string
	 */
	public function get_category() {
		return $this->category;
	}

	/**
	 * Get the meta data.
	 *
	 * @return array
	 */
	public function get_meta_data() {
		return $this->meta;
	}

	/**
	 * Get the tool version.
	 *
	 * @return string
	 */
	public function get_version() {
		return $this->version;
	}

	/**
	 * Register this ability with the Abilities API.
	 *
	 * @return void
	 */
	public static function register() {
		if ( ! function_exists( 'wp_register_ability' ) ) {
			return;
		}

		/** @psalm-suppress UnsafeInstantiation -- Intentional: child classes share the same constructor signature. */
		$instance = new static();

		if ( empty( $instance->id ) ) {
			return;
		}

		$meta = array(
			'tool_type'    => $instance->get_tool_type(),
			'examples'     => $instance->get_examples(),
			'version'      => $instance->get_version(),
			'show_in_rest' => $instance->get_show_in_rest(),
			'annotations'  => $instance->get_annotations(),
			'mcp'          => $instance->get_mcp(),
		);

		$meta = array_replace_recursive( $meta, $instance->meta );

		$args = array(
			'label'               => $instance->label,
			'description'         => $instance->description,
			'category'            => $instance->category,
			'input_schema'        => $instance->get_final_input_schema(),
			'execute_callback'    => array( $instance, 'handle_execute' ),
			'permission_callback' => array( $instance, 'check_permission' ),
			'meta'                => $meta,
		);

		$output_schema = $instance->get_output_schema();
		if ( ! empty( $output_schema ) ) {
			$args['output_schema'] = $output_schema;
		}

		wp_register_ability( $instance->id, $args );
	}
}
