<?php

declare(strict_types=1);

namespace CpCreation\VitiCore\Cart;

use CpCreation\VitiCore\Cart\Model\Cart;
use CpCreation\VitiCore\Cart\Model\Promo;
use CpCreation\VitiCore\Cart\Repository\CartRepository;
use CpCreation\VitiCore\Cart\Repository\PromoRepository;
use CpCreation\VitiCore\Product\Model\Product;
use CpCreation\VitiCore\Product\DegressiveManager;
use CpCreation\VitiCore\Shop\Model\ShippingZone;
use CpCreation\VitiCore\Shop\Repository\ShippingCountryRepository;
use CpCreation\VitiCore\Shop\Repository\ShippingCountyRepository;
use CpCreation\VitiCore\Shop\Repository\ShippingZoneRepository;

final class PromoEligibilityResolver
{
    public function __construct(
        private readonly PromoRepository $promoRepository,
        private readonly CartRepository $cartRepository,
        private readonly DegressiveManager $degressiveManager,
        private readonly ShippingCountryRepository $shippingCountryRepository,
        private readonly ShippingCountyRepository $shippingCountyRepository,
        private readonly ShippingZoneRepository $shippingZoneRepository,
    ) {
    }

    public function resolveManualCode(Cart $cart, mixed $groupPrice = null): PromoEligibilityResult
    {
        $code = $cart->getPromoCodeInput();

        if (null === $code || '' === mb_trim($code)) {
            return new PromoEligibilityResult(
                promo: null,
                status: CartPromo::STATUS_INVALID,
                messageKey: 'cart.promo.invalid',
            );
        }

        $promo = $this->promoRepository->findManualPromoByCode($code, $groupPrice);

        if ($promo instanceof Promo) {
            return $this->resolvePromo($promo, $cart, $groupPrice, true);
        }

        $promo = $this->promoRepository->findPromoByCode($code, $groupPrice);

        if (!$promo instanceof Promo) {
            return new PromoEligibilityResult(
                promo: null,
                status: CartPromo::STATUS_INVALID,
                messageKey: 'cart.promo.invalid',
            );
        }

        $eligibility = $this->resolvePromo($promo, $cart, $groupPrice, false);

        if ($promo->isAuto() && CartPromo::STATUS_INVALID === $eligibility->status) {
            return $eligibility;
        }

        if ($promo->isAuto()) {
            return new PromoEligibilityResult(
                promo: $promo,
                status: CartPromo::STATUS_PENDING_AUTO,
                messageKey: 'cart.promo.auto_next_order',
            );
        }

        return $eligibility;
    }

    public function resolvePromo(
        Promo $promo,
        Cart $cart,
        mixed $groupPrice = null,
        bool $allowPendingAddress = false,
    ): PromoEligibilityResult {
        $now = new \DateTimeImmutable('now');

        if ($promo->isDisabled()) {
            return $this->invalid($promo, 'cart.promo.invalid');
        }

        if (null !== $promo->getBeginAt() && $promo->getBeginAt() > $now) {
            return $this->invalid($promo, 'cart.promo.not_started');
        }

        if (null !== $promo->getEndAt() && $promo->getEndAt() < $now) {
            return $this->invalid($promo, 'cart.promo.expired');
        }

        if (null !== $promo->getMaxCount() && $promo->getCount() >= $promo->getMaxCount()) {
            return $this->invalid($promo, 'cart.promo.max_count');
        }

        if (
            null !== $promo->getAssignedUser()
            && (
                null === $cart->getUser()
                || !$promo->getAssignedUser()->getId()->equals($cart->getUser()->getId())
            )
        ) {
            return $this->invalid($promo, 'cart.promo.reserved_customer');
        }

        $effectiveMaxCountByCustomer = $promo->getEffectiveMaxCountByCustomer();

        if (null !== $effectiveMaxCountByCustomer) {
            $usageCount = $this->cartRepository->countAppliedPromoForCartContext($promo, $cart);

            if ($usageCount >= $effectiveMaxCountByCustomer) {
                return $this->invalid($promo, 'cart.promo.max_count_by_customer');
            }
        }

        $quantity = 0 !== $promo->getProducts()->count()
            ? (float) $cart->getQuantityByProducts($promo->getProducts())
            : (float) $cart->getQuantityAllAbsolute();

        if (null !== $promo->getMinEquivalency() && $quantity < $promo->getMinEquivalency()) {
            return $this->invalid($promo, 'cart.promo.invalid_quantity');
        }

        if (null !== $promo->getMaxEquivalency() && $quantity > $promo->getMaxEquivalency()) {
            return $this->invalid($promo, 'cart.promo.invalid_quantity');
        }

        $itemsBaseTTC = $this->getItemsBaseTTC($cart);

        if (null !== $promo->getMinSubtotalTTC() && $itemsBaseTTC < $promo->getMinSubtotalTTC()) {
            return $this->invalid($promo, 'cart.promo.invalid_amount');
        }

        if (!$this->matchesProducts($cart, $promo)) {
            return $this->invalid($promo, 'cart.promo.invalid_products');
        }

        if ($promo->getZones()->count() > 0) {
            $zone = $this->resolveZone($cart, $groupPrice);

            if (null === $zone) {
                if ($allowPendingAddress && !$this->hasDeliveryAddressContext($cart)) {
                    return new PromoEligibilityResult(
                        promo: $promo,
                        status: CartPromo::STATUS_PENDING_ADDRESS,
                        messageKey: 'cart.promo.pending_address',
                    );
                }

                return $this->invalid($promo, 'cart.promo.invalid_zone');
            }

            if (!$promo->getZones()->exists(static fn (int $key, ShippingZone $promoZone): bool => $promoZone->getId()->equals($zone->getId()))) {
                return $this->invalid($promo, 'cart.promo.invalid_zone');
            }
        }

        return new PromoEligibilityResult(
            promo: $promo,
            status: CartPromo::STATUS_APPLIED,
            messageKey: null,
        );
    }

    public function hasDeliveryAddressContext(Cart $cart): bool
    {
        $deliveryAddress = $cart->getDeliveryAddress() ?? $cart->getInvoiceAddress();

        if (null === $deliveryAddress) {
            return false;
        }

        if (null === $deliveryAddress->getCountry() || '' === mb_trim($deliveryAddress->getCountry())) {
            return false;
        }

        return null !== $deliveryAddress->getPostal() && '' !== mb_trim($deliveryAddress->getPostal());
    }

    private function resolveZone(Cart $cart, mixed $groupPrice = null): ?ShippingZone
    {
        $deliveryAddress = $cart->getDeliveryAddress() ?? $cart->getInvoiceAddress();

        if (null === $deliveryAddress) {
            return null;
        }

        $countryCode = $deliveryAddress->getCountry();
        $postalCode = $deliveryAddress->getPostal();

        if (null === $countryCode || '' === mb_trim($countryCode) || null === $postalCode || '' === mb_trim($postalCode)) {
            return null;
        }

        $country = $this->shippingCountryRepository->findOneByGroup($countryCode, $groupPrice);

        if (null === $country) {
            return null;
        }

        $county = null;
        if ($country->isCounty()) {
            $county = $this->shippingCountyRepository->findDepartmentByCountry(
                mb_substr($postalCode, 0, 2),
                $country->getCode()
            );
        }

        return $this->shippingZoneRepository->findByCountryAndByCounty($country, $county);
    }

    private function matchesProducts(Cart $cart, Promo $promo): bool
    {
        if (0 === $promo->getProducts()->count()) {
            return true;
        }

        $productIds = [];

        foreach ($promo->getProducts() as $product) {
            if ($product instanceof Product) {
                $productIds[$product->getId()->toString()] = true;
            }
        }

        foreach ($cart->getItems() as $item) {
            $product = $item->getVariant()?->getProduct();

            if (null !== $product && isset($productIds[$product->getId()->toString()])) {
                return true;
            }
        }

        return false;
    }

    private function getItemsBaseTTC(Cart $cart): int
    {
        $amount = 0;

        foreach ($cart->getItems() as $item) {
            $amount += max(0, $item->getTotalFinalTTC());
        }

        try {
            $amount -= max(0, (int) ($this->degressiveManager->calculateForCart($cart)?->amountTTC ?? 0));
        } catch (\Throwable) {
            // Fallback to the raw item total when degressivity cannot be resolved.
        }

        return max(0, $amount);
    }

    private function invalid(Promo $promo, string $messageKey): PromoEligibilityResult
    {
        return new PromoEligibilityResult(
            promo: $promo,
            status: CartPromo::STATUS_INVALID,
            messageKey: $messageKey,
        );
    }
}
