<?php

use KupShop\AdminBundle\Util\ActivityLog;
use KupShop\KupShopBundle\Config;
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
use KupShop\OrderingBundle\OrderList\OrderList;
use PayPal\Core\PayPalConstants;
use Query\Operator;
use Symfony\Component\Routing\Router;

/**
 * Dependencies: `composer require paypal/rest-api-sdk-php=^1.14`
 * Example config: $cfg['Modules']['payments']['PayPal'] = [
 *     'clientID' => 'client id',
 *     'secret' => 'client secret',
 *  webProfileID - create temporary (3 hours): symfony kupshop:paypal-create-web-profile
 *  or permanent profile: symfony kupshop:paypal-create-web-profile --permanent
 *  optional image path as an argument: symfony kupshop:paypal-create-web-profile --permanent templates/images/logo.png
 *     'webProfileID' => 'web profile id',
 *     'mode' => 'sandbox' OR 'live',
 *     'enableLog' => false,
 *   ].
 */
class PayPal extends Payment
{
    public static $name = 'PayPal platební brána';

    public $template = 'payment.PayPal.tpl';

    public $class = 'PayPal';

    public $tp_id_payment;

    public $method;

    protected $pay_method = Payment::METHOD_ONLINE;

    private $apiContext;

    protected $allowedCurrencies = ['AUD', 'BRL', 'CAD', 'CZK', 'DKK', 'EUR', 'HKD',
        'HUF', 'ILS', 'JPY', 'MYR', 'MXN', 'TWD', 'NZD', 'NOK', 'PHP', 'PLN', 'GBP',
        'RUB', 'SGD', 'SEK', 'CHF', 'THB', 'USD', ];

    public static function getSettingsConfiguration(): array
    {
        return [
            'fields' => [
                'clientID' => [
                    'title' => 'ID klienta (clientID)',
                    'type' => 'text',
                ],
                'secret' => [
                    'title' => 'Secret',
                    'type' => 'text',
                ],
                'mode' => [
                    'title' => 'Režim',
                    'type' => 'select',
                    'options' => ['live' => 'Produkční režim', 'sandbox' => 'Testovaci režim'],
                ],
                'tracking' => [
                    'title' => 'Tracking info',
                    'type' => 'toggle',
                    'tooltip' => 'Odesílat číslo balíku z e-shopu k platbě do PayPalu.',
                ],
                'webhook' => [
                    'title' => 'Webhook URL',
                    'text' => path('kupshop_ordering_payment_legacypayment', ['class' => 'PayPal', 'step' => 10], Router::ABSOLUTE_URL),
                ],
            ],
        ];
    }

    /* Payment steps */
    public function processStep_1()
    {
        $apiContext = $this->getApiContext();

        // always create new paypal payment in step 1
        //        $kupshopPayment = $this->getPendingPayment();
        $kupshopPayment = false;
        if ($kupshopPayment) {
            // use already created payment
            $payment = $this->getPayPalPayment($apiContext, $kupshopPayment['decoded_data']->session);
        } else {
            // create new payment
            $dbcfg = Settings::getDefault();
            if ($this->order->getRemainingPayment() <= 0) {
                $this->success(translate('paymentSuccess', 'payment'));
            }

            $payerInfoAddress = new \PayPal\Api\Address();
            $payerInfoAddress->setState($this->order->invoice_state ?? '')
                ->setCountryCode($this->order->invoice_country)
                ->setCity($this->order->invoice_city)
                ->setLine1($this->order->invoice_street)
                ->setLine2($this->order->invoice_custom_address ?? '')
                ->setPostalCode($this->order->invoice_zip);

            $payerInfo = new \PayPal\Api\PayerInfo();
            $payerInfo->setEmail($this->order->getUserEmail())
                ->setBillingAddress($payerInfoAddress)
                ->setFirstName($this->order->invoice_name)
                ->setLastName($this->order->invoice_surname);

            $payer = new \PayPal\Api\Payer();
            $payer->setPaymentMethod('paypal')
                ->setPayerInfo($payerInfo);

            if ($useOriginalCurrency = in_array($this->order->currency, $this->allowedCurrencies)) {
                $totalPrice = $this->order->getRemainingPayment();
            } else {
                $totalPrice = $this->convertPriceToEUR(
                    (float) $this->order->getRemainingPayment()
                );
            }

            // shipping address
            $shippingAddress = new \PayPal\Api\ShippingAddress();
            $shippingAddress->setState($this->order->delivery_state ?? '')
                ->setRecipientName(implode(' ', [$this->order->delivery_name, $this->order->delivery_surname]))
                ->setCountryCode($this->order->delivery_country)
                ->setCity($this->order->delivery_city)
                ->setLine1($this->order->delivery_street)
                ->setLine2($this->order->delivery_custom_address ?? '')
                ->setPostalCode($this->order->delivery_zip);

            // Set item list
            $item1 = new \PayPal\Api\Item();
            $item1->setName('Payment for order '.$this->getOrderNumber().' on '.$dbcfg->shop_firm_name)
                ->setCurrency($useOriginalCurrency ? $this->order->currency : 'EUR')
                ->setQuantity(1)
                ->setPrice($totalPrice);

            $itemList = new \PayPal\Api\ItemList();
            $itemList->setItems([$item1])
                ->setShippingAddress($shippingAddress);

            // to set payment details see https://developer.paypal.com/docs/api/quickstart/create-process-order/

            $amount = new \PayPal\Api\Amount();
            $amount->setCurrency($useOriginalCurrency ? $this->order->currency : 'EUR')
                ->setTotal($totalPrice);

            $transaction = new \PayPal\Api\Transaction();
            $transaction->setAmount($amount)
                ->setDescription('Payment for order '.$this->getOrderNumber().' on '.$dbcfg->shop_firm_name)
                ->setInvoiceNumber($this->getOrderNumber())
                ->setItemList($itemList);

            $redirectUrls = new \PayPal\Api\RedirectUrls();
            $redirectUrls->setReturnUrl(
                createScriptURL(
                    [
                        's' => 'payment',
                        'IDo' => $this->order->id,
                        'cf' => $this->order->getSecurityCode(),
                        'step' => 2,
                        'class' => $this->class,
                        'absolute' => true,
                    ]
                )
            )
                ->setCancelUrl(
                    createScriptURL(
                        [
                            's' => 'payment',
                            'IDo' => $this->order->id,
                            'cf' => $this->order->getSecurityCode(),
                            'step' => 4,
                            'class' => $this->class,
                            'absolute' => true,
                        ]
                    )
                );

            $payment = new \PayPal\Api\Payment();
            $payment->setIntent('sale')
                ->setPayer($payer)
                ->setRedirectUrls($redirectUrls)
                ->setTransactions([$transaction]);

            if (isset($this->config['webProfileID'])) {
                $payment->setExperienceProfileId($this->config['webProfileID']);
            }

            // nechci umoznit nastavit pri platbe dorucovaci adresu, protoze oni si ji pak nastavi a ocekavaji, ze jim
            // to prijde na tu adresu co nastavili v PayPalu. Takze bysme museli tu adresu tahat zpatky do e-shopu...
            // a jednodusi je jim tu zmenu neumoznit
            // bohuzel to musim udelat takhle osklive, protoze ta knihovna tu optionu neumoznuje nastavit nejakym setterem
            $payment->application_context = [
                // If available, uses the merchant-provided shipping address, which the customer cannot change on the PayPal pages.
                // If the merchant does not provide an address, the customer can enter the address on PayPal pages.
                'shipping_preference' => 'SET_PROVIDED_ADDRESS',
            ];

            try {
                $payment->create($apiContext);
            } catch (PayPal\Exception\PayPalConnectionException $e) {
                $data = json_decode($e->getData(), true);
                if (($data['name'] ?? false) === 'VALIDATION_ERROR') {
                    logError(__FILE__, __LINE__, 'PayPal exception: '.print_r($e->getData(), true));
                    $this->error(translate('PayPalConnectionException', 'paypal'));
                } else {
                    throw $e;
                }
            }

            // save payment
            $transaction = $payment->getTransactions()[0];
            if ($useOriginalCurrency) {
                $paymentAmount = $transaction->getAmount()->getTotal();
            } else {
                $paymentAmount = $this->order->convertPriceFromEUR($transaction->getAmount()->getTotal());
            }
            $this->createPayment(
                $payment->getId(),
                $paymentAmount,
                ['paymentClass' => self::class]
            );
        }

        // redirect user to PayPal for the payment approval
        $approvalUrl = $payment->getApprovalLink();
        redirection($approvalUrl);
    }

    /**
     * Success return from paypal - execute payment.
     */
    public function processStep_2()
    {
        logError(__FILE__, __LINE__, 'PayPal response: '.print_r([$_GET, $_POST, $_SERVER], true));

        $apiContext = $this->getApiContext();

        $payPalPaymentID = getVal('paymentId', null, false);
        $this->processPayment($this->getPayPalPayment($apiContext, $payPalPaymentID), true);
    }

    /**
     * Waiting for approval - current payment status is created.
     */
    public function processStep_3()
    {
        $apiContext = $this->getApiContext();
        $payPalPaymentID = getVal('paymentId', null, false);
        if (!$payPalPaymentID) {
            $kupshopPayment = $this->getPendingPayment();
            if ($kupshopPayment) {
                // use already created payment
                $payPalPaymentID = $kupshopPayment['decoded_data']->session;
            } else {
                $this->step(-3, 'storno');
            }
        }
        $this->processPayment($this->getPayPalPayment($apiContext, $payPalPaymentID));
    }

    /**
     * Storno.
     */
    public function processStep_4()
    {
        $apiContext = $this->getApiContext();
        $payPalPaymentID = getVal('paymentId', null, false);
        if (!$payPalPaymentID) {
            $kupshopPayment = $this->getPendingPayment();
            if ($kupshopPayment) {
                // use already created payment
                $payPalPaymentID = $kupshopPayment['decoded_data']->session;
            } else {
                $this->step(-3, 'storno');
            }
        }
        $payment = $this->getPayPalPayment($apiContext, $payPalPaymentID);
        $payment->setState('failed');
        $this->processPayment($payment);
    }

    /**
     * PayPal webhook handler - check for approved payment.
     */
    public function processStep_10()
    {
        $logger = ServiceContainer::getService('logger');
        $logger->notice('PayPal: webhook', [
            'data' => $this->request->getContent(),
        ]);

        $this->isNotification = true;

        $data = json_decode($this->request->getContent(), true);
        // only process PAYMENT.SALE.COMPLETED and payment must already exist
        if ($data['event_type'] !== 'PAYMENT.SALE.COMPLETED'
            || empty($data['resource']['parent_payment'])
        ) {
            return;
        }
        $apiContext = $this->getApiContext();
        $payPalPaymentID = $data['resource']['parent_payment'];
        $payment = $this->getPayPalPayment($apiContext, $payPalPaymentID, false);
        if (!$payment) {
            return;
        }

        if (!($orderId = $this->getOrderId($payPalPaymentID))) {
            $this->sendNotificationResponse(400, 'Order not found!');
        }

        // set order - getStatus method needs it
        $this->setOrder($orderId);
        // only proceed if payment already exists
        if (!$this->getStatus($payPalPaymentID)) {
            return;
        }
        $this->processPayment(
            $payment,
            false,
            false
        );

        $this->sendNotificationResponse(200, 'OK');
    }

    /**
     * Process payment procedure
     * (used in step 2 - after return from PayPal, step 3 - waiting for approval, step 4 - storno and step 10 - webhook).
     *
     * @throws Exception
     */
    protected function processPayment(
        PayPal\Api\Payment $payment,
        bool $enableRedirectToStep3 = false,
        bool $enableRedirects = true,
    ) {
        $this->tp_id_payment = $payment->getId();
        $paymentState = $payment->getState();

        if ($paymentState === 'created') {
            $apiContext = $this->getApiContext();

            // Execute payment with payer id
            $execution = new \PayPal\Api\PaymentExecution();
            $execution->setPayerId($payment->getPayer()->getPayerInfo()->getPayerId());

            try {
                // Execute payment
                $payment->execute($execution, $apiContext);
            } catch (Exception $ex) {
                logError(__FILE__, __LINE__, 'PayPal exception: '.$ex->getMessage());

                if ($ex instanceof \PayPal\Exception\PayPalConnectionException) {
                    logError(__FILE__, __LINE__, 'PayPal exception: '.print_r($ex->getData(), true));
                }

                $this->step(-3, 'storno');
            }
        }

        // determine kupshop unified payment state from paypal payment state
        switch ($paymentState) {
            case 'created':
                $this->status = $unifiedState = Payment::STATUS_CREATED;
                break;
            case 'approved':
                $this->status = $unifiedState = Payment::STATUS_FINISHED;
                break;
            case 'failed':
                $this->status = $unifiedState = Payment::STATUS_STORNO;
                break;
            default:
                logError(__FILE__, __LINE__, 'PayPal\Api\Payment invalid state "'.$paymentState.'"');
                if ($enableRedirects) {
                    $this->step(-3, 'storno');
                }

                return;
        }

        // change payment status
        if (!$this->setStatus($unifiedState, $this->tp_id_payment)) {
            logError(__FILE__, __LINE__, 'PayPal::updatePaymentStatus: setStatus failed!');
            throw new \Exception('Set status failed');
        }

        if ($enableRedirects) {
            switch ($unifiedState) {
                case Payment::STATUS_FINISHED:
                    if ($sale = $payment->getTransactions()[0]->getRelatedResources()[0]->getSale()) {
                        if ($transactionID = $sale->getId()) {
                            $payment_data = $this->selectSQL('order_payments', ['id' => $this->paymentId], ['payment_data'])->fetchColumn();
                            $payment_data = json_decode($payment_data, true);
                            $payment_data['transactionID'] = $transactionID;
                            $this->updateSQL('order_payments', ['payment_data' => json_encode($payment_data)], ['id' => $this->paymentId]);
                        }
                    }
                    $this->success(translate('paymentSuccess', 'payment'));
                    break;
                case Payment::STATUS_STORNO:
                    $this->step(-3, 'storno');
                    break;
                case Payment::STATUS_CREATED:
                    if ($enableRedirectToStep3) {
                        $this->step(3, 'wait', ['paymentId' => $payment->getId()]);
                    }
                    break;
            }
        }
    }

    /**
     * @return false|\PayPal\Api\Payment
     */
    protected function getPayPalPayment(
        PayPal\Rest\ApiContext $apiContext,
        $payPalPaymentID,
        bool $enableRedirects = true,
    ) {
        try {
            $payment = \PayPal\Api\Payment::get($payPalPaymentID, $apiContext);
        } catch (Exception $ex) {
            logError(__FILE__, __LINE__, 'PayPal\Api\Payment::get exception');
            if ($enableRedirects) {
                $this->step(-3, 'storno');
            }

            return false;
        }

        return $payment;
    }

    /**
     * @return \PayPal\Rest\ApiContext
     */
    public function getApiContext()
    {
        if (!isset($this->apiContext)) {
            $apiContext = new PayPal\Rest\ApiContext(
                new \PayPal\Auth\OAuthTokenCredential($this->config['clientID'], $this->config['secret'])
            );
            $apiConfig = [
                'mode' => isset($this->config['mode']) ? $this->config['mode'] : 'sandbox',
                'cache.enabled' => true,
                // 'http.CURLOPT_CONNECTTIMEOUT' => 30
                // 'http.headers.PayPal-Partner-Attribution-Id' => '123123123'
                // 'log.AdapterFactory' => '\PayPal\Log\DefaultLogFactory' // Factory class implementing \PayPal\Log\PayPalLogFactory
            ];

            if (isset($this->config['enableLog']) && $this->config['enableLog']) {
                $apiConfig['log.LogEnabled'] = true;
                $apiConfig['log.FileName'] = './PayPal.log';
                $apiConfig['log.LogLevel'] = ($apiConfig['mode'] == 'sandbox') ? 'DEBUG' : 'INFO'; // PLEASE USE `INFO` LEVEL FOR LOGGING IN LIVE ENVIRONMENTS
            }

            $apiContext->setConfig($apiConfig);
            $this->apiContext = $apiContext;
        }

        return $this->apiContext;
    }

    public function hasOnlinePayment()
    {
        return true;
    }

    public static function isEnabled($className)
    {
        $cfg = Config::get();

        if (empty($cfg['Modules']['payments'][$className])) {
            return false;
        }

        return true;
    }

    public function sendOrderTrackings(): void
    {
        if (($this->config['tracking'] ?? 0) != 1) {
            return;
        }

        $orderList = ServiceContainer::getService(OrderList::class);
        $orderList->andSpec(function (Query\QueryBuilder $qb) {
            $qb->join('o', 'delivery_type', 'dt', 'dt.id = o.id_delivery')
                ->join('dt', 'delivery_type_payment', 'dtp_paypal_filter', 'dtp_paypal_filter.id = dt.id_payment');

            return Operator::andX(
                // musi byt vyplnene tracking cislo
                'o.package_id IS NOT NULL',
                // filtruju objednavky, u kterych nebyl tracking jeste odeslany
                'JSON_VALUE(o.note_admin, "$.paypalTrackingSent") IS NULL',
                // filtruju pouze objednavky s PayPal platbou
                Operator::equals(['dtp_paypal_filter.class' => $this->class]),
                // beru v potaz objednavky za max. posledni tyden
                'o.date_handle IS NOT NULL AND o.date_handle >= DATE(NOW() - INTERVAL 7 DAY)',
                // objednavka musi byt vyrizena
                Operator::inIntArray(getStatuses('handled'), 'o.status'),
                // objednavka musi byt zaplacena
                Operator::equals(['o.status_payed' => 1]),
                // objednavka nesmi byt osobni odber
                Operator::not(\Query\Order::byInPersonDelivery())
            );
        });

        foreach ($orderList->getOrders() as $order) {
            $this->sendOrderTrackingToPayPal($order);
        }
    }

    public function sendOrderTrackingToPayPal(Order $order): bool
    {
        $apiContext = $this->getApiContext();
        $token = $apiContext->getCredential()->getAccessToken($apiContext->getConfig());
        $url = $this->config['mode'] === 'sandbox' ? PayPalConstants::REST_SANDBOX_ENDPOINT : PayPalConstants::REST_LIVE_ENDPOINT;

        [$carrier, $carrierOtherName] = $this->getOrderCarrier($order);

        $orderData = [
            'status' => 'SHIPPED',
            'carrier' => $carrier,
            'carrier_other_name' => $carrierOtherName,
            'last_update_time' => $order->date_handle->format('c'),
            'tracking_number' => $order->package_id,
        ];

        $trackers = [];

        $payments = array_filter($order->getPaymentsArray(), fn ($x) => $x['status'] == Payment::STATUS_FINISHED);
        foreach ($payments as $payment) {
            $paymentData = json_decode($payment['payment_data'] ?: '', true) ?: [];
            if (empty($paymentData['transactionID'])) {
                throw new RuntimeException('PayPal payment is missing "transactionID"! Order: '.$order->order_no);
            }

            $trackers[] = array_merge($orderData, ['transaction_id' => $paymentData['transactionID']]);
        }

        if (empty($trackers)) {
            return false;
        }

        $curl = curl_init();

        curl_setopt($curl, CURLOPT_URL, $url.'v1/shipping/trackers-batch');
        curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'POST');
        curl_setopt($curl, CURLOPT_HTTPHEADER, [
            'Content-Type: application/json',
            'Authorization: Bearer '.$token,
        ]);

        curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode(['trackers' => $trackers]));
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);

        $response = curl_exec($curl);
        $responseCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);

        curl_close($curl);

        $result = json_decode($response ?: '', true) ?: [];

        if (!in_array($responseCode, [200, 201])) {
            addActivityLog(
                ActivityLog::SEVERITY_ERROR,
                ActivityLog::TYPE_COMMUNICATION,
                '[PayPal] Nepodařila se odeslat informace o číslu balíku! Objednávka: '.$order->order_no,
                ['errors' => $result['errors'] ?? []]);

            return false;
        }

        $order->setData('paypalTrackingSent', true);
        $order->logHistory('[PayPal] Byla odeslána informace o číslu balíku z e-shopu do PayPalu.');

        return true;
    }

    protected function getOrderCarrier(Order $order): array
    {
        $delivery = null;
        if ($deliveryType = $order->getDeliveryType()) {
            $delivery = $deliveryType->getDelivery();
        }

        if ($delivery instanceof PPL) {
            return ['PPL', null];
        } elseif ($delivery instanceof DHL) {
            return ['DHL', null];
        } elseif ($delivery instanceof BalikDoRuky) {
            return ['CESKA_CZ', null];
        } elseif ($delivery instanceof GLS && $order->delivery_country === 'CZ') {
            return ['GLS_CZ', null];
        }

        return ['OTHER', $delivery->name];
    }
}
