<?php

namespace Give\Framework\PaymentGateways;

use Give\Donations\Models\Donation;
use Give\Framework\Exceptions\Primitives\Exception;
use Give\Framework\PaymentGateways\Actions\GenerateGatewayRouteUrl;
use Give\Framework\PaymentGateways\Contracts\PaymentGatewayInterface;
use Give\Framework\PaymentGateways\Contracts\PaymentGatewayRefundable;
use Give\Framework\PaymentGateways\Contracts\Subscription\SubscriptionAmountEditable;
use Give\Framework\PaymentGateways\Contracts\Subscription\SubscriptionDashboardLinkable;
use Give\Framework\PaymentGateways\Contracts\Subscription\SubscriptionPausable;
use Give\Framework\PaymentGateways\Contracts\Subscription\SubscriptionPaymentMethodEditable;
use Give\Framework\PaymentGateways\Contracts\Subscription\SubscriptionTransactionsSynchronizable;
use Give\Framework\PaymentGateways\Contracts\WebhookNotificationsListener;
use Give\Framework\PaymentGateways\Routes\RouteSignature;
use Give\Framework\PaymentGateways\Traits\HandleHttpResponses;
use Give\Framework\PaymentGateways\Traits\HasRouteMethods;
use Give\Framework\PaymentGateways\Webhooks\Webhook;
use Give\Framework\Support\ValueObjects\Money;
use Give\Log\Log;
use Give\Subscriptions\Models\Subscription;
use ReflectionException;
use ReflectionMethod;
use Give\Framework\Support\Contracts\Arrayable;
use JsonSerializable;

/**
 * @since 4.6.0 Added JSONSerializable and Arrayable interfaces
 * @since 2.30.0 added enqueueScript() and formSettings() methods.
 * @since 2.18.0
 */
abstract class PaymentGateway implements PaymentGatewayInterface,
                                         SubscriptionDashboardLinkable,
                                         SubscriptionAmountEditable,
                                         SubscriptionPaymentMethodEditable,
                                         SubscriptionTransactionsSynchronizable,
    JsonSerializable,
    Arrayable
{
    use HandleHttpResponses;
    use HasRouteMethods {
        supportsMethodRoute as protected SupportsOwnMethodRoute;
        callRouteMethod as protected CallOwnRouteMethod;
    }

    /**
     * @since 2.20.0 Change variable type to SubscriptionModule.
     * @var SubscriptionModule $subscriptionModule
     */
    public $subscriptionModule;

    /**
     * @since 4.5.0
     *
     * @var Webhook $webhook
     */
    public $webhook;

    /**
     * @since 4.6.0 add explicit nullable type to subscriptionModule parameter
     * @since 4.5.0 Add the webhookEvents property
     *
     * @since 2.20.0 Change first argument type to SubscriptionModule abstract class.
     * @since 2.18.0
     *
     * @param  SubscriptionModule|null  $subscriptionModule
     */
    public function __construct(?SubscriptionModule $subscriptionModule = null)
    {
        if ($subscriptionModule !== null) {
            $subscriptionModule->setGateway($this);
        }

        $this->subscriptionModule = $subscriptionModule;
        $this->webhook = new Webhook($this);
    }

    /**
     * @since 4.5.0
     */
    public static function webhook(): Webhook
    {
        $instance = new static();

        return $instance->webhook;
    }

    /**
     * @since 4.5.0
     */
    public function canListeningWebhookNotifications(): bool
    {
        return $this instanceof WebhookNotificationsListener;
    }

    /**
     * @since 2.30.0
     */
    public function supportsFormVersions(): array
    {
        $versions = [];

        if (method_exists($this, 'getLegacyFormFieldMarkup') && $this->isFunctionImplementedInGatewayClass(
                'getLegacyFormFieldMarkup'
            )) {
            $versions[] = 2;
        }

        if (method_exists($this, 'enqueueScript') && $this->isFunctionImplementedInGatewayClass('enqueueScript')) {
            $versions[] = 3;
        }

        return $versions;
    }

    /**
     * Enqueue gateway scripts using WordPress wp_enqueue_script().
     *
     * @since 2.30.0
     *
     * @return void
     */
    public function enqueueScript(int $formId)
    {
        //wp_enqueue_scripts();
    }

    /**
     * Convenient way of localizing data to the JS gateway object accessible from `this.settings`.
     *
     * @since 2.30.0
     */
    public function formSettings(int $formId): array
    {
        return [];
    }

    /**
     * @inheritDoc
     * @since 4.6.0 updated to use PaymentGatewayRefundable interface
     *
     * @since 2.29.0
     */
    public function supportsRefund(): bool
    {
        if ($this instanceof PaymentGatewayRefundable) {
            return true;
        }

        // backward compatibility for add-on legacy gateways that don't implement the PaymentGatewayRefundable interface
        return method_exists($this, 'refundDonation');
    }

    /**
     * @inheritDoc
     */
    public function supportsSubscriptions(): bool
    {
        return isset($this->subscriptionModule) || $this->isFunctionImplementedInGatewayClass('createSubscription');
    }

    /**
     * If a subscription module isn't wanted this method can be overridden by a child class instead.
     * Just make sure to override the supportsSubscriptions method as well.
     *
     * @inheritDoc
     */
    public function createSubscription(
        Donation $donation,
        Subscription $subscription,
        $gatewayData
    ) {
        return $this->subscriptionModule->createSubscription($donation, $subscription, $gatewayData);
    }

    /**
     * @inheritDoc
     */
    public function cancelSubscription(Subscription $subscription)
    {
        $this->subscriptionModule->cancelSubscription($subscription);
    }

    /**
     * @inheritDoc
     *
     * @since 3.17.0
     */
    public function pauseSubscription(Subscription $subscription, array $data = []): void
    {
        if ($this->subscriptionModule instanceof SubscriptionPausable) {
            $this->subscriptionModule->pauseSubscription($subscription, $data);

            return;
        }

        throw new Exception('Gateway does not support pausing the subscription.');
    }

    /**
     * @inheritDoc
     *
     * @since 3.17.0
     */
    public function resumeSubscription(Subscription $subscription): void
    {
        if ($this->subscriptionModule instanceof SubscriptionPausable) {
            $this->subscriptionModule->resumeSubscription($subscription);

            return;
        }

        throw new Exception('Gateway does not support resuming the subscription.');
    }

    /**
     * @inheritDoc
     *
     * @since 3.17.0
     */
    public function canPauseSubscription(): bool
    {
        if ($this->subscriptionModule instanceof SubscriptionPausable) {
            return $this->subscriptionModule->canPauseSubscription();
        }

        return false;
    }

    /**
     * @since 2.21.2
     * @inheritDoc
     */
    public function canSyncSubscriptionWithPaymentGateway(): bool
    {
        if ($this->subscriptionModule) {
            return $this->subscriptionModule->canSyncSubscriptionWithPaymentGateway();
        }

        return $this->isFunctionImplementedInGatewayClass('synchronizeSubscription');
    }

    /**
     * @since 2.21.2
     * @inheritDoc
     */
    public function canUpdateSubscriptionAmount(): bool
    {
        if ($this->subscriptionModule) {
            return $this->subscriptionModule->canUpdateSubscriptionAmount();
        }

        return $this->isFunctionImplementedInGatewayClass('updateSubscriptionAmount');
    }

    /**
     * @since 2.21.2
     * @inheritDoc
     */
    public function canUpdateSubscriptionPaymentMethod(): bool
    {
        if ($this->subscriptionModule) {
            return $this->subscriptionModule->canUpdateSubscriptionPaymentMethod();
        }

        return $this->isFunctionImplementedInGatewayClass('updateSubscriptionPaymentMethod');
    }

    /**
     * @since 2.25.0 update return logic
     * @since 2.21.2
     */
    public function hasGatewayDashboardSubscriptionUrl(): bool
    {
        if ($this->subscriptionModule) {
            return $this->subscriptionModule->hasGatewayDashboardSubscriptionUrl();
        }

        return $this->isFunctionImplementedInGatewayClass('gatewayDashboardSubscriptionUrl');
    }

    /**
     * @since 2.33.0 Return synchronizeSubscription() instead nothing
     * @since      2.21.2
     * @inheritDoc
     * @throws Exception
     */
    public function synchronizeSubscription(Subscription $subscription)
    {
        if ($this->subscriptionModule instanceof SubscriptionTransactionsSynchronizable) {
            return $this->subscriptionModule->synchronizeSubscription($subscription);
        }

        throw new Exception('Gateway does not support syncing subscriptions.');
    }

    /**
     * @since 2.21.2
     * @inheritDoc
     * @throws Exception
     */
    public function updateSubscriptionAmount(Subscription $subscription, Money $newRenewalAmount)
    {
        if ($this->subscriptionModule instanceof SubscriptionAmountEditable) {
            $this->subscriptionModule->updateSubscriptionAmount($subscription, $newRenewalAmount);

            return;
        }

        throw new Exception('Gateway does not support updating the subscription amount.');
    }

    /**
     * @since 2.21.2
     * @inheritDoc
     * @throws Exception
     */
    public function updateSubscriptionPaymentMethod(Subscription $subscription, $gatewayData)
    {
        if ($this->subscriptionModule instanceof SubscriptionPaymentMethodEditable) {
            $this->subscriptionModule->updateSubscriptionPaymentMethod($subscription, $gatewayData);

            return;
        }

        throw new Exception('Gateway does not support updating the subscription payment method.');
    }

    /**
     * @since 2.21.2
     * @inheritDoc
     */
    public function gatewayDashboardSubscriptionUrl(Subscription $subscription): string
    {
        if ($this->subscriptionModule instanceof SubscriptionDashboardLinkable) {
            return $this->subscriptionModule->gatewayDashboardSubscriptionUrl($subscription);
        }

        return false;
    }

    /**
     * Generate gateway route url
     *
     * @since 2.18.0
     * @since 2.19.0 remove $donationId param in favor of args
     */
    public function generateGatewayRouteUrl(string $gatewayMethod, array $args = []): string
    {
        return (new GenerateGatewayRouteUrl())(static::id(), $gatewayMethod, $args);
    }

    /**
     * Generate secure gateway route url
     *
     * @since 2.19.5 replace nonce with hash and expiration
     * @since 2.19.4 replace RouteSignature args with unique donationId
     * @since 2.19.0
     */
    public function generateSecureGatewayRouteUrl(string $gatewayMethod, int $donationId, array $args = []): string
    {
        $signature = new RouteSignature(static::id(), $gatewayMethod, $donationId);

        return (new GenerateGatewayRouteUrl())(
            static::id(),
            $gatewayMethod,
            array_merge($args, [
                'give-route-signature' => $signature->toHash(),
                'give-route-signature-id' => $donationId,
                'give-route-signature-expiration' => $signature->expiration,
            ])
        );
    }

    /**
     * @since 2.20.0
     */
    public function supportsMethodRoute(string $method): bool
    {
        if ($this->subscriptionModule && $this->subscriptionModule->supportsMethodRoute($method)) {
            return true;
        }

        return $this->supportsOwnMethodRoute($method);
    }

    /**
     * @since 2.20.0
     *
     * @param string $method
     *
     * @throws Exception
     */
    public function callRouteMethod($method, $queryParams)
    {
        if ($this->subscriptionModule && $this->subscriptionModule->supportsMethodRoute($method)) {
            return $this->subscriptionModule->callRouteMethod($method, $queryParams);
        }

        return $this->callOwnRouteMethod($method, $queryParams);
    }

    /**
     * Checks to see if the provided method is being used by the child gateway class. This is used as a helper in the "can" methods
     * to see if the gateway is implementing a recurring feature without using a subscription module.
     *
     * @since 2.21.2
     */
    private function isFunctionImplementedInGatewayClass(string $methodName): bool
    {
        try {
            $reflector = new ReflectionMethod($this, $methodName);
        } catch (ReflectionException $e) {
            Log::error(
                sprintf(
                    'ReflectionException thrown when trying to check if %s::%s is implemented in the gateway class.',
                    static::id(),
                    $methodName
                ),
                [
                    'exception' => $e,
                ]
            );

            return false;
        }

        return ($reflector->getDeclaringClass()->getName() === get_class($this));
    }

    /**
     * @since 4.6.0
     */
    public function getTransactionUrl(Donation $donation): ?string
    {
        $link = apply_filters('give_payment_details_transaction_id-' . $donation->gatewayId, $donation->gatewayTransactionId, $donation->id);

        // If no link is returned, return null
        if (empty($link)) {
            return null;
        }

        // Extract URL from anchor tag using regex
        if (preg_match('/href=["\']([^"\']+)["\']/', $link, $matches)) {
            return $matches[1];
        }

        // If it's already a URL (not an anchor tag), return as is
        if (filter_var($link, FILTER_VALIDATE_URL)) {
            return $link;
        }

        return null;
    }

    /**
     * @since 4.6.0
     */
    public function toArray(): array
    {
        return [
            'id' => $this->id(),
            'name' => $this->getName(),
            'label' => $this->getPaymentMethodLabel(),
        ];
    }

    /**
     * @since 4.6.0
     */
    #[\ReturnTypeWillChange]
    public function jsonSerialize()
    {
        return $this->toArray();
    }
}
