<?php

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 KupShop\OrderingBundle\Util\Order\OrderItemInfo;
use Psr\Log\LoggerInterface;

/**
 * Requires composer package ondrakoupil/csob-eapi-paygate.
 *
 *  V configu je nutno mít cestu privátnímu klíč merchanta (privateKeyPath), který je vygenerován CSOBckem. merchantId je taky od CSOBcka
 */
class CSOB extends Payment
{
    public static $name = 'ČSOB platební brána';

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

    public $class = 'CSOB';

    protected $pay_method = Payment::METHOD_ONLINE;
    public static bool $canAutoReturn = true;

    protected ?\OndraKoupil\Csob\Client $client = null;

    protected LoggerInterface $logger;
    protected OrderItemInfo $orderItemInfo;

    protected string $publicKeyIntegration = 'bundles/KupShop/KupShopBundle/Resources/payments/mips_iplatebnibrana.csob.cz.pub';
    protected string $publicKeyProd = 'bundles/KupShop/KupShopBundle/Resources/payments/mips_platebnibrana.csob.cz.pub';

    public function __construct()
    {
        parent::__construct();
        $this->logger = ServiceContainer::getService('logger');
        $this->orderItemInfo = ServiceContainer::getService(OrderItemInfo::class);
    }

    public static function getSettingsConfiguration(): array
    {
        return [
            'fields' => [
                'test' => [
                    'title' => 'Testovací režim',
                    'type' => 'toggle',
                ],
            ],
        ];
    }

    public function processStep_1()
    {
        $this->checkActivePaymentAlreadyExists();

        $payment = $this->initPayment();

        $url = $this->getClient()->getPaymentProcessUrl($payment);

        redirection($url);
    }

    public function initPayment()
    {
        $payment = $this->getInitPayment();

        try {
            $response = $this->getClient()->paymentInit($payment);
        } catch (Exception $e) {
            $errMessage = translate('payment_exception_communication', 'payment');
            addActivityLog(ActivityLog::SEVERITY_ERROR,
                ActivityLog::TYPE_COMMUNICATION,
                'Chyba v komunikaci s CSOB platební bránou',
                ['message' => $e->getMessage()]);
            $this->logger->error('CSOB communication error', ['exception' => $e->getMessage()]);
            throw new PaymentException($errMessage);
        }

        $payId = $payment->getPayId();

        $this->createPayment($payId, $this->order->getRemainingPayment(), ['paymentClass' => $this->class]);

        return $payment;
    }

    protected function checkActivePaymentAlreadyExists()
    {
        $activePaymentExists = 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(\Query\Operator::equals(['o.order_no' => $this->order->order_no]))
            ->andWhere(\Query\Operator::inIntArray([
                static::STATUS_CREATED,
                static::STATUS_PENDING,
            ], 'op.status'))
            ->andWhere(JsonOperator::contains('op.payment_data', 'paymentClass', $this->class))
            ->orderBy('date', 'DESC')
            ->execute()->fetchAssociative();

        $payId = $activePaymentExists['sessionId'] ?? false;

        if (empty($activePaymentExists) || !$payId) {
            return;
        }

        $client = $this->getClient();

        $paymentStatus = $client->paymentStatus($payId);
        $this->paymentChangeStatus($paymentStatus, $payId);
        if (in_array($paymentStatus, [1, 2], true)) {
            // if payment already exists and is active, dont create new payment and redirect user to the existing one
            $url = $client->getPaymentProcessUrl($payId);
            redirection($url);
        }
    }

    // Vrácení z platební brány
    public function processStep_2()
    {
        $client = $this->getClient();

        $response = $client->receiveReturningCustomer();

        $payId = $response['payId'];
        $this->paymentChangeStatus($response['paymentStatus'], $payId);

        switch ($this->status) {
            case self::STATUS_FINISHED:
                $this->info(translate('paymentSuccess', 'payment'));
                break;
            case self::STATUS_PENDING:
                $this->info(translate('payment_unexpected_status', 'payment'));
                break;
            case self::STATUS_STORNO:
                $this->info(translate('payment_rejected_status', 'payment'));
                break;
            case self::STATUS_UNKNOWN:
                $this->info(translate('payment_unexpected_status', 'payment'));
        }
    }

    /**
     * @return void
     *
     * https://github.com/csob/platebnibrana/wiki/Pr%C5%AFb%C4%9Bh-platby#user-content-%C5%BDivotn%C3%AD-cyklus-transakce-
     */
    protected function paymentChangeStatus($status, $payId)
    {
        switch ($status) {
            case 1:
            case 2:
                $this->setStatus(Payment::STATUS_PENDING, $payId);
                break;
            case 4:
            case 7:
            case 8:
                $this->setStatus(Payment::STATUS_FINISHED, $payId);
                break;
            case 3:
            case 5:
            case 6:
                $this->setStatus(Payment::STATUS_STORNO, $payId);
                break;
            case 9:
            case 10:
                // Vrácení platby;
                break;
            default:
                $this->setStatus(Payment::STATUS_UNKNOWN, $payId);
        }
    }

    protected function getInitPayment(): OndraKoupil\Csob\Payment
    {
        $payment = new \OndraKoupil\Csob\Payment($this->order->order_no);

        $payment->orderNo = $this->getPaymentReference();
        $payment->currency = $this->order->currency;
        $payment->customerId = $this->order->id_user;
        $payment->language = $this->order->id_language;

        // ///// CUSTOMER
        $customer = new \OndraKoupil\Csob\Metadata\Customer();
        $customer->name = $this->order->invoice_name.' '.$this->order->invoice_surname;
        $customer->email = $this->order->invoice_email;
        $customer->mobilePhone = $this->order->invoice_phone;

        $customerLogin = new \OndraKoupil\Csob\Metadata\Login();
        $customerLogin->auth = $this->order->id_user ? 'account' : 'guest';

        $userAccountData = $this->getUserAccountData($this->order->id_user, $this->order->invoice_email);

        $customerAccount = new \OndraKoupil\Csob\Metadata\Account();
        if ($userAccountData['createdAt'] ?? false) {
            $customerAccount->setCreatedAt($userAccountData['createdAt']);
        }

        if ($userAccountData['changedAt'] ?? false) {
            $customerAccount->setChangedAt($userAccountData['changedAt']);
        }

        $customerAccount->paymentsDay = $userAccountData['paymentsDay'] ?? 0;
        $customerAccount->paymentsYear = $userAccountData['paymentsYear'] ?? 0;

        $customer->setLogin($customerLogin);
        $customer->setAccount($customerAccount);
        $payment->setCustomer($customer);
        // /////

        $order = new \OndraKoupil\Csob\Metadata\Order();
        $order->type = 'purchase';
        $order->availability = 'now';
        $order->deliveryMode = 3;
        $order->nameMatch = ($this->order->invoice_name.$this->order->invoice_surname) == ($this->order->delivery_name.$this->order->delivery_surname);

        if ($this->order->delivery_street && $this->order->delivery_city && $this->order->delivery_zip && $this->order->delivery_country) {
            $order->setShipping(new \OndraKoupil\Csob\Metadata\Address($this->order->delivery_street,
                $this->order->delivery_city,
                $this->order->delivery_zip,
                $this->order->delivery_country));
        }

        if ($this->order->invoice_street && $this->order->invoice_city && $this->order->invoice_zip && $this->order->invoice_country) {
            $order->setBilling(new \OndraKoupil\Csob\Metadata\Address($this->order->invoice_street,
                $this->order->invoice_city,
                $this->order->invoice_zip,
                $this->order->invoice_country));
        }

        $payment->setOrder($order);

        $itemsPrice = DecimalConstants::zero();
        $deliveryPrice = DecimalConstants::zero();

        foreach ($this->order->fetchItems() as $item) {
            $price = $item['total_price']['value_with_vat'];

            if ($this->orderItemInfo->getItemType($item) == OrderItemInfo::TYPE_DELIVERY) {
                $deliveryPrice = $deliveryPrice->add($price);
            } else {
                $itemsPrice = $itemsPrice->add($price);
            }
        }

        if (!$itemsPrice->isZero()) {
            $payment->addCartItem(translate('purchaseAtShop', 'payment'), 1,
                $this->getAmountHundreds($itemsPrice));
        }

        if (!$deliveryPrice->isZero()) {
            $payment->addCartItem(translate('shipping', 'payment'), 1,
                $this->getAmountHundreds($deliveryPrice));
        }

        return $payment;
    }

    public function getPaymentReference()
    {
        $orderNo = $this->order->order_no;

        if (isLocalDevelopment()) {
            return '0'.substr($orderNo, -9);
        }

        return substr($orderNo, -10);
    }

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

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

            $dateTimeReg = new DateTime($user['date_reg']);
            $dateTimeUpdated = new DateTime($user['date_updated']);

            if ($dateTimeReg > (new DateTime('1990-01-01'))) {
                $userStats['createdAt'] = $dateTimeReg;
            }
            if ($dateTimeUpdated > (new DateTime('1990-01-01'))) {
                $userStats['changedAt'] = $dateTimeUpdated;
            }
        }

        $prevOrdersSql = sqlQueryBuilder()->select('COUNT(*)')
            ->from('order_payments', 'op')
            ->innerJoin('op', 'orders', 'o', 'o.id = op.id_order')
            ->andWhere(\Query\Operator::equals($idUser ? ['id_user' => $idUser] : ['invoice_email' => $email]))
            ->groupBy($idUser ? 'id_user' : 'invoice_email');

        $userStats['paymentsDay'] = $prevOrdersSql->andWhere('op.date >= now() - INTERVAL 1 DAY')
            ->execute()->fetchOne();

        $userStats['paymentsYear'] = $prevOrdersSql->andWhere('op.date >= now() - INTERVAL 1 YEAR')
            ->execute()->fetchOne();

        return $userStats;
    }

    public function getClient($skipCache = false): OndraKoupil\Csob\Client
    {
        if (!$skipCache && $this->client) {
            return $this->client;
        }

        $pathFinder = \KupShop\KupShopBundle\Util\System\PathFinder::getService();

        if ($this->config['test'] ?? false) {
            $publicKeyPath = $pathFinder->enginePath($this->publicKeyIntegration);
            $gatewayUrl = \OndraKoupil\Csob\GatewayUrl::TEST_1_9;
        } else {
            $publicKeyPath = $pathFinder->enginePath($this->publicKeyProd);
            $gatewayUrl = \OndraKoupil\Csob\GatewayUrl::PRODUCTION_1_9;
        }

        $domainContext = \KupShop\KupShopBundle\Util\Contexts::get(\KupShop\KupShopBundle\Context\DomainContext::class);

        $config = new \OndraKoupil\Csob\Config(
            $this->config['merchantId'],
            $this->config['privateKeyPath'],
            $publicKeyPath,
            $this->config['merchantName'] ?? $domainContext->getActiveWithScheme(),
            // Adresa, kam se mají zákazníci vracet poté, co zaplatí
            $this->getGenericPaymentUrl(2),
            // URL adresa API - výchozí je adresa testovacího (integračního) prostředí,
            // až budete připraveni přepnout se na ostré rozhraní, sem zadáte
            // adresu ostrého API. Nezapomeňte také na ostrý veřejný klíč banky.
            $gatewayUrl
        );

        return $this->client = new \OndraKoupil\Csob\Client($config);
    }

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

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

        $client = $this->getClient();

        $paymentStatus = $client->paymentStatus($payId);

        switch ($paymentStatus) {
            case 4:
            case 7:
                if ($payment['price'] + $amount > PHP_FLOAT_EPSILON) {
                    $message = translate('returnFailedOnlyFullAmountWhenNotCharged', 'orderPayment');
                    throw new PaymentException($message);
                }

                $response = $client->paymentReverse($payId);
                break;
            case 8:
                $amountDecimals = ($payment['price'] + $amount > PHP_FLOAT_EPSILON) ? $this->getAmountHundreds(toDecimal($amount)) : null;
                $response = $client->paymentRefund($payId, false, $amountDecimals);
                break;
        }

        if (($response['resultCode'] ?? 1) != 0) {
            $message = translate('returnFailed', 'orderPayment');
            addActivityLog(\KupShop\AdminBundle\Util\ActivityLog::SEVERITY_ERROR,
                \KupShop\AdminBundle\Util\ActivityLog::TYPE_COMMUNICATION,
                $message,
                ['amount' => $amount, 'payment_id' => $payment['payment_data']['session'], 'RESPONSE' => $response]);

            throw new PaymentException($message);
        }

        return $response;
    }

    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,
                static::STATUS_UNKNOWN,
            ], 'op.status'))
            ->andWhere('op.date > (DATE_SUB(CURDATE(), INTERVAL 1 MONTH))')
            ->andWhere(\Query\Operator::inStringArray([$this->class],
                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']);
            $client = $this->getClient(true);
            $payId = $paymentData['session'];
            $paymentStatus = $client->paymentStatus($payId);
            $this->paymentChangeStatus($paymentStatus, $payId);
        }
    }

    protected function getAmountHundreds(Decimal $amount)
    {
        return $amount->mul(DecimalConstants::hundred())->abs()->asFloat();
    }

    public function hasOnlinePayment()
    {
        return true;
    }

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

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

        return true;
    }
}
