<script lang="ts">
	import { goto } from '$app/navigation';
	import { toast } from 'svelte-sonner';
	import { extractErrorMessage } from '$lib/utils/toast-safe';
	import { SvelteDate } from 'svelte/reactivity';
	import { authStore } from '$lib/stores/auth.svelte';
	import { apiFetch } from '$lib/utils/api';
	import CardSelection from '$lib/components/payments/CardSelection.svelte';
	import { savedCardsStore, type SavedCard } from '$lib/stores/savedCards.svelte';
	import { Button } from '$lib/components/ui/button';
	import { Input } from '$lib/components/ui/input';
	import * as Card from '$lib/components/ui/card';
	import { onMount } from 'svelte';

	// Shared tip-payment UI used by /tip and /pay-tip/[id]. The routes resolve
	// the booking (most-recent past booking vs. booking by URL id) and hand it
	// here; everything else — tip selection, CardSelection wiring, nonce + SCA
	// verification caching, idempotency-key derivation, submitTip, success and
	// error handling — lives in ONE place so the two pages can't diverge.
	type BookingService = {
		service_id: string;
		booking_id: string;
		service_name: string;
		price: number;
		duration_minutes: number;
		override_price?: number;
		override_duration_minutes?: number;
	};

	type Payment = {
		id: string;
		payment_type: string;
		payment_method: string;
		status: string;
		amount: number;
		created_at: string;
	};

	type Booking = {
		id: string;
		start_time: string;
		services: BookingService[];
		total_amount: number;
		duration_minutes: number;
		payments?: Payment[];
	};

	const { booking }: { booking: Booking } = $props();

	let paymentState = $state<'idle' | 'processing' | 'success' | 'error'>('idle');

	// Cached idempotency key: generated once per payment attempt, reused on retry
	// (so a network-timeout retry dedupes instead of double-charging), cleared on
	// success. Reset when the tip amount changes so an amount change after a
	// failed attempt gets a fresh key instead of a false dedup (under-charge).
	let tipIdempotencyKey = $state('');
	let tipKeyedAmount = $state(0);

	// Card selection — delegated to CardSelection.svelte (saved-card list,
	// "Use a new card" toggle, SquareCardInput tokenization, consent checkbox).
	let savedCards = $state<SavedCard[]>([]);
	let cardSelection = $state<CardSelection | null>(null);
	let selectedCardId = $state('');
	let cardSelectionValid = $state(false);
	let saveCard = $state(false);
	// Cached nonce: tokenization is one-shot — a retry reuses this token instead
	// of re-tokenizing (the backend idempotency key dedups).
	let tipNonce = $state('');
	// Cached SCA verification token paired with tipNonce (both one-shot, reused
	// together on retry). The verification token is amount-bound, so changing
	// the tip invalidates the cached pair.
	let tipVerificationToken = $state('');
	let tipTokenAmount = $state(0);
	// Epoch ms when the cached pair was tokenized — Square nonces and SCA
	// verification tokens expire after ~5 minutes, so a stale pair is discarded
	// on late retries and re-tokenized instead of rejected by Square.
	let tipTokenizedAt = $state(0);

	const canSaveCards = $derived(
		authStore.currentUser?.role === 'verified_email' || authStore.currentUser?.role === 'affiliate'
	);

	const isCardValid = $derived(cardSelectionValid);

	let selectedTip = $state<number | null>(null);
	let customTip = $state('');
	const tipAmount = $derived(
		selectedTip !== null ? selectedTip : customTip ? parseFloat(customTip) || 0 : 0
	);

	// Sum of completed tip payments already made against this booking. Both
	// routes' responses carry `payments` (the booking-detail endpoint), so this
	// is computed identically everywhere.
	const tipsPaid = $derived(
		booking.payments
			?.filter((p) => p.status === 'completed' && p.payment_type === 'tip')
			.reduce((sum, p) => sum + p.amount, 0) ?? 0
	);

	const subtotal = $derived(booking.total_amount ?? 0);

	const tipPercentages = $derived.by(() => {
		if (subtotal <= 0) return [];
		return [
			{ pct: 10, amount: Math.round(subtotal * 0.1 * 100) / 100 },
			{ pct: 15, amount: Math.round(subtotal * 0.15 * 100) / 100 },
			{ pct: 20, amount: Math.round(subtotal * 0.2 * 100) / 100 }
		];
	});

	function formatDate(dateStr: string): string {
		const date = new SvelteDate(dateStr);
		return date.toLocaleDateString('en-GB', {
			weekday: 'long',
			day: 'numeric',
			month: 'long',
			year: 'numeric'
		});
	}

	// Honours per-service override_duration_minutes when computing the end time,
	// falling back to the booking's duration_minutes (reconciled from the
	// pay-tip route, which is the correct behaviour when durations are edited).
	function formatTimeRange(
		startStr: string,
		services: BookingService[],
		fallbackDuration: number
	): string {
		const start = new SvelteDate(startStr);
		const totalMinutes =
			services?.reduce(
				(sum, s) => sum + (s.override_duration_minutes ?? s.duration_minutes ?? 0),
				0
			) ??
			fallbackDuration ??
			0;
		const end = new SvelteDate(start.getTime() + totalMinutes * 60000);

		const formatOpt: Intl.DateTimeFormatOptions = {
			hour: 'numeric',
			minute: '2-digit',
			hour12: true
		};

		return `${start.toLocaleTimeString('en-GB', formatOpt)} – ${end.toLocaleTimeString('en-GB', formatOpt)}`;
	}

	function formatPrice(pounds: number): string {
		return `£${pounds.toFixed(2)}`;
	}

	function selectTip(amount: number) {
		selectedTip = amount;
		customTip = '';
	}

	function handleCustomTipInput(e: Event) {
		const input = e.target as HTMLInputElement;
		const cleaned = input.value.replace(/[^0-9.]/g, '');
		const firstDot = cleaned.indexOf('.');
		let sanitized: string;
		if (firstDot !== -1) {
			const integerPart = cleaned.substring(0, firstDot);
			const decimalPart = cleaned.substring(firstDot + 1).replace(/\./g, '');
			sanitized = integerPart + '.' + decimalPart;
		} else {
			sanitized = cleaned;
		}
		if (/^\d+(\.\d{0,2})?$/.test(sanitized) || sanitized === '') {
			customTip = sanitized;
		}
		selectedTip = null;
	}

	async function loadSavedCards() {
		if (savedCardsStore.loaded) {
			savedCards = savedCardsStore.cards;
			if (savedCards.length > 0 && !selectedCardId) {
				selectedCardId = savedCards.find((c) => c.is_default)?.id || savedCards[0].id;
			}
			return;
		}
		try {
			await savedCardsStore.fetch();
			savedCards = savedCardsStore.cards;
			if (savedCards.length > 0 && !selectedCardId) {
				selectedCardId = savedCards.find((c) => c.is_default)?.id || savedCards[0].id;
			}
		} catch {
			// ignore — user can enter new card
		}
	}

	onMount(() => {
		loadSavedCards();
	});

	async function submitTip() {
		if (tipAmount <= 0) {
			toast.error('Please select a tip amount');
			return;
		}

		let newCardToken: string | undefined;
		let verificationToken: string | undefined;
		if (selectedCardId) {
			// saved card — nothing to tokenize
		} else if (cardSelection) {
			// New-card mode: tokenize once per attempt, reuse the nonce + SCA
			// verification token on retry (tokenization is one-shot; the backend
			// idempotency key dedups). The verification token is amount-bound, so
			// a changed tip amount forces a fresh tokenization.
			if (!tipNonce || tipTokenAmount !== tipAmount || Date.now() - tipTokenizedAt > 240_000) {
				try {
					const tokenized = await cardSelection.tokenizeWithVerification(
						Math.round(tipAmount * 100),
						{
							givenName: authStore.currentUser?.firstName,
							familyName: authStore.currentUser?.lastName,
							email: authStore.currentUser?.email
						}
					);
					tipNonce = tokenized.nonce;
					tipVerificationToken = tokenized.verificationToken ?? '';
					tipTokenAmount = tipAmount;
					tipTokenizedAt = Date.now();
				} catch (err) {
					toast.error(err instanceof Error ? err.message : 'Card entry failed');
					return;
				}
			}
			newCardToken = tipNonce;
			verificationToken = tipVerificationToken || undefined;
		} else {
			toast.error('Please select a payment method');
			return;
		}

		paymentState = 'processing';

		try {
			if (!tipIdempotencyKey || tipKeyedAmount !== tipAmount) {
				tipIdempotencyKey = crypto.randomUUID();
				tipKeyedAmount = tipAmount;
			}
			const amountInPence = Math.round(tipAmount * 100);
			const body: Record<string, unknown> = {
				amount: amountInPence,
				idempotency_key: tipIdempotencyKey,
				...(selectedCardId ? { card_id: selectedCardId } : {}),
				...(newCardToken ? { new_card_token: newCardToken, save_card: saveCard } : {}),
				...(verificationToken ? { verification_token: verificationToken } : {})
			};

			const response = await apiFetch(`/api/bookings/${booking.id}/tip`, {
				method: 'POST',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify(body)
			});

			if (!response.ok) {
				const errorText = await response.text();
				throw new Error(extractErrorMessage(errorText) || 'Payment failed');
			}

			paymentState = 'success';
			tipIdempotencyKey = '';
			tipKeyedAmount = 0;
			tipNonce = '';
			tipVerificationToken = '';
			tipTokenAmount = 0;
			tipTokenizedAt = 0;
			toast.success('Thank you for your tip!');
		} catch (err) {
			paymentState = 'error';
			const errorMessage = err instanceof Error ? err.message : 'Payment failed';
			toast.error(errorMessage);
			// A definitive charge failure (e.g. declined card) consumes the nonce
			// and SCA verification token — they can never succeed again. Clear the
			// cached pair so the next retry re-tokenizes fresh. The idempotency
			// key is kept: it's still correct for network-timeout dedup.
			tipNonce = '';
			tipVerificationToken = '';
			tipTokenAmount = 0;
			tipTokenizedAt = 0;
		}
	}

	function retryPayment() {
		paymentState = 'idle';
	}
</script>

{#if paymentState === 'success'}
	<Card.Root>
		<Card.Content class="py-8 text-center">
			<div
				class="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-green-100"
			>
				<svg
					class="h-8 w-8 text-green-600"
					viewBox="0 0 24 24"
					fill="none"
					stroke="currentColor"
					stroke-width="2"
				>
					<path d="M20 6L9 17l-5-5" stroke-linecap="round" stroke-linejoin="round" />
				</svg>
			</div>
			<h2 class="text-xl font-semibold text-gray-900">Thank you!</h2>
			<p class="mt-2 text-gray-600">Your generosity is greatly appreciated.</p>
			<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
			<Button class="mt-6" onclick={() => goto('/')}>Go Home</Button>
		</Card.Content>
	</Card.Root>
{:else}
	<Card.Root class="mb-6">
		<Card.Header>
			<Card.Title>Your Appointment</Card.Title>
		</Card.Header>
		<Card.Content class="space-y-3">
			<div class="flex justify-between">
				<span class="text-sm text-gray-500">Date</span>
				<span class="font-medium">{formatDate(booking.start_time)}</span>
			</div>
			<div class="flex justify-between">
				<span class="text-sm text-gray-500">Time</span>
				<span class="font-medium"
					>{formatTimeRange(
						booking.start_time,
						booking.services,
						booking.duration_minutes ?? 0
					)}</span
				>
			</div>
			<div class="flex justify-between">
				<span class="text-sm text-gray-500">Subtotal</span>
				<span class="font-medium">{formatPrice(subtotal)}</span>
			</div>
			{#if tipsPaid > 0}
				<div class="flex justify-between">
					<span class="text-sm text-gray-500">Tips</span>
					<span class="font-medium">{formatPrice(tipsPaid)}</span>
				</div>
			{/if}
			{#if booking.services && booking.services.length > 0}
				<div class="border-t pt-3">
					<div class="text-sm text-gray-500">Services</div>
					<div class="mt-2 space-y-1">
						{#each booking.services as service (service.service_id || service.booking_id)}
							<div class="flex justify-between text-sm">
								<span class="text-gray-700">{service.service_name}</span>
								<span class="text-gray-500"
									>{formatPrice(service.override_price ?? service.price)}</span
								>
							</div>
						{/each}
					</div>
				</div>
			{/if}
		</Card.Content>
	</Card.Root>

	<Card.Root class="mb-6">
		<Card.Header>
			<Card.Title>Choose Tip Amount</Card.Title>
		</Card.Header>
		<Card.Content class="space-y-4">
			<div class="grid grid-cols-3 gap-3">
				{#each tipPercentages as tip (tip.pct)}
					<button
						class="rounded-lg border border-input bg-background py-3 text-center font-semibold transition-colors hover:bg-fuchsia-50 {selectedTip ===
						tip.amount
							? 'bg-fuchsia-100'
							: ''}"
						onclick={() => selectTip(tip.amount)}
						type="button"
					>
						<div>{formatPrice(tip.amount)}</div>
						<div class="text-xs font-normal text-gray-500">{tip.pct}%</div>
					</button>
				{/each}
			</div>

			<div>
				<label for="custom-tip" class="text-sm font-medium text-gray-700"
					>Or enter custom amount</label
				>
				<div class="relative mt-1">
					<span class="absolute top-1/2 left-3 -translate-y-1/2 text-gray-500">£</span>
					<Input
						id="custom-tip"
						type="text"
						inputmode="decimal"
						step="0.01"
						min="0"
						placeholder="0.00"
						class="pl-7"
						value={customTip}
						oninput={handleCustomTipInput}
					/>
				</div>
			</div>
		</Card.Content>
	</Card.Root>

	<!-- Payment Method -->
	<Card.Root class="mb-6">
		<Card.Content class="space-y-4">
			<div class="space-y-3">
				<span class="block text-xs font-semibold tracking-wider text-gray-500 uppercase"
					>Payment Method</span
				>

				<CardSelection
					bind:this={cardSelection}
					cards={savedCards}
					{canSaveCards}
					bind:selectedCardId
					bind:saveCard
					onValidityChange={(v) => (cardSelectionValid = v)}
				/>
			</div>
		</Card.Content>
	</Card.Root>

	{#if paymentState === 'error'}
		<div class="mb-4 rounded-lg border border-red-200 bg-red-50 p-4">
			<p class="text-red-700">Payment failed. Please try again.</p>
			<Button variant="outline" class="mt-3 w-full" onclick={retryPayment}>Try Again</Button>
		</div>
	{/if}

	<Button
		class="w-full"
		size="lg"
		disabled={tipAmount <= 0 || !isCardValid || paymentState === 'processing'}
		loading={paymentState === 'processing'}
		onclick={submitTip}
	>
		{paymentState === 'processing' ? 'Processing...' : `Pay Tip £${tipAmount.toFixed(2)}`}
	</Button>

	<p class="mt-4 text-center text-xs text-gray-500">Secure payment powered by Square</p>
{/if}
