<?php

namespace Daylight\Core\Modules\Cart;

use Daylight\Core\Models\Country;
use Daylight\Core\Models\Shipping\ShippingClass;
use Daylight\Core\Models\Shipping\ShippingZone;
use Daylight\Core\Models\Shipping\ShippingZoneRule;
use Daylight\Core\Models\Variant;
use Daylight\Core\Modules\Cart\Contracts\CalculatesShippingCosts;
use Illuminate\Support\Collection;

class ShippingCostCalculator implements CalculatesShippingCosts
{
    protected ?ShippingZone $shippingZone = null;

    protected ?string $shippingPostalCode = null;

    public function __construct(
        protected Cart $cart
    ) {
        //
    }

    public function setBillingCountry(Country $country): self
    {
        $this->cart->setBillingCountry($country);

        return $this;
    }

    public function setShippingCountry(Country $country): self
    {
        $this->cart->setShippingCountry($country);

        return $this;
    }

    public function forCountry(Country $country): self
    {
        $this->shippingZone = $country->shippingZones?->first();
        $this->shippingPostalCode = null;

        return $this;
    }

    public function setShippingPostalCode(?string $postalCode): self
    {
        $this->shippingPostalCode = $postalCode;

        return $this;
    }

    public function calculateTotalShippingCost(): int
    {
        if (FreeShippingPostalEvaluator::grants($this->shippingPostalCode, $this->cart()->shippingCountry(), $this->getShippingZone())) {
            return 0;
        }

        // Free shipping threshold based on order subtotal
        $freeShippingThreshold = $this->getShippingZone()?->free_shipping_above;

        if (! is_null($freeShippingThreshold) && $this->cart()->subtotal() >= $freeShippingThreshold) {
            return 0;
        }

        $total = $this->shippingRules()->map(function ($shippingClass) {
            return $shippingClass['price'];
        })->sum();

        if (is_null($this->getShippingZone()?->max_amount)) {
            return $total;
        }

        return min($total, $this->getShippingZone()?->max_amount);
    }

    public function calculateTotalShippingTax(): ?Collection
    {
        if (FreeShippingPostalEvaluator::grants($this->shippingPostalCode, $this->cart()->shippingCountry(), $this->getShippingZone())) {
            return collect();
        }

        $freeShippingThreshold = $this->getShippingZone()?->free_shipping_above;

        if (! is_null($freeShippingThreshold) && $this->cart()->subtotal() >= $freeShippingThreshold) {
            return collect();
        }

        return $this->shippingRules()->map(function ($shippingClass) {
            return $shippingClass['vat'];
        })->filter();
    }

    public function shippingRules(): Collection
    {
        return $this->getGroupedShippingClasses()
            ->map(function ($shippingClass) {
                return $this->calculateShippingCostForClass($shippingClass);
            });
    }

    protected function getGroupedShippingClasses(): Collection
    {
        return $this->getVariants()
            ->flatMap(function (Variant $variant) {
                return $variant->shippingClasses;
            })
            ->groupBy('id')
            ->map(function ($shippingClasses) {
                return $this->mapShippingClassesWithVariants($shippingClasses);
            });
    }

    protected function getVariants(): Collection
    {
        return $this->cart()->instance()->variants;
    }

    protected function mapShippingClassesWithVariants($shippingClasses): array
    {
        return [
            'shippingClass' => $shippingClasses->first(),
            'variants' => $this->getVariants()->filter(function (Variant $variant) use ($shippingClasses) {
                return $variant->shippingClasses->contains($shippingClasses->first());
            }),
        ];
    }

    protected function calculateShippingCostForClass($shippingClass): Collection|int
    {
        $totalWeight = $this->calculateTotalWeight($shippingClass['variants']);
        $rule = $this->getApplicableRule($shippingClass['shippingClass'], $totalWeight);

        if (! $rule) {
            return 0;
        }

        return collect([
            'price' => $this->calculatePrice($shippingClass, $rule),
            'vat' => $this->calculateTax($shippingClass['shippingClass'], $this->calculatePrice($shippingClass, $rule)),
        ]);
    }

    protected function calculateTax(ShippingClass $shippingClass, int $price): ?array
    {
        if (! $shippingClass->taxable) {
            return null;
        }

        $taxRate = $this->cart()->billingCountry()->taxRates->where('tax_class_id', $shippingClass->tax_class_id)->first();

        return [
            'rate' => $taxRate->rate,
            'amount' => round($taxRate->rate / 100 * $price),
            'name' => $taxRate->name,
        ];
    }

    protected function calculateTotalWeight(Collection $variants): int
    {
        return $variants->reduce(function ($carry, Variant $variant) {
            return intval($carry) + ($variant->weight * $variant->pivot->quantity);
        }, 0);
    }

    protected function getShippingZone(): ?ShippingZone
    {
        if ($this->shippingZone) {
            return $this->shippingZone;
        }

        $shippingZone = $this->cart()->shippingCountry()->shippingZones->first();

        $this->shippingZone = $shippingZone ?: $this->getDefaultShippingZone();

        return $this->shippingZone;
    }

    protected function getDefaultShippingZone(): ?ShippingZone
    {
        return (daylightModel('shipping_zone'))::query()
            ->with('shippingZoneRules')
            ->whereDefault(1)
            ->first();
    }

    protected function getApplicableRule($shippingClass, $totalWeight): ?ShippingZoneRule
    {
        $shippingZoneRules = $this->getShippingZone()
            ? $this->getShippingZone()->shippingZoneRules->where('shipping_class_id', $shippingClass->id)
            : null;

        if (! $shippingZoneRules) {
            return null;
        }

        return $shippingZoneRules->first(function ($rule) use ($totalWeight) {
            return $totalWeight >= $rule->min_weight && ($totalWeight <= $rule->max_weight || is_null($rule->max_weight));
        });
    }

    protected function calculatePrice($shippingClass, $rule): int
    {
        return $shippingClass['shippingClass']['cumulative']
            ? $shippingClass['variants']->sum('pivot.quantity') * $rule->price
            : $rule->price;
    }

    protected function cart(): Cart
    {
        return $this->cart;
    }
}
