<?php

declare (strict_types=1);
namespace Mollie\WooCommerce\Payment;

use Mollie\Inpsyde\PaymentGateway\PaymentGateway;
use Mollie\Inpsyde\PaymentGateway\PaymentProcessorInterface;
use Mollie\Api\Exceptions\ApiException;
use Mollie\Api\Resources\Payment;
use Mollie\WooCommerce\Notice\NoticeInterface;
use Mollie\WooCommerce\PaymentMethods\PaymentMethodI;
use Mollie\WooCommerce\SDK\Api;
use Mollie\WooCommerce\Settings\Settings;
use Mollie\WooCommerce\Shared\Data;
use Mollie\WooCommerce\Shared\SharedDataDictionary;
use Mollie\WooCommerce\PaymentMethods\Constants;
use Mollie\Psr\Log\LoggerInterface as Logger;
use WC_Order;
class PaymentProcessor implements PaymentProcessorInterface
{
    public const PAYMENT_METHOD_TYPE_ORDER = 'order';
    public const PAYMENT_METHOD_TYPE_PAYMENT = 'payment';
    /**
     * @var
     */
    protected $deprecatedGatewayHelper;
    /**
     * @var NoticeInterface
     */
    protected $notice;
    /**
     * @var Logger
     */
    protected $logger;
    /**
     * @var PaymentFactory
     */
    protected $paymentFactory;
    /**
     * @var Data
     */
    protected $dataHelper;
    protected $apiHelper;
    protected $settingsHelper;
    protected $pluginId;
    /**
     * @var PaymentCheckoutRedirectService
     */
    protected $paymentCheckoutRedirectService;
    private PaymentGateway $gateway;
    private array $deprecatedGatewayInstances;
    /**
     * PaymentProcessor constructor.
     */
    public function __construct(NoticeInterface $notice, Logger $logger, \Mollie\WooCommerce\Payment\PaymentFactory $paymentFactory, Data $dataHelper, Api $apiHelper, Settings $settingsHelper, string $pluginId, \Mollie\WooCommerce\Payment\PaymentCheckoutRedirectService $paymentCheckoutRedirectService, array $deprecatedGatewayInstances)
    {
        $this->notice = $notice;
        $this->logger = $logger;
        $this->paymentFactory = $paymentFactory;
        $this->dataHelper = $dataHelper;
        $this->apiHelper = $apiHelper;
        $this->settingsHelper = $settingsHelper;
        $this->pluginId = $pluginId;
        $this->paymentCheckoutRedirectService = $paymentCheckoutRedirectService;
        $this->deprecatedGatewayInstances = $deprecatedGatewayInstances;
    }
    public function setGatewayHelper(string $paymentGatewayId)
    {
        $this->deprecatedGatewayHelper = $this->deprecatedGatewayInstances[$paymentGatewayId] ?? \false;
    }
    public function setGateway(PaymentGateway $gateway)
    {
        $this->gateway = $gateway;
    }
    public function processPayment($order, $paymentGateway): array
    {
        $orderId = $order->get_id();
        $this->setGateway($paymentGateway);
        $this->setGatewayHelper($paymentGateway->id);
        if ($this->deprecatedGatewayHelper === \false) {
            return ['result' => 'failure'];
        }
        $redirectUrl = $paymentGateway->get_return_url($order);
        $paymentMethod = $this->deprecatedGatewayHelper->paymentMethod();
        $this->logger->debug("{$paymentMethod->getProperty('id')}: Start process_payment for order {$orderId}", [\true]);
        $initialOrderStatus = $this->processInitialOrderStatus($paymentMethod);
        $customerId = $this->getUserMollieCustomerId($order);
        $apiKey = $this->settingsHelper->getApiKey();
        if ($this->needsSubscriptionSwitch($order, $orderId)) {
            return $this->processSubscriptionSwitch($order, $orderId, $customerId, $apiKey);
        }
        $molliePaymentType = $this->paymentTypeBasedOnGateway($paymentMethod);
        $molliePaymentType = $this->paymentTypeBasedOnProducts($order, $molliePaymentType);
        try {
            $paymentObject = $this->paymentFactory->getPaymentObject($molliePaymentType);
        } catch (ApiException $exception) {
            return $this->paymentObjectFailure($exception);
        }
        try {
            $paymentObject = $this->processPaymentForMollie($molliePaymentType, $orderId, $paymentObject, $order, $customerId, $apiKey);
            $this->saveMollieInfo($order, $paymentObject);
            do_action($this->pluginId . '_payment_created', $paymentObject, $order);
            $this->updatePaymentStatusForDelayedMethods($paymentObject, $order, $initialOrderStatus);
            $this->reportPaymentSuccess($paymentObject, $orderId, $order, $paymentMethod);
            return ['result' => 'success', 'redirect' => $this->getProcessPaymentRedirect($paymentMethod, $order, $paymentObject, $redirectUrl)];
        } catch (ApiException $error) {
            $paymentMethodId = $paymentMethod->getProperty('id');
            $this->reportPaymentCreationFailure($orderId, $error, $paymentMethodId);
        }
        return ['result' => 'failure'];
    }
    /**
     * Redirect location after successfully completing process_payment
     *
     * @param WC_Order $order
     * @param MollieOrder|MolliePayment $paymentObject
     *
     *
     */
    public function getProcessPaymentRedirect(PaymentMethodI $paymentMethod, $order, $paymentObject, string $redirectUrl)
    {
        $this->paymentCheckoutRedirectService->setStrategy($paymentMethod);
        return $this->paymentCheckoutRedirectService->executeStrategy($paymentMethod, $order, $paymentObject, $redirectUrl);
    }
    /**
     * @param $order
     * @param $test_mode
     * @return null|string
     */
    protected function getUserMollieCustomerId($order)
    {
        $order_customer_id = $order->get_customer_id();
        $apiKey = $this->settingsHelper->getApiKey();
        return $this->dataHelper->getUserMollieCustomerId($order_customer_id, $apiKey);
    }
    protected function paymentTypeBasedOnGateway($paymentMethod)
    {
        $optionName = $this->pluginId . '_' . 'api_switch';
        $apiSwitchOption = get_option($optionName);
        $paymentType = $apiSwitchOption ?: self::PAYMENT_METHOD_TYPE_PAYMENT;
        $isBankTransferGateway = $paymentMethod->getProperty('id') === Constants::BANKTRANSFER || $paymentMethod->getProperty('id') === Constants::PAYBYBANK;
        if ($isBankTransferGateway && $paymentMethod->isExpiredDateSettingActivated()) {
            $paymentType = self::PAYMENT_METHOD_TYPE_PAYMENT;
        }
        return $paymentType;
    }
    /**
     * CHECK WOOCOMMERCE PRODUCTS
     * Make sure all cart items are real WooCommerce products,
     * not removed products or virtual ones (by WooCommerce Events Manager etc).
     * If products are virtual, use Payments API instead of Orders API
     *
     * @param \WC_Order $order
     *
     * @param string $molliePaymentType
     *
     * @return string
     */
    protected function paymentTypeBasedOnProducts($order, $molliePaymentType)
    {
        foreach ($order->get_items() as $cart_item) {
            if ($cart_item['quantity']) {
                do_action($this->pluginId . '_orderlines_process_items_before_getting_product_id', $cart_item);
                if ($cart_item['variation_id']) {
                    $product = wc_get_product($cart_item['variation_id']);
                } else {
                    $product = wc_get_product($cart_item['product_id']);
                }
                if ($product === \false) {
                    $molliePaymentType = self::PAYMENT_METHOD_TYPE_PAYMENT;
                    do_action($this->pluginId . '_orderlines_process_items_after_processing_item', $cart_item);
                    break;
                }
                do_action($this->pluginId . '_orderlines_process_items_after_processing_item', $cart_item);
            }
        }
        return $molliePaymentType;
    }
    /**
     * @param MollieOrder $paymentObject
     * @param \WC_Order $order
     * @param $customer_id
     * @param $test_mode
     *
     * @return array
     * @throws ApiException
     */
    protected function processAsMollieOrder(\Mollie\WooCommerce\Payment\MollieOrder $paymentObject, $order, $customer_id, $apiKey)
    {
        $molliePaymentType = self::PAYMENT_METHOD_TYPE_ORDER;
        $paymentRequestData = $paymentObject->getPaymentRequestData($order, $customer_id);
        $data = array_filter($paymentRequestData);
        $data = apply_filters('woocommerce_' . $this->gateway->id . '_args', $data, $order);
        do_action($this->pluginId . '_create_payment', $data, $order);
        // Create Mollie payment with customer id.
        try {
            $this->logger->debug('Creating payment object: type Order, first try creating a Mollie Order.');
            // Only enable this for hardcore debugging!
            $apiCallLog = ['amount' => isset($data['amount']) ? $data['amount'] : '', 'redirectUrl' => isset($data['redirectUrl']) ? $data['redirectUrl'] : '', 'webhookUrl' => isset($data['webhookUrl']) ? $data['webhookUrl'] : '', 'method' => isset($data['method']) ? $data['method'] : '', 'payment' => isset($data['payment']) ? $data['payment'] : '', 'locale' => isset($data['locale']) ? $data['locale'] : '', 'metadata' => isset($data['metadata']) ? $data['metadata'] : '', 'orderNumber' => isset($data['orderNumber']) ? $data['orderNumber'] : '', 'lines' => isset($data['lines']) ? $data['lines'] : ''];
            $this->logger->debug(wp_json_encode($apiCallLog));
            $paymentOrder = $paymentObject;
            $paymentObject = $this->apiHelper->getApiClient($apiKey)->orders->create($data);
            $this->logger->debug(wp_json_encode($paymentObject));
            $settingsHelper = $this->settingsHelper;
            if ($settingsHelper->getOrderStatusCancelledPayments() === 'cancelled') {
                $orderId = $order->get_id();
                $orderWithPayments = $this->apiHelper->getApiClient($apiKey)->orders->get($paymentObject->id, ["embed" => "payments"]);
                $paymentOrder->updatePaymentDataWithOrderData($orderWithPayments, $orderId);
            }
        } catch (ApiException $e) {
            $this->handleProcessingException($e);
            // Don't try to create a Mollie Payment for old Klarna payment methods
            $order_payment_method = $order->get_payment_method();
            $orderMandatoryPaymentMethods = ['mollie_wc_gateway_' . Constants::KLARNAPAYLATER, 'mollie_wc_gateway_' . Constants::KLARNASLICEIT, 'mollie_wc_gateway_' . Constants::KLARNAPAYNOW];
            if (in_array($order_payment_method, $orderMandatoryPaymentMethods, \true)) {
                $this->logger->debug('Creating payment object: type Order failed, stopping process.');
                throw $e;
            }
            $this->logger->debug('Creating payment object: type Order, first try failed: ' . $e->getMessage());
            // Unset missing customer ID
            unset($data['payment']['customerId']);
            try {
                if ($e->getField() !== 'payment.customerId') {
                    $this->logger->debug('Creating payment object: type Order, did not fail because of incorrect customerId, so trying Payment now.');
                    throw $e;
                }
                // Retry without customer id.
                $this->logger->debug('Creating payment object: type Order, second try, creating a Mollie Order without a customerId.');
                $paymentObject = $this->apiHelper->getApiClient($apiKey)->orders->create($data);
            } catch (ApiException $e) {
                // Set Mollie payment type to payment, when creating a Mollie Order has failed
                $molliePaymentType = self::PAYMENT_METHOD_TYPE_PAYMENT;
            }
        }
        return [$paymentObject, $molliePaymentType];
    }
    /**
     * @param \WC_Order $order
     * @param $customer_id
     * @param $test_mode
     *
     * @return Payment $paymentObject
     * @throws ApiException
     */
    protected function processAsMolliePayment(\WC_Order $order, $customer_id, $apiKey)
    {
        $paymentObject = $this->paymentFactory->getPaymentObject(self::PAYMENT_METHOD_TYPE_PAYMENT);
        $paymentRequestData = $paymentObject->getPaymentRequestData($order, $customer_id);
        $data = array_filter($paymentRequestData);
        $data = apply_filters('woocommerce_' . $this->gateway->id . '_args', $data, $order);
        $data = apply_filters('woocommerce_' . $this->gateway->id . 'payment_args', $data, $order);
        try {
            // Only enable this for hardcore debugging!
            $apiCallLog = ['amount' => isset($data['amount']) ? $data['amount'] : '', 'description' => isset($data['description']) ? $data['description'] : '', 'redirectUrl' => isset($data['redirectUrl']) ? $data['redirectUrl'] : '', 'webhookUrl' => isset($data['webhookUrl']) ? $data['webhookUrl'] : '', 'method' => isset($data['method']) ? $data['method'] : '', 'issuer' => isset($data['issuer']) ? $data['issuer'] : '', 'locale' => isset($data['locale']) ? $data['locale'] : '', 'dueDate' => isset($data['dueDate']) ? $data['dueDate'] : '', 'metadata' => isset($data['metadata']) ? $data['metadata'] : ''];
            $this->logger->debug(wp_json_encode($apiCallLog, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES));
            // Try as simple payment
            $paymentObject = $this->apiHelper->getApiClient($apiKey)->payments->create($data);
        } catch (ApiException $e) {
            $this->handleProcessingException($e);
            //other exceptions
            $message = $e->getMessage();
            $this->logger->debug($message);
            throw $e;
        }
        return $paymentObject;
    }
    /**
     * @param $molliePaymentType
     * @param $orderId
     * @param MollieOrder|MolliePayment $paymentObject
     * @param \WC_Order $order
     * @param $customer_id
     * @param $test_mode
     *
     * @return mixed|Payment|MollieOrder
     * @throws ApiException
     */
    protected function processPaymentForMollie($molliePaymentType, $orderId, $paymentObject, $order, $customer_id, $apiKey)
    {
        //
        // PROCESS REGULAR PAYMENT AS MOLLIE ORDER
        //
        if ($molliePaymentType === self::PAYMENT_METHOD_TYPE_ORDER) {
            // if the capture is set to manual, and this is a credit card payment, we need to create a payment instead of an order
            $captureType = get_option('mollie-payments-for-woocommerce_place_payment_onhold');
            if ($captureType === 'later_capture' && $this->gateway->id === 'mollie_wc_gateway_creditcard') {
                $this->logger->debug("{$this->gateway->id}: Create payment for order {$orderId} capture set to manual", [\true]);
                $paymentObject = $this->processAsMolliePayment($order, $customer_id, $apiKey);
                return $paymentObject;
            }
            $this->logger->debug("{$this->gateway->id}: Create Mollie payment object for order {$orderId}", [\true]);
            list($paymentObject, $molliePaymentType) = $this->processAsMollieOrder($paymentObject, $order, $customer_id, $apiKey);
        }
        //
        // PROCESS REGULAR PAYMENT AS MOLLIE PAYMENT
        //
        if ($molliePaymentType === self::PAYMENT_METHOD_TYPE_PAYMENT) {
            $this->logger->debug('Creating payment object: type Payment, creating a Payment.');
            $paymentObject = $this->processAsMolliePayment($order, $customer_id, $apiKey);
        }
        return $paymentObject;
    }
    /**
     * @param $order
     * @param $payment
     */
    protected function saveMollieInfo($order, $payment)
    {
        // Get correct Mollie Payment Object
        $payment_object = $this->paymentFactory->getPaymentObject($payment);
        // Set active Mollie payment
        $payment_object->setActiveMolliePayment($order->get_id());
        // Get Mollie Customer ID
        $mollie_customer_id = $payment_object->getMollieCustomerIdFromPaymentObject($payment_object->data()->id);
        // Set Mollie customer
        $this->dataHelper->setUserMollieCustomerId($order->get_customer_id(), $mollie_customer_id);
    }
    /**
     * @param \WC_Order $order
     * @param string $new_status
     * @param string $note
     * @param bool $restore_stock
     */
    public function updateOrderStatus(\WC_Order $order, $new_status, $note = '', $restore_stock = \true)
    {
        $order->update_status($new_status, $note);
        switch ($new_status) {
            case SharedDataDictionary::STATUS_ON_HOLD:
                if ($restore_stock === \true) {
                    if (!$order->get_meta('_order_stock_reduced', \true)) {
                        // Reduce order stock
                        wc_reduce_stock_levels($order->get_id());
                        $this->logger->debug(__METHOD__ . ":  Stock for order {$order->get_id()} reduced.");
                    }
                }
                break;
            case SharedDataDictionary::STATUS_PENDING:
            case SharedDataDictionary::STATUS_FAILED:
            case SharedDataDictionary::STATUS_CANCELLED:
                if ($order->get_meta('_order_stock_reduced', \true)) {
                    // Restore order stock
                    $this->dataHelper->restoreOrderStock($order);
                    $this->logger->debug(__METHOD__ . " Stock for order {$order->get_id()} restored.");
                }
                break;
        }
    }
    /**
     * @param $orderId
     */
    protected function noValidMandateForSubsSwitchFailure($orderId): void
    {
        $this->logger->debug($this->gateway->id . ': Subscription switch failed, no valid mandate for order #' . $orderId);
        $this->notice->addNotice('error', __('Subscription switch failed, no valid mandate found. Place a completely new order to change your subscription.', 'mollie-payments-for-woocommerce'));
        throw new ApiException(esc_html__('Failed switching subscriptions, no valid mandate.', 'mollie-payments-for-woocommerce'));
    }
    protected function subsSwitchCompleted($order): array
    {
        $order->payment_complete();
        $order->add_order_note(sprintf(__('Order completed internally because of an existing valid mandate at Mollie.', 'mollie-payments-for-woocommerce')));
        $this->logger->debug($this->gateway->id . ': Subscription switch completed, valid mandate for order #' . $order->get_id());
        return ['result' => 'success', 'redirect' => $this->gateway->get_return_url($order)];
    }
    /**
     * @param $order
     * @param string|null $customerId
     * @param $apiKey
     * @return bool
     * @throws ApiException
     */
    protected function processValidMandate($order, ?string $customerId, $apiKey): bool
    {
        $paymentObject = $this->paymentFactory->getPaymentObject(self::PAYMENT_METHOD_TYPE_PAYMENT);
        $paymentRequestData = $paymentObject->getPaymentRequestData($order, $customerId);
        $data = array_filter($paymentRequestData);
        $data = apply_filters('woocommerce_' . $this->gateway->id . '_args', $data, $order);
        $mandates = $this->apiHelper->getApiClient($apiKey)->customers->get($customerId)->mandates();
        $validMandate = \false;
        foreach ($mandates as $mandate) {
            if ($mandate->status === 'valid') {
                $validMandate = \true;
                $data['method'] = $mandate->method;
                break;
            }
        }
        return $validMandate;
    }
    protected function processSubscriptionSwitch(WC_Order $order, int $orderId, ?string $customerId, ?string $apiKey)
    {
        //
        // PROCESS SUBSCRIPTION SWITCH - If this is a subscription switch and customer has a valid mandate, process the order internally
        //
        try {
            $this->logger->debug($this->gateway->id . ': Subscription switch started, fetching mandate(s) for order #' . $orderId);
            $validMandate = $this->processValidMandate($order, $customerId, $apiKey);
            if ($validMandate) {
                return $this->subsSwitchCompleted($order);
            } else {
                $this->noValidMandateForSubsSwitchFailure($orderId);
            }
        } catch (ApiException $e) {
            if ($e->getField()) {
                throw $e;
            }
        }
        return ['result' => 'failure'];
    }
    /**
     * @param $orderId
     * @param $e
     * @param $paymentMethodId
     */
    protected function reportPaymentCreationFailure($orderId, $e, $paymentMethodId): void
    {
        $this->logger->debug($paymentMethodId . ': Failed to create Mollie payment object for order ' . $orderId . ': ' . $e->getMessage());
        /* translators: Placeholder 1: Payment method title */
        $message = sprintf(__('Could not create %s payment.', 'mollie-payments-for-woocommerce'), $paymentMethodId);
        if (defined('WP_DEBUG') && \WP_DEBUG) {
            $message .= 'hii ' . $e->getMessage();
        }
        add_action('before_woocommerce_pay_form', static function () use ($message) {
            wc_print_notice($message, 'error');
        });
    }
    /**
     * @param $paymentObject
     * @param $order
     * @param $initialOrderStatus
     */
    protected function updatePaymentStatusForDelayedMethods($paymentObject, $order, $initialOrderStatus): void
    {
        // Update initial order status for payment methods where the payment status will be delivered after a couple of days.
        // See: https://www.mollie.com/nl/docs/status#expiry-times-per-payment-method
        // Status is only updated if the new status is not the same as the default order status (pending)
        if ($paymentObject->method === Constants::BANKTRANSFER || $paymentObject->method === Constants::DIRECTDEBIT) {
            // Don't change the status of the order if it's Partially Paid
            // This adds support for WooCommerce Deposits (by Webtomizer)
            // See https://github.com/mollie/WooCommerce/issues/138
            $order_status = $order->get_status();
            if ($order_status !== 'wc-partially-paid ') {
                $this->updateOrderStatus($order, $initialOrderStatus, __('Awaiting payment confirmation.', 'mollie-payments-for-woocommerce') . "\n");
            }
        }
    }
    /**
     * @param $paymentObject
     * @param $orderId
     * @param $order
     */
    protected function reportPaymentSuccess($paymentObject, $orderId, $order, $paymentMethod): void
    {
        $paymentMethodTitle = $paymentMethod->getProperty('id');
        $this->logger->debug($paymentMethodTitle . ': Mollie payment object ' . $paymentObject->id . ' (' . $paymentObject->mode . ') created for order ' . $orderId);
        $order->add_order_note(sprintf(
            /* translators: Placeholder 1: Payment method title, placeholder 2: payment ID */
            __('%1$s payment started (%2$s).', 'mollie-payments-for-woocommerce'),
            $paymentMethodTitle,
            $paymentObject->id . ($paymentObject->mode === 'test' ? ' - ' . __('test mode', 'mollie-payments-for-woocommerce') : '')
        ));
        $this->logger->debug("For order " . $orderId . " redirect user to Mollie Checkout URL: " . $paymentObject->getCheckoutUrl());
    }
    /**
     * @param $order
     * @param $orderId
     * @return bool
     */
    protected function needsSubscriptionSwitch($order, $orderId): bool
    {
        return '0.00' === $order->get_total() && $this->dataHelper->isWcSubscription($orderId) === \true && 0 !== $order->get_user_id() && wcs_order_contains_switch($order);
    }
    /**
     * @param $exception
     * @return string[]
     */
    protected function paymentObjectFailure($exception): array
    {
        $this->logger->debug($exception->getMessage());
        return ['result' => 'failure'];
    }
    /**
     * @return mixed|void|null
     */
    protected function processInitialOrderStatus($paymentMethod)
    {
        $initialOrderStatus = $paymentMethod->getInitialOrderStatus();
        // Overwrite plugin-wide
        $initialOrderStatus = apply_filters($this->pluginId . '_initial_order_status', $initialOrderStatus);
        // Overwrite gateway-wide
        return apply_filters($this->pluginId . '_initial_order_status_' . $paymentMethod->getProperty('id'), $initialOrderStatus);
    }
    /**
     * Is an outage, bail, log and inform user
     *
     * @param ApiException $e
     * @return void
     * @throws ApiException
     */
    public function handleMollieOutage(ApiException $e): void
    {
        $this->logger->debug("Creating payment object: type Order failed due to a Mollie outage, stopping process. Check Mollie status at https://status.mollie.com/. {$e->getMessage()}");
        throw new ApiException(esc_html__('Payment failed due to: Mollie is out of service. Please try again later.', 'mollie-payments-for-woocommerce'));
    }
    /**
     * Is a fraud rejection, bail, log and inform user
     *
     * @return void
     * @throws ApiException
     */
    public function handleMollieFraudRejection(): void
    {
        $this->logger->debug("Creating payment object: The payment was declined due to suspected fraud, stopping process.");
        throw new ApiException(esc_html__('Payment failed due to:  The payment was declined due to suspected fraud.', 'mollie-payments-for-woocommerce'));
    }
    /**
     * Is a unprocessable phone number, bail, log and inform user
     *
     * @throws ApiException
     */
    private function handleUnprocessablePhone()
    {
        $this->logger->debug("Creating payment object: The phone number provided is invalid, stopping process.");
        throw new ApiException(esc_html__('Payment failed due to: Invalid phone number.', 'mollie-payments-for-woocommerce'));
    }
    /**
     * @throws ApiException
     */
    private function handleProcessingException(ApiException $e)
    {
        switch ($this->apiHelper->isMollieException($e)) {
            case 'unprocessable_phone_number':
                $this->handleUnprocessablePhone();
            //this will throw an exception and stop the process
            case 'fraud_rejection':
                $this->handleMollieFraudRejection();
            //this will throw an exception and stop the process
            case 'outage':
                $this->handleMollieOutage($e);
            //this will throw an exception and stop the process
            default:
        }
    }
}
