<?php
declare(strict_types=1);

namespace Imagify\Abilities;

/**
 * MCP ability: optimize a media on-demand.
 *
 * Registers itself with the WP Abilities API under the slug
 * `imagify/optimize-media` and delegates to the existing
 * `Imagify\Optimization\Process\WP` class.
 *
 * @since 2.3.0
 */
class OptimizeMedia extends AbstractAbility implements CreditConsumingAbilityInterface {

	const ABILITY_ID   = 'imagify/optimize-media';
	const ABILITY_NAME = 'Optimize media';

	/**
	 * Returns the ability slug.
	 *
	 * @return string
	 */
	public function get_id(): string {
		return self::ABILITY_ID;
	}

	/**
	 * Returns the human-readable ability label.
	 *
	 * @return string
	 */
	public function get_name(): string {
		return self::ABILITY_NAME;
	}

	/**
	 * Register the ability with the WP Abilities API.
	 *
	 * No-ops gracefully when the API is not available (WP < 6.9).
	 *
	 * @return void
	 */
	public function register(): void {
		if ( ! function_exists( 'wp_register_ability' ) ) {
			return;
		}

		wp_register_ability(
			'imagify/optimize-media',
			[
				'label'               => __( 'Optimize media', 'imagify' ),
				'description'         => __( 'Optimizes a specific media on-demand using Imagify.', 'imagify' ),
				'category'            => 'imagify',
				'input_schema'        => [
					'type'       => 'object',
					'properties' => [
						'media_id'           => [
							'type'        => 'integer',
							'description' => __( 'The WordPress attachment ID to optimize.', 'imagify' ),
						],
						'optimization_level' => [
							'type'        => 'integer',
							'description' => __( 'Optimization level: 0 (normal), 1 (aggressive), or 2 (ultra). If omitted, uses the global setting.', 'imagify' ),
							'minimum'     => 0,
							'maximum'     => 2,
						],
						'confirm'            => [
							'type'        => 'boolean',
							'description' => __( 'Set to true to execute after reviewing the credit-consumption preview returned by a prior call without this flag.', 'imagify' ),
							'default'     => false,
						],
					],
					'required'   => [ 'media_id' ],
				],
				'output_schema'       => [
					'type'       => 'object',
					'properties' => [
						'status'          => [
							'type'        => 'string',
							'description' => __( 'Result status: "success", "error", "confirmation_required", "insufficient_quota", or "invalid_api_key".', 'imagify' ),
							'enum'        => [ 'success', 'error', 'confirmation_required', 'insufficient_quota', 'invalid_api_key' ],
						],
						'original_size'   => [
							'type'        => [ 'integer', 'null' ],
							'description' => __( 'Original file size in bytes before optimization, or null on error.', 'imagify' ),
						],
						'optimized_size'  => [
							'type'        => [ 'integer', 'null' ],
							'description' => __( 'Optimized file size in bytes after optimization, or null on error or if not yet available.', 'imagify' ),
						],
						'savings_percent' => [
							'type'        => [ 'number', 'null' ],
							'description' => __( 'Percentage savings, or null on error.', 'imagify' ),
						],
						'error_message'   => [
							'type'        => [ 'string', 'null' ],
							'description' => __( 'Human-readable error message on failure, or null on success.', 'imagify' ),
						],
					],
				],
				'execute_callback'    => [ $this, 'execute' ],
				'permission_callback' => [ $this, 'check_permissions' ],
				'meta'                => [
					'show_in_rest' => true,
					'mcp'          => [
						'public' => true,
					],
					'annotations'  => [
						'readonly'    => false,
						'destructive' => true,
						'idempotent'  => false,
					],
				],
			]
		);
	}

	/**
	 * Check if the current user has permission to execute this ability.
	 *
	 * Routes through Imagify's capability abstraction so the `imagify_capacity`
	 * filter and multisite network-admin logic are honoured.
	 *
	 * Note: `manual-optimize` is not used here because `check_permissions()` is
	 * called before `execute()` and receives no `media_id`, making the underlying
	 * `edit_post` check ambiguous. The `manage` descriptor is the correct top-level
	 * gate consistent with existing AJAX equivalents.
	 *
	 * @return bool True when the current user has the Imagify `manage` capability.
	 */
	protected function has_permission(): bool {
		return imagify_get_context( 'wp' )->current_user_can( 'manage' );
	}

	/**
	 * Returns the credit-consumption impact estimate for a single media optimization.
	 *
	 * @param array $args Input arguments (unused: optimizing a single media always costs 1 unit).
	 * @return array{unit: string, count: int, label: string}
	 */
	public function get_impact_estimate( array $args ): array {
		return [
			'unit'  => 'image',
			'count' => 1,
			'label' => 'this media',
		];
	}

	/**
	 * Execute the ability: optimize the media.
	 *
	 * Wraps the real execution behind `guard_credit_confirmation()` so the
	 * caller must pass `confirm: true` once quota is confirmed (and is not
	 * over quota, and the API key is valid). Fires `imagify_mcp_ability_executed`
	 * after the ability resolves so that tracking and other subscribers can
	 * react to every outcome (previews included).
	 *
	 * @param array $args Input arguments. Expects `media_id` (int) and optionally `optimization_level` (int), `confirm` (bool).
	 * @return array<string, mixed> Guard response (invalid_api_key/insufficient_quota/confirmation_required) or the do_execute() result shape.
	 */
	public function execute( array $args = [] ): array {
		$start_time = microtime( true );
		$result     = $this->guard_credit_confirmation(
			$args,
			function ( array $a ) {
				return $this->do_execute( $a );
			}
		);

		$this->fire_executed( $result, $start_time, $args );

		return $result;
	}

	/**
	 * Internal execution logic for the ability.
	 *
	 * Separated from execute() so that the do_action hook fires for every
	 * outcome (success and all error paths) with a single call site.
	 *
	 * @param array $args Input arguments.
	 * @return array{status: string, original_size: int|null, optimized_size: int|null, savings_percent: float|null, error_message: string|null}
	 */
	private function do_execute( array $args ): array {
		$media_id = isset( $args['media_id'] ) ? (int) $args['media_id'] : 0;

		if ( $media_id <= 0 ) {
			return $this->error_response( 'Invalid or missing media_id.' );
		}

		// Verify the attachment exists.
		$post = get_post( $media_id );
		if ( ! $post ) {
			return $this->error_response( 'Invalid media.' );
		}

		// Verify the post is an attachment.
		if ( 'attachment' !== get_post_type( $post ) ) {
			return $this->error_response( 'The provided ID is not a media attachment.' );
		}

		// Determine optimization level.
		$optimization_level = null;
		if ( isset( $args['optimization_level'] ) ) {
			$optimization_level = (int) $args['optimization_level'];
		}

		// Get the process for this media.
		$process = imagify_get_optimization_process( $media_id, 'wp' );

		if ( ! $process ) {
			return $this->error_response( 'Could not initialize optimization process.' );
		}

		// Capture the original size before optimization.
		$original_size = $this->get_media_original_size( $process );

		// Determine whether to optimize or reoptimize.
		$data = $process->get_data();

		if ( $data->is_optimized() ) {
			// Re-optimize the media.
			$result = $process->reoptimize( $optimization_level );
		} else {
			// First-time optimization.
			$result = $process->optimize( $optimization_level );
		}

		// Handle errors from the process.
		if ( is_wp_error( $result ) ) {
			return $this->error_response( $result->get_error_message() );
		}

		// Capture the optimized size after optimization.
		// Note: The process queues a background job, so optimized_size may be 0 until job completes.
		$optimized_size = $this->get_media_optimized_size( $process );

		// Calculate savings percentage.
		$savings_percent = null;
		if ( $original_size > 0 && null !== $optimized_size ) {
			$savings_percent = (float) round( ( ( $original_size - $optimized_size ) / $original_size ) * 100, 1 );
		}

		return [
			'status'          => 'success',
			'original_size'   => $original_size,
			'optimized_size'  => $optimized_size,
			'savings_percent' => $savings_percent,
			'error_message'   => null,
		];
	}

	/**
	 * Build an error response array.
	 *
	 * @param string $error_message The error message.
	 * @return array{status: string, original_size: null, optimized_size: null, savings_percent: null, error_message: string}
	 */
	private function error_response( string $error_message ): array {
		return [
			'status'          => 'error',
			'original_size'   => null,
			'optimized_size'  => null,
			'savings_percent' => null,
			'error_message'   => $error_message,
		];
	}

	/**
	 * Get the original size of the media before optimization.
	 *
	 * Extracted into a protected method so unit tests can override.
	 *
	 * @param \Imagify\Optimization\Process\ProcessInterface $process The optimization process.
	 * @return int Original file size in bytes, or 0 if unavailable.
	 */
	protected function get_media_original_size( $process ): int {
		$data = $process->get_data();

		if ( ! $data ) {
			return 0;
		}

		// If already optimized, use the original_size from optimization stats.
		if ( $data->is_optimized() ) {
			$optimization_data = $data->get_optimization_data();
			if ( isset( $optimization_data['stats']['original_size'] ) ) {
				return (int) $optimization_data['stats']['original_size'];
			}
		}

		// Otherwise, get the original file size from the media object.
		$media = $process->get_media();

		if ( ! $media ) {
			return 0;
		}

		$path = $media->get_raw_original_path();

		if ( ! $path || ! file_exists( $path ) ) {
			return 0;
		}

		return (int) filesize( $path );
	}

	/**
	 * Get the optimized size of the media after optimization.
	 *
	 * For newly-queued jobs, this may return 0 until the background job completes.
	 * Clients should poll imagify_get_media_status to track final results.
	 *
	 * Extracted into a protected method so unit tests can override.
	 *
	 * @param \Imagify\Optimization\Process\ProcessInterface $process The optimization process.
	 * @return int|null Optimized file size in bytes, or null if unavailable.
	 */
	protected function get_media_optimized_size( $process ): ?int {
		$data = $process->get_data();

		if ( ! $data ) {
			return null;
		}

		$optimization_data = $data->get_optimization_data();

		if ( isset( $optimization_data['stats']['optimized_size'] ) ) {
			return (int) $optimization_data['stats']['optimized_size'];
		}

		return null;
	}
}
