<script lang="ts">
	import { Button } from '$lib/components/ui/button';
	import { Input } from '$lib/components/ui/input';
	import { Separator } from '$lib/components/ui/separator';
	import { generateUUID } from '$lib/utils/uuid';
	import { formatCurrency } from '$lib/utils/format';
	import { toast } from 'svelte-sonner';
	import { extractErrorMessage } from '$lib/utils/toast-safe';
	import { apiFetch } from '$lib/utils/api';
	import { SvelteMap } from 'svelte/reactivity';
	import SquareCardInput from '$lib/components/payments/SquareCardInput.svelte';
	import {
		CARD_VERIFICATION_RETRY_MESSAGE,
		isSquareConfigured,
		isVerificationRequiredSignal,
		runSavedCardSCAProactively,
		SCA_REFUSAL_MESSAGE_TILL,
		shouldShowSCARefusal,
		submitPaymentWithRetry,
		tokenizeSavedCardWithVerification,
		PAYMENT_METHOD_SAVED_CARD,
		type SavedCardVerificationResult
	} from '$lib/square/square';
	import ScaFallbackConsentDialog from '$lib/components/payments/ScaFallbackConsentDialog.svelte';

	type CartItem = {
		id: string;
		label: string;
		price: number;
		qty: number;
	};

	type TillPaymentMethod =
		'cash' | 'card_machine' | 'online_square' | typeof PAYMENT_METHOD_SAVED_CARD;

	const PAYMENT_METHODS: Array<{ key: TillPaymentMethod; label: string }> = [
		{ key: 'cash', label: 'Cash' },
		{ key: 'card_machine', label: 'Card Machine' },
		{ key: 'online_square', label: 'Online Card' },
		{ key: PAYMENT_METHOD_SAVED_CARD, label: 'Saved Card' }
	];

	let cart = $state<CartItem[]>([]);
	let giftCardAmount = $state('25');
	let showGiftCardInput = $state(false);

	// Gift card funding is capped at £250 per transaction (the backend enforces
	// the same limit) — mirror the cap client-side so the till cannot queue an
	// oversized gift card.
	const GIFT_CARD_MAX_AMOUNT = 250;

	let paymentMethod = $state<TillPaymentMethod>('cash');
	let onlineSquareCardReady = $state(false);
	let onlineSquareCardInput = $state<SquareCardInput | null>(null);
	let processing = $state(false);
	let paymentError = $state<string | null>(null);
	// Synchronous double-click guard (see BookingFlow) — Svelte 5 reactivity is
	// async, so `processing` may not reach the button before a fast second click.
	let isProcessingPaymentSync = false;

	// Idempotency keys are cached per cart line (item id, quantity index, price,
	// payment method) so a lost-response retry of the SAME cart reuses the keys:
	// the backend re-attempts the charge with the stored key, Square dedups, and
	// the customer is not charged twice. A changed cart/amount/payment method
	// yields a different composite key, so genuinely new sales get fresh keys.
	// Mirrors the BookingFlow/PaymentModal/TipPayment per-charge caching pattern.
	let idempotencyKeys = new SvelteMap<string, string>();

	function idempotencyKeyFor(item: CartItem, qtyIndex: number): string {
		// saved_card charges also key on the selected card id so switching to a
		// different card (or back to another method) yields fresh keys.
		const composite = `${item.id}:${qtyIndex}:${item.price}:${paymentMethod}:${paymentMethod === PAYMENT_METHOD_SAVED_CARD ? (selectedSavedCardId ?? '') : ''}`;
		let key = idempotencyKeys.get(composite);
		if (!key) {
			key = generateUUID();
			idempotencyKeys.set(composite, key);
		}
		return key;
	}

	// ---------- Customer picker (saved-card payments) ----------
	type TillCustomer = {
		id: string;
		name: string;
		email?: string;
	};

	// Fields mirror the backend SavedCard shape (payment-methods endpoint).
	type SavedCard = {
		id: string;
		brand: string;
		last_4: string;
		exp_month: number;
		exp_year: number;
		cardholder_name?: string;
		// Square's card-on-file id (`ccof:...`), needed to run the saved-card SCA
		// challenge (tokenizeSavedCardWithVerification).
		square_card_id?: string;
	};

	let customerQuery = $state('');
	let customerResults = $state<TillCustomer[]>([]);
	let loadingCustomers = $state(false);
	let showCustomerResults = $state(false);
	let selectedCustomer = $state<TillCustomer | null>(null);
	let savedCards = $state<SavedCard[]>([]);
	let loadingSavedCards = $state(false);
	let savedCardsError = $state<string | null>(null);
	let selectedSavedCardId = $state<string | null>(null);

	// Square convention: a card is valid through the end of its exp_month/exp_year.
	const validCards = $derived(
		savedCards.filter((card) => {
			const now = new Date();
			return (
				card.exp_year > now.getFullYear() ||
				(card.exp_year === now.getFullYear() && card.exp_month >= now.getMonth() + 1)
			);
		})
	);

	// Outcome of the last saved-card SCA attempt: 'sca-unavailable' drives the
	// C6 refusal notice (SCA is the ONLY authorisation — there is no 2FA
	// fallback); every other outcome keeps SCA primary for the next retry.
	let lastSCAOutcome = $state('');
	// True while the saved-card 3DS challenge is open and the CUSTOMER must
	// approve it in their banking app — drives the "waiting for approval" panel.
	let awaitingSCA = $state(false);

	// The saved-card option is hidden outright unless a customer is selected
	// AND has at least one currently-valid card on file.
	const showSavedCardOption = $derived(selectedCustomer !== null && validCards.length > 0);

	const availablePaymentMethods = $derived(
		PAYMENT_METHODS.filter((m) => m.key !== PAYMENT_METHOD_SAVED_CARD || showSavedCardOption)
	);

	// If the saved-card option disappears (customer cleared, no valid cards, or
	// a card expires mid-session) fall back to cash instead of leaving the till
	// on an unrenderable method.
	$effect(() => {
		if (paymentMethod === PAYMENT_METHOD_SAVED_CARD && !showSavedCardOption) {
			paymentMethod = 'cash';
			selectedSavedCardId = null;
		}
	});

	async function searchCustomers() {
		if (!customerQuery.trim()) return;
		loadingCustomers = true;
		try {
			const res = await apiFetch(
				`/api/admin/users?page=1&per_page=5&q=${encodeURIComponent(customerQuery.trim())}`
			);
			if (res.ok) {
				const data = await res.json();
				const excludedRoles = ['admin', 'guest', 'affiliate'];
				customerResults = (data.users || [])
					.filter((u: { account_role: string }) => !excludedRoles.includes(u.account_role))
					.map((u: { id: string; fullName: string; email?: string }) => ({
						id: u.id,
						name: u.fullName || 'Customer',
						email: u.email
					}));
				showCustomerResults = true;
			}
		} catch {
			toast.error('Failed to search customers');
		} finally {
			loadingCustomers = false;
		}
	}

	function selectCustomer(customer: TillCustomer) {
		selectedCustomer = customer;
		customerQuery = '';
		customerResults = [];
		showCustomerResults = false;
		fetchSavedCards(customer.id);
	}

	function clearSelectedCustomer() {
		selectedCustomer = null;
		savedCards = [];
		savedCardsError = null;
		selectedSavedCardId = null;
		customerResults = [];
		showCustomerResults = false;
	}

	async function fetchSavedCards(userId: string) {
		loadingSavedCards = true;
		savedCards = [];
		savedCardsError = null;
		selectedSavedCardId = null;
		try {
			const res = await apiFetch(`/api/admin/users/${userId}/payment-methods`);
			if (res.ok) {
				savedCards = await res.json();
			} else {
				savedCardsError = 'Failed to load saved cards';
			}
		} catch {
			savedCardsError = 'Failed to load saved cards';
		} finally {
			loadingSavedCards = false;
		}
	}

	const subtotal = $derived(cart.reduce((sum, item) => sum + item.price * item.qty, 0));
	const itemCount = $derived(cart.reduce((sum, item) => sum + item.qty, 0));

	// The backend till sale API currently only accepts item_type 'gift_card', so
	// retail items cannot be charged yet — gate the Charge button to gift-card-only carts.
	const hasRetailItems = $derived(cart.some((i) => i.label !== 'Gift Card'));
	const canCharge = $derived(
		cart.length > 0 &&
			!hasRetailItems &&
			subtotal > 0 &&
			!cart.some((i) => i.price > GIFT_CARD_MAX_AMOUNT)
	);

	// Client-side parity with the per-transaction gift card cap: the amount
	// typed into the gift-card input must not exceed £250.
	const parsedGiftCardAmount = $derived(parseFloat(giftCardAmount));
	const giftCardAmountTooHigh = $derived(
		!isNaN(parsedGiftCardAmount) && parsedGiftCardAmount > GIFT_CARD_MAX_AMOUNT
	);

	function addItem(label: string, price: number) {
		const existing = cart.find((i) => i.label === label);
		if (existing) {
			existing.qty++;
		} else {
			cart = [...cart, { id: generateUUID(), label, price, qty: 1 }];
		}
	}

	function addGiftCard() {
		const amt = parseFloat(giftCardAmount);
		if (isNaN(amt) || amt <= 0) return;
		// The Add button is disabled via `giftCardAmountTooHigh`, but the
		// input's Enter key bypasses that — reject here too (defense in depth).
		if (amt > GIFT_CARD_MAX_AMOUNT) return;
		addItem('Gift Card', amt);
		giftCardAmount = '25';
		showGiftCardInput = false;
	}

	function removeItem(id: string) {
		cart = cart.filter((i) => i.id !== id);
	}

	function updateQty(id: string, delta: number) {
		cart = cart
			.map((i) => {
				if (i.id !== id) return i;
				const next = i.qty + delta;
				return next <= 0 ? null : { ...i, qty: next };
			})
			.filter((i): i is CartItem => i !== null);
	}

	/** Polls a card-machine checkout until it completes (mirrors the gift-card management flow). */
	async function pollTillCheckout(checkoutId: string): Promise<void> {
		const maxAttempts = 60;
		for (let attempts = 0; attempts < maxAttempts; attempts++) {
			await new Promise((r) => setTimeout(r, 2000));
			try {
				const res = await apiFetch(`/api/admin/till/sale/checkout/${checkoutId}/status`);
				if (res.ok) {
					const data = await res.json();
					if (data.status === 'COMPLETED') return;
				}
			} catch {
				// Keep polling — a transient network error is not fatal.
			}
		}
		throw new Error('Card machine payment timed out. Please check the Square dashboard.');
	}

	async function chargeCart() {
		if (isProcessingPaymentSync) return;
		if (cart.length === 0) {
			toast.error('Cart is empty');
			return;
		}
		if (cart.some((item) => item.price > GIFT_CARD_MAX_AMOUNT)) {
			toast.error(`Gift card amount exceeds maximum (£${GIFT_CARD_MAX_AMOUNT})`);
			return;
		}
		if (hasRetailItems) {
			toast.error(
				'Retail items cannot be charged yet — the till API currently supports gift card sales only'
			);
			return;
		}
		if (
			paymentMethod === PAYMENT_METHOD_SAVED_CARD &&
			(!selectedCustomer || !selectedSavedCardId)
		) {
			toast.error('Select a customer and a saved card before charging');
			return;
		}
		isProcessingPaymentSync = true;
		processing = true;
		paymentError = null;
		let responseStatus = 0;
		try {
			// One sale per cart line × quantity — each till sale funds its own
			// gift card (the backend only accepts item_type 'gift_card').
			const saleBodies: Record<string, unknown>[] = [];
			// M10: proactive saved-card (ccof) SCA. Run the client-side challenge
			// for every sale line BEFORE the first charge so a naked ccof till
			// charge is never sent to the backend (mirrors PaymentModal/UserPaymentModal
			// running SCA at charge init). Each line binds its token to its own
			// amount. 'challenge-cancelled'/'sca-failed' abort the whole sale
			// (retryable); 'sca-unavailable' aborts before any charge and surfaces
			// the C6 refusal notice (no 2FA fallback).
			let scaAborted = false;
			awaitingSCA = true;
			try {
				for (const item of cart) {
					for (let i = 0; i < item.qty; i++) {
						const body: Record<string, unknown> = {
							item_type: 'gift_card',
							action: 'create',
							amount: item.price,
							payment_method: paymentMethod,
							idempotency_key: idempotencyKeyFor(item, i)
						};
						if (paymentMethod === PAYMENT_METHOD_SAVED_CARD) {
							body.user_id = selectedCustomer?.id;
							body.user_saved_card_id = selectedSavedCardId;
							const squareCardId = savedCards.find(
								(c) => c.id === selectedSavedCardId
							)?.square_card_id;
							const sca = await runSavedCardSCAProactively({
								// The till body carries the amount in POUNDS (the
								// backend multiplies by 100); the SCA challenge binds
								// to pence, so convert for the challenge.
								amountPence: Math.round(item.price * 100),
								squareCardId: squareCardId ?? '',
								buyer: { email: selectedCustomer?.email },
								onOutcome: (o) => (lastSCAOutcome = o)
							});
							if (sca.outcome === 'challenge-cancelled' || sca.outcome === 'sca-failed') {
								throw new Error(CARD_VERIFICATION_RETRY_MESSAGE);
							}
							if (sca.outcome === 'sca-unavailable') {
								// C6: SCA genuinely can't run — abort the whole sale
								// BEFORE any charge is submitted; the refusal notice
								// is shown above the Charge button (no 2FA fallback).
								scaAborted = true;
								break;
							}
							// C1: the SCA tokenize-result token is the charge SOURCE
							// (new_card_token) alongside the saved-card ref — never
							// the legacy verification_token.
							if (sca.verificationToken) body.new_card_token = sca.verificationToken;
						} else if (paymentMethod === 'online_square') {
							if (!onlineSquareCardInput) {
								throw new Error('Card form is not ready — please wait a moment and try again');
							}
							// SCA verification amount must match the sale amount (pence).
							const tokenized = await onlineSquareCardInput.tokenizeWithVerification(
								Math.round(item.price * 100)
							);
							body.card_token = tokenized.nonce;
							if (tokenized.verificationToken) {
								body.verification_token = tokenized.verificationToken;
							}
						}
						saleBodies.push(body);
					}
					if (scaAborted) break;
				}
			} finally {
				awaitingSCA = false;
			}
			if (scaAborted) return;

			for (const body of saleBodies) {
				const res = await submitPaymentWithRetry(() =>
					apiFetch('/api/admin/till/sale', {
						method: 'POST',
						headers: { 'Content-Type': 'application/json' },
						body: JSON.stringify(body)
					})
				);
				if (!res.ok) {
					responseStatus = res.status;
					const errText = await res.text();
					// Saved-card (ccof) SCA: the backend returns 402 +
					// `verification_required` when Square requires buyer verification
					// and no verification_token was supplied. Run the client-side 3DS
					// challenge and retry the SAME sale line with the fresh token and
					// its SAME cached idempotency key. runTillSavedCardSCA throws to
					// stop the whole sale on any non-verified outcome.
					if (
						paymentMethod === PAYMENT_METHOD_SAVED_CARD &&
						isVerificationRequiredSignal(responseStatus, errText)
					) {
						await runTillSavedCardSCA(body);
						continue;
					}
					throw new Error(extractErrorMessage(errText) || 'Till sale failed');
				}
				const data = await res.json();
				if (paymentMethod === 'card_machine' && data.status === 'pending' && data.checkout_id) {
					await pollTillCheckout(data.checkout_id as string);
				}
			}

			toast.success('Sale complete');
			cart = [];
			idempotencyKeys.clear();
		} catch (err) {
			const msg = err instanceof Error ? err.message : 'Sale failed';
			// C6: a sca-unavailable refusal is communicated by the refusal dialog
			// above the Charge button — don't duplicate it in the error panel.
			if (shouldShowSCARefusal(lastSCAOutcome)) {
				paymentError = null;
				return;
			}
			paymentError = msg;
			toast.error(msg);
		} finally {
			isProcessingPaymentSync = false;
			processing = false;
		}
	}

	/**
	 * Saved-card (ccof) SCA challenge, run when a till sale line came back 402
	 * with the verification-required signal (the proactive token was stale or
	 * expired between tokenize and charge — the first attempt now always runs
	 * proactive SCA, so this is the defensive path). The CUSTOMER approves the
	 * 3DS challenge in their banking app; the operator's screen shows the waiting
	 * state. 'verified' retries the SAME sale line with the fresh tokenize-result
	 * token as new_card_token and its SAME cached idempotency key (never
	 * regenerated here); 'sca-unavailable' refuses the sale (C6 — the refusal
	 * dialog is driven by lastSCAOutcome); 'challenge-cancelled' / 'sca-failed'
	 * keep the pending row retryable (the idempotency key stays cached). Throws
	 * to stop the whole sale on any non-verified outcome.
	 */
	async function runTillSavedCardSCA(body: Record<string, unknown>): Promise<void> {
		const squareCardId = savedCards.find((c) => c.id === selectedSavedCardId)?.square_card_id;
		// The till body carries the amount in POUNDS (the backend multiplies by
		// 100); the SCA challenge binds to pence, so convert for the challenge.
		const amountPence = Math.round((Number(body.amount) || 0) * 100);
		awaitingSCA = true;
		try {
			if (!squareCardId) {
				lastSCAOutcome = 'sca-unavailable';
				throw new Error(SCA_REFUSAL_MESSAGE_TILL);
			}
			let result: SavedCardVerificationResult;
			try {
				result = await tokenizeSavedCardWithVerification(amountPence, squareCardId, {
					email: selectedCustomer?.email
				});
			} catch (err) {
				lastSCAOutcome = 'sca-unavailable';
				throw err;
			}
			lastSCAOutcome = result.outcome;
			if (result.outcome === 'verified') {
				const retry = await submitPaymentWithRetry(() =>
					apiFetch('/api/admin/till/sale', {
						method: 'POST',
						headers: { 'Content-Type': 'application/json' },
						body: JSON.stringify({
							...body,
							// C1: the SCA tokenize-result token is the charge SOURCE.
							new_card_token: result.verificationToken
						})
					})
				);
				if (!retry.ok) {
					const errText = await retry.text();
					const err = new Error(extractErrorMessage(errText) || 'Till sale failed');
					(err as { bodyText?: string }).bodyText = errText;
					throw err;
				}
				return;
			}
			if (result.outcome === 'sca-unavailable') {
				throw new Error(SCA_REFUSAL_MESSAGE_TILL);
			}
			throw new Error(CARD_VERIFICATION_RETRY_MESSAGE);
		} finally {
			awaitingSCA = false;
		}
	}
</script>

<div class="rounded-xl border bg-card">
	<div class="flex items-center justify-between border-b border-gray-200 px-5 py-4">
		<h3 class="text-base font-semibold">Till Sales</h3>
	</div>

	<div class="grid grid-cols-2 gap-2 p-4 sm:grid-cols-3">
		<Button
			variant="outline"
			size="sm"
			class="justify-start gap-2"
			disabled={processing}
			onclick={() => addItem('Cuticle Oil', 8)}
		>
			Cuticle Oil - &pound;8
		</Button>
		<Button
			variant="outline"
			size="sm"
			class="justify-start gap-2"
			disabled={processing}
			onclick={() => addItem('Nail Files (Pack)', 5)}
		>
			Nail Files - &pound;5
		</Button>
		<Button
			variant="outline"
			size="sm"
			class="justify-start gap-2"
			disabled={processing}
			onclick={() => addItem('Hand Cream', 6)}
		>
			Hand Cream - &pound;6
		</Button>
		<Button
			variant="outline"
			size="sm"
			class="justify-start gap-2"
			disabled={processing}
			onclick={() => addItem('Base Coat', 7)}
		>
			Base Coat - &pound;7
		</Button>
		<Button
			variant="outline"
			size="sm"
			class="justify-start gap-2"
			disabled={processing}
			onclick={() => addItem('Top Coat', 7)}
		>
			Top Coat - &pound;7
		</Button>
		<div class="relative">
			{#if showGiftCardInput}
				<div class="flex flex-col gap-1">
					<div class="flex gap-1">
						<div class="relative flex-1">
							<span class="absolute top-1/2 left-2 -translate-y-1/2 text-xs text-gray-400"
								>&pound;</span
							>
							<Input
								type="text"
								inputmode="decimal"
								bind:value={giftCardAmount}
								max={GIFT_CARD_MAX_AMOUNT}
								class="h-9 pl-5"
								disabled={processing}
								error={giftCardAmountTooHigh ? 'Gift card amount exceeds maximum' : ''}
								onkeydown={(e) => {
									if (e.key === 'Enter') addGiftCard();
								}}
							/>
						</div>
						<Button
							size="sm"
							variant="outline"
							onclick={addGiftCard}
							class="h-9 px-2 text-xs"
							disabled={processing || giftCardAmountTooHigh}>Add</Button
						>
					</div>
					{#if giftCardAmountTooHigh}
						<p class="text-xs text-red-700">
							Gift card amount exceeds maximum (&pound;{GIFT_CARD_MAX_AMOUNT})
						</p>
					{/if}
					<p class="text-xs text-muted-foreground">
						Gift card limit &pound;{GIFT_CARD_MAX_AMOUNT} per transaction
					</p>
				</div>
			{:else}
				<Button
					variant="outline"
					size="sm"
					class="w-full justify-start gap-2"
					disabled={processing}
					onclick={() => (showGiftCardInput = true)}
				>
					Gift Card
				</Button>
			{/if}
		</div>
	</div>

	<div class="border-b border-gray-200 px-4 py-3">
		<span class="block text-xs font-semibold tracking-wider text-gray-500 uppercase"
			>Customer (saved card payments)</span
		>
		{#if selectedCustomer}
			<div
				class="mt-2 flex items-center justify-between gap-2 rounded-md border border-gray-200 bg-gray-50/50 p-3"
			>
				<div class="min-w-0">
					<p class="truncate text-sm font-medium text-card-foreground">{selectedCustomer.name}</p>
					{#if selectedCustomer.email}
						<p class="truncate text-xs text-muted-foreground">{selectedCustomer.email}</p>
					{/if}
					{#if savedCardsError}
						<p class="mt-1 text-xs text-red-700">{savedCardsError}</p>
					{/if}
				</div>
				<div class="flex shrink-0 items-center gap-2">
					{#if loadingSavedCards}
						<span class="text-xs text-muted-foreground">Loading cards...</span>
					{:else if !savedCardsError}
						<span class="text-xs text-muted-foreground">
							{validCards.length} valid card{validCards.length === 1 ? '' : 's'}
						</span>
					{/if}
					<Button
						variant="ghost"
						size="sm"
						class="h-7 px-2 text-xs"
						disabled={processing}
						onclick={clearSelectedCustomer}
					>
						Clear
					</Button>
				</div>
			</div>
		{:else}
			<div class="relative mt-2">
				<div class="flex gap-2">
					<div class="relative flex-1">
						<Input
							type="text"
							placeholder="Search by name, email or phone..."
							bind:value={customerQuery}
							disabled={processing}
							onkeydown={(e) => {
								if (e.key === 'Enter') {
									e.preventDefault();
									searchCustomers();
								} else if (e.key === 'Escape') {
									showCustomerResults = false;
								}
							}}
							onfocus={() => (showCustomerResults = true)}
							onblur={() => (showCustomerResults = false)}
						/>
					</div>
					<Button
						size="sm"
						variant="outline"
						class="h-9"
						disabled={processing || !customerQuery.trim()}
						onclick={searchCustomers}
					>
						{loadingCustomers ? '...' : 'Search'}
					</Button>
				</div>
				{#if showCustomerResults}
					<div
						class="absolute z-10 mt-1 w-full rounded-md border border-gray-200 bg-background shadow-lg"
					>
						{#if loadingCustomers}
							<div class="flex justify-center p-6">
								<div
									class="h-6 w-6 animate-spin rounded-full border-2 border-gray-200 border-t-primary"
								></div>
							</div>
						{:else if customerResults.length === 0}
							<div class="p-4 text-center text-xs text-gray-500">
								{customerQuery.trim()
									? 'No customers found.'
									: 'Type a name, email or phone to search.'}
							</div>
						{:else}
							<ul class="max-h-56 divide-y divide-gray-200 overflow-y-auto">
								{#each customerResults as user (user.id)}
									<li>
										<button
											type="button"
											class="flex w-full flex-col items-start px-4 py-3 text-left transition-colors hover:bg-fuchsia-50/40"
											onmousedown={(e) => e.preventDefault()}
											onclick={() => selectCustomer(user)}
										>
											<span class="text-sm font-semibold text-card-foreground">{user.name}</span>
											{#if user.email}
												<span class="text-xs text-gray-500">{user.email}</span>
											{/if}
										</button>
									</li>
								{/each}
							</ul>
						{/if}
					</div>
				{/if}
			</div>
		{/if}
	</div>

	<Separator />

	<div class="px-4 py-3">
		{#if cart.length === 0}
			<p class="py-6 text-center text-sm text-muted-foreground">
				Tap items above to add them to the sale.
			</p>
		{:else}
			<div class="max-h-48 space-y-1 overflow-y-auto">
				{#each cart as item (item.id)}
					<div class="flex items-center justify-between rounded-md border px-3 py-2 text-sm">
						<div class="min-w-0 flex-1">
							<span class="font-medium text-card-foreground">{item.label}</span>
							<span class="ml-2 text-xs text-muted-foreground"
								>{formatCurrency(item.price)} each</span
							>
						</div>
						<div class="flex shrink-0 items-center gap-2">
							<button
								type="button"
								class="flex min-h-11 min-w-11 items-center justify-center rounded border text-base text-muted-foreground hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50"
								disabled={processing}
								onclick={() => updateQty(item.id, -1)}
							>
								&minus;
							</button>
							<span class="w-5 text-center text-sm font-semibold tabular-nums">{item.qty}</span>
							<button
								type="button"
								class="flex min-h-11 min-w-11 items-center justify-center rounded border text-base text-muted-foreground hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50"
								disabled={processing}
								onclick={() => updateQty(item.id, 1)}
							>
								+
							</button>
							<span class="w-14 text-right text-sm font-semibold tabular-nums"
								>{formatCurrency(item.price * item.qty)}</span
							>
							<button
								type="button"
								aria-label="Remove item"
								class="ml-1 flex min-h-11 min-w-11 items-center justify-center rounded text-base text-muted-foreground hover:bg-red-50 hover:text-red-600 disabled:cursor-not-allowed disabled:opacity-50"
								disabled={processing}
								onclick={() => removeItem(item.id)}
							>
								<svg
									class="h-4 w-4"
									viewBox="0 0 24 24"
									fill="none"
									stroke="currentColor"
									stroke-width="2"
								>
									<line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
								</svg>
							</button>
						</div>
					</div>
				{/each}
			</div>

			<Separator class="my-3" />

			<div class="flex items-center justify-between">
				<span class="text-sm text-muted-foreground">
					{itemCount} item{itemCount !== 1 ? 's' : ''}
				</span>
				<span class="text-lg font-bold tabular-nums">{formatCurrency(subtotal)}</span>
			</div>

			<div class="mt-3">
				<span class="block text-xs font-semibold tracking-wider text-gray-500 uppercase"
					>Payment Method</span
				>
				<div
					class="mt-2 grid grid-cols-2 gap-2 {availablePaymentMethods.length > 3
						? ''
						: 'sm:grid-cols-3'}"
				>
					{#each availablePaymentMethods as m (m.key)}
						<button
							type="button"
							class="min-h-11 rounded-lg border py-3 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 {paymentMethod ===
							m.key
								? 'border-input bg-fuchsia-100 text-foreground'
								: 'border-gray-200 hover:bg-gray-50'}"
							disabled={processing}
							onclick={() => (paymentMethod = m.key)}
						>
							{m.label}
						</button>
					{/each}
				</div>
			</div>

			{#if paymentMethod === 'online_square'}
				<div class="mt-3 rounded-md border border-gray-200 bg-gray-50/50 p-3">
					{#if isSquareConfigured()}
						<SquareCardInput
							bind:this={onlineSquareCardInput}
							onReady={(r) => (onlineSquareCardReady = r)}
							disabled={processing}
						/>
					{:else}
						<p class="text-xs text-gray-500">
							Online card entry is unavailable — Square is not configured.
						</p>
					{/if}
				</div>
			{/if}

			{#if paymentMethod === PAYMENT_METHOD_SAVED_CARD}
				<div class="mt-3 space-y-3 rounded-md border border-gray-200 bg-gray-50/50 p-3">
					{#if loadingSavedCards}
						<div class="flex justify-center py-6">
							<div
								class="h-8 w-8 animate-spin rounded-full border-4 border-gray-200 border-t-primary"
							></div>
						</div>
					{:else if savedCardsError}
						<p class="rounded-md border border-red-200 bg-red-50 p-3 text-xs text-red-800">
							{savedCardsError}
						</p>
					{:else}
						<span class="block text-xs font-semibold tracking-wider text-gray-500 uppercase"
							>Select a Saved Card</span
						>
						<div class="space-y-2">
							{#each validCards as card (card.id)}
								<button
									type="button"
									class="w-full rounded-lg border p-3 text-left transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none {selectedSavedCardId ===
									card.id
										? 'border-input bg-fuchsia-100'
										: 'border-gray-200 hover:bg-gray-50'}"
									onclick={() => (selectedSavedCardId = card.id)}
								>
									<div class="flex items-center justify-between">
										<div class="flex items-center gap-2">
											<svg
												class="h-5 w-5 text-gray-500"
												viewBox="0 0 24 24"
												fill="none"
												stroke="currentColor"
												stroke-width="2"
											>
												<rect x="1" y="4" width="22" height="16" rx="2" ry="2" />
												<line x1="1" y1="10" x2="23" y2="10" />
											</svg>
											<span class="font-medium text-gray-900">{card.brand} ••••{card.last_4}</span>
										</div>
										<span class="text-xs text-gray-500"
											>{String(card.exp_month).padStart(2, '0')}/{card.exp_year}</span
										>
									</div>
								</button>
							{/each}
						</div>

						<div class="flex items-start gap-2 rounded-md border border-amber-200 bg-amber-50 p-3">
							<svg
								class="mt-0.5 h-4 w-4 shrink-0 text-amber-600"
								viewBox="0 0 24 24"
								fill="none"
								stroke="currentColor"
								stroke-width="2"
							>
								<circle cx="12" cy="12" r="10" />
								<line x1="12" y1="8" x2="12" y2="12" />
								<line x1="12" y1="16" x2="12.01" y2="16" />
							</svg>
							<p class="text-xs text-amber-800">
								Your card issuer will ask you to approve this payment in your banking app.
							</p>
						</div>
					{/if}

					<!-- C6: SCA-unavailable refusal — the ONLY behaviour on a genuine
						 sca-unavailable outcome: the charge cannot complete and the
						 customer must pay online later (no 2FA code fallback). -->
					<ScaFallbackConsentDialog
						open={shouldShowSCARefusal(lastSCAOutcome)}
						message={SCA_REFUSAL_MESSAGE_TILL}
						onOk={() => {
							lastSCAOutcome = '';
							paymentError = null;
						}}
					/>
				</div>
			{/if}

			{#if hasRetailItems}
				<p class="mt-3 rounded-md border border-amber-200 bg-amber-50 p-3 text-xs text-amber-800">
					Retail items can&apos;t be charged yet — the till API currently supports gift card sales
					only. Remove retail items to complete this sale.
				</p>
			{/if}

			{#if paymentError}
				<p class="mt-3 rounded-md border border-red-200 bg-red-50 p-3 text-xs text-red-800">
					{paymentError}
				</p>
			{/if}

			{#if awaitingSCA}
				<div
					class="mt-3 flex flex-col items-center justify-center rounded-md border border-gray-200 bg-gray-50/50 p-6"
				>
					<div
						class="h-8 w-8 animate-spin rounded-full border-4 border-gray-200 border-t-primary"
					></div>
					<p class="mt-3 text-sm font-medium text-gray-700">
						Waiting for customer to approve in their banking app…
					</p>
					<p class="mt-1 text-xs text-muted-foreground">
						The customer may need to approve this payment in their banking app
					</p>
				</div>
			{:else}
			<Button
				class="mt-3 min-h-11 w-full active:bg-primary/85 active:shadow-none"
				onclick={chargeCart}
				loading={processing}
				disabled={!canCharge ||
					processing ||
					(paymentMethod === 'online_square' && !onlineSquareCardReady) ||
					(paymentMethod === PAYMENT_METHOD_SAVED_CARD && !selectedSavedCardId)}
			>
				{processing ? 'Processing...' : `Charge ${formatCurrency(subtotal)}`}
			</Button>
			{/if}
			<p class="mt-4 text-center text-xs text-gray-500">Secure payment powered by Square</p>
			<p class="mt-1 text-xs text-muted-foreground">
				Gift card sales are processed through the till; retail items require manual recording for
				now.
			</p>
		{/if}
	</div>
</div>
