<?php

namespace App\Livewire\POS;

use App\Domain\Financial\Models\InvoiceItem;
use App\Domain\Financial\Services\CashSessionService;
use App\Domain\Inventory\Models\Product;
use App\Domain\Inventory\Models\ProductInstallmentPlan;
use App\Domain\Participant\Models\Participant;
use App\Domain\POS\Enums\POSItemType;
use App\Domain\POS\Enums\POSPaymentMethod;
use App\Domain\POS\Services\POSService;
use App\Domain\Pricing\Services\PricingService;
use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Traits\UsesBranchScope;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Title;
use Livewire\Component;

#[Layout('layouts.app')]
#[Title('نقطة البيع')]
class POSTerminal extends Component
{
    use UsesBranchScope;

    // Cart state
    public array $cart = [];

    /**
     * The buyer, and everything read off their record.
     *
     * Locked: the only writers are selectParticipant()/clearParticipant(),
     * which resolve the id through the branch-scoped model. Left plain, the
     * browser could name any participant it liked — and since this id now
     * decides which price list the till quotes and which tier's instalment
     * plans are offered, it also decides what the sale costs.
     */
    #[Locked]
    public ?int $participantId = null;
    #[Locked]
    public string $participantName = '';
    #[Locked]
    public bool $participantIsFree = false;
    public string $participantSearch = '';
    public string $couponCode = '';
    public string $paymentMethod = 'cash';
    public string $notes = '';

    // Split payment
    public array $splitPayments = [];
    public string $splitMethod = 'cash';
    public float $splitAmount = 0;

    // Manual item
    public string $manualItemName = '';
    public float $manualItemPrice = 0;
    public int $manualItemQty = 1;

    // Partial payment (deposit)
    public bool $useDeposit = false;
    public float $depositAmount = 0;

    // UI state
    public bool $showCheckout = false;
    public bool $showReceipt = false;
    public ?string $lastReceiptNumber = null;
    public ?int $lastTransactionTotal = null;
    public ?int $lastTransactionPaid = null;
    public ?string $lastTransactionUuid = null;

    // Cash session
    public bool $hasOpenSession = false;

    // Participant search results
    public array $searchResults = [];

    // Essential product warnings (product was already purchased this year)
    public array $essentialWarnings = [];

    /**
     * Per-request caches. Not public, and not meant to survive the request:
     * anything the browser could set here would decide what a buyer is charged.
     *
     * @var array<int, Product|null>
     */
    private array $productMemo = [];

    private ?string $tierMemo = null;

    public function mount(CashSessionService $cashSessionService): void
    {
        $this->authorize('pos.sell');

        $session = $cashSessionService->getOpenSession(auth()->user());
        $this->hasOpenSession = $session !== null;
    }

    public function updatedParticipantSearch(): void
    {
        if (mb_strlen($this->participantSearch) < 2) {
            $this->searchResults = [];
            return;
        }

        $this->searchResults = Participant::when($this->getActiveBranchId(), fn ($q) => $q->where('branch_id', $this->getActiveBranchId()))
            ->whereHas('person', fn ($q) => $q->where('name_ar', 'ilike', "%{$this->participantSearch}%")
              ->orWhere('phone', 'like', "%{$this->participantSearch}%")
        )->with('person')->limit(10)->get()
            ->map(fn ($p) => ['id' => $p->id, 'name' => $p->person->name_ar, 'phone' => $p->person->phone ?? ''])
            ->toArray();
    }

    public function selectParticipant(int $id): void
    {
        $this->tierMemo = null;
        $participant = Participant::with('person')->findOrFail($id);
        $this->participantId = $participant->id;
        $this->participantName = $participant->person->name_ar;
        $this->participantIsFree = (bool) $participant->is_free;
        $this->participantSearch = '';
        $this->searchResults = [];

        $this->repriceCart();
    }

    public function clearParticipant(): void
    {
        $this->tierMemo = null;
        $this->participantId = null;
        $this->participantName = '';
        $this->participantIsFree = false;
        $this->searchResults = [];
        $this->essentialWarnings = [];

        $this->repriceCart();
    }

    public function addProduct(int $productId): void
    {
        $product = Product::findOrFail($productId);

        // Check if essential product was already purchased this year by this participant
        if ($product->is_essential && $this->participantId) {
            $this->checkEssentialProductHistory($product);
        }

        $existingIndex = collect($this->cart)->search(fn ($item) => $item['item_type'] === POSItemType::Product->value && $item['item_id'] === $product->id);

        if ($existingIndex !== false) {
            $this->updateQuantity($existingIndex, $this->cart[$existingIndex]['quantity'] + 1);
            return;
        }

        ['base' => $basePrice, 'unit' => $unitPrice, 'snapshot' => $snapshot] = $this->priceFor($product);

        $this->cart[] = [
            'item_type' => POSItemType::Product->value,
            'item_id' => $product->id,
            'item_name_ar' => $product->name_ar,
            'quantity' => 1,
            'unit_price' => $unitPrice,
            'base_price' => $basePrice,
            'discount_amount' => $basePrice - $unitPrice,
            'line_total' => $unitPrice,
            'pricing_snapshot' => $snapshot,
            'installment_plan_id' => null,
            'pay_installments' => 1,
            'metadata' => ['product_id' => $product->id, 'sku' => $product->sku],
        ];
    }

    /**
     * Which price list this buyer is on.
     *
     * `membership_type` is cast to an enum on the model but arrives as a plain
     * string from an import or a legacy row, and a walk-in with no participant
     * at all is a non-member. All three answer to the same three values the
     * products table and the instalment plans are keyed by.
     */
    private function buyerTier(): string
    {
        if ($this->tierMemo !== null) {
            return $this->tierMemo;
        }

        $participant = $this->participantId ? Participant::find($this->participantId) : null;
        $type = $participant?->membership_type;

        if ($type instanceof \BackedEnum) {
            $type = $type->value;
        }

        return $this->tierMemo = $type === 'member' ? 'member' : 'non_member';
    }

    /**
     * One lookup per product per request. cartSchedules() runs from three
     * callers on every render, and without this each of them re-queried every
     * line's product and its plans.
     */
    private function product(int $id): ?Product
    {
        return $this->productMemo[$id] ??= Product::find($id);
    }

    /**
     * The instalment plans this buyer may be offered for this product.
     *
     * Plans are configured per tier — member only, non-member only, or both —
     * and a plan is only ever a plan for an annual product. Everything here is
     * read from the database on every call rather than kept in the cart: the
     * cart is a public property, so a plan id sitting in it is a number the
     * browser sent, not a plan.
     */
    private function plansFor(Product $product): \Illuminate\Support\Collection
    {
        if (! $product->isAnnual()) {
            return collect();
        }

        $tier = $this->buyerTier();

        if ($product->relationLoaded('installmentPlans')) {
            return $product->installmentPlans
                ->where('is_active', true)
                ->filter(fn ($plan) => in_array($plan->membership_tier, [$tier, 'any'], true))
                ->sortBy('sort_order')
                ->values();
        }

        return $product->installmentPlans()
            ->active()
            ->forTier($tier)
            ->orderBy('sort_order')
            ->get();
    }

    /**
     * Resolve the plan a cart line claims, or null if it may not have it.
     *
     * Guards the two ways a browser-supplied id goes wrong: a plan belonging to
     * another product, and a plan for the other membership tier — which is
     * exactly what happens on its own when a cashier picks the member plan and
     * then attaches a non-member to the sale.
     */
    private function resolvePlan(array $item): ?ProductInstallmentPlan
    {
        if (empty($item['installment_plan_id']) || empty($item['item_id'])) {
            return null;
        }

        $product = $this->product((int) $item['item_id']);
        if (! $product) {
            return null;
        }

        return $this->plansFor($product)->firstWhere('id', (int) $item['installment_plan_id']);
    }

    public function setItemPlan(int $index, ?int $planId): void
    {
        if (! isset($this->cart[$index])) {
            return;
        }

        if (! $planId) {
            $this->cart[$index]['installment_plan_id'] = null;
            $this->cart[$index]['pay_installments'] = 1;

            return;
        }

        $plan = $this->resolvePlan(['installment_plan_id' => $planId, 'item_id' => $this->cart[$index]['item_id'] ?? null]);

        if (! $plan) {
            session()->flash('error', __('خطة التقسيط غير متاحة لهذا المنتج أو لفئة هذا المشترك'));

            return;
        }

        $this->cart[$index]['installment_plan_id'] = $plan->id;
        $this->cart[$index]['pay_installments'] = 1;
        // A plan already says what is paid today; a deposit on top of it is a
        // second answer to the same question.
        $this->useDeposit = false;
        $this->depositAmount = 0;
    }

    public function setPayInstallments(int $index, int $count): void
    {
        if (! isset($this->cart[$index])) {
            return;
        }

        $plan = $this->resolvePlan($this->cart[$index]);
        if (! $plan) {
            return;
        }

        $this->cart[$index]['pay_installments'] = max(1, min($count, $plan->installments));
    }

    /**
     * Every cart line's schedule, as the screen and the checkout both need it.
     *
     * @return array<int, array{plan: ProductInstallmentPlan, schedule: array<int,int>, pay: int, due_now: int}>
     */
    private function cartSchedules(): array
    {
        $out = [];

        foreach ($this->cart as $index => $item) {
            $plan = $this->resolvePlan($item);
            if (! $plan) {
                continue;
            }

            $schedule = $plan->buildSchedule((int) $item['line_total']);
            $pay = max(1, min((int) ($item['pay_installments'] ?? 1), $plan->installments));

            $out[$index] = [
                'plan' => $plan,
                'schedule' => $schedule,
                'pay' => $pay,
                'due_now' => array_sum(array_slice($schedule, 0, $pay)),
            ];
        }

        return $out;
    }

    public function cartHasPlan(): bool
    {
        return $this->cartSchedules() !== [];
    }

    /**
     * What the customer hands over today.
     *
     * A line on a plan owes the instalments being paid now; every other line
     * owes its full total, and the platform's service fee is charged in full on
     * the first visit rather than dribbled across the schedule.
     */
    public function getAmountDueNow(): int
    {
        $schedules = $this->cartSchedules();

        if ($schedules === []) {
            return $this->getCartGrandTotal();
        }

        $due = $this->getServiceFee();

        foreach ($this->cart as $index => $item) {
            $due += $schedules[$index]['due_now'] ?? (int) $item['line_total'];
        }

        return min($due, $this->getCartGrandTotal());
    }

    /**
     * What this product costs THIS buyer, right now.
     *
     * A product can be priced per membership type — a member pays one rate, a
     * walk-in another — and that only comes out of the pricing engine, which
     * resolves the base price for the buyer and then applies the academy's
     * rules to it. Reading products.selling_price instead, which is what this
     * terminal used to do, charges every member the non-member price and there
     * is nothing on the receipt to show it happened.
     *
     * The engine now reads products.member_price / non_member_price as a
     * membership-specific base price, so a rate typed on the product screen
     * reaches this till too; priceForTier() is the fallback for a product that
     * has no configured price at all.
     *
     * @return array{base: int, unit: int, snapshot: array}
     */
    private function priceFor(Product $product, ?Participant $participant = null): array
    {
        $participant ??= $this->participantId ? Participant::find($this->participantId) : null;

        try {
            $result = app(PricingService::class)->calculate(
                priceable: $product,
                participant: $participant,
                branchId: $this->getActiveBranchId() ?? $participant?->branch_id,
            );

            return [
                'base' => $result->baseAmount,
                'unit' => $result->finalAmount,
                'snapshot' => $result->toArray(),
            ];
        } catch (DomainException) {
            // Nobody priced this product through the engine. priceForTier()
            // still knows whether a member rate was typed on the product itself
            // and falls back to the catalogue price only when it was not —
            // refusing the sale outright would close the shop over a
            // configuration gap.
            $fallback = (int) $product->priceForTier($this->buyerTier());

            return [
                'base' => $fallback,
                'unit' => $fallback,
                'snapshot' => ['source' => 'product_tier_price', 'membership_type' => $this->buyerTier()],
            ];
        }
    }

    /**
     * Re-price every product already in the cart for the buyer now attached to it.
     *
     * Cashiers scan first and identify the customer afterwards at least as often
     * as the other way round, so a member rate that is only applied at the moment
     * a product is added is a member rate half the members never get.
     */
    private function repriceCart(bool $announce = true): bool
    {
        $participant = $this->participantId ? Participant::find($this->participantId) : null;
        $changed = false;
        $unresolved = [];

        foreach ($this->cart as $index => $item) {
            if (($item['item_type'] ?? '') !== POSItemType::Product->value || empty($item['item_id'])) {
                continue;
            }

            // Product is branch-scoped, so a line the browser injected for
            // another branch's product — or for one that has since been
            // deactivated — resolves to null here. Skipping it left that line in
            // the cart carrying the price the browser supplied and let checkout
            // sell it unpriced; it comes out instead, and the caller is told the
            // cart changed.
            $product = Product::find($item['item_id']);
            if (! $product) {
                $unresolved[] = $index;
                $changed = true;
                continue;
            }

            ['base' => $base, 'unit' => $unit, 'snapshot' => $snapshot] = $this->priceFor($product, $participant);

            if ($unit !== (int) $item['unit_price']) {
                $changed = true;
            }

            $quantity = max(1, (int) ($item['quantity'] ?? 1));

            $this->cart[$index]['base_price'] = $base;
            $this->cart[$index]['unit_price'] = $unit;
            $this->cart[$index]['line_total'] = $unit * $quantity;
            $this->cart[$index]['discount_amount'] = ($base - $unit) * $quantity;
            $this->cart[$index]['pricing_snapshot'] = $snapshot;

            // Plans are tier-scoped too. Attaching a non-member to a sale that
            // was built on the members-only plan has to drop the plan, not
            // quietly bill him on it.
            if (! empty($item['installment_plan_id']) && ! $this->resolvePlan($this->cart[$index])) {
                $this->cart[$index]['installment_plan_id'] = null;
                $this->cart[$index]['pay_installments'] = 1;
                $changed = true;
            }
        }

        foreach ($unresolved as $index) {
            unset($this->cart[$index]);
        }

        if ($unresolved) {
            $this->cart = array_values($this->cart);
        }

        if ($changed && $announce) {
            session()->flash('info', __('تم تحديث أسعار السلة حسب فئة المشترك'));
        }

        return $changed;
    }

    private function checkEssentialProductHistory(Product $product): void
    {
        $yearStart = now()->startOfYear();
        $productType = 'App\\Domain\\Inventory\\Models\\Product';

        $previousPurchase = InvoiceItem::where('itemable_type', $productType)
            ->where('itemable_id', $product->id)
            ->where('created_at', '>=', $yearStart)
            ->whereHas('invoice', fn ($q) => $q
                ->where('billable_type', 'App\\Domain\\Participant\\Models\\Participant')
                ->where('billable_id', $this->participantId)
                ->whereNotIn('status', ['cancelled', 'draft'])
            )
            ->orderByDesc('created_at')
            ->first();

        if ($previousPurchase) {
            $this->essentialWarnings[$product->id] = [
                'product_name' => $product->name_ar,
                'purchased_at' => $previousPurchase->created_at->format('Y-m-d'),
                'participant_name' => $this->participantName,
            ];
            $this->dispatch('essential-product-warning', productId: $product->id);
        }
    }

    public function dismissEssentialWarning(int $productId): void
    {
        unset($this->essentialWarnings[$productId]);
    }

    public function addManualItem(): void
    {
        if (empty($this->manualItemName) || $this->manualItemPrice <= 0) {
            session()->flash('error', __('يرجى إدخال اسم العنصر والسعر'));
            return;
        }

        $unitPrice = (int) round($this->manualItemPrice * 100);

        $this->cart[] = [
            'item_type' => POSItemType::Other->value,
            'item_id' => null,
            'item_name_ar' => $this->manualItemName,
            'quantity' => max(1, $this->manualItemQty),
            'unit_price' => $unitPrice,
            'base_price' => $unitPrice,
            'discount_amount' => 0,
            'line_total' => $unitPrice * max(1, $this->manualItemQty),
            'pricing_snapshot' => [],
            'metadata' => [],
        ];

        $this->manualItemName = '';
        $this->manualItemPrice = 0;
        $this->manualItemQty = 1;
    }

    public function removeItem(int $index): void
    {
        unset($this->cart[$index]);
        $this->cart = array_values($this->cart);
    }

    public function updateQuantity(int $index, int $quantity): void
    {
        // setItemPlan() and setPayInstallments() both check this; without it
        // here, an index the browser invented wrote a new cart line made of a
        // quantity and nothing else — no product, no name, no price — which
        // checkout then tried to sell.
        if (! isset($this->cart[$index])) {
            return;
        }

        if ($quantity <= 0) {
            $this->removeItem($index);
            return;
        }
        $this->cart[$index]['quantity'] = $quantity;
        $this->cart[$index]['line_total'] = $this->cart[$index]['unit_price'] * $quantity;
        $this->cart[$index]['discount_amount'] = (int) (($this->cart[$index]['base_price'] - $this->cart[$index]['unit_price']) * $quantity);
    }

    public function getCartTotal(): int
    {
        return collect($this->cart)->sum(fn ($item) => (int) $item['line_total']);
    }

    public function getCartDiscount(): int
    {
        return collect($this->cart)->sum(fn ($item) => (int) $item['discount_amount']);
    }

    public function getCartSubtotal(): int
    {
        return collect($this->cart)->sum(fn ($item) => (int) $item['base_price'] * (int) $item['quantity']);
    }

    public function getServiceFee(): int
    {
        $service = app(\App\Domain\Shared\Services\PlatformFeeService::class);
        if (!$service->customerPays()) {
            return 0;
        }
        return $service->calculate($this->getCartTotal());
    }

    public function getCartGrandTotal(): int
    {
        return $this->getCartTotal() + $this->getServiceFee();
    }

    public function addSplitPayment(): void
    {
        if ($this->splitAmount <= 0) {
            return;
        }

        $this->splitPayments[] = [
            'method' => $this->splitMethod,
            'amount' => (int) round($this->splitAmount * 100),
            'reference' => null,
        ];

        $this->splitAmount = 0;
    }

    public function removeSplitPayment(int $index): void
    {
        unset($this->splitPayments[$index]);
        $this->splitPayments = array_values($this->splitPayments);
    }

    public function getSplitTotal(): int
    {
        return collect($this->splitPayments)->sum(fn ($sp) => (int) $sp['amount']);
    }

    public function cartAllowsDeposit(): bool
    {
        foreach ($this->cart as $item) {
            if (($item['item_type'] ?? '') === POSItemType::Product->value && isset($item['item_id'])) {
                $product = Product::find($item['item_id']);
                if ($product && $product->allows_partial_payment) {
                    return true;
                }
            }
        }
        return false;
    }

    public function getMinimumDepositPercent(): int
    {
        $maxPercent = 10;
        foreach ($this->cart as $item) {
            if (($item['item_type'] ?? '') === POSItemType::Product->value && isset($item['item_id'])) {
                $product = Product::find($item['item_id']);
                if ($product && $product->allows_partial_payment) {
                    $maxPercent = max($maxPercent, $product->minimum_deposit_percent);
                }
            }
        }
        return $maxPercent;
    }

    public function getMinimumDepositAmount(): int
    {
        $grandTotal = $this->getCartGrandTotal();
        return (int) ceil($grandTotal * $this->getMinimumDepositPercent() / 100);
    }

    public function updatedUseDeposit(): void
    {
        if ($this->useDeposit) {
            $this->depositAmount = round($this->getMinimumDepositAmount() / 100, 2);
        } else {
            $this->depositAmount = 0;
        }
    }

    public function openCheckout(): void
    {
        if (empty($this->cart)) {
            session()->flash('error', __('السلة فارغة'));
            return;
        }
        $this->showCheckout = true;
    }

    public function closeCheckout(): void
    {
        $this->showCheckout = false;
    }

    public function checkout(POSService $posService): void
    {
        if (empty($this->cart)) {
            session()->flash('error', __('السلة فارغة'));
            return;
        }

        // The cart is a public property, so the prices arriving here are
        // whatever the browser last sent. Price every product again from the
        // engine before any of it is believed, and stop if the answer differs
        // from what the cashier is looking at — the total charged has to be the
        // total on screen.
        if ($this->repriceCart(announce: false)) {
            $this->showCheckout = false;
            session()->flash('error', __('تغيّر سعر أحد المنتجات — راجع السلة قبل إتمام البيع'));
            return;
        }

        // A plan is its own answer to "how much today", so the deposit rules
        // below do not apply and the two must not both be in play.
        $hasPlan = $this->cartHasPlan();
        if ($hasPlan) {
            $this->useDeposit = false;
            $this->depositAmount = 0;
        }

        // Validate deposit amount if using deposit
        if ($this->useDeposit && $this->cartAllowsDeposit()) {
            $depositPiasters = (int) round($this->depositAmount * 100);
            $minDeposit = $this->getMinimumDepositAmount();
            $grandTotal = $this->getCartGrandTotal();

            if ($depositPiasters < $minDeposit) {
                session()->flash('error', __('مبلغ المقدم أقل من الحد الأدنى'));
                return;
            }
            if ($depositPiasters >= $grandTotal) {
                session()->flash('error', __('مبلغ المقدم يجب أن يكون أقل من الإجمالي'));
                return;
            }
        }

        // Validate split total (must cover what is being collected today)
        if ($this->paymentMethod === 'split' && !$this->useDeposit) {
            $splitTotal = $this->getSplitTotal();
            $grandTotal = $this->getAmountDueNow();
            if ($splitTotal < $grandTotal) {
                session()->flash('error', __('المبلغ المدفوع أقل من الإجمالي'));
                return;
            }
        }

        try {
            $participant = $this->participantId ? Participant::find($this->participantId) : null;

            $depositPiasters = null;
            if ($this->useDeposit && $this->cartAllowsDeposit()) {
                $depositPiasters = (int) round($this->depositAmount * 100);
            }

            // The plan is rebuilt from the database inside POSService too — this
            // is what the cashier is looking at, not what the sale is billed on.
            $dueNow = $hasPlan ? $this->getAmountDueNow() : null;

            $transaction = $posService->processTransaction(
                cartItems: $this->cart,
                cashier: auth()->user(),
                branchId: $this->getActiveBranchId() ?? auth()->user()->branch_id ?? 0,
                paymentMethod: $this->paymentMethod,
                participant: $participant,
                couponCode: $this->couponCode ?: null,
                splitPayments: $this->paymentMethod === 'split' ? $this->splitPayments : null,
                notes: $this->notes ?: null,
                depositAmount: $depositPiasters,
            );

            if ($dueNow !== null) {
                $depositPiasters = $transaction->metadata['amount_due_now'] ?? $dueNow;
            }

            $this->lastReceiptNumber = $transaction->receipt_number;
            $this->lastTransactionTotal = $transaction->total_amount;
            $this->lastTransactionPaid = $depositPiasters ?? $transaction->total_amount;
            $this->lastTransactionUuid = $transaction->uuid;
            $this->showReceipt = true;

            // Reset cart
            $this->cart = [];
            $this->couponCode = '';
            $this->notes = '';
            $this->splitPayments = [];
            $this->useDeposit = false;
            $this->depositAmount = 0;
            $this->showCheckout = false;

            session()->flash('success', __('تمت العملية بنجاح'));
        } catch (DomainException $e) {
            session()->flash('error', $e->getMessage());
        }
    }

    public function closeReceipt(): void
    {
        $this->showReceipt = false;
        $this->lastReceiptNumber = null;
        $this->lastTransactionTotal = null;
        $this->lastTransactionPaid = null;
        $this->lastTransactionUuid = null;
    }

    public function newTransaction(): void
    {
        $this->cart = [];
        $this->participantId = null;
        $this->participantName = '';
        $this->couponCode = '';
        $this->paymentMethod = 'cash';
        $this->notes = '';
        $this->splitPayments = [];
        $this->useDeposit = false;
        $this->depositAmount = 0;
        $this->showCheckout = false;
        $this->showReceipt = false;
        $this->lastReceiptNumber = null;
        $this->lastTransactionTotal = null;
        $this->lastTransactionPaid = null;
        $this->lastTransactionUuid = null;
    }

    public function render()
    {
        $branchId = $this->getActiveBranchId() ?? auth()->user()->branch_id ?? null;

        $tier = $this->buyerTier();

        $products = Product::where('is_active', true)
            ->when($branchId, fn ($q) => $q->where(function ($sub) use ($branchId) {
                $sub->where('branch_id', $branchId);
            }))
            // The grid needs to say which items can be put on a schedule, and
            // only plans this buyer's tier is entitled to count.
            ->with(['installmentPlans' => fn ($q) => $q->active()->forTier($tier)->orderBy('sort_order')])
            ->orderBy('name_ar')
            ->get();

        $productHasPlans = [];
        foreach ($products as $product) {
            $productHasPlans[$product->id] = $product->isAnnual() && $product->installmentPlans->isNotEmpty();
        }

        // Show the grid the price this buyer will actually be charged. A member
        // rate that only appears once the item is in the cart is a rate the
        // cashier cannot quote out loud.
        $participant = $this->participantId ? Participant::find($this->participantId) : null;
        $productPrices = [];
        foreach ($products as $product) {
            $productPrices[$product->id] = $this->priceFor($product, $participant);
        }

        // Which lines can be put on a plan, and what each plan would cost —
        // read fresh, so the options on screen are the options checkout will
        // accept and nothing here comes back off the cart.
        $cartPlans = [];
        $cartSchedules = [];
        foreach ($this->cart as $index => $item) {
            if (($item['item_type'] ?? '') !== POSItemType::Product->value || empty($item['item_id'])) {
                continue;
            }

            $product = $products->firstWhere('id', $item['item_id']) ?? Product::find($item['item_id']);
            if (! $product) {
                continue;
            }

            $plans = $this->plansFor($product);
            if ($plans->isEmpty()) {
                continue;
            }

            $cartPlans[$index] = $plans->map(fn ($plan) => [
                'id' => $plan->id,
                'label_ar' => $plan->label_ar,
                'installments' => $plan->installments,
                'frequency_label' => $plan->frequencyLabel(),
                'tier_label' => $plan->tierLabel(),
                'schedule' => array_values($plan->buildSchedule((int) $item['line_total'])),
            ])->values()->all();
        }

        foreach ($this->cartSchedules() as $index => $row) {
            $cartSchedules[$index] = [
                'label_ar' => $row['plan']->label_ar,
                'installments' => $row['plan']->installments,
                'frequency_label' => $row['plan']->frequencyLabel(),
                'schedule' => array_values($row['schedule']),
                'pay' => $row['pay'],
                'due_now' => $row['due_now'],
            ];
        }

        return view('livewire.pos.pos-terminal', [
            'products' => $products,
            'productPrices' => $productPrices,
            'cartTotal' => $this->getCartTotal(),
            'cartDiscount' => $this->getCartDiscount(),
            'cartSubtotal' => $this->getCartSubtotal(),
            'serviceFee' => $this->getServiceFee(),
            'cartGrandTotal' => $this->getCartGrandTotal(),
            'paymentMethods' => POSPaymentMethod::cases(),
            'productHasPlans' => $productHasPlans,
            'cartPlans' => $cartPlans,
            'cartSchedules' => $cartSchedules,
            'amountDueNow' => $this->getAmountDueNow(),
            'hasInstallmentPlan' => $cartSchedules !== [],
            'buyerTier' => $this->buyerTier(),
            'allowsDeposit' => $this->cartAllowsDeposit(),
            'minimumDepositAmount' => $this->getMinimumDepositAmount(),
            'minimumDepositPercent' => $this->getMinimumDepositPercent(),
        ]);
    }
}
