<?php
/**
 * OAuth access-token validation. Phase 6 of AI-9.
 *
 * Validates `access_`-prefixed opaque bearer tokens on incoming requests and
 * sets the current WP user, so the existing per-role/per-ability RBAC governs
 * what the MCP client can do. JwtAuth deliberately passes `access_` tokens
 * through to this validator, so the two auth paths never conflict.
 *
 * @package AtlasAiDev\Awfah
 * @subpackage OAuth
 */

declare(strict_types=1);

namespace AtlasAiDev\Awfah\OAuth;

defined( 'ABSPATH' ) || exit;

/**
 * Class Token_Validator
 */
class Token_Validator {

	/**
	 * The authenticated user id for the current request (cache), or 0.
	 *
	 * @var int
	 */
	private $resolved = 0;

	/**
	 * Wire hooks. Runs after JwtAuth (which passes `access_` tokens through).
	 */
	public function __construct() {
		add_filter( 'rest_authentication_errors', array( $this, 'authenticate_rest' ), 20 );
		add_filter( 'awfah_authenticate_request', array( $this, 'authenticate_mcp' ), 10, 2 );
	}

	/**
	 * Authenticate a REST request bearing an OAuth access token.
	 *
	 * @param mixed $result Current auth result.
	 * @return mixed true on success, WP_Error on bad token, or $result to pass through.
	 */
	public function authenticate_rest( $result ) {
		if ( ! empty( $result ) || ! OAuth_Server::is_enabled() ) {
			return $result;
		}
		$token = $this->bearer();
		if ( '' === $token || ! str_starts_with( $token, 'access_' ) ) {
			return $result; // Not an OAuth token — let other authenticators handle it.
		}
		$user_id = $this->resolve( $token );
		if ( $user_id > 0 ) {
			wp_set_current_user( $user_id );
			return true;
		}
		return new \WP_Error( 'invalid_token', __( 'The access token is invalid, expired, or revoked.', 'ai-workflow-automation-ai-agent-hub' ), array( 'status' => 401 ) );
	}

	/**
	 * Authenticate within the MCP-specific auth filter.
	 *
	 * @param mixed                 $result  Current auth result.
	 * @param \WP_REST_Request|null $request Request.
	 * @return mixed
	 */
	public function authenticate_mcp( $result, $request = null ) {
		if ( ! empty( $result ) || ! OAuth_Server::is_enabled() ) {
			return $result;
		}
		$token = '';
		if ( $request instanceof \WP_REST_Request ) {
			$auth  = (string) $request->get_header( 'authorization' );
			$token = ( preg_match( '/Bearer\s+(.+)/i', $auth, $m ) ) ? trim( $m[1] ) : '';
		}
		if ( '' === $token ) {
			$token = $this->bearer();
		}
		if ( '' === $token || ! str_starts_with( $token, 'access_' ) ) {
			return $result;
		}
		$user_id = $this->resolve( $token );
		if ( $user_id > 0 ) {
			wp_set_current_user( $user_id );
			return true;
		}
		return $result;
	}

	/**
	 * Resolve an access token to a WP user id (0 if invalid).
	 *
	 * @param string $token Raw access token.
	 * @return int
	 */
	private function resolve( string $token ): int {
		if ( $this->resolved > 0 ) {
			return $this->resolved;
		}
		$row = OAuth_Repository::find_active( $token, 'access' );
		if ( ! $row ) {
			return 0;
		}
		$user = get_user_by( 'id', (int) $row['user_id'] );
		if ( ! $user ) {
			return 0;
		}
		// Make the granted scope available to downstream RBAC if needed.
		$GLOBALS['awfah_oauth_current_scope'] = (string) $row['scope'];
		$this->resolved                        = (int) $user->ID;

		/**
		 * Fires when an OAuth access token successfully authenticates a request
		 * (audit/notify; Pro may also enforce per-scope tool access elsewhere).
		 *
		 * @param int    $user_id   Authenticated WP user id.
		 * @param string $scope     Granted scope on the token.
		 * @param string $client_id Client the token was issued to.
		 */
		do_action( 'awfah_oauth_token_validated', $this->resolved, (string) $row['scope'], (string) $row['client_id'] );

		return $this->resolved;
	}

	/**
	 * Extract the Bearer token from the Authorization header.
	 *
	 * @return string
	 */
	private function bearer(): string {
		$auth = OAuth_Server::get_auth_header();
		if ( '' !== $auth && preg_match( '/Bearer\s+(.+)/i', $auth, $m ) ) {
			return trim( $m[1] );
		}
		return '';
	}
}
