<?php

declare(strict_types=1);

use KupShop\AdminBundle\Util\ActivityLog;
use KupShop\KupShopBundle\Config;
use KupShop\KupShopBundle\Query\JsonOperator;
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
use KupShop\OrderingBundle\Exception\PaymentException;
use Query\Operator;

/**
 * Implementace pouze pro "Immediate Capture"
 * Klient musí mít v twisto portalu povoleno "Okamžité odbavení" (Immediate Capture).
 */
class TwistoPay extends Payment
{
    public static $name = 'Twisto Pay';

    public $class = 'TwistoPay';

    protected ?string $defaultIcon = '../../common/static/payments/twisto_pay.svg';

    public static bool $canAutoReturn = true;

    protected string $apiUrl = 'https://api.twisto.cz/psp/smi';

    protected $pay_method = Payment::METHOD_ONLINE;

    public function getPaymentUrl()
    {
        return createScriptURL([
            's' => 'payment',
            'IDo' => $this->order->id,
            'cf' => $this->order->getSecurityCode(),
            'step' => 1,
            'class' => $this->class,
            'absolute' => true,
        ]);
    }

    public function processStep_1()
    {
        if ($paymentExists = $this->checkPaymentAlreadyExists()) {
            if (empty($paymentExists['sessionId'])) {
                $this->info(translate('payment_already_exists', 'payment'));
            }
            $redirectExists = $this->requestCheckoutData($paymentExists['sessionId']);
            if (empty($redirectExists['uri'])) {
                $this->info(translate('payment_already_exists', 'payment'));
            }
            redirection($redirectExists['uri']);
        }

        $checkoutData = $this->getCheckoutData();

        $redirectData = $this->getCheckoutRedirect($checkoutData);

        if (empty($redirectData['id']) || empty($redirectData['uri'])) {
            $this->createApiException(['message' => 'ID or URI not received', 'id' => $redirectData['id'], 'uri' => $redirectData['uri']]);
        }

        $this->createPayment($redirectData['id'], $redirectData['order']['amount'], ['paymentClass' => $this->class]);

        redirection($redirectData['uri']);
    }

    protected function checkPaymentAlreadyExists()
    {
        return sqlQueryBuilder()->select("op.id, JSON_UNQUOTE(JSON_EXTRACT(op.payment_data, '$.session')) sessionId")->from('order_payments', 'op')
            ->innerJoin('op', 'orders', 'o', 'o.id = op.id_order')
            ->where(Operator::equals(['o.order_no' => $this->order->order_no]))
            ->andWhere(Operator::orX(
                JsonOperator::contains('op.payment_data', 'paymentClass', 'TwistoPay'),
                JsonOperator::contains('op.payment_data', 'paymentClass', 'TwistoPayIn3')
            ))->execute()->fetchAssociative();
    }

    public function processStep_2()
    {
        $checkoutId = getVal('checkout_id');
        $status = getVal('status');

        switch ($status) {
            case 'captured':
            case 'authorized':
                $this->setStatus(Payment::STATUS_FINISHED, $checkoutId);
                $this->info(translate('paymentSuccess', 'payment'));
                break;
            case 'rejected':
                $this->setStatus(Payment::STATUS_STORNO, $checkoutId);
                $this->info(translate('payment_rejected_status', 'payment'));
                // no break
            case 'error':
                $this->setStatus(Payment::STATUS_STORNO, $checkoutId);
                $this->info(translate('payment_unexpected_status', 'payment'));
        }
    }

    protected function getCheckoutRedirect($checkoutData)
    {
        $response = $this->requestCurl('/checkouts', json_encode($checkoutData));

        return json_decode($response, true);
    }

    protected function getCheckoutData()
    {
        $items = [];
        foreach ($this->order->fetchItems() as $item) {
            $items[] = $this->getCheckoutItem($item);
        }

        $this->setItemsRoundingDiscount($items);

        $requestBody = [
            'type' => $this->getCheckoutType(),
            'config' => [
                'redirect_uri' => $this->getGenericPaymentUrl(2),
            ],
            'shopper' => $this->removeEmptyValues([
                'first_name' => $this->order->invoice_name,
                'last_name' => $this->order->invoice_surname,
                'phone' => $this->order->invoice_phone,
                'billing_address' => $this->removeEmptyValues([
                    'line1' => $this->order->invoice_street,
                    'line2' => $this->order->invoice_custom_address,
                    'city' => $this->order->invoice_city,
                    'state' => $this->order->invoice_state,
                    'postal_code' => $this->order->invoice_zip,
                    'country' => $this->order->invoice_country,
                ]),
                'statistics' => $this->getCheckoutUserStatistics($this->order->id_user, $this->order->invoice_email),
                //                "personal_id" => $this->order->id_user ?? '',
                'email' => $this->order->invoice_email,
            ]),
            'order' => $this->removeEmptyValues([
                'reference' => $this->order->order_no,
                'amount' => $this->getOrderPrice(),
                'currency' => $this->order->getTotalPrice()->getCurrency()->getId(),
                'items' => $items,
            ]),
        ];

        /** @var LoggerInterface $logger */
        $logger = ServiceContainer::getService('logger');
        $logger->notice('Twisto - objednavka', [
            'Request' => $requestBody,
        ]);

        return $requestBody;
    }

    protected function removeEmptyValues(array $array): array
    {
        foreach ($array as $key => $value) {
            if ((is_string($value) && !$value) || is_null($value)) {
                unset($array[$key]);
            }
        }

        return $array;
    }

    protected function getCheckoutUserStatistics($idUser, string $email): array
    {
        $userStats = [];

        if ($idUser) {
            $user = sqlQueryBuilder()->select('*')->from('users')->where(Operator::equals(['id' => $this->order->id_user]))
                ->execute()->fetchAssociative();

            if ($user['date_reg'] && $user['date_reg'] !== '0000-00-00 00:00:00') {
                $userStats['account_created'] = (new DateTime($user['date_reg']))->format('Y-m-d\TH:i:s\Z');
            }
            $userStats['currency'] = $user['currency'];
        }

        $prevOrdersStats = sqlQueryBuilder()->select('AVG(o.total_price) avg, SUM(o.total_price) sum, MAX(o.total_price) max, COUNT(*) count')->from('orders',
            'o')
            ->where(Operator::equals($idUser ? ['id_user' => $idUser] : ['invoice_email' => $email]))
            ->andWhere('status_storno = 0')
            ->andWhere(Operator::inIntArray(getStatuses('handled'), 'o.status'))
            ->groupBy($idUser ? 'id_user' : 'invoice_email')
            ->execute()->fetchAssociative() ?: [];

        $stats = ($prevOrdersStats['count'] ?? false) ? [
            'sales_total_count' => $prevOrdersStats['count'] ?? 0,
            'sales_total_amount' => $prevOrdersStats['sum'] ?? 0,
            'sales_avg_amount' => $prevOrdersStats['avg'] ?? 0,
            'sales_max_amount' => $prevOrdersStats['max'] ?? 0,
            'has_previous_purchases' => true,
        ] : ['has_previous_purchases' => false];

        // skipped fields
        // "refunds_total_amount" => "string",
        // "previous_chargeback" => true,
        // "last_login" => "2019-08-24T14:15:22Z",
        return array_merge($userStats, $stats);
    }

    protected function getCheckoutItem(array $item): array
    {
        if (($item['note']['item_type'] ?? '') == 'delivery') {
            $type = 'shipping';
        } elseif (($item['note']['item_type'] ?? '') == 'discount') {
            $type = 'discount';
        } elseif (!empty($item['note']['bonus_points'])) {
            $type = 'store_credit';
        } else {
            $type = 'sku';
        }

        return [
            'name' => $item['descr'],
            'amount' => $item['piece_price']['value_with_vat']->asFloat(),
            'quantity' => (int) $item['pieces'],
            'type' => $type,
        ];
    }

    protected function setItemsRoundingDiscount(&$items)
    {
        $itemsPrice = array_sum(array_map(function ($item) {
            return $item['amount'] * $item['quantity'];
        }, $items));

        $difference = $this->getOrderPrice() - $itemsPrice;
        if (abs($difference) > PHP_FLOAT_EPSILON && abs($difference) < 10) {
            $items[] = [
                'name' => 'Rounding discount',
                'amount' => round($difference, 3),
                'quantity' => 1,
                'type' => ($difference < 0) ? 'discount' : 'sku',
            ];
        }
    }

    protected function getOrderPrice(): float
    {
        return $this->order->getTotalPrice()->getPriceWithVat()->asFloat();
    }

    public function createPayment($session, $price = null, $data = [])
    {
        $fields = [
            'id_order' => $this->orderId,
            'price' => $price,
            'note' => "Platba modulu {$this->class}",
            'status' => self::STATUS_CREATED,
            'payment_data' => json_encode(array_merge($data, ['session' => $session])),
            'date' => date('Y-m-d H:i:s'),
            'method' => self::METHOD_ONLINE,
        ];

        $this->insertSQL('order_payments', $fields);

        $this->status = self::STATUS_CREATED;
        $this->paymentId = (int) sqlInsertId();

        $this->order->updatePayments();

        return true;
    }

    protected function requestCheckoutData($checkoutId)
    {
        $response = $this->requestCurl('/checkouts/'.$checkoutId, '', false);

        return json_decode($response, true);
    }

    protected function requestRefund($checkoutId, $amount, $reference)
    {
        $body = $this->removeEmptyValues([
            'checkout_id' => $checkoutId,
            'amount' => abs($amount),
            'reference' => $reference,
        ]);

        return $this->requestCurl('/refunds', json_encode($body));
    }

    protected function requestCheckoutCancel($checkoutId)
    {
        return $this->requestCurl("/checkouts/{$checkoutId}/cancel", '');
    }

    protected function requestCurl(string $path, string $encodedBody, $post = true)
    {
        $url = $this->getApiUrl().$path;

        $ch = curl_init();

        if ($post) {
            curl_setopt($ch, CURLOPT_POST, true);
            curl_setopt($ch, CURLOPT_POSTFIELDS, $encodedBody);
        }

        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'Authorization: Bearer '.$this->getAccessToken(),
            'Content-Type: application/json',
        ]);

        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_TIMEOUT, 20);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

        $response = curl_exec($ch);
        $responseInfo = curl_getinfo($ch);
        $curl_errno = curl_errno($ch);
        curl_close($ch);

        if ($responseInfo['http_code'] >= 300 || $responseInfo['http_code'] === 0) {
            $debugInfo = ['url' => $url, 'responseInfo' => $responseInfo, 'curl_errno' => $curl_errno, 'RESPONSE' => $response];
            $this->createApiException($debugInfo);
        }

        return $response;
    }

    protected function createApiException($debugInfo)
    {
        $errMessage = 'Komunikace s platební bránou Twisto selhala. Zkuste prosím znovu později.';
        addActivityLog(ActivityLog::SEVERITY_ERROR, ActivityLog::TYPE_COMMUNICATION, $errMessage, $debugInfo);
        getRaven()->captureMessage('Twisto API error', [], ['extra' => $debugInfo]);
        throw new PaymentException($errMessage);
    }

    protected function createWebhookError($httpCode, $debugInfo, $responseText = '')
    {
        $debugInfo = is_array($debugInfo) ? $debugInfo : [$debugInfo];
        $errMessage = 'Twisto webhook se nepodařilo zpracovat.';
        addActivityLog(ActivityLog::SEVERITY_ERROR, ActivityLog::TYPE_COMMUNICATION, $errMessage, $debugInfo);
        getRaven()->captureMessage('Twisto webhook error', [], ['extra' => $debugInfo]);
        $this->sendNotificationResponse($httpCode, $responseText);
    }

    /**
     * Webhook handler.
     */
    public function processStep_10()
    {
        if (!$this->request->isMethod('POST')) {
            $this->sendNotificationResponse(405, '');
        }

        try {
            $requestBody = $this->request->toArray();
        } catch (Exception $e) {
            $this->createWebhookError(422, $this->request->getContent(), 'Unreadable JSON');
        }

        $checkoutId = getVal('checkout_id', $requestBody);
        $state = getVal('state', $requestBody);

        if (!$checkoutId) {
            $this->createWebhookError(400, array_merge($requestBody, ['err' => 'missing checkout id']), '');
        }

        $idOrder = $this->getOrderId($checkoutId);

        if (!$idOrder) {
            $this->sendNotificationResponse(400, 'Order not found');
        }

        $this->setOrder($idOrder);
        $this->getStatus($checkoutId);
        $this->updateStatus($state, $checkoutId);

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

    public function checkPaidOrders()
    {
        $orderPayments = sqlQueryBuilder()->select('op.id, op.id_order, op.payment_data')
            ->from('order_payments', 'op')
            ->where(\Query\Operator::inIntArray([
                static::STATUS_CREATED,
                static::STATUS_PENDING,
            ], 'op.status'))
            ->andWhere('op.date > (DATE_SUB(CURDATE(), INTERVAL 1 MONTH))')
            ->andWhere(Operator::inStringArray(['TwistoPay', 'TwistoPayIn3'],
                JsonOperator::value('op.payment_data', 'paymentClass')))
            ->andWhere(JsonOperator::exists('op.payment_data', 'session'))
            ->execute();

        foreach ($orderPayments as $orderPayment) {
            $paymentData = json_decode($orderPayment['payment_data'], true);
            if (empty($paymentData['session'])) {
                continue;
            }

            $this->setOrder($orderPayment['id_order']);
            $checkoutId = $paymentData['session'];

            $response = $this->requestCheckoutData($checkoutId);

            $this->updateStatus($response['state'], $checkoutId);
        }
    }

    protected function updateStatus($state, $checkoutId)
    {
        switch ($state) {
            case 'created':
                $this->setStatus(Payment::STATUS_PENDING, $checkoutId);
                break;
            case 'approved':
            case 'completed':
                $this->setStatus(Payment::STATUS_FINISHED, $checkoutId);
                break;
            case 'cancelled':
            case 'error':
                $this->setStatus(Payment::STATUS_STORNO, $checkoutId);
        }
    }

    public function doReturnPayment(array $payment, float $amount)
    {
        if ($payment['status'] != Payment::STATUS_FINISHED) {
            throw new PaymentException(translate('returnFailed', 'orderPayment'));
        }

        $checkoutId = $payment['payment_data']['session'] ?? null;

        if (!$checkoutId) {
            throw new PaymentException('Payment does not have assigned checkout_id');
        }

        $checkoutData = $this->requestCheckoutData($checkoutId);

        switch ($checkoutData['state']) {
            case 'completed':
                $reference = $checkoutData['order']['reference'].'_'.$payment['id'].'_'.rand();
                $response = $this->requestRefund($checkoutId, $amount, $reference);
                break;
            case 'created':
            case 'approved':
                if ($checkoutData['order']['amount'] + $amount > PHP_FLOAT_EPSILON) {
                    $message = translate('returnFailedOnlyFullAmount', 'orderPayment');
                    addActivityLog(ActivityLog::SEVERITY_ERROR,
                        ActivityLog::TYPE_COMMUNICATION,
                        $message,
                        ['amount' => $amount, 'payment_id' => $checkoutData, 'RESPONSE' => $checkoutData['state']]);
                    throw new PaymentException($message);
                }
                $response = $this->requestCheckoutCancel($checkoutId);
                break;
            default:
                $message = translate('returnFailedInvalidState', 'orderPayment').": {$checkoutData['state']}";
                addActivityLog(ActivityLog::SEVERITY_ERROR,
                    ActivityLog::TYPE_COMMUNICATION,
                    $message,
                    ['amount' => $amount, 'payment_id' => $checkoutData, 'RESPONSE' => $checkoutData['state']]);
                throw new PaymentException($message);
        }

        return ['response' => $response];
    }

    public static function getSettingsConfiguration(): array
    {
        return [
            'fields' => [
                'token' => [
                    'title' => 'Token',
                    'type' => 'text',
                ],
                'webhook' => [
                    'title' => 'Webhook url',
                    'text' => path('kupshop_ordering_payment_legacypayment', ['class' => 'TwistoPay', 'step' => 10], \Symfony\Component\Routing\Router::ABSOLUTE_URL),
                ],
            ],
        ];
    }

    protected function getCheckoutType(): string
    {
        return 'standard';
    }

    public function hasOnlinePayment()
    {
        return true;
    }

    protected function getApiUrl(): string
    {
        return $this->apiUrl;
    }

    protected function getAccessToken(): string
    {
        return $this->config['token'] ?? '';
    }

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

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

        return true;
    }
}
