<?php

declare(strict_types=1);

namespace Plugin\tnt_coupons;

use DateTimeImmutable;
use JTL\Cart\CartHelper;
use JTL\Catalog\Category\MenuItem;
use JTL\Catalog\Product\Artikel;
use JTL\Checkout\Kupon;
use JTL\DB\DbInterface;
use JTL\DB\ReturnType;
use JTL\Helpers\Category;
use JTL\Helpers\Product;
use JTL\Language\LanguageHelper;
use JTL\Plugin\PluginInterface;
use JTL\Session\Frontend;
use JTL\Shop;
use JTL\Smarty\JTLSmarty;
use JTL\Statistik;
use Plugin\tnt_coupons\src\Helper\DateWindowTrigger;
use Plugin\tnt_coupons\src\Helper\WeekdayScheduleTrigger;
use stdClass;

use function array_filter;
use function date_create_immutable;
use function Functional\none;
use function in_array;
use function is_array;
use function is_object;
use function is_scalar;
use function mb_strpos;

use const C_WARENKORBPOS_TYP_ARTIKEL;

/**
 * @phpstan-type CouponRow stdClass&object{
 *   kKupon: int,
 *   kKundengruppe: int|string|null,
 *   cArtikel: string,
 *   cKategorien: string,
 *   cHersteller: string,
 *   cKunden: string,
 *   nVerwendungen: int|string|null,
 *   nVerwendungenBisher: int|string|null,
 *   nVerwendungenProKunde: int|string|null,
 *   fMindestbestellwert: float|int|numeric-string|null,
 *   cLieferlaender: string,
 *   cAktiv: string,
 *   dGueltigAb: string,
 *   dGueltigBis: string,
 *   cWertTyp: string,
 *   fWert: float|int|numeric-string|null,
 *   nGanzenWKRabattieren: int|string|null,
 *   cCode: string,
 *   cName: string,
 *   textra: string,
 *   localizedName: string,
 *   Titel: string,
 *   Gespart: float|int|numeric-string|null
 * }
 * @phpstan-type CouponHintData stdClass&object{hints: list<string>}
 * @phpstan-type CouponStatsPoint stdClass&object{dZeit: string, nCount: numeric-string|int|float}
 * @phpstan-type CouponStatsRow stdClass&object{
 *   nVerwendungenBisher: int,
 *   dGueltigAb: string,
 *   dGueltigBis: string,
 *   cName: string,
 *   Gespart: float,
 *   totalOrders: int,
 *   umsatzSale: list<CouponStatsPoint>,
 *   umsatzDavor: list<CouponStatsPoint>,
 *   dGueltigAbFormat: string,
 *   dGueltigBisFormat: string,
 *   chart: string
 * }
 * @phpstan-type ProductRow stdClass&object{cName: string, cSeo: string, cName_translated: string, cSeo_translated: string}
 * @phpstan-type CountdownData stdClass&object{days: int|string, hours: int|string, minutes: int|string, seconds: int|string}
 *
 * Legacy class adapted from JTL core coupon handling; size/complexity
 * baseline accepted until the roadmap refactor:
 * @SuppressWarnings("PHPMD")
 */
class Sales
{
    private DbInterface $db;
    private PluginInterface $extension;

    /** @var CouponRow */
    private object $coupon;

    private ?JTLSmarty $smarty = null;
    private ?string $currency_symbol;

    public function __construct(PluginInterface $extension, DbInterface $db)
    {
        $this->extension       = $extension;
        $this->db              = $db;
        $this->coupon          = $this->createEmptyCoupon();
        $this->currency_symbol = Frontend::getCurrency()->getHtmlEntity();
    }

    private function smarty(): JTLSmarty
    {
        return $this->smarty ??= Shop::Smarty();
    }

    private function stringify(mixed $value): string
    {
        return is_scalar($value) ? (string)$value : '';
    }

    private function parseDateTime(?string $value): ?DateTimeImmutable
    {
        if ($value === null || $value === '') {
            return null;
        }

        $date = date_create_immutable($value);

        return $date instanceof DateTimeImmutable ? $date : null;
    }

    private function parseDateTimeFormat(string $format, string $value): ?DateTimeImmutable
    {
        $date = DateTimeImmutable::createFromFormat($format, $value);

        return $date instanceof DateTimeImmutable ? $date : null;
    }

    /** @return CouponRow */
    private function createEmptyCoupon(): object
    {
        return (object)[
            'kKupon'                => 0,
            'kKundengruppe'         => '',
            'cArtikel'              => '',
            'cKategorien'           => '',
            'cHersteller'           => '',
            'cKunden'               => '',
            'nVerwendungen'         => 0,
            'nVerwendungenBisher'   => 0,
            'nVerwendungenProKunde' => 0,
            'fMindestbestellwert'   => 0.0,
            'cLieferlaender'        => '',
            'cAktiv'                => '',
            'dGueltigAb'            => '',
            'dGueltigBis'           => '',
            'cWertTyp'              => '',
            'fWert'                 => 0.0,
            'nGanzenWKRabattieren'  => 0,
            'cCode'                 => '',
            'cName'                 => '',
            'textra'                => '',
            'localizedName'         => '',
            'Titel'                 => '',
            'Gespart'               => 0.0,
        ];
    }

    /** @param object $row @return CouponRow */
    private function normalizeCoupon(object $row): object
    {
        $coupon                        = $this->createEmptyCoupon();
        $coupon->kKupon                = (int)($row->kKupon ?? 0);
        $coupon->kKundengruppe         = $row->kKundengruppe ?? '';
        $coupon->cArtikel              = $this->stringify($row->cArtikel ?? '');
        $coupon->cKategorien           = $this->stringify($row->cKategorien ?? '');
        $coupon->cHersteller           = $this->stringify($row->cHersteller ?? '');
        $coupon->cKunden               = $this->stringify($row->cKunden ?? '');
        $coupon->nVerwendungen         = $row->nVerwendungen ?? 0;
        $coupon->nVerwendungenBisher   = $row->nVerwendungenBisher ?? 0;
        $coupon->nVerwendungenProKunde = $row->nVerwendungenProKunde ?? 0;
        $coupon->fMindestbestellwert   = $row->fMindestbestellwert ?? 0.0;
        $coupon->cLieferlaender        = $this->stringify($row->cLieferlaender ?? '');
        $coupon->cAktiv                = $this->stringify($row->cAktiv ?? '');
        $coupon->dGueltigAb            = $this->stringify($row->dGueltigAb ?? '');
        $coupon->dGueltigBis           = $this->stringify($row->dGueltigBis ?? '');
        $coupon->cWertTyp              = $this->stringify($row->cWertTyp ?? '');
        $coupon->fWert                 = $row->fWert ?? 0.0;
        $coupon->nGanzenWKRabattieren  = $row->nGanzenWKRabattieren ?? 0;
        $coupon->cCode                 = $this->stringify($row->cCode ?? '');
        $coupon->cName                 = $this->stringify($row->cName ?? ($row->Titel ?? ''));
        $coupon->textra                = $this->stringify($row->textra ?? '');
        $coupon->localizedName         = $this->stringify($row->localizedName ?? '');
        $coupon->Titel                 = $this->stringify($row->Titel ?? $coupon->cName);
        $coupon->Gespart               = $row->Gespart ?? 0.0;

        return $coupon;
    }

    /** @return CouponStatsRow */
    private function createEmptyStats(): object
    {
        return (object)[
            'nVerwendungenBisher' => 0,
            'dGueltigAb'          => '',
            'dGueltigBis'         => '',
            'cName'               => '',
            'Gespart'             => 0.0,
            'totalOrders'         => 0,
            'umsatzSale'          => [],
            'umsatzDavor'         => [],
            'dGueltigAbFormat'    => '',
            'dGueltigBisFormat'   => '',
            'chart'               => '',
        ];
    }

    /**
     * @param mixed $row
     * @return CouponStatsPoint
     */
    private function normalizeStatsPoint(mixed $row): object
    {
        $count = is_object($row) ? ($row->nCount ?? 0) : 0;
        if (!is_int($count) && !is_float($count) && !is_string($count)) {
            $count = 0;
        }

        $point = (object)[
            'dZeit'  => is_object($row) ? $this->stringify($row->dZeit ?? '') : '',
            'nCount' => $count,
        ];

        /** @var CouponStatsPoint $point */
        return $point;
    }

    /**
     * @param mixed $rows
     * @return list<CouponStatsPoint>
     */
    private function normalizeStatsSeries(mixed $rows): array
    {
        if (!is_array($rows)) {
            return [];
        }

        /** @var list<CouponStatsPoint> $result */
        $result = [];
        foreach ($rows as $row) {
            /** @var CouponStatsPoint $point */
            $point    = $this->normalizeStatsPoint($row);
            $result[] = $point;
        }

        return $result;
    }

    /** @param object $row @return CouponStatsRow */
    private function normalizeStats(object $row): object
    {
        $stats                     = $this->createEmptyStats();
        $stats->nVerwendungenBisher = (int)($row->nVerwendungenBisher ?? 0);
        $stats->dGueltigAb          = $this->stringify($row->dGueltigAb ?? '');
        $stats->dGueltigBis         = $this->stringify($row->dGueltigBis ?? '');
        $stats->cName               = $this->stringify($row->cName ?? ($row->Titel ?? ''));
        $stats->Gespart             = (float)($row->Gespart ?? 0.0);

        return $stats;
    }

    /** @param CouponRow $coupon */
    private function toKupon(object $coupon): Kupon
    {
        $kupon                        = new Kupon();
        $kupon->kKupon                = $coupon->kKupon;
        $kupon->kKundengruppe         = (int)$coupon->kKundengruppe;
        $kupon->cName                 = $coupon->cName;
        $kupon->fWert                 = $coupon->fWert;
        $kupon->cWertTyp              = $coupon->cWertTyp;
        $kupon->dGueltigAb            = $coupon->dGueltigAb;
        $kupon->dGueltigBis           = $coupon->dGueltigBis;
        $kupon->fMindestbestellwert   = $coupon->fMindestbestellwert;
        $kupon->cCode                 = $coupon->cCode;
        $kupon->nVerwendungen         = (int)$coupon->nVerwendungen;
        $kupon->nVerwendungenBisher   = (int)$coupon->nVerwendungenBisher;
        $kupon->nVerwendungenProKunde = (int)$coupon->nVerwendungenProKunde;
        $kupon->cArtikel              = $coupon->cArtikel;
        $kupon->cHersteller           = $coupon->cHersteller;
        $kupon->cKategorien           = $coupon->cKategorien;
        $kupon->cKunden               = $coupon->cKunden;
        $kupon->cLieferlaender        = $coupon->cLieferlaender;
        $kupon->cAktiv                = $coupon->cAktiv;
        $kupon->nGanzenWKRabattieren  = (int)$coupon->nGanzenWKRabattieren;

        return $kupon;
    }

    /** @phpstan-return CountdownData */
    private function createCountdown(DateTimeImmutable $target): stdClass
    {
        $timeRemaining = $target->diff(new DateTimeImmutable());
        /** @var CountdownData $tmp */
        $tmp           = new stdClass();
        $tmp->days     = $timeRemaining->days;
        $tmp->hours    = $timeRemaining->h;
        $tmp->minutes  = $timeRemaining->i;
        $tmp->seconds  = $timeRemaining->s;
        if ($tmp->days < 10) {
            $tmp->days = '0' . $tmp->days;
        }
        if ($tmp->hours < 10) {
            $tmp->hours = '0' . $tmp->hours;
        }
        if ($tmp->minutes < 10) {
            $tmp->minutes = '0' . $tmp->minutes;
        }
        if ($tmp->seconds < 10) {
            $tmp->seconds = '0' . $tmp->seconds;
        }

        return $tmp;
    }

    /**
     * @param CouponRow $data
     * @phpstan-return CouponHintData
     */
    public function convertToSale(object $data, bool $assign = false, int $showLimit = 10): stdClass
    {
        /** @var CouponHintData $frD */
        $frD        = new stdClass();
        $frD->hints = [];
        $baseURL    = Shop::getUrl();
        if (\str_ends_with($baseURL, '/') === false) {
            $baseURL .= '/';
        }
        $lz         = $this->extension->getLocalization();
        $langHelper = LanguageHelper::getInstance();
        $langISO    = $langHelper->gibISO();
        if (
            $data->kKundengruppe !== ''
            && $data->kKundengruppe !== '-1'
            && (int)$data->kKundengruppe !== Frontend::getCustomerGroup()->getID()
        ) {
            $frD->hints[] = $lz->getTranslation('couponErr5', 'global');
        }
        $showAllApplicableProducts = $this->getConfig($this->coupon, 'tnt_cp_check11') === 'Y';
        if ($data->cArtikel !== '' && $data->cArtikel !== '-1') {
            $artikel  = '';
            $inClause = substr(str_replace(';', '\',\'', $data->cArtikel), 2, -2);
            $products = $this->getProducts($inClause, $langHelper->kSprache);
            if ($showAllApplicableProducts === false) {
                $products = array_slice($products, 0, 10);
            }
            foreach ($products as $pr) {
                $cSeo  = $pr->cSeo;
                $cName = $pr->cName;
                if (\strlen($cSeo) <= 0) {
                    $cSeo = $pr->cSeo_translated;
                }
                if (\strlen($cName) <= 0) {
                    $cName = $pr->cName_translated;
                }
                if ($showLimit === 0) {
                    if (\str_ends_with($artikel, ', ')) {
                        $artikel = \substr($artikel, 0, -2);
                    }
                    $artikel .= "<a class='d-inline-block mx-2' data-toggle='collapse' href='#clSaleArtikel'"
                        . " role='button' aria-expanded='false' aria-controls='clSaleArtikel'>...</a>";
                    $artikel .= "<div class='collapse' id='clSaleArtikel'>";
                }
                $artikel .= "<a href='" . $baseURL . $cSeo . "' target='_blank' class='text-dark'>"
                    . \strip_tags($cName) . '</a>, ';
                $showLimit--;
            }
            if (substr($artikel, -2) === ', ') {
                $artikel = substr($artikel, 0, -2);
            }
            if ($showLimit <= 0) {
                $artikel .= '</div>';
            }
            $frD->hints[] = vsprintf(
                $lz->getTranslation('tnt_coupons_lang_hints_cArtikel', $langISO) ?? '',
                [$artikel]
            );
        }
        if ($data->cKategorien !== '' && $data->cKategorien !== '-1') {
            $kategorien = $this->recurseCats($data->cKategorien, Category::getInstance()->combinedGetAll());
            if (\str_ends_with($kategorien, ', ')) {
                $kategorien = \substr($kategorien, 0, -2);
            }
            $frD->hints[] = \vsprintf(
                $lz->getTranslation(
                    'tnt_coupons_lang_hints_cKategorien',
                    $langISO
                ) ?? '',
                [$kategorien]
            );
        }
        if ($data->cHersteller !== '' && $data->cHersteller !== '-1') {
            $filter       = Shop::getProductFilter();
            $manufacturer = $filter->getManufacturerFilter();
            $hersteller   = '';
            foreach ($manufacturer->getOptions() as $o) {
                $val = $o->getValue();
                if (!is_scalar($val)) {
                    continue;
                }
                $manufacturerValue = (string)$val;
                if (\str_contains($data->cHersteller, $manufacturerValue . ';')) {
                    $hersteller .= "<a href='" . $o->getURL() . "' target='_blank'>"
                        . \strip_tags((string)$o->getName()) . '</a>, ';
                }
            }
            if (\str_ends_with($hersteller, ', ')) {
                $hersteller = \substr($hersteller, 0, -2);
            }
            $frD->hints[] = \vsprintf(
                $lz->getTranslation(
                    'tnt_coupons_lang_hints_cHersteller',
                    $langISO
                ) ?? '',
                [$hersteller]
            );
        }
        if ($data->cKunden !== '' && $data->cKunden !== '-1') {
            $frD->hints[] = $langHelper->getTranslation('couponErr9', 'global');
        }
        if ((int)$data->nVerwendungen > 0) {
            $frD->hints[] = \vsprintf(
                $lz->getTranslation('tnt_coupons_lang_hints_nVerwendungen', $langISO) ?? '',
                [$data->nVerwendungen]
            );
            if ((int)$data->nVerwendungen <= (int)$data->nVerwendungenBisher) {
                $frD->hints[] = $langHelper->getTranslation('couponErr6', 'global');
            }
        }
        if (!empty($data->nVerwendungenProKunde) && (int)$data->nVerwendungenProKunde > 0) {
            $frD->hints[] = \vsprintf(
                $lz->getTranslation('tnt_coupons_lang_hints_nVerwendungenProKunde', $langISO) ?? '',
                [$data->nVerwendungenProKunde]
            );
        }
        if ((int)$data->fMindestbestellwert > 0) {
            $frD->hints[] = vsprintf(
                $lz->getTranslation('tnt_coupons_lang_hints_fMindestbestellwert', $langISO) ?? '',
                [$data->fMindestbestellwert . $this->currency_symbol]
            );
        }
        if ($data->cLieferlaender !== '' && $data->cLieferlaender !== '-1') {
            $frD->hints[] = \vsprintf(
                $lz->getTranslation('tnt_coupons_lang_hints_cLieferlaender', $langISO) ?? '',
                [\str_replace(';', ', ', $data->cLieferlaender)]
            );
        }
        if ($assign) {
            $this->smarty()->assign('tnt_coupons_frD', $frD);
        }
        return $frD;
    }

    /** @param CouponRow $coupon */
    private function validateCoupon(object $coupon, Artikel $artikel): bool
    {
        if ($coupon->cAktiv !== 'Y') {
            return false;
        }

        $now        = new DateTimeImmutable();
        $validUntil = $this->parseDateTime($coupon->dGueltigBis);
        if ($validUntil !== null && $validUntil < $now) {
            return false;
        }

        $validFrom = $this->parseDateTime($coupon->dGueltigAb);
        if ($validFrom === null || $validFrom > $now) {
            return false;
        }
        if ($coupon->cArtikel !== '' && $coupon->cArtikel !== '-1') {
            if (
                empty($artikel->cArtNr)
                || \str_contains($coupon->cArtikel, $artikel->cArtNr . ';') === false
            ) {
                return false;
            }
        }
        if ($coupon->cKategorien !== '' && $coupon->cKategorien !== '-1') {
            $categories = [];
            $productID  = (int)$artikel->kArtikel;
            if (Product::isVariChild($productID)) {
                $productID = Product::getParent($productID);
            }
            $categoryIDs = $this->db->selectAll(
                'tkategorieartikel',
                'kArtikel',
                $productID,
                'kKategorie'
            );
            foreach ($categoryIDs as $categoryID) {
                $categoryID->kKategorie = (int)$categoryID->kKategorie;
                if (!in_array($categoryID->kKategorie, $categories, true)) {
                    $categories[] = $categoryID->kKategorie;
                }
            }
            $return_false = true;
            foreach ($categories as $category) {
                if (mb_strpos($coupon->cKategorien, $category . ';') !== false) {
                    $return_false = false;
                    break;
                }
            }
            if ($return_false) {
                return false;
            }
        }
        if ($coupon->cHersteller !== '' && $coupon->cHersteller !== '-1') {
            if (
                (!empty($artikel->kHersteller)
                    && mb_strpos($coupon->cHersteller, $artikel->kHersteller . ';') === false)
                || !isset($artikel->kHersteller)
                || empty($artikel->kHersteller)
            ) {
                return false;
            }
        }
        if ($coupon->cKunden !== '' && $coupon->cKunden !== '-1') {
            if (
                (!empty($_SESSION['Kunde']->kKunde)
                    && mb_strpos($coupon->cKunden, $_SESSION['Kunde']->kKunde . ';') === false)
                || !isset($_SESSION['Kunde']->kKunde)
                || empty($_SESSION['Kunde']->kKunde)
            ) {
                return false;
            }
        }
        if (
            $coupon->kKundengruppe !== ''
            && $coupon->kKundengruppe !== '-1'
            && (int)$coupon->kKundengruppe !== Frontend::getCustomerGroup()->getID()
        ) {
            return false;
        }
        if ($coupon->nVerwendungen > 0 && $coupon->nVerwendungen <= $coupon->nVerwendungenBisher) {
            return false;
        }
        if (!empty($coupon->nVerwendungenProKunde) && $coupon->nVerwendungenProKunde > 0) {
            if (isset($_SESSION['Kunde'])) {
                $countCouponUsed = Shop::Container()->getDB()->getSingleObject(
                    'SELECT nVerwendungen
							FROM tkuponkunde
							WHERE kKupon = :coupon
							AND cMail = :email',
                    [
                        'coupon' => (int)$coupon->kKupon,
                        'email' => Kupon::hash($_SESSION['Kunde']->cMail)
                    ]
                );
                if ($countCouponUsed !== null && $countCouponUsed->nVerwendungen >= $coupon->nVerwendungenProKunde) {
                    return false;
                }
            }
        }
        return true;
    }

    private function discountPrice(Artikel &$artikel): void
    {
        $coupons = $this->getCoupons();
        $this->setCoupon($coupons);
        if (isset($this->coupon->kKupon) && $this->validateCoupon($this->coupon, $artikel)) {
            $showDiscounts = $this->getConfig($this->coupon, 'tnt_cp_check4');
            if (
                $showDiscounts !== ''
                && (int)$this->coupon->fWert > 0
            ) {
                $artHasDiscount = true;
                if ($this->coupon->cArtikel !== '' && $this->coupon->cArtikel !== '-1') {
                    $artHasDiscount = false;
                    if (
                        !empty($artikel->cArtNr)
                        && \str_contains($this->coupon->cArtikel, $artikel->cArtNr . ';')
                    ) {
                        $artHasDiscount = true;
                    }
                }
                $preise = $artikel->Preise;
                if ($artHasDiscount && $preise !== null) {
                    if ($this->coupon->cWertTyp === 'prozent') {
                        $discount = ((float)$this->coupon->fWert * (float)$preise->fVKBrutto) / 100;
                        $newPrice = (float)$preise->fVKBrutto - $discount;
                    } else {
                        $discount = (float)$this->coupon->fWert;
                        $newPrice = (float)$preise->fVKBrutto - $discount;
                    }
                    $newPrice = \number_format(
                        \round($newPrice, 2),
                        2,
                        ',',
                        '.'
                    ) . ' ' . $this->currency_symbol;

                    $discount = \number_format(
                        \round($discount, 2),
                        2,
                        ',',
                        '.'
                    ) . ' ' . $this->currency_symbol;

                    $_SESSION['tnt_coupons_product'][$artikel->kArtikel] = (object)[
                        'new_price' => $newPrice,
                        'discount'  => $discount,
                    ];
                }
            }
        }
    }

    /**
     * @param array{oArtikel: Artikel} $param
     */
    public function fuelleArtikel(array $param): void
    {
        $this->discountPrice($param['oArtikel']);
    }

    /**
     * @param array{oArtikel: Artikel} $param
     */
    public function artikelseite(array $param): void
    {
        $this->discountPrice($param['oArtikel']);
    }

    /**
     * Plugin-level display window (independent of the coupon's own JTL validity):
     * an absolute date window plus an optional recurring weekday schedule. When
     * nothing is configured (no bounds, no weekdays) this is always active, so
     * behaviour is unchanged from before the window feature existed.
     *
     * @param CouponRow $coupon
     */
    private function windowActive(object $coupon, DateTimeImmutable $now): bool
    {
        $start = $this->parseDateTime($this->getConfig($coupon, 'tnt_cp_window_start'));
        $end   = $this->parseDateTime($this->getConfig($coupon, 'tnt_cp_window_end'));
        $days  = WeekdayScheduleTrigger::fromConfig($this->getConfig($coupon, 'tnt_cp_window_days'));

        return DateWindowTrigger::isActive($start, $end, $now)
            && WeekdayScheduleTrigger::isActive($days, $now);
    }

    /**
     * @param array<int, CouponRow> $coupons
     */
    private function setCoupon(array $coupons): void
    {
        $this->coupon = $this->createEmptyCoupon();
        $now          = new DateTimeImmutable();
        foreach ($coupons as $coupon) {
            if ($coupon->cAktiv !== 'Y') {
                continue;
            }
            $validUntil = $this->parseDateTime($coupon->dGueltigBis);
            if ($validUntil !== null && $validUntil < $now) {
                continue;
            }
            $validFrom = $this->parseDateTime($coupon->dGueltigAb);
            if ($validFrom === null || $validFrom > $now) {
                continue;
            }
            if ($this->windowActive($coupon, $now) === false) {
                continue;
            }
            $this->coupon = $coupon;
            break;
        }
    }

    public function details(): void
    {
        $cartVal = Frontend::getCart()->gibGesamtsummeWarenExt([C_WARENKORBPOS_TYP_ARTIKEL], true);
        if ($cartVal > 0) {
            $minNeeded = 0;
            $coupons   = $this->getCoupons();
            $this->setCoupon($coupons);
            if (isset($this->coupon->kKupon)) {
                $this->smarty()->assign('tnt_coupons_sale_active', 1);
                $missing = $this->basketMissingValue($this->coupon);
                $this->smarty()->assign('tnt_coupons_missing_amount', $missing);
                $varDisplay = 'd-flex';
                if ($missing <= 0) {
                    $reached = 'Y';
                } else {
                    $reached = 'N';
                }
                $this->smarty()->assign('tnt_coupons_reached_min', $reached);
                $unit = $this->coupon->cWertTyp;
                if ($unit === 'prozent') {
                    $unit = '&#37;';
                } else {
                    $unit = ' ' . $this->currency_symbol;
                }
                $fWert = $this->coupon->fWert;
                $this->smarty()->assign('tnt_coupons_basket_display', $varDisplay);
                $this->smarty()->assign('tnt_coupons_basket_max', $minNeeded);
                $this->smarty()->assign(
                    'tnt_coupons_lang_var_congratulation',
                    \vsprintf(
                        (string)$this->extension->getLocalization()->getTranslation('tnt_coupons_lang_var_congratulation'),
                        [
                            $this->coupon->fMindestbestellwert . ' ' . $this->currency_symbol,
                            $fWert . ' ' . $unit, $this->coupon->cCode
                        ]
                    )
                );
                $this->smarty()->assign(
                    'tnt_coupons_lang_var_still_missing',
                    \vsprintf(
                        (string)$this->extension->getLocalization()->getTranslation('tnt_coupons_lang_var_still_missing'),
                        ["<a href='#modalCoupons' data-bs-toggle='modal' data-toggle='modal'>"
                            . $this->coupon->fMindestbestellwert . ' ' . $this->currency_symbol . '</a>',
                            $fWert . ' ' . $unit
                        ]
                    )
                );
            }
        }
    }

    /** @param CouponRow $coupon */
    private function basketMissingValue(object $coupon): float
    {
        $result = 0.00;
        $min    = (float)$coupon->fMindestbestellwert;
        if ($min > 0) {
            if ($coupon->cWertTyp === 'festpreis' && (string)$coupon->nGanzenWKRabattieren === '0') {
                $result = $min - CartHelper::getCouponProductsTotal(
                    $this->toKupon($coupon),
                    Frontend::getCart()->PositionenArr
                );
            } else {
                $result = $min - Frontend::getCart()->gibGesamtsummeWarenExt([C_WARENKORBPOS_TYP_ARTIKEL], true);
            }
        }
        if ($result > 0) {
            $result = \round($result, 2);
        }
        return $result;
    }

    /**
     * @param array<int, MenuItem> $cats
     */
    private function recurseCats(string $cKategorien, array $cats): string
    {
        $result = '';
        foreach ($cats as $o) {
            $id = $o->getID();
            if (\str_contains($cKategorien, $id . ';')) {
                $result .= "<a href='" . $o->getURL() . "' target='_blank'>"
                    . \strip_tags($o->getShortName()) . '</a>, ';
            }
            $childCats = Category::getInstance()->getChildCategoriesById($id);
            if (\count($childCats) > 0) {
                $result .= $this->recurseCats($cKategorien, $childCats);
            }
        }
        return $result;
    }

    public function wkHinweis(): void
    {
        $this->smarty()->assign('tnt_coupons_currency_symbol', $this->currency_symbol);
        $coupons = $this->getCoupons();
        $this->setCoupon($coupons);
        if (
            isset($this->coupon->kKupon)
            && $this->getConfig($this->coupon, 'tnt_cp_check3') !== ''
        ) {
            $data = $this->coupon;
            $this->smarty()->assign('tnt_coupons_sale_active', 1);
            $this->smarty()->assign('tnt_coupons_wk_anzeige', 'Y');
            $unit = $data->cWertTyp;
            if ($unit === 'prozent') {
                $unit = '&#37;';
            } else {
                $unit = ' ' . $this->currency_symbol;
            }
            $fWert     = $data->fWert;
            $minNeeded = (float)$data->fMindestbestellwert;
            $missing   = $this->basketMissingValue($data);
            if ($missing <= 0) {
                $missing = 0;
                $reached = 'Y';
            } else {
                $reached = 'N';
            }
            $this->smarty()->assign('tnt_coupons_reached_min', $reached);
            $this->smarty()->assign('tnt_coupons_missing_amount', $missing);
            $this->smarty()->assign('tnt_coupons_currency_symbol', $this->currency_symbol);
            $this->smarty()->assign('tnt_coupons_basket_display', 'd-flex');
            $this->smarty()->assign('tnt_coupons_basket_max', $minNeeded);
            $this->smarty()->assign(
                'tnt_coupons_lang_var_congratulation',
                \vsprintf(
                    (string)$this->extension->getLocalization()->getTranslation(
                        'tnt_coupons_lang_var_congratulation'
                    ),
                    [
                        $this->coupon->fMindestbestellwert . ' ' . $this->currency_symbol, $fWert . ' ' . $unit,
                        $data->cCode
                    ]
                )
            );
            $this->smarty()->assign(
                'tnt_coupons_lang_var_still_missing',
                \vsprintf(
                    (string)$this->extension->getLocalization()->getTranslation('tnt_coupons_lang_var_still_missing'),
                    [
                        "<a href='#modalCoupons' data-bs-toggle='modal' data-toggle='modal'>"
                        . $data->fMindestbestellwert . ' ' . $this->currency_symbol . '</a>',
                        $fWert . ' ' . $unit
                    ]
                )
            );
        }
    }

    /** @param CouponRow $cp */
    private function setCSS(object $cp): void
    {
        $balkenColor = $this->getConfig($cp, 'tnt_cp_color1');
        if ($balkenColor !== '') {
            $this->smarty()->assign(
                'tnt_coupons_balkenColor',
                $balkenColor
            );
        }
        $txtColor = $this->getConfig($cp, 'tnt_cp_color2');
        if ($txtColor !== '') {
            $this->smarty()->assign('tnt_coupons_txtColor', 'color: ' . $txtColor . '!important;');
        }
    }

    public function init(): void
    {
        $this->smarty()->assign('tnt_coupons_currency_symbol', $this->currency_symbol);
        $coupons = $this->getCoupons();
        $this->setCoupon($coupons);
        if (empty($this->coupon->kKupon) === false) {
            $data = $this->coupon;
            $this->smarty()->assign('tnt_coupons_sale_active', 1);
            $data->localizedName = $this->getCouponTranslation((int)$data->kKupon);
            $this->smarty()->assign('tnt_coupons_title', $data->localizedName);
            $this->setCSS($data);
            $timeRemaining = null;
            $unit          = $data->cWertTyp;
            if ($unit === 'prozent') {
                $unit = '&#37;';
            } else {
                $unit = ' ' . $this->currency_symbol;
            }

            $popupFile    = $this->getConfig($data, 'tnt_cp_file');
            $popupSize    = $this->getConfig($data, 'tnt_cp_popupSize');
            $popupContent = $this->getConfig($data, 'tnt_cp_popup_content');
            $barContent   = $this->getConfig($data, 'tnt_cp_bar_content');
            $this->smarty()->assign('tnt_cp_bar_content', $barContent);
            if (
                (
                    $popupFile !== ''
                    || $popupContent !== ''
                ) && \time() > \strtotime($data->dGueltigAb)
            ) {
                if (($_SESSION['tnt_cp_popup_shown'] ?? '') !== $data->cName) {
                    $file = $this->extension->getPaths()->getFrontendPath() . 'template/popup.tpl';
                    $this->smarty()->assign(
                        'tnt_cp_showPopUp',
                        $this->getConfig($data, 'tnt_cp_popupDisplay') !== 'none'
                    );
                    $this->smarty()->assign('tnt_cp_popupTitle', $data->localizedName);
                    $this->smarty()->assign(
                        'tnt_cp_popupImg',
                        $this->getConfig($data, 'tnt_cp_popupDisplay') !== 'content'
                            ? $popupFile
                            : ''
                    );
                    $this->smarty()->assign(
                        'tnt_cp_popup_content',
                        $this->getConfig($data, 'tnt_cp_popupDisplay') !== 'image'
                            ? $popupContent
                            : ''
                    );
                    $this->smarty()->assign('tnt_cp_popupSize', $popupSize);
                    $tpl = $this->smarty()->fetch($file);
                    \pq('body')->append($tpl);
                }
                $_SESSION['tnt_cp_popup_shown'] = $data->cName;
            }
            if (
                empty($data->dGueltigBis) === false
                && $this->getConfig($this->coupon, 'tnt_cp_check2') !== ''
            ) {
                $expireDate = $this->parseDateTimeFormat('Y-m-d H:i:s', $data->dGueltigBis);
                if ($expireDate !== null) {
                    $timeRemaining = $this->createCountdown($expireDate);
                }
            }

            $this->smarty()->assign('tnt_coupons_discount', $data->fWert . $unit);
            $this->smarty()->assign(
                'tnt_coupons_lang_var_total_discount',
                $this->extension->getLocalization()->getTranslation('tnt_coupons_lang_var_total_discount')
            );
            $this->smarty()->assign(
                'tnt_coupons_discount_text',
                \vsprintf(
                    (string)$this->extension->getLocalization()->getTranslation('tnt_coupons_lang_var_get_discount'),
                    [$data->fWert . ' ' . $unit]
                )
            );
            $this->smarty()->assign(
                'tnt_coupons_lang_var_discount_code',
                $this->extension->getLocalization()->getTranslation('tnt_coupons_lang_var_discount_code')
            );
            $this->smarty()->assign(
                'tnt_coupons_lang_var_type_in',
                $this->extension->getLocalization()->getTranslation('tnt_coupons_lang_var_type_in')
            );
            $this->smarty()->assign(
                'tnt_coupons_lang_var_type_in_basket',
                $this->extension->getLocalization()->getTranslation('tnt_coupons_lang_var_type_in_basket')
            );
            $this->smarty()->assign(
                'tnt_coupons_lang_var_details',
                $this->extension->getLocalization()->getTranslation('tnt_coupons_lang_var_details')
            );
            $this->smarty()->assign(
                'tnt_coupons_lang_var_got_it',
                $this->extension->getLocalization()->getTranslation('tnt_coupons_lang_var_got_it')
            );

            $this->smarty()->assign(
                'tnt_coupons_lang_var_bis_zu',
                $this->extension->getLocalization()->getTranslation('tnt_coupons_lang_var_bis_zu')
            );
            $this->smarty()->assign(
                'tnt_coupons_lang_var_noch',
                $this->extension->getLocalization()->getTranslation('tnt_coupons_lang_var_noch')
            );
            if (\is_null($timeRemaining) === false) {
                $this->smarty()->assign('tnt_coupons_time_remaining', $timeRemaining);
            }
            $this->smarty()->assign(
                'tnt_coupons_lang_var_seconds',
                $this->extension->getLocalization()->getTranslation('tnt_coupons_lang_var_seconds')
            );
            $this->smarty()->assign(
                'tnt_coupons_lang_var_minutes',
                $this->extension->getLocalization()->getTranslation('tnt_coupons_lang_var_minutes')
            );
            $this->smarty()->assign(
                'tnt_coupons_lang_var_hours',
                $this->extension->getLocalization()->getTranslation('tnt_coupons_lang_var_hours')
            );
            $this->smarty()->assign(
                'tnt_coupons_lang_var_days',
                $this->extension->getLocalization()->getTranslation('tnt_coupons_lang_var_days')
            );
            $this->smarty()->assign(
                'tnt_coupons_lang_var_day',
                $this->extension->getLocalization()->getTranslation('tnt_coupons_lang_var_day')
            );

            $this->smarty()->assign('tnt_coupons_code', $data->cCode);
            $this->smarty()->assign(
                'tnt_coupons_show_modal',
                $this->getConfig($this->coupon, 'tnt_cp_check6') !== ''
            );
            $this->smarty()->assign(
                'tnt_coupons_showContainer',
                $this->getConfig($this->coupon, 'tnt_cp_check7') === ''
            );
            $this->smarty()->assign(
                'showFigureLabels',
                $this->getConfig($this->coupon, 'tnt_cp_check9') !== ''
            );
            $this->smarty()->assign(
                'tnt_coupons_daysAsText',
                $this->getConfig($this->coupon, 'tnt_cp_check10') !== ''
            );

            $this->convertToSale($data, true);
            $layout = match ($this->getConfig($data, 'tnt_cp_layout')) {
                'menu'   => 'header:not(#opcHeader)',
                'header' => 'body',
                default  => '',
            };
            if ($layout !== '') {
                $file = $this->extension->getPaths()->getFrontendPath() . 'template/sales_action.tpl';
                $tpl  = $this->smarty()->fetch($file);
                if ($layout === 'body') {
                    \pq($layout)->prepend($tpl);
                } else {
                    \pq($layout)->append($tpl);
                }
            }
            $file = $this->extension->getPaths()->getFrontendPath() . 'template/footer.tpl';
            $tpl  = $this->smarty()->fetch($file);
            \pq('footer')->append($tpl);
        } else {
            // Coupon is not active yet
            $coupons = $this->getCoupons();
            /** @var CouponRow|null $cp */
            $cp = null;
            foreach ($coupons as $coupon) {
                if ($coupon->cAktiv !== 'Y') {
                    continue;
                }

                $validUntil = $this->parseDateTime($coupon->dGueltigBis);
                if ($validUntil !== null && $validUntil < new DateTimeImmutable()) {
                    continue;
                }

                if (
                    empty($coupon->dGueltigAb)
                ) {
                    continue;
                }
                $showCDStart = $this->getConfig($coupon, 'tnt_cp_check1');
                if ($showCDStart !== '') {
                    $cp = $coupon;
                    break;
                }
            }
            if ($cp !== null) {
                $layout = match ($this->getConfig($cp, 'tnt_cp_layout')) {
                    'menu'   => 'header:not(#opcHeader)',
                    'header' => 'body',
                    default  => '',
                };
                if ($layout !== '') {
                    $this->setCSS($cp);
                    $lz                = $this->extension->getLocalization();
                    $cp->localizedName = $this->getCouponTranslation((int)$cp->kKupon);
                    $this->smarty()->assign('tnt_coupons_title', $cp->localizedName);
                    $unit = $cp->cWertTyp;
                    if ($unit === 'prozent') {
                        $unit = '&#37;';
                    } else {
                        $unit = ' ' . $this->currency_symbol;
                    }
                    $this->smarty()->assign(
                        'tnt_coupons_discount',
                        $cp->fWert . $unit
                    );
                    $this->smarty()->assign(
                        'tnt_coupons_lang_var_bis_zu',
                        $lz->getTranslation('tnt_coupons_lang_var_bis_zu')
                    );
                    $this->smarty()->assign(
                        'tnt_coupons_lang_var_noch',
                        $lz->getTranslation('tnt_coupons_lang_var_startet_in')
                    );

                    $this->smarty()->assign(
                        'tnt_coupons_lang_var_total_discount',
                        $this->extension->getLocalization()->getTranslation('tnt_coupons_lang_var_total_discount')
                    );
                    $this->smarty()->assign(
                        'tnt_coupons_discount_text',
                        \vsprintf(
                            (string)$this->extension->getLocalization()->getTranslation('tnt_coupons_lang_var_get_discount'),
                            [$cp->fWert . ' ' . $unit]
                        )
                    );
                    $this->smarty()->assign(
                        'tnt_coupons_lang_var_discount_code',
                        $this->extension->getLocalization()->getTranslation('tnt_coupons_lang_var_discount_code')
                    );
                    $this->smarty()->assign(
                        'tnt_coupons_lang_var_type_in',
                        $this->extension->getLocalization()->getTranslation('tnt_coupons_lang_var_type_in')
                    );
                    $this->smarty()->assign(
                        'tnt_coupons_lang_var_type_in_basket',
                        $this->extension->getLocalization()->getTranslation('tnt_coupons_lang_var_type_in_basket')
                    );
                    $this->smarty()->assign(
                        'tnt_coupons_lang_var_details',
                        $this->extension->getLocalization()->getTranslation('tnt_coupons_lang_var_details')
                    );
                    $this->smarty()->assign(
                        'tnt_coupons_lang_var_got_it',
                        $this->extension->getLocalization()->getTranslation('tnt_coupons_lang_var_got_it')
                    );
                    $this->smarty()->assign(
                        'tnt_coupons_code',
                        $cp->cCode
                    );
                    $barContent = $this->getConfig($cp, 'tnt_cp_bar_content');
                    $this->smarty()->assign('tnt_cp_bar_content', $barContent);

                    $startDate     = $this->parseDateTimeFormat('Y-m-d H:i:s', $cp->dGueltigAb);
                    $timeRemaining = $startDate instanceof DateTimeImmutable
                        ? $this->createCountdown($startDate)
                        : null;

                    $this->convertToSale($cp, true);
                    if ($timeRemaining !== null) {
                        $this->smarty()->assign('tnt_coupons_time_remaining', $timeRemaining);
                    }
                    $this->smarty()->assign(
                        'tnt_coupons_lang_var_seconds',
                        $this->extension->getLocalization()->getTranslation('tnt_coupons_lang_var_seconds')
                    );
                    $this->smarty()->assign(
                        'tnt_coupons_lang_var_minutes',
                        $this->extension->getLocalization()->getTranslation('tnt_coupons_lang_var_minutes')
                    );
                    $this->smarty()->assign(
                        'tnt_coupons_lang_var_hours',
                        $this->extension->getLocalization()->getTranslation('tnt_coupons_lang_var_hours')
                    );
                    $this->smarty()->assign(
                        'tnt_coupons_lang_var_days',
                        $this->extension->getLocalization()->getTranslation('tnt_coupons_lang_var_days')
                    );
                    $this->smarty()->assign(
                        'tnt_coupons_lang_var_day',
                        $this->extension->getLocalization()->getTranslation('tnt_coupons_lang_var_day')
                    );

                    $this->smarty()->assign(
                        'tnt_coupons_show_modal',
                        $this->getConfig($cp, 'tnt_cp_check6') !== ''
                    );
                    $this->smarty()->assign(
                        'tnt_coupons_showContainer',
                        $this->getConfig($cp, 'tnt_cp_check7') === ''
                    );
                    $this->smarty()->assign(
                        'showFigureLabels',
                        $this->getConfig($cp, 'tnt_cp_check9') !== ''
                    );
                    $this->smarty()->assign(
                        'tnt_coupons_daysAsText',
                        $this->getConfig($cp, 'tnt_cp_check10') !== ''
                    );

                    $file = $this->extension->getPaths()->getFrontendPath() . 'template/sales_action.tpl';
                    $tpl  = $this->smarty()->fetch($file);

                    if ($layout === 'body') {
                        \pq($layout)->prepend($tpl);
                    } else {
                        \pq($layout)->append($tpl);
                    }
                    $file = $this->extension->getPaths()->getFrontendPath() . 'template/footer.tpl';
                    $tpl  = $this->smarty()->fetch($file);
                    \pq('footer')->append($tpl);
                }
            }
        }
    }

    /**
     * @param array<int|string, int> $missingData
     */
    private function angabenKorrekt(array $missingData): int
    {
        return (int)none($missingData, static function ($e) {
            return $e > 0;
        });
    }

    /** @param CouponRow $coupon */
    private function getConfig(object $coupon, string $key = ''): string
    {
        $jsonString = $coupon->textra;
        if ($jsonString === '') {
            Shop::Container()->getLogService()->debug(
                'There is no offset <' . $key . '> in sales-action settings data in tnt_coupons plugin.'
                . ' The json string is empty.'
            );
            return '';
        }
        $configJson = new stdClass();
        try {
            $decoded = \json_decode($jsonString, false, 512, JSON_THROW_ON_ERROR);
            if ($decoded instanceof stdClass) {
                $configJson = $decoded;
            }
        } catch (\JsonException $e) {
            Shop::Container()->getLogService()->debug(
                'Error (' . \json_last_error() . ') while decoding sales-action settings data in tnt_coupons plugin.'
                . ' The json string to decode is: ' . $jsonString,
                ['error' => $e->getMessage()]
            );
        }

        return is_scalar($configJson->{$key} ?? null) ? (string)$configJson->{$key} : '';
    }

    /**
     * @param array<string, mixed> $args
     */
    public function applyCoupon(array $args): void
    {
        $pageType = (int)$args['pageType'];
        if ($pageType !== \PAGE_WARENKORB && $pageType !== \PAGE_BESTELLVORGANG) {
            return;
        }
        $coupons = $this->getCoupons();
        $this->setCoupon($coupons);
        if (
            isset($this->coupon->cCode)
            && empty($this->coupon->cCode) === false
        ) {
            $autoApply = $this->getConfig($this->coupon, 'tnt_cp_check5');
            if (empty($autoApply) === false) {
                $this->plausiKupon($this->coupon->cCode);
            }
        }
    }

    /**
     * @return array<int|string, int>|int
     */
    public function plausiKupon(string $code = ''): array|int
    {
        $cartVal = Frontend::getCart()->gibGesamtsummeWarenExt([C_WARENKORBPOS_TYP_ARTIKEL], true);
        if ($cartVal <= 0) {
            return 0;
        }
        $errors = [];
        $coupon = new Kupon();
        if ($code === '') {
            if (
                isset($this->coupon->cCode)
                && empty($this->coupon->cCode) === false
            ) {
                $code = $this->coupon->cCode;
            }
        }
        $coupon = $coupon->getByCode($code);
        if ($coupon !== false && $coupon->kKupon > 0) {
            $errors = Kupon::checkCoupon($coupon);
            if (\count($errors) === 0) {
                if ($this->angabenKorrekt($errors)) {
                    Kupon::acceptCoupon($coupon);
                    /*
                    if ($coupon->cKuponTyp === Kupon::TYPE_SHIPPING) {
                        $_SESSION['oVersandfreiKupon'] = $coupon;
                    }
                    */
                }
            } else {
                return 0;
            }
        } else {
            $errors['ungueltig'] = 11;
        }
        Kupon::mapCouponErrorMessage($errors['ungueltig'] ?? 0);
        $this->plausiNeukundenKupon();
        return
            (count($errors) > 0)
                ? $errors
                : 0;
    }

    private function plausiNeukundenKupon(): void
    {
        if (
            isset($_SESSION['NeukundenKuponAngenommen'])
            && $_SESSION['NeukundenKuponAngenommen'] === true
        ) {
            return;
        }
        $customer = Frontend::getCustomer();
        if (
            (
                isset($_SESSION['Kupon']->cKuponTyp) === false
                || $_SESSION['Kupon']->cKuponTyp !== 'standard'
            ) && empty($customer->cMail) === false
        ) {
            $conf = Shop::getSettings([CONF_KAUFABWICKLUNG]);
            if (
                ($conf['kaufabwicklung']['bestellvorgang_unregneukundenkupon_zulassen'] ?? '') === 'N'
                && $customer->getID() <= 0
            ) {
                // unregistrierte Neukunden, keine Kupons für Gastbestellungen zugelassen
                return;
            }
            // not for already registered customers with order(s)
            if ($customer->getID() > 0) {
                $order = Shop::Container()->getDB()->getSingleObject(
                    'SELECT kBestellung
						FROM tbestellung
						WHERE kKunde = :customerID
						LIMIT 1',
                    ['customerID' => $customer->getID()]
                );
                if ($order !== null) {
                    return;
                }
            }
            $coupons = (new Kupon())->getNewCustomerCoupon();
            if (
                empty($coupons) === false
                && !Kupon::newCustomerCouponUsed($customer->cMail)
            ) {
                foreach ($coupons as $coupon) {
                    if ($this->angabenKorrekt(Kupon::checkCoupon($coupon))) {
                        Kupon::acceptCoupon($coupon);
                        break;
                    }
                }
            }
        }
    }

    /** @return CouponStatsRow */
    public function stats(int $kKupon): object
    {
        $data = $this->db->queryPrepared(
            'SELECT tkupon.nVerwendungenBisher, tkupon.dGueltigAb, tkupon.dGueltigBis,
       tkuponsprache.cName AS cName, SUM(tkuponbestellung.fKuponwertBrutto) AS Gespart
				FROM tkupon
				LEFT JOIN tkuponsprache
				ON tkupon.kKupon = tkuponsprache.kKupon AND tkuponsprache.cISOSprache = :lang
				LEFT JOIN tkuponbestellung
				ON tkupon.kKupon = tkuponbestellung.kKupon
				WHERE tkupon.kKupon = :kupon
				GROUP BY tkupon.kKupon',
            [
                'kupon' => $kKupon,
                'lang'  => 'ger'
            ],
            ReturnType::SINGLE_OBJECT
        );

        if (!is_object($data)) {
            return $this->createEmptyStats();
        }

        /** @var CouponStatsRow $statsData */
        $statsData = $this->normalizeStats($data);
        $validFrom = $this->parseDateTime($statsData->dGueltigAb);
        if ($validFrom === null) {
            return $statsData;
        }

        $validUntil = $this->parseDateTime($statsData->dGueltigBis) ?? new DateTimeImmutable();
        $stats      = new Statistik($validFrom->getTimestamp(), $validUntil->getTimestamp());
        $stats->getAnzeigeIntervall();
        $statsData->umsatzSale = $this->normalizeStatsSeries($stats->holeUmsatzStats());
        $days                  = $stats->getAnzahlTage();
        $previousFrom          = $validFrom->modify('-' . $days . ' day');
        $previousUntil         = $validUntil->modify('-' . $days . ' day');
        $stats->setDatumStampVon($previousFrom->getTimestamp());
        $stats->setDatumStampBis($previousUntil->getTimestamp());
        $stats->getAnzeigeIntervall();
        $statsData->umsatzDavor = $this->normalizeStatsSeries($stats->holeUmsatzStats());
        $orders = $this->db->queryPrepared(
            'SELECT COUNT(tbestellung.kBestellung) AS bestellungen
					FROM tbestellung
					WHERE tbestellung.fGesamtsumme > 0
					AND tbestellung.dErstellt BETWEEN :_von AND :_bis',
            ['_von' => $statsData->dGueltigAb, '_bis' => $validUntil->format('Y-m-d H:i:s')],
            ReturnType::SINGLE_OBJECT
        );
        if (is_object($orders) && isset($orders->bestellungen)) {
            $statsData->totalOrders = (int)$orders->bestellungen;
        }

        return $statsData;
    }

    /** @return array<int, CouponRow> */
    public function getCoupons(bool $all = false): array
    {
        if ($all) {
            $cps = $this->db->queryPrepared(
                'SELECT tkupon.*, tkuponsprache.cName AS Titel, SUM(tkuponbestellung.fKuponwertBrutto) AS Gespart,
                    tnt_coupons_config.textra
					FROM tkupon
					LEFT JOIN tkuponsprache
					ON tkupon.kKupon = tkuponsprache.kKupon AND tkuponsprache.cISOSprache = :lang
					LEFT JOIN tkuponbestellung
					ON tkupon.kKupon = tkuponbestellung.kKupon
					RIGHT JOIN tnt_coupons_config
					ON tkupon.kKupon = tnt_coupons_config.tvalue AND tnt_coupons_config.tkey = :key
					WHERE tkupon.cKuponTyp = :cKuponTyp
					GROUP BY tkupon.kKupon, tkupon.dGueltigAb
					ORDER BY tkupon.dGueltigAb DESC',
                [
                    'lang'      => 'ger',
                    'key'       => 'kupon',
                    'cKuponTyp' => 'standard'
                ],
                ReturnType::ARRAY_OF_OBJECTS
            );
        } else {
            $cps = $this->db->queryPrepared(
                'SELECT tkupon.*, tnt_coupons_config.textra
					FROM tkupon
					RIGHT JOIN tnt_coupons_config
					ON tkupon.kKupon = tnt_coupons_config.tvalue AND tnt_coupons_config.tkey = :tkey
					WHERE cKuponTyp = :cKuponTyp
					ORDER BY tkupon.dGueltigAb',
                [
                    'cKuponTyp' => 'standard',
                    'tkey'      => 'kupon'
                ],
                ReturnType::ARRAY_OF_OBJECTS
            );
        }

        if (!is_array($cps)) {
            return [];
        }

        /** @var array<int, CouponRow> $coupons */
        $coupons = [];
        foreach ($cps as $coupon) {
            if (!is_object($coupon)) {
                continue;
            }
            /** @var CouponRow $normalizedCoupon */
            $normalizedCoupon = $this->normalizeCoupon($coupon);
            $coupons[]        = $normalizedCoupon;
        }

        return $coupons;
    }

    private function getCouponTranslation(int $kKupon): string
    {
        if (empty($_SESSION['currentLanguage']->iso) === false) {
            return $this->db->queryPrepared(
                'SELECT tkuponsprache.cName
                FROM tkuponsprache
                WHERE tkuponsprache.kKupon = :kuponID AND tkuponsprache.cISOSprache = :langISO',
                [
                    'kuponID' => $kKupon,
                    'langISO' => $_SESSION['currentLanguage']->iso
                ],
                ReturnType::SINGLE_OBJECT
            )->cName ?? '';
        }

        return '';
    }

    /** @return array<int, ProductRow> */
    private function getProducts(string $artNrs, int $langID): array
    {
        $products = $this->db->queryPrepared(
            'SELECT tartikel.cName, tartikel.cSeo, tartikelsprache.cName AS cName_translated,
       tartikelsprache.cSeo AS cSeo_translated
				FROM tartikel
				LEFT JOIN tartikelsprache
				    ON tartikelsprache.kArtikel = tartikel.kArtikel AND tartikelsprache.kSprache = :langID
				WHERE tartikel.cArtNr IN (' . $artNrs . ') AND tartikel.kVaterArtikel = 0',
            ['langID' => $langID],
            ReturnType::ARRAY_OF_OBJECTS
        );
        if (!is_array($products)) {
            return [];
        }

        $result = [];
        foreach ($products as $product) {
            if (!is_object($product)) {
                continue;
            }
            $result[] = (object)[
                'cName'            => $this->stringify($product->cName ?? ''),
                'cSeo'             => $this->stringify($product->cSeo ?? ''),
                'cName_translated' => $this->stringify($product->cName_translated ?? ''),
                'cSeo_translated'  => $this->stringify($product->cSeo_translated ?? ''),
            ];
        }

        return $result;
    }
}
