<?php

namespace App\Services;

use App\Models\Lookups\DiscountType;
use App\Models\Promotions\Deal;
use Illuminate\Support\Collection;

/**
 * The item-level promotional engine ("Deals").
 *
 * A deal attaches a discount to individual products inside a cart whenever
 * the deal is live. This service is the single evaluation point: every
 * consumer — the POS catalog payload, the checkout credit estimate, the
 * held-sale summary, and the authoritative per-line repricing in
 * InvoiceService::create() — resolves "which deals are active right now and
 * for which products" through these methods, so the UI can never show a
 * discount the engine would not also apply.
 */
class DealService
{
    /**
     * All deals currently live: switched on AND inside their validity window.
     * NULL starts_at = no lower bound, NULL ends_at = no upper bound (the
     * same convention as ProductBundle).
     *
     * @return Collection<int, Deal>
     */
    public function activeDeals(): Collection
    {
        $now = now();

        return Deal::query()
            ->with('discountType')
            ->where('is_active', true)
            ->where(fn ($q) => $q->whereNull('starts_at')->orWhere('starts_at', '<=', $now))
            ->where(fn ($q) => $q->whereNull('ends_at')->orWhere('ends_at', '>=', $now))
            ->orderBy('id')
            ->get();
    }

    /**
     * The single active deal applying to each of the given product ids.
     *
     * Keyed by product id; when several live deals cover the same product the
     * lowest-id deal wins (deterministic, documented, never a coin flip).
     *
     * @param  array<int, int>  $productIds
     * @return array<int, Deal>
     */
    public function activeDealsForProducts(array $productIds): array
    {
        $productIds = array_values(array_unique(array_filter(array_map('intval', $productIds))));

        if ($productIds === []) {
            return [];
        }

        $deals = Deal::query()
            ->with('discountType')
            ->where('is_active', true)
            ->where(fn ($q) => $q->whereNull('starts_at')->orWhere('starts_at', '<=', now()))
            ->where(fn ($q) => $q->whereNull('ends_at')->orWhere('ends_at', '>=', now()))
            ->whereHas('products', fn ($q) => $q->whereIn('products.id', $productIds))
            ->orderBy('id')
            ->get()
            ->loadMissing('products:id');

        $byProduct = [];
        foreach ($deals as $deal) {
            foreach ($deal->products as $product) {
                $byProduct[(int) $product->id] ??= $deal;
            }
        }

        return $byProduct;
    }

    /**
     * The active deal for a single product, if any.
     */
    public function dealForProduct(int $productId): ?Deal
    {
        return $this->activeDealsForProducts([$productId])[$productId] ?? null;
    }

    /**
     * Amount this deal takes off the given line. A fixed discount is a
     * per-unit amount (100 Rs. on a 2-unit line is 200 Rs. off, not 100) —
     * exactly the same convention as InvoiceService::computeLineDiscount().
     * The result is clamped to the line's gross total and rounded to 2 dp.
     */
    public function computeDiscount(Deal $deal, float $grossTotal, float $quantity): float
    {
        if ($grossTotal <= 0) {
            return 0.0;
        }

        $discount = $deal->discountType?->code === DiscountType::PERCENTAGE
            ? $grossTotal * ((float) $deal->discount_value / 100)
            : (float) $deal->discount_value * $quantity;

        return round(min(max($discount, 0), $grossTotal), 2);
    }
}
