<?php

namespace QUI\ERP\Coupons;

use DateTime;
use Exception;
use QUI;
use QUI\ERP\Discount\Handler as DiscountHandler;
use QUI\ERP\Order\AbstractOrder;
use QUI\ERP\Order\OrderInterface;
use QUI\ExceptionStack;
use QUI\Interfaces\Users\User;
use QUI\Permissions\Permission;

use function array_key_first;
use function count;
use function current;
use function in_array;
use function is_array;
use function is_null;
use function is_numeric;
use function json_decode;
use function json_encode;
use function method_exists;

/**
 * Class CouponCode
 */
class CouponCode
{
    /**
     * CouponCode ID
     *
     * @var int
     */
    protected int $id;

    /**
     * Actual code
     *
     * @var string
     */
    protected mixed $code;

    /**
     * IDs of users that this CouponCode is restricted to
     *
     * @var int[]
     */
    protected mixed $userIds = [];

    /**
     * IDs of groups that this CouponCode is restricted to
     *
     * @var int[]
     */
    protected mixed $groupIds = [];

    /**
     * IDs of all linked discounts
     *
     * @var array
     */
    protected array $discountIds = [];

    /**
     * List of usages of this CouponCode
     *
     * @var array
     */
    protected mixed $usages = [];

    /**
     * Creation Date
     *
     * @var DateTime|null
     */
    protected ?DateTime $CreateDate = null;

    /**
     * Date until the CouponCode is valid
     *
     * @var DateTime|null
     */
    protected ?DateTime $ValidUntilDate = null;

    /**
     * CouponCode title
     *
     * @var string|null
     */
    protected ?string $title = null;

    /**
     * Flag - Is the CouponCode valid?
     *
     * @var bool
     */
    protected bool $valid = true;

    /**
     * Max usages
     *
     * @var string
     */
    protected string $maxUsages = Handler::MAX_USAGE_ONCE_PER_USER;

    /**
     * CouponCode constructor.
     *
     * @param int $id - Invite Code ID
     *
     * @throws CouponCodeException
     * @throws Exception
     */
    public function __construct(int $id)
    {
        try {
            $result = QUI::getDataBase()->fetch([
                'from' => Handler::getTable(),
                'where' => [
                    'id' => $id
                ]
            ]);
        } catch (QUI\Database\Exception $Exception) {
            QUI\System\Log::addError($Exception->getMessage());

            throw new CouponCodeException([
                'quiqqer/coupons',
                'exception.CouponCode.not_found',
                [
                    'id' => $id
                ]
            ], 404);
        }

        if (empty($result)) {
            throw new CouponCodeException([
                'quiqqer/coupons',
                'exception.CouponCode.not_found',
                [
                    'id' => $id
                ]
            ], 404);
        }

        $data = current($result);

        $this->id = (int)$data['id'];
        $this->code = $data['code'];
        $this->title = $data['title'];

        if (!empty($data['usages'])) {
            $this->usages = json_decode($data['usages'], true) ?? [];
        }

        if (!empty($data['userIds'])) {
            $this->userIds = json_decode($data['userIds'], true) ?? [];
        }

        // migrate user ids
        foreach ($this->userIds as $k => $userId) {
            if (is_numeric($userId)) {
                try {
                    $this->userIds[$k] = QUI::getUsers()->get($userId)->getUUID();
                } catch (QUI\Exception) {
                }
            }
        }

        if (!empty($data['groupIds'])) {
            $this->groupIds = json_decode($data['groupIds'], true) ?? [];
        }

        // migrate user ids
        foreach ($this->groupIds as $k => $groupId) {
            if (is_numeric($groupId)) {
                try {
                    $this->groupIds[$k] = QUI::getGroups()->get($groupId)->getUUID();
                } catch (QUI\Exception) {
                }
            }
        }

        if (!empty($data['maxUsages'])) {
            $this->maxUsages = $data['maxUsages'];
        }

        if (!empty($data['discountIds'])) {
            $this->discountIds = json_decode($data['discountIds'], true) ?? [];
        }

        $this->CreateDate = new DateTime($data['createDate']);

        if (!empty($data['validUntilDate'])) {
            $this->ValidUntilDate = new DateTime($data['validUntilDate']);
            $this->ValidUntilDate->setTime(23, 59, 59);
        }

        $this->checkValidity();
    }

    /**
     * @return int
     */
    public function getId(): int
    {
        return $this->id;
    }

    /**
     * @return string
     */
    public function getCode(): string
    {
        return $this->code;
    }

    /**
     * @return DateTime
     */
    public function getCreateDate(): DateTime
    {
        return $this->CreateDate;
    }

    /**
     * Get usage data
     *
     * @return array
     */
    public function getUsages(): array
    {
        return $this->usages;
    }

    /**
     * @return DateTime|null
     */
    public function getValidUntilDate(): ?DateTime
    {
        return $this->ValidUntilDate;
    }

    /**
     * @return string|null
     */
    public function getTitle(): ?string
    {
        return $this->title;
    }

    /**
     * @return int[]
     */
    public function getDiscountIds(): array
    {
        return $this->discountIds;
    }

    /**
     * Get all discounts associated with this CouponCode
     *
     * @return QUI\ERP\Discount\Discount[]
     */
    public function getDiscounts(): array
    {
        $discounts = [];
        $DiscountHandler = DiscountHandler::getInstance();

        foreach ($this->discountIds as $discountId) {
            try {
                $discounts[] = $DiscountHandler->getChild($discountId);
            } catch (Exception $Exception) {
                QUI\System\Log::writeDebugException($Exception);
            }
        }

        // @phpstan-ignore-next-line
        return $discounts;
    }

    /**
     * @return int[]
     */
    public function getUserIds(): array
    {
        return $this->userIds;
    }

    /**
     * @return int[]
     */
    public function getGroupIds(): array
    {
        return $this->groupIds;
    }

    /**
     * Redeems this CouponCode
     *
     * Hint: This may invalidate the code for future use
     *
     * @param User|null $User - The user that redeems the CouponCode [if omitted use Session User]
     * @param AbstractOrder|null $Order (optional) - Link redemption to a specific Order
     * @return void
     * @throws CouponCodeException
     * @throws QUI\Database\Exception
     * @throws ExceptionStack
     */
    public function redeem(null | QUI\Interfaces\Users\User $User = null, null | AbstractOrder $Order = null): void
    {
        if (is_null($User)) {
            $User = QUI::getUserBySession();
        }

        $this->checkRedemption($User);

        $Now = new DateTime();

        $usage = [
            'userId' => $User->getUUID(),
            'date' => $Now->format('Y-m-d H:i:s'),
            'orderPrefixedId' => false
        ];

        if ($Order instanceof QUI\ERP\Order\Order) {
            $usage['orderPrefixedId'] = $Order->getPrefixedNumber();
        }

        $this->usages[] = $usage;

        QUI::getDataBase()->update(
            Handler::getTable(),
            ['usages' => json_encode($this->usages)],
            ['id' => $this->id]
        );

        $this->checkValidity();

        QUI::getEvents()->fireEvent(
            'quiqqerCouponsRedeem',
            [
                'User' => $User,
                'CouponCode' => $this
            ]
        );
    }

    /**
     * Check if the given User can redeem this CouponCode
     *
     * @param QUI\Interfaces\Users\User $User - If omitted, use session user
     * @return void
     * @throws CouponCodeException - Thrown if not redeemable by the given User
     */
    public function checkRedemption(QUI\Interfaces\Users\User $User): void
    {
        if (!$this->isValid()) {
            throw new CouponCodeException([
                'quiqqer/coupons',
                'exception.CouponCode.no_longer_valid'
            ]);
        }

        $DiscountHandler = DiscountHandler::getInstance();
        $discountsValid = false;
        $discountError = false;

        foreach ($this->discountIds as $discountId) {
            try {
                /** @var QUI\ERP\Discount\Discount $Discount */
                $Discount = $DiscountHandler->getChild($discountId);
            } catch (Exception $Exception) {
                $discountError = $Exception->getMessage();
                continue;
            }

            if ($Discount->canUsedBy($User)) {
                $discountsValid = true;
                break;
            }
        }

        if (!$discountsValid) {
            if (count($this->discountIds) === 1) {
                throw new CouponCodeException([
                    'quiqqer/coupons',
                    'exception.CouponCode.discount_invalid',
                    [
                        'reason' => $discountError
                    ]
                ]);
            } else {
                throw new CouponCodeException([
                    'quiqqer/coupons',
                    'exception.CouponCode.discounts_invalid'
                ]);
            }
        }

        // Max usage restrictions
        switch ($this->maxUsages) {
            case Handler::MAX_USAGE_ONCE_PER_USER:
                if ($this->hasUserRedeemed($User)) {
                    throw new CouponCodeException([
                        'quiqqer/coupons',
                        'exception.CouponCode.already_used'
                    ]);
                }
                break;

            case Handler::MAX_USAGE_ONCE:
                if (!empty($this->usages)) {
                    throw new CouponCodeException([
                        'quiqqer/coupons',
                        'exception.CouponCode.already_used'
                    ]);
                }
                break;
        }

        // Restriction to QUIQQER user(s)
        if (!empty($this->userIds)) {
            if (in_array($User->getUUID(), $this->userIds)) {
                if (
                    $this->maxUsages !== Handler::MAX_USAGE_UNLIMITED
                    && $this->hasUserRedeemed($User)
                ) {
                    throw new CouponCodeException([
                        'quiqqer/coupons',
                        'exception.CouponCode.already_used'
                    ]);
                }
            } else {
                throw new CouponCodeException([
                    'quiqqer/coupons',
                    'exception.CouponCode.user_not_allowed'
                ]);
            }
        }

        // Restriction to QUIQQER group(s)
        if (!empty($this->groupIds)) {
            $userInGroup = false;

            foreach ($this->groupIds as $groupId) {
                if ($User->isInGroup($groupId)) {
                    $userInGroup = true;
                    break;
                }
            }

            if ($userInGroup) {
                if (
                    $this->maxUsages !== Handler::MAX_USAGE_UNLIMITED
                    && $this->hasUserRedeemed($User)
                ) {
                    throw new CouponCodeException([
                        'quiqqer/coupons',
                        'exception.CouponCode.already_used'
                    ]);
                }
            } else {
                throw new CouponCodeException([
                    'quiqqer/coupons',
                    'exception.CouponCode.user_not_allowed_group'
                ]);
            }
        }
    }

    /**
     * Check if the given Order can redeem this CouponCode
     *
     * @param OrderInterface|null $Order
     * @throws CouponCodeException
     */
    public function checkOrderRedemption(?OrderInterface $Order): void
    {
        if ($Order === null) {
            return;
        }

        $DiscountHandler = DiscountHandler::getInstance();
        $discountsValid = false;
        $discountError = false;

        foreach ($this->discountIds as $discountId) {
            try {
                $Discount = $DiscountHandler->getChild($discountId);
            } catch (Exception $Exception) {
                $discountError = $Exception->getMessage();
                continue;
            }

            if (method_exists($Discount, 'canUsedInOrder') && $Discount->canUsedInOrder($Order)) {
                $discountsValid = true;
                break;
            }
        }

        if (!$discountsValid) {
            if (count($this->discountIds) === 1) {
                throw new CouponCodeException([
                    'quiqqer/coupons',
                    'exception.CouponCode.discount_invalid',
                    [
                        'reason' => $discountError
                    ]
                ]);
            } else {
                throw new CouponCodeException([
                    'quiqqer/coupons',
                    'exception.CouponCode.discounts_invalid'
                ]);
            }
        }
    }

    /**
     * Check if the given User can redeem this CouponCode
     *
     * @param User|null $User - If omitted, use session user
     * @param OrderInterface|null $Order
     * @return bool
     */
    public function isRedeemable(
        null | QUI\Interfaces\Users\User $User = null,
        null | OrderInterface $Order = null
    ): bool {
        try {
            $this->checkRedemption($User);
        } catch (CouponCodeException) {
            return false;
        }

        if ($Order) {
            try {
                $this->checkOrderRedemption($Order);
            } catch (CouponCodeException $Exception) {
                $Order->addFrontendMessage($Exception->getMessage());

                return false;
            }
        }

        return true;
    }

    /**
     * Check if this CouponCode is still valid
     *
     * @return bool
     */
    public function isValid(): bool
    {
        return $this->valid;
    }

    /**
     * Checks if an CouponCode has been redeemed by a user
     *
     * @param QUI\Interfaces\Users\User $User
     * @return bool
     */
    public function hasUserRedeemed(QUI\Interfaces\Users\User $User): bool
    {
        $userId = $User->getUUID();

        foreach ($this->usages as $usage) {
            if ($usage['userId'] === $userId) {
                return true;
            }
        }

        return false;
    }

    /**
     * Permanently delete this CouponCode
     *
     * @return void
     * @throws QUI\Permissions\Exception
     */
    public function delete(): void
    {
        Permission::checkPermission(Handler::PERMISSION_DELETE);

        try {
            QUI::getDataBase()->delete(
                Handler::getTable(),
                ['id' => $this->id]
            );
        } catch (QUI\Database\Exception $Exception) {
            QUI\System\Log::addError($Exception->getMessage());
        }

        // If hidden discount are connected to this coupon -> delete them as well
        foreach ($this->getDiscounts() as $Discount) {
            if (!empty($Discount->getAttribute('hidden'))) {
                try {
                    $Discount->delete();
                } catch (Exception $Exception) {
                    QUI\System\Log::writeException($Exception);
                }
            }
        }
    }

    /**
     * Get CouponCode attributes as array
     *
     * @return array
     */
    public function toArray(): array
    {
        $data = [
            'id' => $this->getId(),
            'code' => $this->getCode(),
            'userIds' => $this->userIds,
            'groupIds' => $this->groupIds,
            'createDate' => $this->getCreateDate()->format('Y-m-d H:i:s'),
            'usages' => $this->usages,
            'validUntilDate' => false,
            'title' => $this->getTitle() ?: false,
            'isValid' => $this->isValid(),
            'maxUsages' => $this->maxUsages,
            'discountIds' => $this->discountIds
        ];

        $ValidUntilDate = $this->getValidUntilDate();

        if ($ValidUntilDate) {
            $data['validUntilDate'] = $ValidUntilDate->format('Y-m-d');
        }

        return $data;
    }

    /**
     * Checks if this CouponCode is still valid
     *
     * @return void
     */
    protected function checkValidity(): void
    {
        // Check if the expiration date has been reached
        if (!empty($this->ValidUntilDate)) {
            $Now = new DateTime();

            if ($Now > $this->ValidUntilDate) {
                $this->valid = false;

                return;
            }
        }

        if ($this->maxUsages === Handler::MAX_USAGE_UNLIMITED) {
            return;
        }

        if ($this->maxUsages === Handler::MAX_USAGE_ONCE && !empty($this->usages)) {
            $this->valid = false;

            return;
        }

        // If the CouponCode is restricted to certain users -> Check if all those
        // users have already redeemed the code
        if (!empty($this->userIds)) {
            $usedByAllUsers = true;

            foreach ($this->userIds as $userId) {
                foreach ($this->usages as $usage) {
                    if ($userId == $usage['userId']) {
                        continue 2;
                    }
                }

                $usedByAllUsers = false;
                break;
            }

            if ($usedByAllUsers) {
                $this->valid = false;
            }
        }
    }

    /**
     * @param QUI\ERP\Order\OrderInProcess $Order
     * @throws QUI\Exception
     */
    public function addToOrder(QUI\ERP\Order\OrderInProcess $Order): void
    {
        $coupons = $Order->getDataEntry('quiqqer-coupons');

        if (!$coupons) {
            return;
        }

        if (!is_array($coupons)) {
            return;
        }

        $priceFactors = [];
        $articles = [];
        $calculations = $Order->getArticles()->getCalculations();
        $vatArray = $calculations['vatArray'];
        $vat = false;

        if (count($vatArray) === 1) {
            $vat = array_key_first($vatArray);
        }

        foreach ($coupons as $coupon) {
            /* @var $Coupon CouponCode */
            try {
                $Coupon = Handler::getCouponCodeByCode($coupon);
            } catch (Exception) {
                continue;
            }

            // coupon check
            if (!$Coupon->isRedeemable($Order->getCustomer(), $Order)) {
                continue;
            }

            /* @var $Discount QUI\ERP\Discount\Discount */
            $discounts = $Coupon->getDiscounts();

            foreach ($discounts as $Discount) {
                $PriceFactor = $Discount->toPriceFactor(null, $Order->getCustomer());

                if ($vat !== false && method_exists($PriceFactor, 'setVat')) {
                    $PriceFactor->setVat($vat);
                }

                $PriceFactor->setTitle(
                    QUI::getLocale()->get('quiqqer/coupons', 'coupon.discount.title', [
                        'code' => $Coupon->getCode()
                    ])
                );

                $priceFactors[] = $PriceFactor;

                // @todo wenn fest preis (zb 10$), dann eigener produkt typ hinzufügen

                $articles[] = new QUI\ERP\Accounting\Articles\Text([
                    'id' => -1,
                    'articleNo' => $Coupon->getCode(),
                    'title' => $PriceFactor->getTitle(),
                    'description' => '',
                    'unitPrice' => 0,
                    'control' => '',
                    'quantity' => 1,
                    'customData' => [
                        'package' => 'quiqqer/coupon',
                        'code' => $Coupon->getCode()
                    ]
                ]);
            }
        }

        if (empty($priceFactors)) {
            return;
        }

        /**
         * @param $Article
         * @return boolean
         */
        $isInArticles = function ($Article) use ($Order) {
            $articles = $Order->getArticles();
            $code = $Article->getCustomData()['code'];

            foreach ($articles as $Entry) {
                if (!method_exists($Entry, 'getCustomData')) {
                    continue;
                }

                $customData = $Entry->getCustomData();

                if (!$customData || !is_array($customData)) {
                    continue;
                }

                if (!isset($customData['package']) || !isset($customData['code'])) {
                    continue;
                }

                return $customData['package'] === 'quiqqer/coupon'
                    && $customData['code'] === $code;
            }

            return false;
        };

        foreach ($articles as $Article) {
            /* @var $PriceFactor QUI\ERP\Accounting\Articles\Text */
            if ($isInArticles($Article) === false) {
                $Order->addArticle($Article);
            }
        }

        $Order->update();
        $Order->addPriceFactors($priceFactors);
    }
}