<?php

declare(strict_types=1);

namespace Xentral\Modules\FiskalyApi\Service;

use erpAPI;
use Exception;
use DateTime;
use DateTimeZone;
use Xentral\Components\Database\Database;
use Xentral\Modules\FiskalyApi\Data\CashPointClosing\BusinessCaseCollection;
use Xentral\Modules\FiskalyApi\Data\CashPointClosing\CashPointClosing;
use Xentral\Modules\FiskalyApi\Data\CashPointClosing\CashPointClosingCashStatement;
use Xentral\Modules\FiskalyApi\Data\CashPointClosing\CashPointClosingHead;
use Xentral\Modules\FiskalyApi\Data\CashPointClosing\CashPointClosingPayment;
use Xentral\Modules\FiskalyApi\Data\Transaction\TransactionReponse;
use Xentral\Modules\FiskalyApi\Data\Transaction\TransactionReponseCollection;
use Xentral\Modules\FiskalyApi\Factory\FiskalyCashPointClosingFactory;
use Xentral\Modules\FiskalyApi\Wrapper\TaxSettingWrapper;

class FiskalyPosClosingService implements FiskalyPosClosingInterface
{
    /** @var Database $db */
    private $db;

    /** @var FiskalyCashPointClosingFactory $cashPointFactory */
    private $cashPointFactory;

    /** @var TaxSettingWrapper $taxSettingWrapper */
    private $taxSettingWrapper;

    /**
     * FiskalyPosClosingService constructor.
     *
     * @param Database                       $db
     * @param FiskalyCashPointClosingFactory $cashPointFactory
     * @param TaxSettingWrapper              $taxSettingWrapper
     */
    public function __construct(
        Database $db,
        FiskalyCashPointClosingFactory $cashPointFactory,
        TaxSettingWrapper $taxSettingWrapper
    ) {
        $this->db = $db;
        $this->cashPointFactory = $cashPointFactory;
        $this->taxSettingWrapper = $taxSettingWrapper;
    }

    public function getNextCashPointClosingExportId(string $clientId): int
    {
        return 1 + (int)$this->db->fetchValue(
                'SELECT  MAX(`cash_point_closing_export_id`) 
                FROM `fiskaly_cash_point_closing`
                WHERE `client_id` = :client_id',
                ['client_id' => $clientId]
            );
    }

    /**
     * @param string      $clientId
     * @param string|null $date
     *
     * @throws Exception
     * @return TransactionReponseCollection
     */
    public function getOpenTransactions(string $clientId, ?string $date = null): TransactionReponseCollection
    {
        $transactions = $this->db->fetchCol(
            'SELECT ft.json_response
            FROM `fiskaly_transaction` AS `ft`
            INNER JOIN `fiskaly_tranaction_mapping` AS `ftm` ON ft.id = ftm.fiskaly_transaction_id
            LEFT JOIN `fiskaly_cash_point_closing_transaction` AS `fcpct` ON ft.id = fcpct.fiskaly_transaction_id
            WHERE ft.client_id = :client_id AND fcpct.id IS NULL
            GROUP BY ft.id',
            ['client_id' => $clientId]
        );
        $instance = new TransactionReponseCollection();
        foreach ($transactions as $transactionJson) {
            $instance->addTransactionResponse(TransactionReponse::fromApiResult(json_decode($transactionJson, false)));
        }
        if ($date !== null) {
            return $instance->filterDate($date);
        }

        return $instance;
    }

    /**
     * @param string $clientId
     *
     * @throws Exception
     * @return array
     */
    public function getOpenPointClosingDates(string $clientId): array
    {
        $openTransactions = $this->getOpenTransactions($clientId);

        return $openTransactions->getTransactionDates();
    }

    /**
     * @param int    $projectId
     * @param string $date
     *
     * @return bool
     */
    public function isPosCountingExistsForDate(int $projectId, string $date): bool
    {
        return $this->db->fetchValue(
                'SELECT `id`
            FROM `pos_zaehlungen`
            WHERE `projekt` = :project_id AND DATE(`zeitstempel`) = :date
            LIMIT 1',
                [
                    'project_id' => $projectId,
                    'date'       => $date,
                ]
            ) !== false;
    }

    /**
     * @param string      $clientId
     * @param string|null $date
     *
     * @throws Exception
     * @return CashPointClosing
     */
    public function getNextCashPointClosing(string $clientId, ?string $date = null): CashPointClosing
    {
        if ($date === null) {
            $openTransactions = $this->getOpenTransactions(
                $clientId,
                (new DateTime('now', new DateTimeZone('UTC')))->format(
                    'Y-m-d'
                )
            );
        } else {
            $openTransactions = $this->getOpenTransactions($clientId, $date);
        }
        $trxs = $openTransactions->getTrxIds();
        $firstTransaction = $openTransactions->getBoundedTransactionWithClientId($clientId, true);
        $lastLastTransaction = $openTransactions->getBoundedTransactionWithClientId($clientId, false);
        $this->cashPointFactory->setTaxNormal($this->getNormalTaxForClientId($clientId));
        $paymentCollection = $this->cashPointFactory->getPaymentTypesFromTransactionCollection($openTransactions);
        $sum = $paymentCollection->getSum();
        $cashCollection = $paymentCollection->filterByType('CASH');
        $cashSum = $cashCollection->getSum();

        $posJournalEntries = $this->getPosJounralEntriesByTrxs($trxs);
        $posSessions = $this->getPosSessionsByTrxs($trxs);
        $businessCases = new BusinessCaseCollection();
        foreach ($posJournalEntries as $posJournalArray) {
            $businessCases = $businessCases->combine(
                $this->cashPointFactory->createBusinessCaseCollection($posJournalArray)
            );
        }
        $payment = new CashPointClosingPayment(
            $sum,
            $cashSum,
            $this->cashPointFactory->getCashAmountByCurrencyCollection($paymentCollection),
            $this->cashPointFactory->getCashPointClosingPaymentTypeCollectionByPosJournalCollection($posJournalEntries)
        );

        $instance = new CashPointClosing(
            $clientId, $this->getNextCashPointClosingExportId($clientId),
            new CashPointClosingHead(
                new DateTime('now', new DateTimeZone('UTC')),
                $firstTransaction->getId(),
                $lastLastTransaction->getId(),
                $date === null ? null : (new DateTime($date, new DateTimeZone('UTC')))
            ),
            new CashPointClosingCashStatement($businessCases, $payment),
            $this->cashPointFactory->getCashPointClosingTransactionCollection(
                $openTransactions,
                $posJournalEntries,
                $posSessions
            )
        );


        return $instance;
    }

    /**
     * @param array $trxs
     *
     * @return array
     */
    private function getPosJounralEntriesByTrxs(array $trxs): array
    {
        return $this->db->fetchGroup(
            "SELECT ft.trx_id, pj.*
            FROM `fiskaly_transaction` AS `ft`
            INNER JOIN `fiskaly_tranaction_mapping` AS `ftm` ON ft.id = ftm.fiskaly_transaction_id
            INNER JOIN `pos_journal` AS `pj` ON ftm.document = 'pos_journal' AND ftm.document_id = pj.id
            WHERE ft.trx_id IN (:trx_ids)",
            [
                'trx_ids' => $trxs,
            ]
        );
    }

    /**
     * @param array $trxs
     *
     * @return array
     */
    private function getPosSessionsByTrxs(array $trxs): array
    {
        $posSessions = $this->db->fetchPairs(
            "SELECT ft.trx_id, ps.data
            FROM `fiskaly_transaction` AS `ft`
            INNER JOIN `fiskaly_tranaction_mapping` AS `ftm` ON ft.id = ftm.fiskaly_transaction_id
            INNER JOIN `pos_sessions` AS `ps` ON ftm.document = 'pos_session' AND ftm.document_id = ps.id
            WHERE ft.trx_id IN (:trx_ids)",
            [
                'trx_ids' => $trxs,
            ]
        );

        $posSessions = array_map(
            static function ($posSession) {
                return unserialize($posSession, ['allowed_classes' => false]);
            },
            $posSessions
        );
        $addressIds = [];
        $cashierIds = [];
        foreach ($posSessions as $posSession) {
            $cashierId = $posSession['kassiererId'] ?? null;
            $addressId = $posSession['addrid'] ?? null;
            if ($addressId !== null && !in_array($addressId, $addressIds, true)) {
                $addressIds[] = $addressId;
            }
            if ($cashierId !== null && !in_array($cashierId, $cashierIds, true)) {
                $cashierIds[] = $cashierId;
            }
        }
        $cashierAddresses = $this->getAddressesFromCashierIds($cashierIds);

        $addesses = $this->getAddressesFromIds($addressIds);
        foreach ($posSessions as $positionKey => $posSession) {
            $posSessions[$positionKey]['address'] = !empty($addesses[$posSession['addrid'] ?? '']) ? reset(
                $addesses[$posSession['addrid']]
            ) : null;
        }
        foreach ($posSessions as $positionKey => $posSession) {
            if (!empty($posSessions[$positionKey]['address']) || empty($posSession['kassiererId'])) {
                continue;
            }
            if (empty($cashierAddresses[$posSession['kassiererId']])) {
                continue;
            }
            $posSessions[$positionKey]['address'] = reset($cashierAddresses[$posSession['kassiererId']]);
            $posSessions[$positionKey]['addrid'] = $posSessions[$positionKey]['address']['id'];
            $posSessions[$positionKey]['addr']['name'] = $posSessions[$positionKey]['address']['name'];
        }

        return $posSessions;
    }

    /**
     * @param array $addressIds
     *
     * @return array
     */
    private function getAddressesFromIds(array $addressIds): array
    {
        if (empty($addressIds)) {
            return [];
        }

        return $this->db->fetchGroup(
            "SELECT adr.id, IF(l.iso3 IS NULL OR l.iso3 = '', IF(l.iso = 'AT', 'AUT', 'DEU'), l.iso3) AS `land_iso3`,
                adr.* 
            FROM `adresse` AS `adr`
            LEFT JOIN `laender` AS `l` ON adr.land = l.iso
            WHERE adr.id IN (:address_ids)",
            ['address_ids' => $addressIds]
        );
    }

    /**
     * @param array $cashierIds
     *
     * @return array
     */
    private function getAddressesFromCashierIds(array $cashierIds): array
    {
        if (empty($cashierIds)) {
            return [];
        }

        return $this->db->fetchGroup(
            "SELECT pk.kassenkennung,
                IF(l.iso3 IS NULL OR l.iso3 = '', IF(l.iso = 'AT', 'AUT', 'DEU'), l.iso3) AS `land_iso3`, adr.* 
            FROM `adresse` AS `adr`
            INNER JOIN `pos_kassierer` AS `pk` ON adr.id = pk.adresse
            LEFT JOIN `laender` AS `l` ON adr.land = l.iso
            WHERE pk.kassenkennung IN (:cashier_ids)",
            ['cashier_ids' => $cashierIds]
        );
    }

    /**
     * @param string $clientId
     *
     * @return float
     */
    private function getNormalTaxForClientId(string $clientId): float
    {
        $projectId = $this->db->fetchValue(
            'SELECT pr.id 
            FROM `fiskaly_pos_mapping` AS `fpm` 
            INNER JOIN `projekt` AS `pr` 
            WHERE fpm.client_uuid = :client_id
            LIMIT 1',
            ['client_id' => $clientId]
        );

        return $this->taxSettingWrapper->getStandardTaxRate($projectId);
    }
}