<?php
/**
 * OAuth 2.1 Dynamic Client Registration (RFC 7591). Phase 3 of AI-9.
 *
 * POST /wp-json/awfah_oauth/v1/register — lets MCP clients (Claude, Cursor)
 * register themselves at runtime so the user only pastes the server URL.
 *
 * @package AtlasAiDev\Awfah
 * @subpackage OAuth
 */

declare(strict_types=1);

namespace AtlasAiDev\Awfah\OAuth;

defined( 'ABSPATH' ) || exit;

/**
 * Class Registration_Controller
 */
class Registration_Controller {

	/**
	 * Wire hooks.
	 */
	public function __construct() {
		add_action( 'rest_api_init', array( $this, 'register_routes' ) );
	}

	/**
	 * Register the DCR route.
	 *
	 * @return void
	 */
	public function register_routes(): void {
		register_rest_route(
			OAuth_Server::REST_NAMESPACE,
			'/register',
			array(
				'methods'             => 'POST',
				'permission_callback' => '__return_true',
				'callback'            => array( $this, 'handle' ),
			)
		);
	}

	/**
	 * Handle a dynamic client registration request.
	 *
	 * @param \WP_REST_Request $request Request.
	 * @return \WP_REST_Response
	 */
	public function handle( \WP_REST_Request $request ) {
		if ( ! OAuth_Server::is_enabled() ) {
			return new \WP_REST_Response( array( 'error' => 'oauth_disabled' ), 404 );
		}

		// Basic per-IP rate limit on registration.
		if ( ! $this->within_rate_limit() ) {
			return new \WP_REST_Response( array( 'error' => 'rate_limited' ), 429 );
		}

		/**
		 * Allow Pro / third parties to disable open dynamic client registration
		 * (allowlist-only, IP restrictions, …). Return false to reject with 403.
		 *
		 * @param bool             $allowed Whether registration is permitted.
		 * @param \WP_REST_Request $request The registration request.
		 */
		if ( ! apply_filters( 'awfah_oauth_allow_registration', true, $request ) ) {
			return new \WP_REST_Response( array( 'error' => 'registration_disabled' ), 403 );
		}

		$body = $request->get_json_params();
		if ( ! is_array( $body ) ) {
			$body = $request->get_params();
		}

		$redirect_uris = $this->sanitize_redirect_uris( $body['redirect_uris'] ?? array() );
		if ( empty( $redirect_uris ) ) {
			return new \WP_REST_Response(
				array(
					'error'             => 'invalid_redirect_uri',
					'error_description' => 'At least one valid redirect_uri (https or http loopback) is required.',
				),
				400
			);
		}

		$auth_method = in_array( ( $body['token_endpoint_auth_method'] ?? 'none' ), array( 'none', 'client_secret_post', 'client_secret_basic' ), true )
			? $body['token_endpoint_auth_method']
			: 'none';

		$client_id     = OAuth_Server::random_token( 'client_' );
		$client_secret = '';
		$secret_hash   = '';
		if ( 'none' !== $auth_method ) {
			$client_secret = OAuth_Server::random_token( 'secret_' );
			$secret_hash   = OAuth_Repository::hash( $client_secret );
		}

		$scope = $this->sanitize_scope( $body['scope'] ?? implode( ' ', OAuth_Server::scopes() ) );

		/**
		 * Filter the stored client args before registration (Pro may set an approval
		 * status, restrict scope, tag the client, …). Do not change client_id here.
		 *
		 * @param array            $client_args Args passed to OAuth_Repository::insert_client().
		 * @param \WP_REST_Request $request     The registration request.
		 */
		$client_args = apply_filters(
			'awfah_oauth_client_registration_args',
			array(
				'client_id'                  => $client_id,
				'client_secret_hash'         => $secret_hash,
				'client_name'                => sanitize_text_field( (string) ( $body['client_name'] ?? 'MCP Client' ) ),
				'redirect_uris'              => $redirect_uris,
				'grant_types'                => 'authorization_code refresh_token',
				'scope'                      => $scope,
				'token_endpoint_auth_method' => $auth_method,
				'created_by_ip'              => OAuth_Server::client_ip(),
			),
			$request
		);

		$ok = OAuth_Repository::insert_client( $client_args );

		if ( ! $ok ) {
			return new \WP_REST_Response( array( 'error' => 'server_error' ), 500 );
		}

		/**
		 * Fires after a client is dynamically registered (audit/notify).
		 *
		 * @param string $client_id   The new client id.
		 * @param array  $client_args The stored client args.
		 */
		do_action( 'awfah_oauth_client_registered', $client_id, $client_args );

		$response = array(
			'client_id'                  => $client_id,
			'client_id_issued_at'        => time(),
			'redirect_uris'              => $redirect_uris,
			'grant_types'                => array( 'authorization_code', 'refresh_token' ),
			'response_types'             => array( 'code' ),
			'token_endpoint_auth_method' => $auth_method,
			'client_name'                => sanitize_text_field( (string) ( $body['client_name'] ?? 'MCP Client' ) ),
			'scope'                      => $scope,
		);
		if ( '' !== $client_secret ) {
			$response['client_secret']            = $client_secret;
			$response['client_secret_expires_at'] = 0; // Never expires.
		}

		return new \WP_REST_Response( $response, 201 );
	}

	/**
	 * Validate + sanitize redirect URIs (https or http loopback only).
	 *
	 * @param mixed $uris Raw value.
	 * @return string[]
	 */
	private function sanitize_redirect_uris( $uris ): array {
		if ( is_string( $uris ) ) {
			$uris = array( $uris );
		}
		if ( ! is_array( $uris ) ) {
			return array();
		}
		$out = array();
		foreach ( $uris as $uri ) {
			$uri  = esc_url_raw( (string) $uri, array( 'https', 'http' ) );
			$host = wp_parse_url( $uri, PHP_URL_HOST );
			$sch  = wp_parse_url( $uri, PHP_URL_SCHEME );
			if ( '' === $uri || ! $host ) {
				continue;
			}
			// http only allowed for loopback (native clients); everything else must be https.
			$is_loopback = in_array( $host, array( '127.0.0.1', '::1', 'localhost' ), true );
			if ( 'http' === $sch && ! $is_loopback ) {
				continue;
			}
			$out[] = $uri;
		}
		return array_values( array_unique( $out ) );
	}

	/**
	 * Keep only known scopes.
	 *
	 * @param string $scope Space-delimited scopes.
	 * @return string
	 */
	private function sanitize_scope( string $scope ): string {
		$allowed   = OAuth_Server::scopes();
		$requested = array_filter( array_map( 'trim', explode( ' ', $scope ) ) );
		$kept      = array_values( array_intersect( $requested, $allowed ) );
		return empty( $kept ) ? implode( ' ', $allowed ) : implode( ' ', $kept );
	}

	/**
	 * Simple per-IP registration rate limit (20 / hour).
	 *
	 * @return bool
	 */
	private function within_rate_limit(): bool {
		$key   = 'awfah_oauth_reg_' . md5( OAuth_Server::client_ip() );
		$count = (int) get_transient( $key );
		if ( $count >= (int) apply_filters( 'awfah_oauth_register_rate_limit', 20 ) ) {
			return false;
		}
		set_transient( $key, $count + 1, HOUR_IN_SECONDS );
		return true;
	}
}
