<?php

namespace KupShop\SKEETBundle\Util;

use KupShop\KupShopBundle\Config;
use KupShop\KupShopBundle\Util\System\PathFinder;
use NineDigit\eKasa\Client\AccessTokenSource;
use NineDigit\eKasa\Client\ApiClient;
use NineDigit\eKasa\Client\ApiClientAuthenticationAccessTokenOptions;
use NineDigit\eKasa\Client\ApiClientAuthenticationOptions;
use NineDigit\eKasa\Client\ApiClientOptions;
use NineDigit\eKasa\Client\ApiRequestBuilder;
use NineDigit\eKasa\Client\EKasaServer;
use NineDigit\eKasa\Client\Exceptions\ProblemDetailsException;
use NineDigit\eKasa\Client\Exceptions\ValidationProblemDetailsException;
use NineDigit\eKasa\Client\HttpClient;
use NineDigit\eKasa\Client\HttpClientInterface;
use NineDigit\eKasa\Client\Models\QuantityDto;
use NineDigit\eKasa\Client\Models\Registrations\Receipts\PosReceiptPrinterOptions;
use NineDigit\eKasa\Client\Models\Registrations\Receipts\PosRegisterReceiptPrintContextDto;
use NineDigit\eKasa\Client\Models\Registrations\Receipts\ReceiptBuilder;
use NineDigit\eKasa\Client\Models\Registrations\Receipts\ReceiptItemDto;
use NineDigit\eKasa\Client\Models\Registrations\Receipts\ReceiptItemType;
use NineDigit\eKasa\Client\Models\Registrations\Receipts\ReceiptPaymentDto;
use NineDigit\eKasa\Client\Models\Registrations\Receipts\RegisterReceiptRequestContextDto;
use NineDigit\eKasa\Client\Models\Registrations\Receipts\RegisterReceiptRequestDto;
use Psr\Log\LoggerInterface;
use Query\Operator;

class SKEETWorker
{
    private ?LoggerInterface $logger = null;
    private ?\Order $order = null;
    private array $ALLOWED_PAYMENT_METHODS = [
        \Payment::METHOD_CASH,
        \Payment::METHOD_CARD,
        \Payment::METHOD_EET_INVOICE_CASH,
        \Payment::METHOD_EET_INVOICE_CARD,
    ];
    private $dbcfg;
    private ?HttpClientInterface $httpClient = null;
    private ?string $orp = null;
    private int $cashLimit = 1000;
    private int $cardLimit = 1600;

    /**
     * @throws ProblemDetailsException
     * @throws ValidationProblemDetailsException
     * @throws \Exception
     */
    public function processPosPayment(): void
    {
        $this->checkRequirements();
        $client = $this->setupApiClient();

        $correction = 0;
        $items = $this->getItemReceiptCollection(1, null, $correction);
        if ($this->dbcfg->skeet['testing'] == 'testing') {
            $receipt = ReceiptBuilder::invalid($this->orp, $items);
        } else {
            $receipt = ReceiptBuilder::cashRegister($this->orp, $items);
        }

        if ($correction != 0) {
            $receipt->setRoundingAmount($correction);
        }

        $payments = [];
        $openDrawer = false;
        foreach ($this->order->getPaymentsArray() as $payment) {
            $paymentObject = $this->createReceiptPaymentDto($payment['id']);
            if (!empty($paymentObject)) {
                $receipt->addPayment($paymentObject['object']);
                if (!(($payment['price'] > $this->cashLimit && $payment['method'] == \Payment::METHOD_CASH) || ($payment['price'] > $this->cardLimit && $payment['method'] == \Payment::METHOD_CARD))) {
                    $openDrawer = true;
                }
                $payments[] = $paymentObject;
            }
        }

        $this->processEETCommunication($client, $receipt, $payments, $openDrawer);
    }

    /**
     * @throws \Exception
     */
    public function openEETDrawer(): void
    {
        $this->checkRequirements(true);
        $options = $this->setupApiClient(true);

        //        TODO: ideálně odkomentovat a zakomentovat spodní curl (aktuálně na jejich curl padá CURLOPT_HTTP09_ALLOWED)
        //        $httpClient = new HttpClient($options);
        //        $httpClient->send(ApiRequestBuilder::createPost('/v1/printers/open_drawer')->build());

        $ch = curl_init();

        $params = [];
        if ($options->authentication->accessToken ?? false) {
            $params['access_token'] = $options->authentication->accessToken->value;
        }

        curl_setopt($ch, CURLOPT_URL, "{$options->url}/v1/printers/open_drawer?".http_build_query($params));
        curl_setopt($ch, CURLOPT_POST, 1);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

        $server_output = curl_exec($ch);
        $this->logger?->notice('SK EET: open drawer (response from server)', ['data' => json_encode($server_output)]);

        curl_close($ch);
    }

    public function processReturnPaymentOfTheOrder($returnedPayments): void
    {
        $this->checkRequirements();
        $client = $this->setupApiClient();

        $firstPayment = sqlQueryBuilder()
            ->select('*')
            ->from('sk_eet_payment')
            ->andWhere(Operator::equals(['id_order' => $this->order->id]))
            ->andWhere('uid is not null')
            ->setMaxResults(1)
            ->execute()
            ->fetchAssociative();

        if (empty($firstPayment['uid'])) {
            throw new \Exception('Původní platba nejspíš nebyla přes EET, nemáme její ID, nelze stornovat přes EET!');
        }

        $correction = 0;
        $items = $this->getItemReceiptCollection(-1, $firstPayment['uid'], $correction);
        $receipt = ReceiptBuilder::cashRegister($this->orp, $items);

        if ($correction != 0) {
            $receipt->setRoundingAmount($correction);
        }

        $paymentNames = [\Payment::METHOD_CARD => 'Karta', \Payment::METHOD_CASH => 'Hotovosť'];
        $payments = [];
        $openDrawer = false;
        foreach ($returnedPayments as $payment) {
            $paymentObject = new ReceiptPaymentDto(floatval($payment['price'] * -1), $paymentNames[$payment['method']]);
            sqlQueryBuilder()
                ->insert('sk_eet_payment')
                ->setValue('date_sent', 'NOW()')
                ->directValues([
                    'id_order' => $payment['id_order'],
                    'id_payment' => $payment['id_payment'],
                ])->execute();

            $receipt->addPayment($paymentObject);
            if (!(($payment['price'] > $this->cashLimit && $payment['method'] == \Payment::METHOD_CASH) || ($payment['price'] > $this->cardLimit && $payment['method'] == \Payment::METHOD_CARD))) {
                $openDrawer = true;
            }

            $payments[] = ['id_eet' => sqlInsertId(), 'object' => $paymentObject];
        }

        $this->processEETCommunication($client, $receipt, $payments, $openDrawer, 'R_'.$this->getExternalIds($payments));
    }

    /**
     * @throws \Exception
     */
    public function processPaymentOfTheInvoice($payment): void
    {
        $this->checkRequirements();
        $client = $this->setupApiClient();

        if ($payment['method'] == \Payment::METHOD_EET_INVOICE_CASH || $payment['method'] == \Payment::METHOD_EET_INVOICE_CARD) {
            if (empty($this->order->invoice_no)) {
                throw new \Exception('Nepodařilo se najít fakturu.');
            }
            $receipt = ReceiptBuilder::invoice($this->orp, (int) $this->order->invoice_no, toDecimal($payment['price'])->asFloat());

            // Jedná se vždy o jednu platbu -> sjednocení s vícero platbama
            $payments = [$this->createReceiptPaymentDto($payment['id'])];

            // Napáruje se platba k pokladně
            if (empty($this->dbcfg->skeet['pos_invoice_payment_relation'])) {
                throw new \Exception('SK EET: Není vybrána pokladna k napárování.');
            }
            sqlQueryBuilder()
                ->insert('pos_payments_relation')
                ->directValues([
                    'id_pos' => $this->dbcfg->skeet['pos_invoice_payment_relation'],
                    'id_payment' => $payment['id'],
                ])->execute();

            $openDrawer = !(($payment['price'] > $this->cashLimit && $payment['method'] == \Payment::METHOD_CASH) || ($payment['price'] > $this->cardLimit && $payment['method'] == \Payment::METHOD_CARD));
            $this->processEETCommunication($client, $receipt, $payments, $openDrawer, 'PI_'.$this->getExternalIds([$payment['id'] => $payment['id']]));
        }
    }

    /**
     * @throws \Exception
     */
    private function checkRequirements($skipOrderCheck = false): void
    {
        if ($this->dbcfg === null) {
            $this->dbcfg = \Settings::getDefault();
        }

        if (!$this->order && !$skipOrderCheck) {
            throw new \Exception('SK EET není nastavena objednávka.');
        }

        if (empty($this->dbcfg->skeet['active']) || $this->dbcfg->skeet['active'] != 'Y' || empty($this->dbcfg->skeet['subdomain']) || empty($this->dbcfg->skeet['access_token']) || empty($this->dbcfg->skeet['orp'])) {
            throw new \Exception('SK EET není nastaveno, nebo povoleno.');
        }
    }

    private function setupApiClient($returnOprions = false): ApiClient|HttpClientInterface|ApiClientOptions
    {
        $url = EKasaServer::exposeDefault($this->dbcfg->skeet['subdomain']);

        $this->cashLimit = $this->dbcfg->skeet['print_cash_limit'] ?? 1000;
        $this->cardLimit = $this->dbcfg->skeet['print_card_limit'] ?? 1600;

        if (isFunctionalTests() || isLocalDevelopment()) {
            $url = 'http://192.168.43.89:3010/api';
            //            $url = EKasaServer::LOCALHOST;
            $authentication = null;
            $this->orp = '88812345678900001';
        } else {
            $accessTokenSource = AccessTokenSource::queryString('access_token');
            $accessTokenOptions = new ApiClientAuthenticationAccessTokenOptions($this->dbcfg->skeet['access_token'], $accessTokenSource);
            $authentication = new ApiClientAuthenticationOptions(null, $accessTokenOptions);
            $this->orp = $this->dbcfg->skeet['orp'];
        }

        $clientOptions = new ApiClientOptions($url, $authentication);

        if ($returnOprions) {
            return $clientOptions;
        }

        if ($this->httpClient) {
            return $this->httpClient;
        } else {
            return new ApiClient($clientOptions);
        }
    }

    private function processEETCommunication($client, $receipt, array $payments, $openDrawer = false, $externalId = null): void
    {
        // Nastavení tiskárny (pro invalid dokument se nepoužívá)
        if ($this->dbcfg->skeet['testing'] == 'testing') {
            $print = null;
        } else {
            $posPrinterOptions = new PosReceiptPrinterOptions();
            $posPrinterOptions->openDrawer = $openDrawer;
            $print = new PosRegisterReceiptPrintContextDto($posPrinterOptions);
        }

        // Je sem pridany parametr, protoze to potrebujeme customizovat z nekterejch akci
        // - pri vraceni penez nemuze prijit stejny ID jako pri placeni - IDObj_IDPlatby, je to pak totiz duplicitni v EET
        if (!$externalId) {
            $externalId = $this->getExternalIds($payments);
        }

        $receipt->setHeaderText($this->dbcfg->shop_title)->setFooterText('Ďakujeme za váš nákup.');
        $request = new RegisterReceiptRequestDto($receipt->build(), $externalId);
        $requestContext = new RegisterReceiptRequestContextDto($print, $request);

        $cfg = Config::get();
        // Zastaví komunikaci na locale
        if (isLocalDevelopment() && !isFunctionalTests() && empty($cfg['skeet_enabled'])) {
            return;
        }

        $this->logger?->notice('SK EET: request', ['data' => serialize($requestContext)]);
        // Naprdet, proste to tak bude
        if (isLocalDevelopment()) {
            $pathFinder = new PathFinder();

            file_put_contents($pathFinder->getDataDir().'skeet_request.json', json_encode($requestContext, JSON_PRETTY_PRINT));
        }
        try {
            $result = $client->registerReceipt($requestContext);
        } catch (\Exception $e) {
            if (isLocalDevelopment()) {
                file_put_contents($pathFinder->getDataDir().'skeet_response_error.json', json_encode($e->details, JSON_PRETTY_PRINT));
            }
            $this->logger?->notice('SK EET: response exception', ['data' => json_encode($e->details, JSON_PRETTY_PRINT)]);
            throw $e;
        }

        $this->logger?->notice('SK EET: response', ['data' => serialize($result)]);
        if (isLocalDevelopment()) {
            file_put_contents($pathFinder->getDataDir().'skeet_response.json', json_encode($result));
        }
        $eetIds = array_column($payments, 'id_eet');
        if (($result->isSuccessful ?? false) && $result->isSuccessful === true) {
            // Doklad bol úspešne spracovaný v režíme ON-LINE
            sqlQueryBuilder()
                ->update('sk_eet_payment')
                ->setValue('date_confirmed', 'NOW()')
                ->directValues([
                    'uid' => $result->response->data->id ?? null,
                ])
                ->andWhere(Operator::inStringArray($eetIds, 'id'))
                ->execute();
        } elseif (($result->isSuccessful ?? false) && $result->isSuccessful === null) {
            $eetIdsString = join(',', $eetIds);
            throw new \Exception("SK EET: Odpověď z eKasy pro platby ({$eetIdsString}) k objednávce ({$this->order->id}) provedena v OFF-LINE režimu.");
        } else {
            $eetIdsString = join(',', $eetIds);
            throw new \Exception("SK EET: Odpověď z eKasy pro platby ({$eetIdsString}) k objednávce ({$this->order->id}) skončila s chybou.");
        }
    }

    /** @throws \Exception */
    public function createReceiptPaymentDto($id_payment): ?array
    {
        $payment = sqlQueryBuilder()
            ->select('op.method, op.status, esp.id as id_eet, op.id_order, price, op.id as id_payment')
            ->from('order_payments', 'op')
            ->leftJoin('op', 'sk_eet_payment', 'esp', 'esp.id_payment = op.id')
            ->where(Operator::equals(['op.id' => $id_payment]))
            ->sendToMaster()
            ->execute()
            ->fetch();

        if (!$payment) {
            throw new \Exception('Neexistuje žádná platba!');
        }

        if (in_array($payment['method'], $this->ALLOWED_PAYMENT_METHODS) && empty($payment['id_eet'])) {
            sqlQueryBuilder()
                ->insert('sk_eet_payment')
                ->setValue('date_sent', 'NOW()')
                ->directValues([
                    'id_order' => $payment['id_order'],
                    'id_payment' => $payment['id_payment'],
                ])->execute();
            $payment['id_eet'] = sqlInsertId();

            $paymentNames = [
                \Payment::METHOD_CARD => 'Karta',
                \Payment::METHOD_CASH => 'Hotovosť',
                \Payment::METHOD_EET_INVOICE_CASH => 'Úhrada faktury - hotově',
                \Payment::METHOD_EET_INVOICE_CARD => 'Úhrada faktury - kartou',
            ];

            return ['id_eet' => $payment['id_eet'], 'object' => new ReceiptPaymentDto(toDecimal($payment['price'])->asFloat(), $paymentNames[$payment['method']])];
        }

        return null;
    }

    private function getItemReceiptCollection($multiplier = 1, $uid = null, ?float &$correctionFloat = null): array
    {
        $items = [];
        $sumForCorrection = \DecimalConstants::zero();

        foreach ($this->order->fetchItems() as $item) {
            $quantity = floatval($item['pieces']);
            $unitPrice = $item['piece_price']['value_with_vat']->mul(toDecimal($multiplier))->asFloat();
            $price = $item['piece_price']['value_with_vat']->mul(toDecimal($item['pieces']))->mul(toDecimal($multiplier))->asFloat();

            if ($multiplier == -1) {                            // Vyrovnání
                $type = ReceiptItemType::CORRECTION;
            } elseif (floatval($item['pieces']) < 0) {         // Vrácení
                $type = ReceiptItemType::RETURNED;
                // U Vrácení požadují mít kusy kladné a jednotkovou cenu zápornou -> prohodím znaménka
                $unitPrice = $unitPrice * -1;
                $quantity = $quantity * -1;
                if (!$uid) {
                    $uid = 'Vrácení přes pokladnu';
                }
            } elseif ($item['total_price']['value_with_vat']->isNegative()) { // Sleva
                $type = ReceiptItemType::DISCOUNT;
            } else {                                            // Standardní položka
                $type = ReceiptItemType::POSITIVE;
            }

            $items[] = new ReceiptItemDto(
                type: $type,                                        // Kladný typ položky
                name: $item['descr'],                               // Názov
                unitPrice: $unitPrice,                              // Jednotková cena
                vatRate: floatval($item['vat']),                    // Daňová hladina
                quantity: new QuantityDto($quantity, 'ks'),   // Množstvo
                price: $price,                                      // Cena
                description: null,
                seller: null,
                taxFreeReason: null,
                voucherNumber: null,
                referenceReceiptId: $uid
            );

            $sumForCorrection = $sumForCorrection->add($item['piece_price']['value_with_vat']->mul(toDecimal($item['pieces']))->mul(toDecimal($multiplier)));
        }

        if ($multiplier == -1) {
            $correction = $this->order->total_price->add($sumForCorrection);
        } else {
            $correction = $this->order->total_price->sub($sumForCorrection);
        }

        if (!$correction->isZero()) {
            $correctionFloat = $correction->asFloat();
        }

        return $items;
    }

    public function getExternalIds($payments): string
    {
        return $this->order->id.'_'.join('_', array_keys($payments));
    }

    /** @required */
    public function setLogger(LoggerInterface $logger): void
    {
        $this->logger = $logger;
    }

    public function setOrder(\Order $order): void
    {
        $this->order = $order;
    }

    public function setHttpClient(?HttpClientInterface $httpClient): void
    {
        $this->httpClient = $httpClient;
    }
}
