<?php

namespace Daylight\Core\Modules\Pricing;

use Daylight\Core\Models\CustomerGroup;
use Daylight\Core\Models\Variant;
use Daylight\Core\Modules\Pricing\Contracts\PricingManagerInterface;
use Daylight\Core\Modules\Pricing\DTO\Price;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Context;

class PricingManager implements PricingManagerInterface
{
    public Price $price;

    public ?Authenticatable $user = null;

    public Variant $purchasable;

    public int $quantity = 1;

    public ?Collection $customerGroups = null;

    public function for(Variant $variant): self
    {
        $this->purchasable = $variant;

        return $this;
    }

    public function user(?Authenticatable $user): self
    {
        $this->user = $user;

        return $this;
    }

    public function guest(): self
    {
        $this->user = null;

        return $this;
    }

    public function quantity(int $quantity): self
    {
        $this->quantity = $quantity;

        return $this;
    }

    public function customerGroups(?Collection $customerGroups): self
    {
        $this->customerGroups = $customerGroups;

        return $this;
    }

    public function customerGroup(?CustomerGroup $customerGroup): self
    {
        $this->customerGroups(
            collect([$customerGroup])
        );

        return $this;
    }

    public function get(): Price
    {
        if (! $this->purchasable) {
            throw new \ErrorException('No purchasable set.');
        }

        $prices = $this->getEligiblePrices()->sortBy('price');

        $basePrice = $prices->first(fn ($price) => $price->min_quantity == 1 && ! $price->customer_group_id && ! $price->customer_id);
        $matched = $basePrice;

        $potentialCustomerPrice = $prices->filter(function ($price) {
            return (bool) $price->customer_id && ($price->min_quantity == 1);
        })->sortBy('price');

        if ($potentialCustomerPrice->isNotEmpty()) {
            $matched = $potentialCustomerPrice->first();
        }

        $potentialGroupPrice = $prices->filter(function ($price) {
            return (bool) $price->customer_group_id && ($price->min_quantity == 1);
        })->sortBy('price');

        if ($matched === $basePrice) {
            $matched = $potentialGroupPrice->first() ?: $matched;
        }

        $priceBreaks = $prices->filter(function ($price) {
            return $price->min_quantity > 1 && $this->quantity >= $price->min_quantity;
        })->sortBy('price');

        $matched = $priceBreaks->first() ?: $matched;

        if (! $matched) {
            throw new \ErrorException('No price set.');
        }

        $this->price = new Price(
            matched: $matched,
            base: $basePrice,
            priceBreaks: $prices->filter(fn ($price) => $price->min_quantity > 1),
            customerGroupPrices: $prices->filter(fn ($price) => (bool) $price->customer_group_id),
            customerPrices: $prices->filter(fn ($price) => (bool) $price->customer_id)
        );

        $this->reset();

        return $this->price;
    }

    /**
     * Get eligible prices — uses scopedPrices (pre-filtered at DB level) when available,
     * otherwise falls back to PHP filtering on the prices collection.
     */
    private function getEligiblePrices(): Collection
    {
        if ($this->purchasable->relationLoaded('scopedPrices')) {
            return $this->purchasable->scopedPrices;
        }

        return $this->filterPricesInPhp();
    }

    /**
     * PHP-based price filtering — used when scopedPrices is not eager-loaded
     * (admin, artisan, queue jobs, explicit user override, lazy-loaded variants).
     */
    private function filterPricesInPhp(): Collection
    {
        $customerId = $this->resolveCustomerId();
        $customerGroups = $this->resolveCustomerGroups();
        $now = Context::get('pricing.now', now());

        return $this->purchasable->prices
            ->filter(function ($price) use ($customerId, $customerGroups, $now) {
                $startsAtOk = is_null($price->starts_at) || $now->greaterThanOrEqualTo(Carbon::parse($price->starts_at));
                $endsAtOk = is_null($price->ends_at) || $now->toDateString() <= Carbon::parse($price->ends_at)->toDateString();

                if (! $startsAtOk || ! $endsAtOk) {
                    return false;
                }

                if (! is_null($price->customer_id)) {
                    return $customerId && ((int) $price->customer_id === (int) $customerId);
                }

                if (! is_null($price->customer_group_id)) {
                    return $customerGroups && $customerGroups->pluck('id')->contains($price->customer_group_id);
                }

                return true;
            });
    }

    private function resolveCustomerId(): ?int
    {
        if ($this->user?->customer) {
            return $this->user->customer->id;
        }

        if (Context::has('pricing.customer_id')) {
            return Context::get('pricing.customer_id');
        }

        if (auth()->check() && auth()->user()->customer) {
            return auth()->user()->customer->id;
        }

        return null;
    }

    private function resolveCustomerGroups(): Collection
    {
        if ($this->customerGroups && $this->customerGroups->count()) {
            return $this->customerGroups;
        }

        if (Context::has('pricing.customer_group_ids')) {
            return Context::remember('pricing.customer_group_models', function () {
                $ids = Context::get('pricing.customer_group_ids', []);

                return CustomerGroup::whereIn('id', $ids)->get();
            });
        }

        if (auth()->check() && auth()->user()->customer) {
            auth()->user()->loadMissing('customer.customerGroups');
            $groups = auth()->user()->customer->customerGroups;

            if ($groups->count()) {
                return $groups;
            }
        }

        return collect([CustomerGroup::getDefault()]);
    }

    private function reset(): void
    {
        $this->quantity = 1;
        $this->customerGroups = null;
    }
}
