<script lang="ts">
	import { Button } from '$lib/components/ui/button/index.js';
	import * as Card from '$lib/components/ui/card/index.js';
	import { Input } from '$lib/components/ui/input/index.js';
	import { EmailInput } from '$lib/components/ui/email-input/index.js';
	import { Label } from '$lib/components/ui/label/index.js';
	import { Textarea } from '$lib/components/ui/textarea/index.js';
	import CharCounter from '$lib/components/ui/CharCounter.svelte';
	import { Separator } from '$lib/components/ui/separator/index.js';
	import { PhoneInput } from '$lib/components/ui/phone-input/index.js';
	import { isValidUKPhone, toE164UK } from '$lib/utils/phone';
	// INTENTIONAL: We use the browser's local timezone (getLocalTimeZone) because Crussell is a UK-only
	// salon app. All customers are physically in the UK and book UK appointment slots. We do NOT
	// auto-adjust for international timezones — the slot time shown is the actual UK salon time.
	// Cloudflare geo-blocking prevents non-UK access. BST/GMT transitions are handled automatically:
	// formatLocalDateTime converts wall-clock time to UTC using the correct DST offset for the
	// target date (via @internationalized/date's CalendarDate.toDate which applies the target
	// date's timezone rules, not the current date's). The backend stores all timestamps as
	// TIMESTAMPTZ (UTC) and converts to Europe/London for display. This ensures a booking at
	// "10am June 15" stays at 10am BST regardless of when the booking was made.
	import { CalendarDate, getLocalTimeZone, type DateValue } from '@internationalized/date';
	import { goto } from '$app/navigation';
	import { resolve } from '$app/paths';
	import { authStore } from '$lib/stores/auth.svelte';
	import { apiFetch, getAuthHeaders } from '$lib/utils/api';
	import { ensureBusinessInfo, getBusinessInfo } from '$lib/stores/businessInfo.svelte';
	import { toast } from 'svelte-sonner';
	import { onDestroy } from 'svelte';
	import { SvelteDate } from 'svelte/reactivity';

	// Components
	import BookingActions from '$lib/components/booking/BookingActions.svelte';
	import BookingSummary from '$lib/components/booking/BookingSummary.svelte';
	import StepIndicator from '$lib/components/booking/StepIndicator.svelte';
	import DatePicker from '$lib/components/booking/DatePicker.svelte';
	import TimeSlotPicker from '$lib/components/booking/TimeSlotPicker.svelte';
	import ServiceSelector from '$lib/components/booking/ServiceSelector.svelte';
	import CardInput from '$lib/components/payments/CardInput.svelte';
	import PolicyPopover from '$lib/components/ui/policyPopover.svelte';
	import { POLICY } from '$lib/constants/policy';
	import UserPaymentModal from '$lib/components/payments/UserPaymentModal.svelte';
	import { extractBookedSlots, getLunchProtectionForSlots } from '$lib/lunchProtection';
	import { formatLocalDateTime, getLondonTodayCalendarDate } from '$lib/utils/timeSlots';
	import { generateUUID } from '$lib/utils/uuid';

	import type {
		Service,
		CustomerInfo,
		WorkingHoursDay,
		AvailableHoursDay,
		BookingService,
		BookingStatus,
		Payment
	} from '$lib/types/booking';

	// =============== State Management ===============
	let currentStep = $state<number>(0);
	let authReady = $state(false);

	// Wait for auth store to finish initializing before deciding which step to show.
	// This prevents a flash of the login prompt on SSR + hydration — the skeleton
	// displays while auth checks are pending, then the correct screen appears.
	$effect(() => {
		if (authStore.hasLoaded && !authReady) {
			authReady = true;
			currentStep = authStore.isAuthenticated ? 1 : 0;
		}
	});
	let selectedServices = $state<Service[]>([]);
	let selectedDate = $state<CalendarDate | undefined>(undefined);
	let selectedTime = $state<string | null>(null);
	const customerInfo = $state<CustomerInfo>({
		firstName: '',
		lastName: '',
		email: '',
		phone: '',
		specialRequests: ''
	});
	let _isSubmitting = $state(false);
	let idempotencyKey = $state<string>('');

	// =============== Payment State ===============
	let userDepositsRequired = $state<number>(0);
	let hasActiveBooking = $state<boolean>(false);
	let _activeBookingCheckDone = $state<boolean>(false);
	let paymentMethods = $state<
		Array<{ id: string; brand: string; last4: string; expiry_month: number; expiry_year: number }>
	>([]);
	let paymentMethodsLoading = $state(false);
	let selectedPaymentMethod = $state<string | null>(null);
	let showNewCardForm = $state(false);
	let isProcessingPayment = $state(false);
	// Synchronous double-click guard. Svelte 5 reactivity is async (effects run
	// on the next microtask), so `isProcessingPayment` may not propagate to the
	// button's `disabled` binding before a fast second click fires. This non-
	// reactive flag is checked synchronously at the start of processPayment.
	let isProcessingPaymentSync = false;

	// New card form fields
	let newCardNumber = $state('');
	let newCardExpiry = $state('');
	let newCardCVC = $state('');
	const _saveCardForFuture = $state(false);

	// Payment flow state
	let depositPaid = $state(false);
	let _showPaymentForm = $state(false);

	const depositCardFormValid = $derived(
		selectedPaymentMethod !== null ||
			(showNewCardForm &&
				newCardNumber.replace(/\s/g, '').length >= 13 &&
				/^\d{2}\/\d{2}$/.test(newCardExpiry) &&
				newCardCVC.length >= 3)
	);

	// VAT registration status from public business info (via shared store)
	const vatRegistered = $derived(getBusinessInfo()?.is_vat_registered ?? false);

	// Email existence check (guest flow only)
	let emailChecking = $state(false);
	let emailSuggestion = $state<string | null>(null);
	let emailError = $state('');
	const emailFormatValid = $derived(
		!customerInfo.email ||
			/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(customerInfo.email)
	);
	const allGuestFieldsValid = $derived(
		customerInfo.firstName &&
			customerInfo.lastName &&
			customerInfo.email &&
			emailFormatValid &&
			customerInfo.phone &&
			isValidUKPhone(customerInfo.phone)
	);

	async function checkEmailExists(email: string) {
		if (!email || !/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(email)) {
			emailSuggestion = null;
			return;
		}
		// Don't check unless ALL guest fields have valid input
		if (!allGuestFieldsValid) {
			emailSuggestion = null;
			return;
		}

		emailChecking = true;
		try {
			// Send all 4 fields to enable the backend to match beyond just email
			const params = new URLSearchParams({
				email: email.toLowerCase(),
				firstName: customerInfo.firstName,
				lastName: customerInfo.lastName,
				phone: toE164UK(customerInfo.phone) ?? customerInfo.phone
			});
			const resp = await fetch(`/api/check-email?${params}`);
			if (resp.ok) {
				const data = await resp.json();
				emailSuggestion = data.suggestion ?? null;
			}
		} catch {
			// Network error - silently ignore, don't block booking
			emailSuggestion = null;
		} finally {
			emailChecking = false;
		}
	}

	let emailCheckTimeout: ReturnType<typeof setTimeout> | null = null;

	function debouncedEmailCheck() {
		if (emailCheckTimeout) clearTimeout(emailCheckTimeout);
		if (allGuestFieldsValid) {
			emailCheckTimeout = setTimeout(() => checkEmailExists(customerInfo.email), 500);
		}
	}

	// Confirmation state
	let confirmedBooking = $state<{
		id: string;
		status: string;
		start_time: string;
		notes: string;
		deposit_required: boolean;
		deposit_paid: boolean;
		deposit_amount: number;
		amount_paid: number;
		amount_due: number;
		payments: Payment[];
		total_amount: number;
		duration_minutes: number;
	} | null>(null);

	let showPayEarlyModal = $state(false);
	let discountPreview = $state<{
		eligible: boolean;
		discounts: Array<{ source: string; name: string; percent: number; amount: number }>;
		original_total: number;
		discounted_total: number;
	} | null>(null);

	// =============== Payment Functions ===============
	async function fetchUserDepositsRequired() {
		if (!authStore.isAuthenticated) {
			userDepositsRequired = 0;
			return;
		}

		try {
			const response = await apiFetch('/api/user/profile');
			if (response.ok) {
				const user = await response.json();
				userDepositsRequired = user.deposits_required ?? 0;
			}
		} catch (_err) {
			userDepositsRequired = 0;
		}
	}

	async function fetchActiveBookingStatus() {
		if (!authStore.isAuthenticated) {
			hasActiveBooking = false;
			_activeBookingCheckDone = true;
			return;
		}

		_activeBookingCheckDone = false;
		try {
			// Check for pending bookings
			const pendingResp = await apiFetch('/api/bookings?status=pending&perPage=1');
			if (pendingResp.ok) {
				const data = await pendingResp.json();
				if (data.bookings && data.bookings.length > 0) {
					hasActiveBooking = true;
					_activeBookingCheckDone = true;
					return;
				}
			}

			// Check for confirmed bookings
			const confirmedResp = await apiFetch('/api/bookings?status=confirmed&perPage=1');
			if (confirmedResp.ok) {
				const data = await confirmedResp.json();
				hasActiveBooking = data.bookings && data.bookings.length > 0;
			}
		} catch (_err) {
			hasActiveBooking = false;
		} finally {
			_activeBookingCheckDone = true;
		}
	}

	function calculateDepositRequired(): boolean {
		if (!selectedDate || !selectedTime) return false;

		// Deposit required if user has deposits_required > 0, regardless of booking window
		return userDepositsRequired > 0;
	}

	function calculateDepositAmount(): number {
		return Math.round(getTotalPrice() * 0.2 * 100) / 100;
	}

	async function fetchDiscountPreview() {
		if (!confirmedBooking?.id) return;
		try {
			const resp = await apiFetch(`/api/bookings/${confirmedBooking.id}/discount-preview`);
			if (resp.ok) {
				const data = await resp.json();
				// Only show time-based (auto-apply) discounts on the confirmation screen
				if (data.eligible && data.discounts?.length > 0) {
					discountPreview = data;
				}
			}
		} catch (_err) {
			console.error('Failed to fetch discount preview:', _err);
		}
	}

	async function processPayment(amount: number) {
		// Synchronous double-click guard — set BEFORE any await so a rapid second
		// click is rejected immediately, even before the reactive `disabled` has
		// propagated to the button.
		if (isProcessingPaymentSync) return;
		isProcessingPaymentSync = true;
		isProcessingPayment = true;
		paymentAttempted = false;
		try {
			await submitAndProceed();
			if (!confirmedBooking) {
				toast.error('Booking was not created. Please try again.');
				return;
			}
			const bookingId = confirmedBooking.id;
			const amountCents = Math.round(amount * 100);

			const body: Record<string, unknown> = {
				payment_type: 'deposit',
				amount: amountCents,
				idempotency_key: generateUUID()
			};

			if (selectedPaymentMethod) {
				body.card_id = selectedPaymentMethod;
			} else {
				const rawNumber = newCardNumber.replace(/\s/g, '');
				if (rawNumber.length >= 13) {
					body.new_card_token = rawNumber;
				}
			}

			paymentAttempted = true;

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

			if (response.ok) {
				depositPaid = true;
				// Immutable update — avoid mutating the existing object so
				// concurrent renders (e.g. a stale fetch) can't observe partial
				// state. (See audit: HIGH issue #3 — confirmedBooking mutated
				// in place, potential overcharge on double-click race.)
				confirmedBooking = {
					...confirmedBooking,
					deposit_paid: true,
					amount_paid: (confirmedBooking.amount_paid || 0) + amount,
					amount_due: Math.max(0, (confirmedBooking.amount_due || 0) - amount)
				};
				toast.success('Payment successful!');
			} else {
				const text = await response.text();
				toast.warning(text || 'Payment failed — you can pay again from your booking details.');
			}
		} catch (_err) {
			toast.error(
				'An error occurred. Your booking may still be confirmed — check your appointments.'
			);
		} finally {
			isProcessingPayment = false;
			isProcessingPaymentSync = false;
		}
	}

	let paymentAttempted = $state(false);

	function formatCardExpiry(month: number, year: number): string {
		return `${String(month).padStart(2, '0')}/${year.toString().slice(-2)}`;
	}

	// Fetch user deposit and active booking status when step 1 is reached
	$effect(() => {
		if (currentStep === 1 && authStore.isAuthenticated) {
			fetchUserDepositsRequired();
			fetchActiveBookingStatus();
		}
	});

	// =============== Slot Reservation System ===============
	let _reservationId = $state<string | null>(null);
	let reservationExpiresAt = $state<Date | null>(null);
	let reservationCountdown = $state<string>('');
	let reservationExpired = $state(false);
	let _isReserving = $state(false);

	// Track the reserved slot so we can detect changes on back-navigate in nextStep()
	let _reservedSlotTime: string | null = $state(null);
	let _reservedSlotDate: string | null = $state(null);

	// =============== Slot Reservation Functions ===============
	async function reserveSlot() {
		_isReserving = true;
		try {
			if (!selectedDate || !selectedTime) {
				toast.error('Please select a date and time');
				return false;
			}

			const [hours, minutes] = selectedTime.split(':').map(Number);
			const bookingDate = selectedDate.toDate(getLocalTimeZone());
			bookingDate.setHours(hours, minutes, 0, 0);
			const startTimeISO = formatLocalDateTime(bookingDate);
			const serviceIds = selectedServices.map((s) => s.id);

			const response = await apiFetch('/api/bookings/reserve', {
				method: 'POST',
				headers: {
					'Content-Type': 'application/json',
					...getAuthHeaders()
				},
				body: JSON.stringify({ start_time: startTimeISO, service_ids: serviceIds })
			});

			if (!response.ok) {
				const _errorText = await response.text();
				if (response.status === 429) {
					toast.error('Too many active reservations. Please wait or log in.');
				} else if (response.status === 409) {
					toast.error('This time slot is no longer available. Please choose a different time.');
					await refreshAvailableHours();
				} else {
					toast.error('Failed to reserve slot. Please try again.');
				}
				return false;
			}

			const data = await response.json();
			_reservationId = data.id;
			reservationExpiresAt = new Date(data.expires_at);
			reservationExpired = false;
			_reservedSlotTime = selectedTime;
			_reservedSlotDate = selectedDate ? selectedDate.toString() : null;
			startCountdown();
			return true;
		} catch (_error) {
			toast.error('Network error while reserving slot.');
			return false;
		} finally {
			_isReserving = false;
		}
	}

	// Best-effort release of the currently held reservation. Idempotent: safe
	// to call when no reservation exists. Clears all reservation state so a
	// subsequent reservation must be re-acquired.
	async function releaseReservation() {
		if (!_reservationId) return;
		const idToRelease = _reservationId;
		// Clear local state first so a slow DELETE doesn't block the UI.
		_reservationId = null;
		reservationExpiresAt = null;
		reservationCountdown = '';
		reservationExpired = false;
		_reservedSlotTime = null;
		_reservedSlotDate = null;
		if (window.__bookingFlowCountdownInterval) {
			clearInterval(window.__bookingFlowCountdownInterval);
			window.__bookingFlowCountdownInterval = null;
		}
		try {
			const res = await apiFetch('/api/bookings/reserve', { method: 'DELETE' });
			if (!res.ok) console.warn('Failed to release reservation', idToRelease, res.status);
		} catch (e) {
			console.warn('Error releasing reservation', idToRelease, e);
		}
	}

	function startCountdown() {
		if (!reservationExpiresAt) return;

		// Clear any existing countdown interval to prevent duplicates
		if (window.__bookingFlowCountdownInterval) {
			clearInterval(window.__bookingFlowCountdownInterval);
			window.__bookingFlowCountdownInterval = null;
		}

		const updateCountdown = () => {
			if (!reservationExpiresAt) {
				reservationCountdown = '';
				return;
			}

			const now = new SvelteDate();
			const diff = reservationExpiresAt.getTime() - now.getTime();

			if (diff <= 0) {
				reservationCountdown = '00:00';
				reservationExpired = true;
				_reservationId = null;
				reservationExpiresAt = null;
				_reservedSlotTime = null;
				_reservedSlotDate = null;
				if (window.__bookingFlowCountdownInterval) {
					clearInterval(window.__bookingFlowCountdownInterval);
					window.__bookingFlowCountdownInterval = null;
				}
				toast.error('Your reservation has expired. Please select a new time slot.');
				return;
			}

			const minutes = Math.floor(diff / 60000);
			const seconds = Math.floor((diff % 60000) / 1000);
			reservationCountdown = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
		};

		updateCountdown();
		window.__bookingFlowCountdownInterval = setInterval(() => {
			if (reservationExpired) {
				if (window.__bookingFlowCountdownInterval) {
					clearInterval(window.__bookingFlowCountdownInterval);
					window.__bookingFlowCountdownInterval = null;
				}
				return;
			}
			updateCountdown();
		}, 1000);
	}

	async function refreshAvailableHours() {
		if (!selectedDate) return;

		const monthKey = `${selectedDate.year}-${String(selectedDate.month).padStart(2, '0')}`;
		delete availableHoursCache[monthKey];
		await fetchHoursForMonth(selectedDate);
	}

	// =============== Services Management ===============
	let services = $state<Service[]>([]);
	let servicesLoading = $state(true);

	async function fetchServices() {
		servicesLoading = true;
		try {
			const response = await apiFetch('/api/services');

			if (response.ok) {
				const data: Service[] = await response.json();
				// Sort: valid patch tests first (by name), then grayed out (by name)
				const valid: Service[] = [];
				const grayedOut: Service[] = [];

				for (const service of data) {
					if (service.patch_test_status === 'required' || service.patch_test_status === 'expired') {
						grayedOut.push(service);
					} else {
						valid.push(service);
					}
				}

				// Sort each group alphabetically
				valid.sort((a, b) => a.name.localeCompare(b.name));
				grayedOut.sort((a, b) => a.name.localeCompare(b.name));

				// Combine: valid first, then grayed out
				services = [...valid, ...grayedOut];
			} else {
				toast.error('Failed to load services');
			}
		} catch (_err) {
			toast.error('Network error loading services');
		} finally {
			servicesLoading = false;
		}
	}

	// =============== Working Hours & Available Hours ===============
	let workingHours = $state<Record<
		string,
		{ isOpen: boolean; startTime: string; endTime: string }
	> | null>(null);

	let availableHours = $state<Record<
		string,
		{ isOpen: boolean; slots: Array<{ startTime: string; endTime: string }> }
	> | null>(null);

	let loadingWorkingHours = $state<boolean>(false);
	let loadingAvailableHours = $state<boolean>(false);

	const workingHoursCache: Record<
		string,
		Record<string, { isOpen: boolean; startTime: string; endTime: string }>
	> = {};

	const availableHoursCache: Record<
		string,
		Record<string, { isOpen: boolean; slots: Array<{ startTime: string; endTime: string }> }>
	> = {};

	// Initialize date boundaries
	const today = getLondonTodayCalendarDate();
	const minDate = today;
	const maxDate = new SvelteDate(today.year, today.month - 1, today.day);
	maxDate.setMonth(today.month - 1 + 6);
	const maxCalendarDate = new CalendarDate(
		maxDate.getFullYear(),
		maxDate.getMonth() + 1,
		maxDate.getDate()
	);

	let placeholder = $state<CalendarDate>(minDate);
	let userNavigatedCalendar = $state(false);
	let bookingFlowAutoSelectDone = $state(false);

	$effect(() => {
		fetchServices();
		ensureBusinessInfo();
	});

	// Track which months are currently being fetched (prevents duplicate requests)
	const loadingMonths: Record<string, boolean> = {};

	// Preload current + next month on first render; subsequent months fetched individually
	let initialLoadDone = $state(false);
	$effect(() => {
		if (!initialLoadDone) {
			// Pre-seed cache for current + next month
			for (let i = 0; i < 2; i++) {
				let mYear = placeholder.year;
				let mMonth = placeholder.month + i;
				while (mMonth > 12) {
					mMonth -= 12;
					mYear++;
				}
				const key = `${mYear}-${String(mMonth).padStart(2, '0')}`;
				if (!(key in workingHoursCache)) {
					workingHoursCache[key] = null as unknown as Record<
						string,
						{ isOpen: boolean; startTime: string; endTime: string }
					>;
					availableHoursCache[key] = null as unknown as Record<
						string,
						{ isOpen: boolean; slots: Array<{ startTime: string; endTime: string }> }
					>;
					loadingMonths[key] = true;
				}
			}
			fetchHoursRange(placeholder, 2);
			initialLoadDone = true;
		}
	});

	// Safety net: fetch silently when navigating to an uncached month
	// (uses skipLoadingFlags=true to prevent layout shift / scroll snap)
	$effect(() => {
		const monthKey = `${placeholder.year}-${String(placeholder.month).padStart(2, '0')}`;
		if (initialLoadDone && !(monthKey in workingHoursCache)) {
			fetchHoursForMonth(placeholder, true);
		}
	});

	// Data-driven auto-selection: auto-select the first available date when data loads
	$effect(() => {
		if (
			workingHours &&
			availableHours &&
			!selectedDate &&
			selectedServices.length > 0 &&
			!userNavigatedCalendar &&
			!bookingFlowAutoSelectDone
		) {
			bookingFlowAutoSelectDone = true;

			const currentDate = new SvelteDate(getLondonTodayCalendarDate().toString() + 'T00:00:00');
			const maxDateJs = new SvelteDate(
				maxCalendarDate.year,
				maxCalendarDate.month - 1,
				maxCalendarDate.day
			);

			const daysDifference = Math.floor(
				(maxDateJs.getTime() - currentDate.getTime()) / (1000 * 60 * 60 * 24)
			);
			const daysToCheck = Math.min(daysDifference, 180);

			for (let i = 1; i <= daysToCheck; i++) {
				const nextDate = new SvelteDate(currentDate);
				nextDate.setDate(currentDate.getDate() + i);
				const dateStr = nextDate.toLocaleDateString('en-CA', { timeZone: 'Europe/London' });

				if (workingHours[dateStr]?.isOpen) {
					const calDate = new CalendarDate(
						nextDate.getFullYear(),
						nextDate.getMonth() + 1,
						nextDate.getDate()
					);
					if (!isDateUnavailable(calDate)) {
						selectedDate = calDate;
						if (!userNavigatedCalendar) {
							placeholder = new CalendarDate(nextDate.getFullYear(), nextDate.getMonth() + 1, 1);
						}
						return;
					}
				}
			}

			const tomorrowCal = getLondonTodayCalendarDate();
			const tomorrowDate = new CalendarDate(
				tomorrowCal.year,
				tomorrowCal.month,
				tomorrowCal.day + 1
			);
			selectedDate = tomorrowDate;
			if (!userNavigatedCalendar) {
				placeholder = new CalendarDate(tomorrowCal.year, tomorrowCal.month, 1);
			}
		}
	});

	async function fetchHoursRange(startDate: CalendarDate, months: number) {
		// Calculate end month manually (CalendarDate is immutable)
		let endYear = startDate.year;
		let endMonth = startDate.month + months - 1;
		while (endMonth > 12) {
			endMonth -= 12;
			endYear++;
		}
		const endMonthDate = new CalendarDate(endYear, endMonth, 1);
		const daysInEndMonth = endMonthDate.calendar.getDaysInMonth(endMonthDate);

		const startStr = startDate.toString();
		const endStr = `${endYear}-${String(endMonth).padStart(2, '0')}-${String(daysInEndMonth).padStart(2, '0')}`;

		loadingWorkingHours = true;
		loadingAvailableHours = true;

		try {
			const [whRes, ahRes] = await Promise.all([
				apiFetch(`/api/scheduling/working-hours?start=${startStr}&end=${endStr}`),
				apiFetch(`/api/scheduling/available-hours?start=${startStr}&end=${endStr}`)
			]);
			if (!whRes.ok || !ahRes.ok) {
				throw new Error(`HTTP error! wh: ${whRes.status}, ah: ${ahRes.status}`);
			}

			const whData: Array<WorkingHoursDay> = await whRes.json();
			const ahData: Array<AvailableHoursDay> = await ahRes.json();

			const whMap: Record<string, { isOpen: boolean; startTime: string; endTime: string }> = {};
			whData.forEach((d) => {
				whMap[d.date] = { isOpen: d.isOpen, startTime: d.startTime, endTime: d.endTime };
			});

			const ahMap: Record<
				string,
				{ isOpen: boolean; slots: Array<{ startTime: string; endTime: string }> }
			> = {};
			ahData.forEach((d) => {
				ahMap[d.date] = { isOpen: d.isOpen, slots: d.slots };
			});

			// Cache by month key
			for (let i = 0; i < months; i++) {
				let mYear = startDate.year;
				let mMonth = startDate.month + i;
				while (mMonth > 12) {
					mMonth -= 12;
					mYear++;
				}
				const key = `${mYear}-${String(mMonth).padStart(2, '0')}`;
				workingHoursCache[key] = whMap;
				availableHoursCache[key] = ahMap;
				delete loadingMonths[key];
			}

			// MERGE instead of replace — preserves data from previously loaded months
			workingHours = { ...workingHours, ...whMap };
			availableHours = { ...availableHours, ...ahMap };
		} catch (_error) {
			// Clean up loadingMonths for the range
			for (let i = 0; i < months; i++) {
				let mYear = startDate.year;
				let mMonth = startDate.month + i;
				while (mMonth > 12) {
					mMonth -= 12;
					mYear++;
				}
				const key = `${mYear}-${String(mMonth).padStart(2, '0')}`;
				delete loadingMonths[key];
			}
			if (!selectedDate) {
				selectedDate = minDate;
			}
		} finally {
			loadingWorkingHours = false;
			loadingAvailableHours = false;
		}
	}

	async function fetchHoursForMonth(date: CalendarDate, skipLoadingFlags = false) {
		const monthKey = `${date.year}-${String(date.month).padStart(2, '0')}`;

		// Only use cache if the value is truthy (not a pre-seeded null placeholder)
		if (workingHoursCache[monthKey] && availableHoursCache[monthKey]) {
			// MERGE instead of replace — preserves data from other loaded months
			workingHours = { ...workingHours, ...workingHoursCache[monthKey] };
			availableHours = { ...availableHours, ...availableHoursCache[monthKey] };
			return;
		}

		// Prevent duplicate concurrent requests for the same month
		if (loadingMonths[monthKey]) return;
		loadingMonths[monthKey] = true;

		if (!skipLoadingFlags) {
			loadingWorkingHours = true;
			loadingAvailableHours = true;
		}

		try {
			const startOfMonth = new CalendarDate(date.year, date.month, 1);
			const endOfMonth = new CalendarDate(
				date.year,
				date.month,
				date.calendar.getDaysInMonth(date)
			);

			const startStr = startOfMonth.toString();
			const endStr = endOfMonth.toString();

			// Fetch working hours
			const workingHoursResponse = await apiFetch(
				`/api/scheduling/working-hours?start=${startStr}&end=${endStr}`
			);
			if (!workingHoursResponse.ok) {
				throw new Error(`HTTP error! status: ${workingHoursResponse.status}`);
			}

			const workingHoursData: Array<WorkingHoursDay> = await workingHoursResponse.json();
			const workingHoursMap: Record<
				string,
				{ isOpen: boolean; startTime: string; endTime: string }
			> = {};

			workingHoursData.forEach((day) => {
				workingHoursMap[day.date] = {
					isOpen: day.isOpen,
					startTime: day.startTime,
					endTime: day.endTime
				};
			});

			workingHoursCache[monthKey] = workingHoursMap;
			workingHours = { ...workingHours, ...workingHoursMap };

			// Fetch available hours
			const availableHoursResponse = await apiFetch(
				`/api/scheduling/available-hours?start=${startStr}&end=${endStr}`
			);
			if (!availableHoursResponse.ok) {
				throw new Error(`HTTP error! status: ${availableHoursResponse.status}`);
			}

			const availableHoursData: Array<AvailableHoursDay> = await availableHoursResponse.json();
			const availableHoursMap: Record<
				string,
				{ isOpen: boolean; slots: Array<{ startTime: string; endTime: string }> }
			> = {};

			availableHoursData.forEach((day) => {
				availableHoursMap[day.date] = {
					isOpen: day.isOpen,
					slots: day.slots
				};
			});

			availableHoursCache[monthKey] = availableHoursMap;
			availableHours = { ...availableHours, ...availableHoursMap };
		} catch (_error) {
			if (!selectedDate) {
				selectedDate = minDate;
			}
		} finally {
			delete loadingMonths[monthKey];
			if (!skipLoadingFlags) {
				loadingWorkingHours = false;
				loadingAvailableHours = false;
			}
		}
	}

	// =============== Time Slot Generation ===============
	function calculateEndTime(startTime: string, durationMinutes: number): string {
		const [hours, minutes] = startTime.split(':').map(Number);
		const date = new SvelteDate();
		date.setHours(hours, minutes, 0, 0);
		date.setMinutes(date.getMinutes() + durationMinutes);
		const endHours = date.getHours().toString().padStart(2, '0');
		const endMinutes = date.getMinutes().toString().padStart(2, '0');
		return `${endHours}:${endMinutes}`;
	}

	function timeToMinutes(time: string): number {
		const [hours, minutes] = time.split(':').map(Number);
		return hours * 60 + minutes;
	}

	function calculatePreviousTime(time: string): string {
		const [hours, minutes] = time.split(':').map(Number);
		let totalMinutes = hours * 60 + minutes;
		totalMinutes -= 15;

		const prevHours = Math.floor(totalMinutes / 60);
		const prevMinutes = totalMinutes % 60;
		return `${String(prevHours).padStart(2, '0')}:${String(prevMinutes).padStart(2, '0')}`;
	}

	function generateAvailableTimeSlots(duration: number, date: CalendarDate | undefined): string[] {
		if (!date || !workingHours || !availableHours) {
			return [];
		}

		const dateStr = date.toString();
		const dayWorkingHours = workingHours[dateStr];
		const dayAvailableHours = availableHours[dateStr];

		if (
			!dayWorkingHours ||
			!dayWorkingHours.isOpen ||
			!dayAvailableHours ||
			!dayAvailableHours.slots
		) {
			return [];
		}

		const slots: string[] = [];
		const today = getLondonTodayCalendarDate();
		const now = new Date();
		const londonTimeStr = now.toLocaleTimeString('en-GB', {
			timeZone: 'Europe/London',
			hour: '2-digit',
			minute: '2-digit',
			hour12: false
		});
		const [londonHours, londonMinutes] = londonTimeStr.split(':').map(Number);
		const isToday = date.compare(today) === 0;

		for (const slot of dayAvailableHours.slots) {
			const [startHour, startMinute] = slot.startTime.split(':').map(Number);
			const [endHour, endMinute] = slot.endTime.split(':').map(Number);

			let startTotalMinutes = Math.ceil((startHour * 60 + startMinute) / 15) * 15;
			const endTotalMinutes = endHour * 60 + endMinute;

			if (isToday) {
				const currentMinutes = londonHours * 60 + londonMinutes;
				const minimumStartMinutes = Math.ceil((currentMinutes + 60) / 15) * 15;
				startTotalMinutes = Math.max(startTotalMinutes, minimumStartMinutes);
			}

			for (let minutes = startTotalMinutes; minutes < endTotalMinutes; minutes += 15) {
				const slotEndMinutes = minutes + duration;

				if (slotEndMinutes <= endTotalMinutes) {
					const hour = Math.floor(minutes / 60);
					const minute = minutes % 60;
					const timeStr = `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
					slots.push(timeStr);
				}
			}
		}

		return slots;
	}

	function generateGroupedTimeSlots(
		duration: number,
		date: CalendarDate | undefined,
		lunchProtectionMap: Map<
			string,
			{ isBlocked: boolean; showWarning: boolean; warningMessage?: string }
		> = new Map()
	): Array<{
		type: 'available' | 'unavailable';
		startTime: string;
		endTime: string;
		isGrouped?: boolean;
	}> {
		if (!date || !workingHours) {
			return [];
		}

		const dateStr = date.toString();
		const dayWorkingHours = workingHours[dateStr];

		if (!dayWorkingHours || !dayWorkingHours.isOpen) {
			return [];
		}

		const groupedSlots: Array<{
			type: 'available' | 'unavailable';
			startTime: string;
			endTime: string;
			isGrouped?: boolean;
		}> = [];
		const [startHour, startMinute] = dayWorkingHours.startTime.split(':').map(Number);
		const [endHour, endMinute] = dayWorkingHours.endTime.split(':').map(Number);

		let startTotalMinutes = startHour * 60 + startMinute;
		const endTotalMinutes = endHour * 60 + endMinute;

		const todayCal = getLondonTodayCalendarDate();
		const now = new Date();
		const londonTimeStr = now.toLocaleTimeString('en-GB', {
			timeZone: 'Europe/London',
			hour: '2-digit',
			minute: '2-digit',
			hour12: false
		});
		const [londonHours, londonMinutes] = londonTimeStr.split(':').map(Number);
		const isToday = date.compare(todayCal) === 0;

		if (isToday) {
			const currentMinutes = londonHours * 60 + londonMinutes;
			const minimumStartMinutes = Math.ceil((currentMinutes + 60) / 15) * 15;
			startTotalMinutes = Math.max(startTotalMinutes, minimumStartMinutes);
		}

		const availableSlots = generateAvailableTimeSlots(duration, date);

		let currentUnavailableStart: string | null = null;
		let lastAvailableEndTime: string | null = null;

		for (let minutes = startTotalMinutes; minutes < endTotalMinutes; minutes += 15) {
			const hour = Math.floor(minutes / 60);
			const minute = minutes % 60;
			const timeStr = `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;

			const slotDateTime = date.toDate(getLocalTimeZone());
			slotDateTime.setHours(hour, minute, 0, 0);
			const hoursUntilSlot = (slotDateTime.getTime() - now.getTime()) / (1000 * 60 * 60);
			const isBlockedByDepositAdvance =
				userDepositsRequired > 0 && hoursUntilSlot < POLICY.DEPOSIT_ADVANCE_HOURS;

			const isAvailable =
				availableSlots.includes(timeStr) &&
				!lunchProtectionMap.get(timeStr)?.isBlocked &&
				!isBlockedByDepositAdvance;

			if (isAvailable) {
				if (currentUnavailableStart !== null) {
					const groupEndTime = calculatePreviousTime(timeStr);
					const unavailableStartTime = currentUnavailableStart || lastAvailableEndTime;
					if (
						unavailableStartTime &&
						timeToMinutes(unavailableStartTime) < timeToMinutes(groupEndTime)
					) {
						groupedSlots.push({
							type: 'unavailable',
							startTime: unavailableStartTime,
							endTime: groupEndTime,
							isGrouped: true
						});
					}
					currentUnavailableStart = null;
				}

				const slotEndTime = calculateEndTime(timeStr, duration);
				lastAvailableEndTime = slotEndTime;
				groupedSlots.push({
					type: 'available',
					startTime: timeStr,
					endTime: slotEndTime
				});

				if (timeToMinutes(slotEndTime) >= endTotalMinutes) {
					break;
				}
			} else {
				if (currentUnavailableStart === null) {
					currentUnavailableStart = timeStr;
				}
			}
		}

		if (currentUnavailableStart !== null) {
			const lastAvailableSlot = groupedSlots.filter((s) => s.type === 'available').pop();
			const lastAvailableEnd = lastAvailableSlot ? timeToMinutes(lastAvailableSlot.endTime) : 0;

			const unavailableStartMinutes = timeToMinutes(currentUnavailableStart);

			if (unavailableStartMinutes < endTotalMinutes && lastAvailableEnd < endTotalMinutes) {
				// Use the end time of the last available slot for the final unavailable period
				const unavailableStartTime = lastAvailableSlot
					? lastAvailableSlot.endTime
					: currentUnavailableStart;
				groupedSlots.push({
					type: 'unavailable',
					startTime: unavailableStartTime,
					endTime: dayWorkingHours.endTime,
					isGrouped: true
				});
			}
		}

		return groupedSlots;
	}

	// =============== Date Availability Check ===============
	function isDateUnavailable(date: DateValue): boolean {
		if (!(date instanceof CalendarDate)) {
			return true;
		}

		if (date.compare(minDate) < 0 || date.compare(maxCalendarDate) > 0) {
			return true;
		}

		if (!workingHours) return true;

		const dateStr = date.toString();
		const dayHours = workingHours[dateStr];

		if (!dayHours) return true;
		if (!dayHours.isOpen) return true;

		// No available hours data for this date = data not loaded = unavailable
		if (!availableHours?.[dateStr]) return true;
		// API returned empty slots = no availability at all
		if (!availableHours[dateStr].slots || availableHours[dateStr].slots.length === 0) return true;

		if (selectedServices.length === 0) {
			return false;
		}

		const duration = getTotalDuration();
		const availableSlots = generateAvailableTimeSlots(duration, date);
		if (availableSlots.length === 0) return true;

		const dayAvailableHours = availableHours[dateStr];
		// dayAvailableHours.slots is already checked above, but keep this guard for safety
		if (dayAvailableHours.slots) {
			const existingBookings = extractBookedSlots(
				dayHours.startTime,
				dayHours.endTime,
				dayAvailableHours.slots
			);
			const lunchProtection = getLunchProtectionForSlots(
				dayHours.startTime,
				dayHours.endTime,
				existingBookings,
				duration,
				15,
				false
			);
			const now = new SvelteDate();
			const validSlots = availableSlots.filter((t) => {
				if (lunchProtection.get(t)?.isBlocked) return false;
				if (userDepositsRequired > 0) {
					const [h, m] = t.split(':').map(Number);
					const slotDate = date.toDate(getLocalTimeZone());
					slotDate.setHours(h, m, 0, 0);
					const hoursUntil = (slotDate.getTime() - now.getTime()) / (1000 * 60 * 60);
					if (hoursUntil < POLICY.DEPOSIT_ADVANCE_HOURS) return false;
				}
				return true;
			});
			if (validSlots.length === 0) return true;
		}

		return false;
	}

	// =============== Helper Functions ===============
	function getTotalDuration() {
		return selectedServices.reduce(
			(total, service: Service) => total + service.duration_minutes,
			0
		);
	}

	function getTotalPrice() {
		return selectedServices.reduce((total, service: Service) => total + service.price, 0);
	}

	function toggleService(service: Service) {
		const index = selectedServices.findIndex((s) => s.id === service.id);
		const wasSelected = index >= 0;

		if (wasSelected) {
			selectedServices = selectedServices.filter((s) => s.id !== service.id);
		} else {
			selectedServices = [...selectedServices, service];
		}

		// Service change invalidates any held reservation (duration may have changed).
		// Release before re-fetching hours so the next reservation starts fresh.
		if (currentStep >= 2 && _reservationId) {
			releaseReservation();
		}

		// Only clear if we're on the date/time selection step
		if (currentStep === 2 && selectedDate) {
			const monthKey = `${selectedDate.year}-${String(selectedDate.month).padStart(2, '0')}`;
			delete availableHoursCache[monthKey];
			fetchHoursForMonth(selectedDate);
		}

		// Reset date selection when services change so auto-select can re-run
		bookingFlowAutoSelectDone = false;
		selectedDate = undefined;
		selectedTime = null;
	}

	// =============== Lunch Protection for Rendering ===============
	function getLunchProtectionStatus() {
		if (!selectedDate || !workingHours || !availableHours || selectedServices.length === 0) {
			return new Map<
				string,
				{ isBlocked: boolean; showWarning: boolean; warningMessage?: string }
			>();
		}

		const dateStr = selectedDate.toString();
		const dayWH = workingHours[dateStr];
		const dayAH = availableHours[dateStr];
		if (!dayWH?.isOpen || !dayAH?.slots) return new Map();

		const existingBookings = extractBookedSlots(dayWH.startTime, dayWH.endTime, dayAH.slots);
		return getLunchProtectionForSlots(
			dayWH.startTime,
			dayWH.endTime,
			existingBookings,
			getTotalDuration(),
			15,
			false
		);
	}

	// Select a time slot with server-side re-validation
	async function selectTimeWithValidation(time: string) {
		// Race-condition guard: capture the user's click and pass it explicitly
		// to the validator. If the user clicks a different time before this
		// validation completes, the stale result must not clobber the newer
		// selection.
		selectedTime = time;

		// Time change invalidates any held reservation (slot is now stale).
		// Only release if the new time differs from the reserved one — if the
		// user is re-clicking the same reserved time, keep the reservation.
		if (_reservationId && time !== _reservedSlotTime) {
			await releaseReservation();
		}

		await refreshAndValidateSlot(time);
	}

	// Re-fetch available hours silently and check if the requested time is still
	// available. `validateForTime` should be the time the user clicked, NOT the
	// current selectedTime — this prevents a slow validation for a stale click
	// from clearing a newer selection.
	// Uses skipLoadingFlags=true to prevent UI judder (loading spinners hide DatePicker/TimeSlotPicker)
	async function refreshAndValidateSlot(validateForTime: string | null = null) {
		const timeToCheck = validateForTime ?? selectedTime;
		if (!selectedDate || !timeToCheck) return false;

		const monthKey = `${selectedDate.year}-${String(selectedDate.month).padStart(2, '0')}`;
		delete availableHoursCache[monthKey];
		await fetchHoursForMonth(selectedDate, true);

		const dateStr = selectedDate.toString();
		const dayAvailable = availableHours?.[dateStr]?.slots;
		if (!dayAvailable || dayAvailable.length === 0) {
			toast.error('Sorry, this slot is no longer available. Please choose a different time.');
			if (selectedTime === timeToCheck) selectedTime = null;
			return false;
		}

		const duration = getTotalDuration();
		const [selHour, selMinute] = timeToCheck.split(':').map(Number);
		const selStart = selHour * 60 + selMinute;
		const selEnd = selStart + duration;

		const stillAvailable = dayAvailable.some((slot) => {
			const [sH, sM] = slot.startTime.split(':').map(Number);
			const [eH, eM] = slot.endTime.split(':').map(Number);
			return selStart >= sH * 60 + sM && selEnd <= eH * 60 + eM;
		});

		if (!stillAvailable) {
			toast.error('Sorry, this slot was just taken. Please choose a different time.');
			if (selectedTime === timeToCheck) selectedTime = null;
			return false;
		}

		return true;
	}

	function formatDuration(minutes: number): string {
		const hours = Math.floor(minutes / 60);
		const remainingMinutes = minutes % 60;

		if (hours === 0) {
			return `${remainingMinutes} minutes`;
		} else if (remainingMinutes === 0) {
			return `${hours} ${hours === 1 ? 'hour' : 'hours'}`;
		} else {
			return `${hours} ${hours === 1 ? 'hour' : 'hours'} ${remainingMinutes} minutes`;
		}
	}

	function getDayWithOrdinal(date: CalendarDate): string {
		const monthName = new SvelteDate(date.year, date.month - 1, date.day).toLocaleDateString(
			'en-GB',
			{
				month: 'long'
			}
		);
		const day = date.day;
		if (day > 3 && day < 21) return monthName + ' ' + day + 'th';
		switch (day % 10) {
			case 1:
				return monthName + ' ' + day + 'st';
			case 2:
				return monthName + ' ' + day + 'nd';
			case 3:
				return monthName + ' ' + day + 'rd';
			default:
				return monthName + ' ' + day + 'th';
		}
	}

	// =============== Derived Values ===============
	const formattedTotalDuration = $derived(formatDuration(getTotalDuration()));
	const lunchProtectionMap = $derived(getLunchProtectionStatus());
	const groupedTimeSlots = $derived(
		currentStep === 2 && selectedServices.length > 0 && selectedDate
			? generateGroupedTimeSlots(getTotalDuration(), selectedDate, lunchProtectionMap)
			: []
	);
	const formattedSelectedDate = $derived(
		selectedDate ? getDayWithOrdinal(selectedDate) : undefined
	);

	const depositRequired = $derived(calculateDepositRequired());
	const totalSteps = $derived(authStore.isAuthenticated ? 4 : 5);
	// StepIndicator uses displayNumber = startAt + index. currentStep aligns with displayNumber,
	// not the array index. For auth: startAt=1, totalSteps=4 → last displayNumber=4.
	// For guest: startAt=0, totalSteps=5 → last displayNumber=4. Always evaluates to 4.
	const finalStep = $derived(totalSteps - 1 + (authStore.isAuthenticated ? 1 : 0));
	const stepLabels = $derived(
		authStore.isAuthenticated
			? ['Service', 'Date & Time', 'Details', 'Payment']
			: userDepositsRequired > 0
				? ['Welcome', 'Service', 'Date & Time', 'Details', 'Payment']
				: ['Welcome', 'Service', 'Date & Time', 'Details', 'Confirmation']
	);

	// =============== Navigation ===============
	async function nextStep() {
		if (currentStep === 0) {
			currentStep = 1;
			setTimeout(() => {
				window.scrollTo({ top: 0, behavior: 'smooth' });
			}, 50);
			return;
		}

		// Step 2 -> Step 3: Check if we already hold a valid reservation for this exact slot
		if (currentStep === 2) {
			const sameSlot =
				_reservationId &&
				!reservationExpired &&
				_reservedSlotTime === selectedTime &&
				_reservedSlotDate === (selectedDate ? selectedDate.toString() : null);

			if (sameSlot) {
				// Same slot — keep existing reservation, go straight to step 3
				currentStep = 3;
			} else {
				// Different slot or no reservation — release old one first, then reserve new
				await releaseReservation();

				const slotStillFree = await refreshAndValidateSlot();
				if (!slotStillFree) return;
				const reserved = await reserveSlot();
				if (!reserved) return;
				// reserveSlot() stores _reservedSlotTime/_reservedSlotDate internally
				currentStep = 3;
			}
			return;
		}

		// Step 3 -> Final step (Payment if deposit required, else submit booking)
		if (currentStep === 3) {
			if (calculateDepositRequired()) {
				currentStep = finalStep;
			} else {
				await submitAndProceed();
			}
			return;
		}

		// Final step with deposit: user must pay before booking is created.
		// Payment is handled by processPayment(), not nextStep().
		// This guards against manual increment from the payment step.
		if (currentStep === finalStep && calculateDepositRequired()) {
			return;
		}

		if (currentStep < finalStep) {
			currentStep++;
			setTimeout(() => {
				window.scrollTo({ top: 0, behavior: 'smooth' });
			}, 50);
		}
	}

	async function submitAndProceed() {
		_isSubmitting = true;
		try {
			// Always generate a fresh idempotency key per submission attempt. The
			// previous behavior kept the key across the component lifetime, which
			// meant a second submission (after a success, back-nav, change) would
			// hit the backend with the same key and get the ORIGINAL booking back,
			// making the user believe they made a new booking when they didn't.
			idempotencyKey = generateUUID();

			if (!selectedDate || !selectedTime) {
				toast.error('Please select a date and time');
				_isSubmitting = false;
				return;
			}

			const [hours, minutes] = selectedTime.split(':').map(Number);
			const bookingDate = selectedDate.toDate(getLocalTimeZone());
			bookingDate.setHours(hours, minutes, 0, 0);
			const startTimeISO = formatLocalDateTime(bookingDate);

			const serviceIds = selectedServices.map((s) => s.id);

			let guestUserId: string | null = null;
			if (!authStore.isAuthenticated) {
				const guestResponse = await fetch('/api/users/guest', {
					method: 'POST',
					headers: { 'Content-Type': 'application/json' },
					body: JSON.stringify({
						firstName: customerInfo.firstName,
						lastName: customerInfo.lastName,
						email: customerInfo.email,
						phone: toE164UK(customerInfo.phone) ?? customerInfo.phone
					})
				});

				if (!guestResponse.ok) {
					const _errorText = await guestResponse.text();
					if (guestResponse.status === 409) {
						toast.error('Email already registered — please log in to book.');
					} else {
						toast.error('Failed to create guest account. Please try again.');
					}
					_isSubmitting = false;
					return;
				}

				const guestData = await guestResponse.json();
				guestUserId = guestData.id;
			}

			const requestBody: Record<string, unknown> = {
				service_ids: serviceIds,
				start_time: startTimeISO,
				notes: customerInfo.specialRequests || null
			};

			if (guestUserId) {
				requestBody.user_id = guestUserId;
			}

			const response = await apiFetch('/api/bookings', {
				method: 'POST',
				headers: {
					'Content-Type': 'application/json',
					'Idempotency-Key': idempotencyKey,
					...getAuthHeaders()
				},
				body: JSON.stringify(requestBody)
			});

			if (response.ok) {
				const booking = await response.json();
				confirmedBooking = {
					id: booking.id,
					status: booking.status,
					start_time: booking.start_time,
					notes: booking.notes || '',
					deposit_required: booking.deposit_required ?? false,
					deposit_paid: booking.deposit_paid ?? false,
					deposit_amount: booking.deposit_amount ?? 0,
					amount_paid: booking.amount_paid ?? 0,
					amount_due: booking.amount_due || getTotalPrice(),
					payments: booking.payments ?? [],
					total_amount: booking.total_amount || getTotalPrice(),
					duration_minutes: booking.duration_minutes || getTotalDuration()
				};
				// Booking is now persisted — release the temporary reservation
				// (best-effort, no UI block). Also clear the idempotency key so
				// any future submission in this component lifetime gets a fresh
				// key and is treated as a NEW booking attempt.
				releaseReservation();
				idempotencyKey = '';
				currentStep = finalStep;
				fetchDiscountPreview();
				setTimeout(() => {
					window.scrollTo({ top: 0, behavior: 'smooth' });
				}, 50);
			} else {
				const errorText = await response.text();
				let errorMessage = errorText.trim();
				if (!errorMessage) {
					errorMessage = 'Failed to submit booking. Please try again.';
				}
				// Backend may return plain text or JSON
				try {
					const errorData = JSON.parse(errorText);
					if (errorData.error) errorMessage = errorData.error;
				} catch {
					/* use raw text from backend */
				}

				if (response.status === 409) {
					if (errorMessage.includes('active booking') || errorMessage.includes('already have')) {
						toast.error(
							'You already have an active booking. Please complete or cancel it before creating a new one.'
						);
					} else {
						toast.error('This time slot is no longer available. Please choose a different time.');
					}
				} else if (errorMessage.includes('patch test') || errorMessage.includes('Patch test')) {
					toast.error(errorMessage + ' Please complete a patch test first.');
				} else if (
					errorMessage.includes('48 hours') ||
					errorMessage.includes('48h') ||
					errorMessage.includes('advance')
				) {
					toast.error(errorMessage);
				} else if (errorMessage.includes('deposit') || errorMessage.includes('Deposit')) {
					toast.error(errorMessage);
				} else if (response.status === 400) {
					toast.error(errorMessage);
				} else {
					toast.error('Failed to submit booking: ' + errorMessage);
				}
			}
		} catch (_error) {
			toast.error('Network error. Please check your connection and try again.');
		} finally {
			_isSubmitting = false;
		}
	}

	function prevStep() {
		if (currentStep > 1) {
			// If leaving step 2 (date/time) and a reservation is still held, release
			// it — the user is abandoning this selection. Going from step 3 back to
			// step 2 keeps the reservation (the user is just reviewing).
			if (currentStep === 2 && _reservationId) {
				releaseReservation();
			}
			currentStep--;
			setTimeout(() => {
				window.scrollTo({ top: 0, behavior: 'smooth' });
			}, 50);
		}
	}

	// Clean up countdown interval on component destroy (do NOT call DELETE — let TTL expire naturally)
	onDestroy(() => {
		if (typeof window !== 'undefined' && window.__bookingFlowCountdownInterval) {
			clearInterval(window.__bookingFlowCountdownInterval);
			window.__bookingFlowCountdownInterval = null;
		}
	});

	// =============== Validation ===============
	const isBlockedByActiveBooking = $derived(
		authStore.isAuthenticated && userDepositsRequired > 0 && hasActiveBooking
	);

	const canProceedStep1 = $derived(selectedServices.length > 0 && !isBlockedByActiveBooking);
	const canProceedStep2 = $derived(!!(selectedDate && selectedTime) && !isBlockedByActiveBooking);
	const canProceedStep3 = $derived(
		!isBlockedByActiveBooking &&
			(authStore.isAuthenticated
				? !!(
						authStore.currentUser?.firstName &&
						authStore.currentUser?.lastName &&
						authStore.currentUser?.email &&
						authStore.currentUser?.phone
					)
				: !!(
						customerInfo.firstName &&
						customerInfo.lastName &&
						customerInfo.email &&
						emailFormatValid &&
						customerInfo.phone &&
						isValidUKPhone(customerInfo.phone) &&
						!emailSuggestion
					)) &&
			!reservationExpired
	);
	const _canProceedStep4 = $derived(true);
</script>

<div class="mx-auto max-w-4xl p-6">
	<div class="mb-8 text-center">
		<h1 class="mb-2 font-['Playfair_Display'] text-4xl font-bold">Book Your Appointment</h1>
		<p class="text-gray-600">Professional beauty treatments in a calm and friendly environment</p>
	</div>

	{#if !authReady}
		<div class="mb-6 flex items-center justify-center gap-2">
			{#each [1, 2, 3, 4] as _ (_)}
				<div class="flex items-center gap-1">
					<div class="h-8 w-8 animate-pulse rounded-full bg-gray-200"></div>
					<div class="h-3 w-16 animate-pulse rounded bg-gray-200"></div>
				</div>
				{#if _ < 4}
					<div class="mx-1 h-0.5 w-8 animate-pulse rounded bg-gray-200"></div>
				{/if}
			{/each}
		</div>

		<Card.Root>
			<Card.Header>
				<div class="h-7 w-40 animate-pulse rounded bg-gray-200"></div>
				<div class="mt-2 h-4 w-64 animate-pulse rounded bg-gray-200"></div>
			</Card.Header>
			<Card.Content class="space-y-4">
				<div class="h-16 animate-pulse rounded-lg bg-gray-100"></div>
				<div class="h-16 animate-pulse rounded-lg bg-gray-100"></div>
				<div class="h-16 animate-pulse rounded-lg bg-gray-100"></div>
			</Card.Content>
		</Card.Root>
	{:else}
		<StepIndicator
			{currentStep}
			steps={stepLabels}
			startAt={authStore.isAuthenticated ? 1 : 0}
			className={currentStep === 0 ? 'md:hidden' : ''}
		/>

		{#if currentStep === 0}
			<Card.Root class="border-fuchsia-200 bg-fuchsia-50">
				<Card.Header class="text-center">
					<Card.Title>Welcome</Card.Title>
					<Card.Description>Log in for the best booking experience</Card.Description>
				</Card.Header>
				<Card.Content class="p-6 text-center">
					<p class="mb-4 text-sm text-muted-foreground">
						Guest checkout does not receive loyalty stamps or seasonal discounts.
					</p>
					<div class="flex flex-col gap-3 sm:flex-row sm:justify-center sm:gap-4">
						<Button
							onclick={() => goto(resolve('/login'))}
							variant="outline"
							class="border-fuchsia-200 hover:bg-fuchsia-100"
						>
							Log In
						</Button>
						<Button onclick={nextStep}>Continue as Guest</Button>
					</div>
				</Card.Content>
			</Card.Root>
		{/if}

		<!-- Step 1: Service Selection -->
		{#if currentStep === 1}
			<Card.Root>
				<Card.Header>
					<Card.Title>Choose Your Services</Card.Title>
					<Card.Description>Select one or more treatments for your appointment</Card.Description>
				</Card.Header>
				<Card.Content class="space-y-4">
					<!-- Warning: Active booking limit for deposit-owing users -->
					{#if authStore.isAuthenticated && userDepositsRequired > 0 && hasActiveBooking}
						<div class="rounded-lg border border-amber-200/60 bg-amber-50 p-4">
							<div class="flex gap-3">
								<div class="flex-shrink-0">
									<svg
										xmlns="http://www.w3.org/2000/svg"
										class="h-5 w-5 text-amber-600"
										viewBox="0 0 20 20"
										fill="currentColor"
									>
										<path
											fill-rule="evenodd"
											d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
											clip-rule="evenodd"
										/>
									</svg>
								</div>
								<div class="space-y-1 text-sm text-amber-900">
									<p class="font-semibold text-amber-800">One Booking at a Time</p>
									<p>
										We are currently asking for deposits on upcoming bookings. While this is active,
										only one online booking can be made at a time. If you need another appointment
										please <a
											href="/contact"
											target="_blank"
											rel="external"
											class="font-medium underline">contact us</a
										>.
									</p>
								</div>
							</div>
						</div>
					{/if}

					<ServiceSelector
						{services}
						selected={selectedServices}
						loading={servicesLoading}
						ontoggle={toggleService}
					/>

					<div
						class="mt-4 rounded-lg border border-dashed border-gray-300 bg-gray-50 p-4 text-center"
					>
						<p class="text-sm text-gray-600">
							Need something different? Select the closest service and add a note, or contact us for
							a bespoke treatment.
						</p>
						<a
							href={resolve('/contact')}
							class="mt-1 inline-block text-sm font-medium text-blue-600 hover:underline"
						>
							Arrange a custom booking →
						</a>
					</div>

					{#if selectedServices.length > 0}
						<div class="rounded-lg bg-gray-50 p-4">
							<h4 class="mb-2 font-semibold">Selected Services</h4>
							<div class="space-y-2">
								{#each selectedServices as service (service.id)}
									<div class="flex justify-between text-sm">
										<span>{service.name}</span>
										<span>{service.duration_minutes} mins • £{service.price}</span>
									</div>
								{/each}
								<Separator class="my-2" />
								<div class="flex justify-between text-sm font-semibold">
									<span>Estimated Duration:</span>
									<span>{formattedTotalDuration}</span>
								</div>
								<div class="flex justify-between text-sm font-semibold">
									<span>Total Cost:</span>
									<span
										>£{getTotalPrice()}{#if vatRegistered}
											<span class="text-xs font-normal text-gray-400">incl. VAT</span>{/if}</span
									>
								</div>
							</div>
						</div>
					{/if}
				</Card.Content>
				<Card.Footer class="flex justify-end">
					<BookingActions
						canBack={!authStore.isAuthenticated}
						canNext={canProceedStep1}
						nextLabel="Next: Select Date & Time"
						on:next={nextStep}
					/>
				</Card.Footer>
			</Card.Root>
		{/if}

		<!-- Step 2: Date & Time Selection -->
		{#if currentStep === 2}
			<Card.Root>
				<Card.Header>
					<Card.Title>Choose Date & Time</Card.Title>
					<Card.Description>
						{selectedServices.map((s) => s.name).join(', ')} • {formattedTotalDuration} total • £{getTotalPrice()}
					</Card.Description>
				</Card.Header>
				<Card.Content class="p-0">
					<Card.Root class="gap-0 border-0 p-0">
						<Card.Content class="relative p-0 md:pr-56">
							{#if loadingWorkingHours}
								<div class="flex items-center justify-center p-6">
									<p>Loading available dates...</p>
								</div>
							{:else}
								<DatePicker
									date={selectedDate}
									{placeholder}
									minValue={minDate}
									maxValue={maxCalendarDate}
									{isDateUnavailable}
									onchange={(newDate) => {
										// Date change invalidates any held reservation (slot is now stale).
										if (_reservationId) {
											releaseReservation();
										}
										selectedDate = newDate;
										selectedTime = null;
									}}
									onPlaceholderChange={(newPlaceholder) => {
										placeholder = newPlaceholder;
										userNavigatedCalendar = true;
									}}
								/>
							{/if}

							{#if loadingAvailableHours}
								<div
									class="absolute inset-y-0 right-0 flex w-56 items-center justify-center border-l p-6"
								>
									<p class="text-sm text-gray-500">Loading times...</p>
								</div>
							{:else}
								<TimeSlotPicker
									date={selectedDate}
									{groupedTimeSlots}
									{selectedTime}
									formattedDate={formattedSelectedDate}
									onselect={(time) => {
										selectTimeWithValidation(time);
									}}
								/>
							{/if}
						</Card.Content>
					</Card.Root>
				</Card.Content>

				<!-- Mobile appointment summary -->
				<div class="border-t px-6 py-4 text-center text-sm md:hidden">
					{#if selectedDate && selectedTime}
						Appointment for
						<span class="font-medium">
							{selectedDate.toDate(getLocalTimeZone()).toLocaleDateString('en-US', {
								weekday: 'long',
								day: 'numeric',
								month: 'short'
							})}
						</span>
						<br />at <span class="font-medium">{selectedTime}</span>
					{:else}
						Select a date and time
					{/if}
				</div>

				<Card.Footer class="flex justify-between border-t px-6 !py-5">
					<Button variant="outline" onclick={prevStep}>Back</Button>
					<div class="flex items-center space-x-4">
						<!-- Desktop appointment summary -->
						<div class="hidden text-sm md:block">
							{#if selectedDate && selectedTime}
								Appointment for
								<span class="font-medium">
									{selectedDate.toDate(getLocalTimeZone()).toLocaleDateString('en-US', {
										weekday: 'long',
										day: 'numeric',
										month: 'short'
									})}
								</span>
								at <span class="font-medium">{selectedTime}</span>
							{:else}
								Select a date and time
							{/if}
						</div>
						<Button disabled={!canProceedStep2} onclick={nextStep}>Next: Your Details</Button>
					</div>
				</Card.Footer>
			</Card.Root>
		{/if}

		<!-- Step 3: Customer Details -->
		{#if currentStep === 3}
			<div class="mb-6 text-center">
				<h2 class="font-['Playfair_Display'] text-2xl font-bold">Almost There</h2>
				{#if !authStore.isAuthenticated}
					<p class="mt-1 text-gray-500">Just a couple more details</p>
				{/if}
			</div>
			<Card.Root>
				<Card.Header>
					<Card.Title>Your Details</Card.Title>
					<Card.Description>Please confirm your contact information</Card.Description>
				</Card.Header>
				<Card.Content class="space-y-6">
					{#if reservationExpired}
						<div class="rounded-lg bg-red-50 p-4 text-center">
							<p class="text-red-600">Reservation expired — please go back and select a new time</p>
						</div>
					{:else}
						<div class="rounded-lg bg-blue-50 p-4 text-center">
							<p class="text-blue-700">
								Your slot will be held for {reservationCountdown} — complete your booking before time
								expires
							</p>
						</div>
					{/if}

					<BookingSummary
						services={selectedServices}
						date={selectedDate}
						time={selectedTime}
						showCustomer={false}
					/>

					{#if !authStore.isAuthenticated}
						<p class="mb-4 text-center text-sm text-yellow-600">
							You are checking out as a guest, so you will miss out on a loyalty stamp and any
							possible seasonal discounts. Please login for full membership benefits.
						</p>

						<div class="grid gap-4 md:grid-cols-2">
							<div class="space-y-2">
								<Label for="firstName">First Name *</Label>
								<Input
									id="firstName"
									bind:value={customerInfo.firstName}
									placeholder="Enter your first name"
									onblur={debouncedEmailCheck}
								/>
							</div>
							<div class="space-y-2">
								<Label for="lastName">Last Name *</Label>
								<Input
									id="lastName"
									bind:value={customerInfo.lastName}
									placeholder="Enter your last name"
									onblur={debouncedEmailCheck}
								/>
							</div>
							<div class="space-y-2">
								<Label for="email">Email *</Label>
								<EmailInput
									id="email"
									bind:value={customerInfo.email}
									placeholder="Enter your email"
									required
									onvaluechange={() => {
										emailSuggestion = null;
										debouncedEmailCheck();
									}}
									onerrorchange={(err) => {
										emailError = err;
									}}
									onblur={() => {
										debouncedEmailCheck();
									}}
								/>
								{#if emailError}
									<span class="text-xs font-medium text-red-500">{emailError}</span>
								{/if}
								{#if emailChecking}
									<span class="text-xs text-gray-500">Checking...</span>
								{/if}
							</div>
							<div class="space-y-2">
								<Label for="phone">Phone Number *</Label>
								<PhoneInput
									id="phone"
									bind:value={customerInfo.phone}
									placeholder="07123 456789"
									onerrorchange={(err) => {
										if (!err) debouncedEmailCheck();
									}}
								/>
							</div>
						</div>

						{#if emailSuggestion === 'login'}
							<p class="text-xs font-medium text-red-500">
								This email belongs to a registered user. Please
								<a href={resolve('/login')} class="underline hover:text-red-800">log in</a>
								instead to access your bookings and rewards.
							</p>
						{:else if emailSuggestion === 'check'}
							<p class="text-xs font-medium text-amber-600">
								This email might belong to an existing account. Please double-check or
								<a href={resolve('/login')} class="underline hover:text-amber-800">log in</a>.
							</p>
						{/if}
					{/if}

					<div class="space-y-2">
						<Label for="requests">Special Requests (Optional)</Label>
						<Textarea
							id="requests"
							bind:value={customerInfo.specialRequests}
							placeholder="Any allergies, preferences, or special requirements..."
							rows={3}
						/>
						<CharCounter text={customerInfo.specialRequests} />
					</div>

					<div class="text-sm text-gray-600">
						{#if !authStore.isAuthenticated}
							<p>* Required fields</p>
						{/if}
						<p class="mt-2">
							By booking, you agree to our Terms & Conditions and Privacy Policy. We'll send you
							appointment reminders via email and/or SMS.
						</p>
						<p class="mt-2 text-xs text-gray-500">
							<strong>Cancellation Policy:</strong> If paying early a free full refund will be given if
							cancelled more than 72 hours before your appointment. Between 24-72 hours, up to 50% of
							the booking total may be retained as a protected deposit. Cancellations within 24 hours
							are non-refundable and count as a no-show against your account.
						</p>
						<p class="mt-1 text-xs text-gray-500">
							<PolicyPopover>
								{#snippet trigger()}
									<span class="underline">Read full cancellation policy →</span>
								{/snippet}
							</PolicyPopover>
						</p>
					</div>
				</Card.Content>
				<Card.Footer class="flex justify-between">
					<Button variant="outline" onclick={prevStep}>Back</Button>
					<Button
						disabled={!canProceedStep3}
						onclick={nextStep}
						class="bg-primary text-primary-foreground"
					>
						{#if reservationExpired}
							Reservation Expired
						{:else if depositRequired}
							Next: Payment
						{:else}
							Confirm Booking
						{/if}
					</Button>
				</Card.Footer>
			</Card.Root>
		{/if}

		<!-- Step 4: Payment & Confirmation (final step) -->
		{#if currentStep === finalStep}
			{#if confirmedBooking}
				{@const isRequested = confirmedBooking.notes && confirmedBooking.notes.length > 0}
				{@const bookingDate = new SvelteDate(confirmedBooking.start_time)}
				{@const dateStr = bookingDate.toLocaleDateString('en-GB', {
					weekday: 'long',
					day: 'numeric',
					month: 'long',
					year: 'numeric'
				})}
				{@const timeStr = bookingDate.toLocaleTimeString('en-GB', {
					hour: 'numeric',
					minute: '2-digit',
					hour12: true
				})}

				<Card.Root class="border-emerald-200">
					{#if isProcessingPayment && paymentAttempted}
						<Card.Header class="text-center">
							<div
								class="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-amber-100"
							>
								<div
									class="h-8 w-8 animate-spin rounded-full border-4 border-gray-200 border-t-amber-600"
								></div>
							</div>
							<Card.Title class="text-2xl font-bold">Processing Payment</Card.Title>
							<Card.Description class="mt-2 text-base">
								Your booking is confirmed. We're processing your payment — this should only take a
								moment.
							</Card.Description>
						</Card.Header>
					{:else}
						<Card.Header class="text-center">
							<div
								class="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full {isRequested
									? 'bg-amber-100'
									: 'bg-emerald-100'}"
							>
								<svg
									xmlns="http://www.w3.org/2000/svg"
									class="h-8 w-8 {isRequested ? 'text-amber-600' : 'text-emerald-600'}"
									viewBox="0 0 20 20"
									fill="currentColor"
								>
									<path
										fill-rule="evenodd"
										d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
										clip-rule="evenodd"
									/>
								</svg>
							</div>
							<Card.Title class="text-2xl font-bold"
								>{isRequested ? 'Booking Requested' : 'Booking Confirmed'}</Card.Title
							>
							<Card.Description class="mt-2 text-base">
								{isRequested
									? "Your booking has been submitted and is awaiting approval. We'll notify you once it's confirmed."
									: 'Your appointment has been booked successfully.'}
							</Card.Description>
						</Card.Header>
					{/if}
					<Card.Content class="space-y-6">
						{#if !(isProcessingPayment && paymentAttempted)}
							<div class="rounded-lg border border-gray-200 bg-gray-50 p-6">
								<div class="mb-4 flex items-center justify-between">
									<span class="text-sm font-medium text-gray-500">Confirmation Number</span>
									<span class="font-mono text-lg font-bold text-gray-900"
										>{confirmedBooking.id}</span
									>
								</div>
								<div class="grid gap-4 md:grid-cols-2">
									<div>
										<div class="text-xs text-gray-500">Date</div>
										<div class="font-medium">{dateStr}</div>
									</div>
									<div>
										<div class="text-xs text-gray-500">Time</div>
										<div class="font-medium">{timeStr}</div>
									</div>
									<div>
										<div class="text-xs text-gray-500">Duration</div>
										<div class="font-medium">{getTotalDuration()} minutes (estimated)</div>
									</div>
									<div>
										<div class="text-xs text-gray-500">Status</div>
										<div class="font-medium">
											<span
												class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium {isRequested
													? 'bg-amber-100 text-amber-800'
													: 'bg-emerald-100 text-emerald-800'}"
											>
												{isRequested ? 'Pending Approval' : 'Confirmed'}
											</span>
										</div>
									</div>
								</div>
							</div>

							<div class="rounded-lg border border-gray-200 bg-gray-50 p-6">
								<h4 class="mb-3 text-sm font-semibold text-gray-600 uppercase">Services</h4>
								<div class="space-y-2">
									{#each selectedServices as service (service.id)}
										<div class="flex justify-between text-sm">
											<span>{service.name}</span>
											<span class="text-gray-600"
												>{service.duration_minutes} min • £{service.price}</span
											>
										</div>
									{/each}
									<div class="border-t pt-2">
										{#if discountPreview?.eligible}
											{#each discountPreview.discounts as d (d.name)}
												<div class="flex justify-between text-sm text-gray-600">
													<span>{d.name}</span>
													<span>-£{d.amount.toFixed(2)}</span>
												</div>
											{/each}
											<div class="flex justify-between font-semibold text-emerald-700">
												<span>Estimated Total After Discount</span>
												<span>£{discountPreview.discounted_total.toFixed(2)}</span>
											</div>
										{:else}
											<div class="flex justify-between font-semibold">
												<span>Total (estimated)</span>
												<span
													>£{getTotalPrice()}{#if vatRegistered}
														<span class="text-xs font-normal text-gray-400">incl. VAT</span
														>{/if}</span
												>
											</div>
										{/if}
									</div>
								</div>
							</div>

							{#if isRequested}
								<div class="rounded-lg border border-amber-200 bg-amber-50 p-4">
									<p class="text-sm text-amber-800">
										<strong>Please note:</strong> Because you included special requests, the cost
										and duration shown are estimates. We may adjust these after reviewing your
										requirements. You'll receive
										{authStore.isAuthenticated ? ' a notification' : ' an email'} once your booking is
										approved.
									</p>
								</div>
							{/if}

							{#if authStore.isAuthenticated && authStore.currentUser?.role !== 'admin' && authStore.currentUser?.role !== 'guest'}
								{#if depositPaid}
									<div class="rounded-lg border border-emerald-200 bg-emerald-50 p-6 text-center">
										<div
											class="mb-2 inline-flex h-10 w-10 items-center justify-center rounded-full bg-emerald-100"
										>
											<svg class="h-5 w-5 text-emerald-600" viewBox="0 0 20 20" fill="currentColor"
												><path
													fill-rule="evenodd"
													d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
													clip-rule="evenodd"
												/></svg
											>
										</div>
										<h3 class="text-lg font-semibold text-emerald-800">Deposit Paid</h3>
										<p class="mt-1 text-emerald-700">
											Your deposit of <strong>£{calculateDepositAmount().toFixed(2)}</strong> has been
											paid successfully. See you at your appointment!
										</p>
									</div>
								{:else if depositRequired}
									<div class="rounded-lg border border-amber-200 bg-amber-50 p-6 text-center">
										<h3 class="mb-2 text-lg font-semibold text-amber-800">Deposit Not Paid</h3>
										<p class="mb-4 text-amber-700">
											Your booking is confirmed but the deposit of <strong
												>£{calculateDepositAmount().toFixed(2)}</strong
											>
											was not paid. If the deposit remains unpaid within 24 hours of your appointment,
											the slot may be released and the booking could be cancelled or rebooked by someone
											else.
										</p>
										<p class="mb-4 text-xs text-amber-600">
											<PolicyPopover>
												{#snippet trigger()}
													<span class="underline">Read our cancellation policy →</span>
												{/snippet}
											</PolicyPopover>
										</p>
										<Button
											onclick={() => (showPayEarlyModal = true)}
											class="bg-amber-600 text-white hover:bg-amber-700"
										>
											Pay Deposit Now
										</Button>
									</div>
								{:else}
									<div class="rounded-lg border border-gray-200 bg-gray-50 p-6 text-center">
										<h3 class="mb-2 text-lg font-semibold">Pay on the Day</h3>
										<p class="mb-4 text-gray-600">
											You can pay when you arrive, or pay ahead of time to speed things up.
										</p>
										<Button
											onclick={() => (showPayEarlyModal = true)}
											class="bg-emerald-600 text-white hover:bg-emerald-700"
										>
											Pay Early
										</Button>
									</div>
								{/if}
							{/if}
						{/if}
					</Card.Content>
					<Card.Footer class="flex justify-center">
						<Button
							onclick={() => (window.location.href = authStore.isAuthenticated ? '/schedule' : '/')}
							class="w-full"
						>
							{authStore.isAuthenticated ? 'View My Bookings' : 'Return Home'}
						</Button>
					</Card.Footer>
				</Card.Root>
			{:else if depositRequired}
				<Card.Root>
					<Card.Header>
						<Card.Title>Pay Your Deposit</Card.Title>
						<Card.Description>
							A deposit of <span class="font-semibold">£{calculateDepositAmount()}</span> is required
							to secure your appointment.
						</Card.Description>
					</Card.Header>
					<Card.Content class="space-y-6">
						<BookingSummary
							services={selectedServices}
							date={selectedDate}
							time={selectedTime}
							customer={authStore.isAuthenticated
								? {
										firstName: authStore.currentUser?.firstName ?? '',
										lastName: authStore.currentUser?.lastName ?? '',
										email: authStore.currentUser?.email ?? '',
										phone: authStore.currentUser?.phone ?? '',
										specialRequests: customerInfo.specialRequests
									}
								: customerInfo}
							showCustomer={true}
						/>

						<div class="rounded-lg border border-gray-200 bg-white p-6">
							<h3 class="mb-4 text-xl font-semibold">Pay Deposit</h3>

							{#if authStore.isAuthenticated}
								{#if paymentMethodsLoading}
									<div class="py-4 text-center text-gray-500">Loading payment methods...</div>
								{:else if paymentMethods.length > 0}
									<div class="mb-6">
										<h4 class="mb-3 text-sm font-medium text-gray-700">Saved Cards</h4>
										<div class="space-y-3">
											{#each paymentMethods as method (method.id)}
												<div
													class="flex items-center justify-between rounded-lg border border-gray-200 p-3 {selectedPaymentMethod ===
													method.id
														? 'border-primary bg-primary/5'
														: ''}"
												>
													<div class="flex items-center gap-3">
														<div
															class="flex h-10 w-14 items-center justify-center rounded bg-gray-100 text-xs font-medium"
														>
															{method.brand}
														</div>
														<div class="text-sm">
															<span class="font-mono">**** {method.last4}</span>
															<span class="ml-2 text-gray-500">
																{formatCardExpiry(method.expiry_month, method.expiry_year)}
															</span>
														</div>
													</div>
													<Button
														size="sm"
														variant={selectedPaymentMethod === method.id ? 'default' : 'outline'}
														onclick={() => {
															selectedPaymentMethod = method.id;
															showNewCardForm = false;
														}}
													>
														{selectedPaymentMethod === method.id ? 'Selected' : 'Use this card'}
													</Button>
												</div>
											{/each}
										</div>
									</div>
								{/if}

								{#if !showNewCardForm}
									<Button
										variant="outline"
										class="mb-6"
										onclick={() => {
											showNewCardForm = true;
											selectedPaymentMethod = null;
										}}
									>
										+ Add new card
									</Button>
								{/if}
							{/if}

							{#if showNewCardForm || !authStore.isAuthenticated}
								<CardInput
									bind:cardNumber={newCardNumber}
									bind:cardExpiry={newCardExpiry}
									bind:cardCVC={newCardCVC}
									disabled={isProcessingPayment}
								/>
							{/if}

							<div class="flex items-center justify-between border-t pt-4">
								<Button variant="ghost" onclick={prevStep}>Back</Button>
								<Button
									disabled={isProcessingPayment || !depositCardFormValid}
									onclick={() => processPayment(calculateDepositAmount())}
									class="bg-primary text-primary-foreground"
								>
									{isProcessingPayment
										? 'Processing...'
										: `Pay Deposit £${calculateDepositAmount()}`}
								</Button>
							</div>
						</div>
					</Card.Content>
				</Card.Root>
			{/if}
		{/if}

		{#if showPayEarlyModal && confirmedBooking}
			{@const bk = confirmedBooking}
			<UserPaymentModal
				booking={{
					id: bk.id,
					status: bk.status as BookingStatus,
					start_time: bk.start_time,
					notes: bk.notes,
					services: selectedServices.map((s) => ({
						booking_id: bk.id,
						service_id: s.id,
						service_name: s.name,
						price: s.price,
						duration_minutes: s.duration_minutes
					})) as BookingService[],
					total_amount: bk.total_amount,
					amount_paid: bk.amount_paid,
					amount_due: bk.amount_due,
					deposit_required: bk.deposit_required,
					deposit_paid: bk.deposit_paid,
					payments: bk.payments,
					duration_minutes: bk.duration_minutes,
					created_at: new SvelteDate().toISOString(),
					updated_at: new SvelteDate().toISOString()
				}}
				onClose={() => (showPayEarlyModal = false)}
				onComplete={() => {
					showPayEarlyModal = false;
				}}
				canSaveCards={authStore.isAuthenticated}
			/>
		{/if}
	{/if}
</div>
