<?php
/**
 * Ability_Registrar — single chokepoint that hooks wp_abilities_api_init and
 * calls wp_register_ability() for every ability this plugin contributes.
 *
 * Write-side execute_callbacks are wrapped by Event_Logger so every write —
 * success or failure — produces one audit row. Read-side abilities pass
 * through unwrapped (zero logging overhead).
 *
 * @package Ppok\AIToolkit
 * @license GPL-2.0-or-later
 */

namespace Ppok\AIToolkit\Abilities;

use Ppok\AIToolkit\Event_Logger;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

final class Ability_Registrar {

	public function __construct( private Event_Logger $logger ) {}

	public function register_categories(): void {
		if ( ! function_exists( 'wp_register_ability_category' ) ) {
			return;
		}
		\wp_register_ability_category(
			'ppok',
			array(
				'label'       => __( 'Ppok AI Toolkit', 'ppok-ai-toolkit' ),
				'description' => __( 'Abilities contributed by the Ppok AI Toolkit plugin.', 'ppok-ai-toolkit' ),
			)
		);
	}

	public function register_all(): void {
		if ( ! function_exists( 'wp_register_ability' ) ) {
			return;
		}

		$enabled = self::abilities_enabled_setting();

		foreach ( self::abilities() as $slug => $descriptor ) {
			$is_write = (bool) ( $descriptor['meta']['ppok']['write'] ?? false );

			// Asymmetric gating per architecture.exposure_policy: reads are always
			// public, writes default to off and only flip on when the admin
			// adds the slug to abilities_enabled.
			if ( $is_write ) {
				$descriptor['meta']['mcp']['public'] = in_array( $slug, $enabled, true );
				$descriptor['execute_callback']      = $this->wrap_with_logger( $slug, $descriptor['execute_callback'] );
			} else {
				$descriptor['meta']['mcp']['public'] = true;
			}

			\wp_register_ability( $slug, $descriptor );
		}
	}

	/**
	 * Full ability roster — slug => descriptor.
	 *
	 * Exposed statically so the admin settings page can render labels and
	 * descriptions for the abilities_enabled control without re-instantiating
	 * the registrar.
	 *
	 * @return array<string, array<string, mixed>>
	 */
	public static function abilities(): array {
		return array(
			System_Ping::SLUG          => System_Ping::descriptor(),
			Rest_Call::SLUG            => Rest_Call::descriptor(),
			Post_List::SLUG            => Post_List::descriptor(),
			Post_Get_Blocks::SLUG      => Post_Get_Blocks::descriptor(),
			Post_Set_Blocks::SLUG      => Post_Set_Blocks::descriptor(),
			Replace_In_Content::SLUG   => Replace_In_Content::descriptor(),
			Preview_Url::SLUG          => Preview_Url::descriptor(),
			Blocks_Registered::SLUG    => Blocks_Registered::descriptor(),
			Blocks_Find_Usage::SLUG    => Blocks_Find_Usage::descriptor(),
			Patterns_Registered::SLUG  => Patterns_Registered::descriptor(),
			Abilities_List::SLUG       => Abilities_List::descriptor(),
		);
	}

	/**
	 * Write-side ability slugs paired with a short admin-facing label.
	 *
	 * @return array<string, string>
	 */
	public static function write_ability_choices(): array {
		$choices = array();
		foreach ( self::abilities() as $slug => $descriptor ) {
			if ( ! empty( $descriptor['meta']['ppok']['write'] ) ) {
				$choices[ $slug ] = (string) ( $descriptor['label'] ?? $slug );
			}
		}
		return $choices;
	}

	private static function abilities_enabled_setting(): array {
		$settings = get_option( 'ppok_ai_toolkit_settings', array() );
		if ( ! is_array( $settings ) || empty( $settings['abilities_enabled'] ) ) {
			return array();
		}
		return array_values( array_filter( (array) $settings['abilities_enabled'], 'is_string' ) );
	}

	// Public for testability — pure wrapper, no security boundary.
	public function wrap_with_logger( string $slug, callable $callback ): callable {
		$logger = $this->logger;
		return function ( array $args = array() ) use ( $slug, $callback, $logger ) {
			$start        = microtime( true );
			$reason       = isset( $args['reason'] ) ? (string) $args['reason'] : null;
			$payload_args = $args;
			unset( $payload_args['reason'] );

			try {
				$result   = call_user_func( $callback, $args );
				$is_error = $result instanceof \WP_Error;

				$logger->record(
					array(
						'ability_slug'   => $slug,
						'actor_user_id'  => function_exists( 'get_current_user_id' ) ? get_current_user_id() : null,
						'target_post_id' => isset( $args['post_id'] ) ? (int) $args['post_id'] : null,
						'reason'         => $reason,
						'result'         => $is_error ? self::classify_error_result( $result ) : 'ok',
						'error_code'     => $is_error ? $result->get_error_code() : null,
						'duration_ms'    => (int) ( ( microtime( true ) - $start ) * 1000 ),
						'payload'        => array( 'input_args' => $payload_args ),
					)
				);

				return $result;
			} catch ( \Throwable $e ) {
				$logger->record(
					array(
						'ability_slug'   => $slug,
						'actor_user_id'  => function_exists( 'get_current_user_id' ) ? get_current_user_id() : null,
						'target_post_id' => isset( $args['post_id'] ) ? (int) $args['post_id'] : null,
						'reason'         => $reason,
						'result'         => 'server_error',
						'error_code'     => 'php_exception',
						'duration_ms'    => (int) ( ( microtime( true ) - $start ) * 1000 ),
						'payload'        => array(
							'input_args'      => $payload_args,
							'exception_class' => get_class( $e ),
							'message'         => $e->getMessage(),
						),
					)
				);
				throw $e;
			}
		};
	}

	/**
	 * Map an ability's WP_Error onto the audit-row 'result' enum so the
	 * activity viewer's category filter is operationally useful. Falls back
	 * to 'server_error' for anything unknown; the error_code column always
	 * carries the precise WP_Error code regardless.
	 *
	 * Enum values must stay in sync with .context/project.json →
	 * architecture.logging.schema_columns.
	 */
	// Public for testability — pure mapping function, no security boundary.
	public static function classify_error_result( \WP_Error $err ): string {
		$code = (string) $err->get_error_code();
		$map  = array(
			'rest_forbidden'                => 'permission_denied',
			'rest_cannot_view'              => 'permission_denied',
			'rest_cannot_edit'              => 'permission_denied',
			'rest_cannot_read'              => 'permission_denied',
			'rest_cannot_create'            => 'permission_denied',
			'rest_cannot_delete'            => 'permission_denied',
			'ppok_cap_denied'               => 'permission_denied',
			'rest_post_invalid_id'          => 'not_found',
			'rest_post_invalid_page_number' => 'not_found',
			'ppok_post_not_found'           => 'not_found',
			'rest_invalid_param'            => 'validation_error',
			'rest_missing_callback_param'   => 'validation_error',
			'response_too_large'            => 'response_too_large',
			'unknown_block'                 => 'unknown_block',
			'scope_blocked'                 => 'scope_blocked',
		);
		return $map[ $code ] ?? 'server_error';
	}
}
