<?php

/**
 * Controls the endpoint for the incoming webhooks.
 *
 * @package WooCommerce\PayPalCommerce\Webhooks
 */
declare (strict_types=1);
namespace WooCommerce\PayPalCommerce\Webhooks;

use phpDocumentor\Reflection\Types\This;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\WebhookEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Webhook;
use WooCommerce\PayPalCommerce\ApiClient\Entity\WebhookEvent;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookEventFactory;
use WooCommerce\PayPalCommerce\Webhooks\Handler\RequestHandler;
use WooCommerce\PayPalCommerce\Vendor\Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\Webhooks\Handler\RequestHandlerTrait;
use WooCommerce\PayPalCommerce\Webhooks\Status\WebhookSimulation;
/**
 * Class IncomingWebhookEndpoint
 */
class IncomingWebhookEndpoint
{
    use RequestHandlerTrait;
    const NAMESPACE = 'paypal/v1';
    const ROUTE = 'incoming';
    /**
     * The Webhook endpoint.
     *
     * @var WebhookEndpoint
     */
    private $webhook_endpoint;
    /**
     * Our registered webhook.
     *
     * @var Webhook|null
     */
    private $webhook;
    /**
     * The Request handlers.
     *
     * @var RequestHandler[]
     */
    private $handlers;
    /**
     * The logger.
     *
     * @var LoggerInterface
     */
    private $logger;
    /**
     * Whether requests need to be verified.
     *
     * @var bool
     */
    private $verify_request;
    /**
     * The webhook event factory.
     *
     * @var WebhookEventFactory
     */
    private $webhook_event_factory;
    /**
     * The simulation handler.
     *
     * @var WebhookSimulation
     */
    private $simulation;
    /**
     * The last webhook event storage.
     *
     * @var WebhookEventStorage
     */
    private $last_webhook_event_storage;
    /**
     * Cached webhook verification results
     * to avoid repeating requests when permission_callback is called multiple times.
     *
     * @var array<string, bool>
     */
    private $verification_results = array();
    /**
     * IncomingWebhookEndpoint constructor.
     *
     * @param WebhookEndpoint     $webhook_endpoint The webhook endpoint.
     * @param Webhook|null        $webhook Our registered webhook.
     * @param LoggerInterface     $logger The logger.
     * @param bool                $verify_request Whether requests need to be verified or not.
     * @param WebhookEventFactory $webhook_event_factory The webhook event factory.
     * @param WebhookSimulation   $simulation The simulation handler.
     * @param WebhookEventStorage $last_webhook_event_storage The last webhook event storage.
     * @param RequestHandler      ...$handlers The handlers, which process a request in the end.
     */
    public function __construct(WebhookEndpoint $webhook_endpoint, ?Webhook $webhook, LoggerInterface $logger, bool $verify_request, WebhookEventFactory $webhook_event_factory, WebhookSimulation $simulation, \WooCommerce\PayPalCommerce\Webhooks\WebhookEventStorage $last_webhook_event_storage, RequestHandler ...$handlers)
    {
        $this->webhook_endpoint = $webhook_endpoint;
        $this->webhook = $webhook;
        $this->handlers = $handlers;
        $this->logger = $logger;
        $this->verify_request = $verify_request;
        $this->webhook_event_factory = $webhook_event_factory;
        $this->last_webhook_event_storage = $last_webhook_event_storage;
        $this->simulation = $simulation;
    }
    /**
     * Registers the endpoint.
     *
     * @return bool
     */
    public function register(): bool
    {
        return (bool) register_rest_route(self::NAMESPACE, self::ROUTE, array('methods' => array('POST'), 'callback' => array($this, 'handle_request'), 'permission_callback' => array($this, 'verify_request')));
    }
    /**
     * Verifies the current request.
     *
     * @param \WP_REST_Request $request The request.
     *
     * @return bool
     */
    public function verify_request(\WP_REST_Request $request): bool
    {
        $content_type = $request->get_content_type();
        if (!isset($content_type['value']) || 'application/json' !== $content_type['value']) {
            $this->logger->error('Webhook request rejected: expected application/json content type.');
            return \false;
        }
        // We only want to use the request body for retrieving the webhook parameters,
        // so clear other sources of parameters so that we never fall back to them.
        $request->set_query_params(array());
        $request->set_url_params(array());
        if (!$this->verify_request) {
            return \true;
        }
        if (!$this->webhook) {
            $this->logger->error('Failed to retrieve stored webhook data.');
            return \false;
        }
        try {
            $event = $this->event_from_request($request);
        } catch (RuntimeException $exception) {
            $this->logger->error('Webhook parsing failed: ' . $exception->getMessage());
            return \false;
        }
        $cache_key = $event->id();
        if (isset($this->verification_results[$cache_key])) {
            return $this->verification_results[$cache_key];
        }
        try {
            if ($this->simulation->is_simulation_event($event)) {
                return \true;
            }
            $result = $this->webhook_endpoint->verify_current_request_for_webhook($this->webhook);
            if (!$result) {
                $this->logger->error('Webhook verification failed.');
            }
            $this->verification_results[$cache_key] = $result;
            return $result;
        } catch (RuntimeException $exception) {
            $this->logger->error('Webhook verification failed: ' . $exception->getMessage());
            $this->verification_results[$cache_key] = \false;
            return \false;
        }
    }
    /**
     * Handles the request.
     *
     * @param \WP_REST_Request $request The request.
     *
     * @return \WP_REST_Response
     */
    public function handle_request(\WP_REST_Request $request): \WP_REST_Response
    {
        try {
            return $this->process_request($request);
        } catch (\Throwable $exception) {
            /**
             * Catches any exception a handler's business logic might throw
             * (not just the types it documents), so an unexpected error
             * returns a graceful failure response instead of an uncaught
             * fatal. Without this, PayPal would retry the webhook
             * indefinitely since it never receives a response.
             */
            $this->logger->error(sprintf('Uncaught %s while handling incoming webhook: %s', get_class($exception), $exception->getMessage()));
            return $this->failure_response();
        }
    }
    /**
     * Processes the request. Split out from handle_request() so the latter
     * can wrap it in a single catch-all try/catch.
     *
     * @param \WP_REST_Request $request The request.
     *
     * @return \WP_REST_Response
     */
    private function process_request(\WP_REST_Request $request): \WP_REST_Response
    {
        $event = $this->event_from_request($request);
        $this->logger->debug(sprintf('Webhook %s received of type %s and by resource "%s"', $event->id(), $event->event_type(), $event->resource_type()));
        $this->last_webhook_event_storage->save($event);
        if ($this->simulation->is_simulation_event($event)) {
            $this->logger->info('Received simulated webhook.');
            $this->simulation->receive($event);
            return $this->success_response();
        }
        foreach ($this->handlers as $handler) {
            if ($handler->responsible_for_request($request)) {
                $event_type = ($handler->event_types() ? current($handler->event_types()) : '') ?: '';
                $this->logger->debug(sprintf('Webhook is going to be handled by %s on %s', $event_type, get_class($handler)));
                $response = $handler->handle_request($request);
                $this->logger->info(sprintf('Webhook has been handled by %s on %s', $event_type, get_class($handler)));
                return $response;
            }
        }
        $event_type = $request['event_type'] ?: '';
        if (in_array($event_type, array('BILLING_AGREEMENTS.AGREEMENT.CREATED'), \true)) {
            return $this->success_response();
        }
        $message = sprintf('Could not find handler for request type %s', $event_type);
        return $this->failure_response($message);
    }
    /**
     * Returns the URL to the endpoint.
     *
     * @return string
     */
    public function url(): string
    {
        $url = rest_url(self::NAMESPACE . '/' . self::ROUTE);
        $url = str_replace('http://', 'https://', $url);
        $ngrok_host = getenv('NGROK_HOST');
        if ($ngrok_host) {
            $host = wp_parse_url($url, \PHP_URL_HOST);
            if ($host) {
                $url = str_replace($host, $ngrok_host, $url);
            }
        }
        return $url;
    }
    /**
     * Returns the event types, which are handled by the endpoint.
     *
     * @return string[]
     */
    public function handled_event_types(): array
    {
        $event_types = array();
        foreach ($this->handlers as $handler) {
            $event_types = array_merge($event_types, $handler->event_types());
        }
        return array_unique($event_types);
    }
    /**
     * Creates WebhookEvent from request data.
     *
     * @param \WP_REST_Request $request The request with event data.
     *
     * @return WebhookEvent
     * @throws RuntimeException When failed to create.
     */
    private function event_from_request(\WP_REST_Request $request): WebhookEvent
    {
        return $this->webhook_event_factory->from_array($request->get_params());
    }
}
