<?php

namespace SugarCalendarBookings\Integrations\Webhooks;

use Exception;
use SugarCalendarBookings\Integrations\IntegrationCapabilityRegistry;
use SugarCalendarBookings\Integrations\WebhookEventHandlerInterface;
use SugarCalendarBookings\Plugin;
use SugarCalendarBookings\Support\Bootable;
use SugarCalendarBookings\Vendor\ProductApi\Auth\AuthOptions;
use SugarCalendarBookings\Vendor\ProductApi\Context;
use SugarCalendarBookings\Vendor\ProductApi\Options;
use SugarCalendarBookings\Vendor\ProductApi\ProductApi;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;

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

/**
 * Incoming webhook handler.
 *
 * Provider-agnostic REST endpoint that receives forwarded webhook events
 * from the Product API relay and dispatches them to the appropriate
 * WebhookEventHandlerInterface implementation via the capability registry.
 *
 * @since 1.4.0
 */
class IncomingWebhookHandler implements Bootable {

	/**
	 * REST API namespace.
	 *
	 * @since 1.4.0
	 *
	 * @var string
	 */
	const REST_NAMESPACE = 'scb/v1';

	/**
	 * Whether we are currently processing an incoming webhook.
	 *
	 * Used by MeetingManager to skip redundant provider API calls
	 * when the change originated from the provider itself.
	 *
	 * @since 1.4.0
	 *
	 * @var bool
	 */
	private static bool $processing = false;

	/**
	 * Check if a webhook is currently being processed.
	 *
	 * @since 1.4.0
	 *
	 * @return bool
	 */
	public static function is_processing(): bool {

		return self::$processing;
	}

	/**
	 * Register hooks.
	 *
	 * @since 1.4.0
	 */
	public function hooks(): void {

		add_action( 'rest_api_init', [ $this, 'register_routes' ] );
	}

	/**
	 * Register REST API routes.
	 *
	 * @since 1.4.0
	 */
	public function register_routes(): void {

		register_rest_route(
			self::REST_NAMESPACE,
			'/webhooks/(?P<provider>[a-z]+)',
			[
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => [ $this, 'handle_webhook' ],
				'permission_callback' => [ $this, 'verify_request' ],
				'args'                => [
					'provider' => [
						'required'          => true,
						'validate_callback' => function ( $value ) {
							return $this->resolve_handler( $value ) !== null;
						},
					],
				],
			]
		);
	}

	/**
	 * Verify the incoming webhook request via HMAC signature.
	 *
	 * The Product API relay signs outgoing webhook payloads using
	 * the site's signing_secret as the HMAC key and exposes the
	 * signature on the `X-SCB-Relay-Signature` / `X-SCB-Relay-Timestamp`
	 * headers.
	 *
	 * The legacy `X-Signature` / `X-Timestamp` headers are still accepted as a
	 * fallback so that webhooks keep verifying during the relay rollout window —
	 * older relay deployments (and the Zoom webhooks shipped in 1.4.0) still sign
	 * with those names. The fallback can be removed once every relay environment
	 * is confirmed to emit the `X-SCB-Relay-*` headers.
	 *
	 * @since 1.4.0
	 *
	 * @param WP_REST_Request $request Incoming request.
	 *
	 * @return bool|WP_Error True if valid, WP_Error otherwise.
	 */
	public function verify_request( WP_REST_Request $request ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh

		$signature = $request->get_header( 'X-SCB-Relay-Signature' ) ?: $request->get_header( 'X-Signature' );
		$timestamp = $request->get_header( 'X-SCB-Relay-Timestamp' ) ?: $request->get_header( 'X-Timestamp' );

		if ( empty( $signature ) || empty( $timestamp ) ) {
			return new WP_Error( 'missing_signature', 'Missing signature headers.', [ 'status' => 401 ] );
		}

		// Reject requests older than 5 minutes.
		if ( abs( time() - (int) $timestamp ) > 300 ) {
			return new WP_Error( 'timestamp_expired', 'Request timestamp expired.', [ 'status' => 401 ] );
		}

		$auth_options   = new AuthOptions( ProductApi::get( Context::class ), ProductApi::get( Options::class ) );
		$signing_secret = $auth_options->get( 'signing_secret' );

		if ( empty( $signing_secret ) ) {
			return new WP_Error( 'not_registered', 'Site not registered.', [ 'status' => 401 ] );
		}

		$body     = $request->get_body();
		$expected = hash_hmac( 'sha256', "{$timestamp}.{$body}", $signing_secret );

		if ( ! hash_equals( $expected, $signature ) ) {
			return new WP_Error( 'invalid_signature', 'Invalid signature.', [ 'status' => 401 ] );
		}

		return true;
	}

	/**
	 * Handle an incoming webhook event.
	 *
	 * Resolves the provider-specific handler from the capability registry
	 * and delegates event processing. The envelope shape differs per provider
	 * (Zoom forwards its native `{event, payload}` body, the relay's
	 * Microsoft forwarder normalises to `{events: [...]}` or
	 * `{event_type: connection_failed, ...}`), so envelope validation
	 * lives in each handler — the dispatcher just routes.
	 *
	 * @since 1.4.0
	 *
	 * @param WP_REST_Request $request Incoming request.
	 *
	 * @return WP_REST_Response
	 */
	public function handle_webhook( WP_REST_Request $request ): WP_REST_Response {

		$provider = $request->get_param( 'provider' );
		$body     = $request->get_json_params();

		if ( ! is_array( $body ) ) {
			$body = [];
		}

		$handler = $this->resolve_handler( $provider );

		if ( ! $handler ) {
			return new WP_REST_Response( [ 'error' => 'Unsupported provider.' ], 400 );
		}

		self::$processing = true;

		try {
			$handler->handle_event( $body );
		} catch ( Exception $e ) {
			return new WP_REST_Response( [ 'error' => 'Processing failed.' ], 500 );
		} finally {
			self::$processing = false;
		}

		return new WP_REST_Response( [ 'status' => 'ok' ], 200 );
	}

	/**
	 * Resolve the webhook event handler for a provider.
	 *
	 * @since 1.4.0
	 *
	 * @param string $provider Provider slug.
	 *
	 * @return WebhookEventHandlerInterface|null
	 */
	private function resolve_handler( string $provider ): ?WebhookEventHandlerInterface {

		$handler = Plugin::get( IntegrationCapabilityRegistry::class )
			->find( WebhookEventHandlerInterface::class, $provider );

		return $handler instanceof WebhookEventHandlerInterface ? $handler : null;
	}

	/**
	 * Get the webhook URL for a provider.
	 *
	 * @since 1.4.0
	 *
	 * @param string $provider Provider slug.
	 *
	 * @return string Webhook URL.
	 */
	public static function get_webhook_url( string $provider ): string {

		return rest_url( self::REST_NAMESPACE . '/webhooks/' . $provider );
	}
}
