<script lang="ts">
	import { apiFetch } from '$lib/utils/api';
	import { SvelteDate, SvelteMap } from 'svelte/reactivity';
	import { CalendarDate, getLocalTimeZone, type DateValue } from '@internationalized/date';
	import { toast } from 'svelte-sonner';
	import * as Modal from '$lib/components/ui/dialog';
	import { Button } from '$lib/components/ui/button';
	import * as Textarea from '$lib/components/ui/textarea';
	import * as Label from '$lib/components/ui/label';
	import DatePicker from '$lib/components/booking/DatePicker.svelte';
	import PolicyPopover from '$lib/components/ui/policyPopover.svelte';
	import type { Booking, Service, WorkingHoursDay, AvailableHoursDay } from '$lib/types/booking';
	import {
		extractBookedSlots,
		getLunchProtectionForSlots,
		timeToMinutes
	} from '$lib/lunchProtection';
	import { formatLocalDateTime, getLondonTodayCalendarDate } from '$lib/utils/timeSlots';
	import ClockIcon from '@lucide/svelte/icons/clock';
	import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw';
	import ArrowLeftIcon from '@lucide/svelte/icons/arrow-left';
	import ArrowRightIcon from '@lucide/svelte/icons/arrow-right';
	import CharCounter from '$lib/components/ui/CharCounter.svelte';

	interface Props {
		open: boolean;
		booking: Booking;
		onSubmitted: () => void;
	}

	let { open = $bindable(), booking, onSubmitted }: Props = $props();

	// ─── Mode ───────────────────────────────────────────────
	type EditMode = 'select' | 'time' | 'services' | 'both-services' | 'both-time';
	let editMode = $state<EditMode>('select');

	// ─── Service selection ──────────────────────────────────
	let selectedServices = $state<Service[]>([]);
	let availableServices = $state<Service[]>([]);
	let loadingServices = $state(false);

	// ─── Time selection ─────────────────────────────────────
	let newDate = $state<CalendarDate | undefined>(undefined);
	let newTime = $state('');
	let notes = $state('');
	let originalNotes = $state('');
	let submitting = $state(false);

	// ─── Working / available hours with caching ─────────────
	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 loadingHours = $state(false);

	const workingHoursCache = new SvelteMap<
		string,
		Record<string, { isOpen: boolean; startTime: string; endTime: string }>
	>();
	const availableHoursCache = new SvelteMap<
		string,
		Record<string, { isOpen: boolean; slots: Array<{ startTime: string; endTime: string }> }>
	>();
	let userNavigatedCalendar = $state(false);
	let editRequestAutoSelectDone = $state(false);
	// ─── Date constants ─────────────────────────────────────
	const todayCalendarDate = getLondonTodayCalendarDate();
	const minDate = todayCalendarDate;
	const maxDate = new SvelteDate(
		todayCalendarDate.year,
		todayCalendarDate.month - 1,
		todayCalendarDate.day
	);
	maxDate.setMonth(todayCalendarDate.month - 1 + 6);
	const maxCalendarDate = new CalendarDate(
		maxDate.getFullYear(),
		maxDate.getMonth() + 1,
		maxDate.getDate()
	);
	let placeholderDate = $state<CalendarDate>(minDate);

	const hoursUntilAppointment = $derived(
		(new SvelteDate(booking.start_time).getTime() - new SvelteDate().getTime()) / (1000 * 60 * 60)
	);
	const hasPayments = $derived((booking.amount_paid ?? 0) > 0);
	const noticePeriodBlocked = $derived(
		hasPayments ? hoursUntilAppointment < 72 : hoursUntilAppointment < 24
	);
	const noticePeriodWarning = $derived(
		!hasPayments && hoursUntilAppointment < 24 && hoursUntilAppointment >= 0
	);
	const noticeBlockedMessage = $derived(
		hasPayments
			? 'This booking has payments and is too close to the original appointment to reschedule online.'
			: 'This booking is too close to the original appointment time to reschedule online.'
	);

	const discountTotal = $derived(
		booking.discounts?.reduce((sum, d) => sum + d.discount_amount, 0) ?? 0
	);

	const bookingTotalDuration = $derived(
		booking.services?.reduce(
			(sum, s) => sum + (s.override_duration_minutes ?? s.duration_minutes ?? 0),
			0
		) || 0
	);

	const selectedServicesDuration = $derived(
		selectedServices.reduce((sum, s) => sum + (s.duration_minutes ?? 0), 0)
	);

	const hasOverrides = $derived(
		booking?.services?.some(
			(s) => s.override_price != null || s.override_duration_minutes != null
		) ?? false
	);

	const slotDuration = $derived(
		editMode === 'time' ? bookingTotalDuration : selectedServicesDuration
	);

	const canSubmit = $derived(
		submitting === false &&
			((editMode === 'time' && !!newDate && newTime.length >= 4) ||
				(editMode === 'services' && selectedServices.length > 0) ||
				(editMode === 'both-time' && !!newDate && newTime.length >= 4))
	);

	const notesChanged = $derived(notes.trim() !== originalNotes.trim());

	const modalTitle = $derived(() => {
		switch (editMode) {
			case 'select':
				return 'Edit/Reschedule';
			case 'time':
				return 'Change Time';
			case 'services':
				return 'Change Services';
			case 'both-services':
				return 'Change Services';
			case 'both-time':
				return 'Choose Time';
		}
	});

	const lunchProtection = $derived(() => {
		if (!newDate || !workingHours || !availableHours || slotDuration === 0) {
			return new Map();
		}

		const dateStr = newDate.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,
			slotDuration,
			15,
			false
		);
	});

	// ─── Helpers ────────────────────────────────────────────
	function formatTime(time: string): string {
		const parts = time.split(':').map(Number);
		const hours = parts[0];
		const minutes = parts.length > 1 ? parts[1] : 0;
		if (hours === 12 && minutes === 0) return 'Noon';
		if (hours === 0 && minutes === 0) return 'Midnight';
		const period = hours >= 12 ? 'PM' : 'AM';
		const displayHours = hours % 12 || 12;
		return `${displayHours}:${minutes.toString().padStart(2, '0')} ${period}`;
	}

	function calculateEndTime(startTime: string, durationMinutes: number): string {
		const [hours, minutes] = startTime.split(':').map(Number);
		const total = hours * 60 + minutes + durationMinutes;
		const h = Math.floor(total / 60);
		const m = total % 60;
		return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
	}

	function calculatePreviousTime(time: string): string {
		const [h, m] = time.split(':').map(Number);
		const total = h * 60 + m - 15;
		return `${String(Math.floor(total / 60)).padStart(2, '0')}:${String(total % 60).padStart(2, '0')}`;
	}

	function generateAvailableTimeSlots(duration: number, date: CalendarDate): string[] {
		if (!workingHours || !availableHours) return [];
		const dateStr = date.toString();
		const dayWH = workingHours[dateStr];
		const dayAH = availableHours[dateStr];
		if (!dayWH || !dayWH.isOpen || !dayAH || !dayAH.slots) return [];

		const slots: string[] = [];
		const todayCal = getLondonTodayCalendarDate();
		const isToday = date.compare(todayCal) === 0;

		for (const slot of dayAH.slots) {
			const [sh, sm] = slot.startTime.split(':').map(Number);
			const [eh, em] = slot.endTime.split(':').map(Number);
			// Round up to next 15-min boundary so generated times align with
			// the 15-min grid from working hours start used in generateGroupedTimeSlots
			let startMin = Math.ceil((sh * 60 + sm) / 15) * 15;
			const endMin = eh * 60 + em;

			if (isToday) {
				const now = new Date();
				const londonTime = now.toLocaleTimeString('en-GB', {
					timeZone: 'Europe/London',
					hour: '2-digit',
					minute: '2-digit',
					hour12: false
				});
				const [londonHours, londonMinutes] = londonTime.split(':').map(Number);
				const currentMin = londonHours * 60 + londonMinutes;
				startMin = Math.max(startMin, Math.ceil((currentMin + 60) / 15) * 15);
			}

			for (let m = startMin; m < endMin; m += 15) {
				if (m + duration <= endMin) {
					slots.push(
						`${String(Math.floor(m / 60)).padStart(2, '0')}:${String(m % 60).padStart(2, '0')}`
					);
				}
			}
		}
		return slots;
	}

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

		const grouped: Array<{
			type: 'available' | 'unavailable';
			startTime: string;
			endTime: string;
			isGrouped?: boolean;
		}> = [];
		const [sh, sm] = dayWH.startTime.split(':').map(Number);
		const [eh, em] = dayWH.endTime.split(':').map(Number);
		let startMin = sh * 60 + sm;
		const endMin = eh * 60 + em;

		const todayCal = getLondonTodayCalendarDate();
		const isToday = date.compare(todayCal) === 0;
		if (isToday) {
			const now = new Date();
			const londonTime = now.toLocaleTimeString('en-GB', {
				timeZone: 'Europe/London',
				hour: '2-digit',
				minute: '2-digit',
				hour12: false
			});
			const [londonHours, londonMinutes] = londonTime.split(':').map(Number);
			const currentMin = londonHours * 60 + londonMinutes;
			startMin = Math.max(startMin, Math.ceil((currentMin + 60) / 15) * 15);
		}

		const availableSlots = generateAvailableTimeSlots(duration, date);
		let currentUnavailableStart: string | null = null;
		let lastAvailableEnd: string | null = null;

		for (let m = startMin; m < endMin; m += 15) {
			const timeStr = `${String(Math.floor(m / 60)).padStart(2, '0')}:${String(m % 60).padStart(2, '0')}`;
			const isAvailable =
				availableSlots.includes(timeStr) && !lunchProtectionMap.get(timeStr)?.isBlocked;

			if (isAvailable) {
				if (currentUnavailableStart !== null) {
					const groupEnd = calculatePreviousTime(timeStr);
					const unavailableStartTime = lastAvailableEnd || currentUnavailableStart;
					if (
						unavailableStartTime &&
						timeToMinutes(unavailableStartTime) < timeToMinutes(groupEnd)
					) {
						grouped.push({
							type: 'unavailable',
							startTime: unavailableStartTime,
							endTime: groupEnd,
							isGrouped: true
						});
					}
					currentUnavailableStart = null;
				}
				const slotEnd = calculateEndTime(timeStr, duration);
				lastAvailableEnd = slotEnd;
				grouped.push({ type: 'available', startTime: timeStr, endTime: slotEnd });
			} else {
				if (currentUnavailableStart === null) {
					currentUnavailableStart = timeStr;
				}
			}
		}

		if (currentUnavailableStart !== null) {
			const lastAvail = grouped.filter((s) => s.type === 'available').pop();
			const lastAvailEnd = lastAvail ? timeToMinutes(lastAvail.endTime) : 0;
			const unavailableStartMinutes = timeToMinutes(currentUnavailableStart);
			if (unavailableStartMinutes < endMin && lastAvailEnd < endMin) {
				grouped.push({
					type: 'unavailable',
					startTime: lastAvail ? lastAvail.endTime : currentUnavailableStart,
					endTime: dayWH.endTime,
					isGrouped: true
				});
			}
		}

		return grouped;
	}

	function isDateUnavailable(date: DateValue): boolean {
		const d = date as CalendarDate;
		if (d.compare(minDate) < 0 || d.compare(maxCalendarDate) > 0) return true;
		if (!workingHours) return true;
		const dateStr = d.toString();
		const dayHours = workingHours[dateStr];
		if (!dayHours || !dayHours.isOpen) return true;
		if (slotDuration === 0) return false;
		const slots = generateAvailableTimeSlots(slotDuration, d);
		if (slots.length === 0) return true;

		const dayAH = availableHours?.[dateStr];
		if (dayAH?.slots) {
			const existingBookings = extractBookedSlots(
				dayHours.startTime,
				dayHours.endTime,
				dayAH.slots
			);
			const lunchProtection = getLunchProtectionForSlots(
				dayHours.startTime,
				dayHours.endTime,
				existingBookings,
				slotDuration,
				15,
				false
			);
			const validSlots = slots.filter((t) => !lunchProtection.get(t)?.isBlocked);
			if (validSlots.length === 0) return true;
		}

		return false;
	}

	function initializeServices(): void {
		selectedServices = (booking.services || []).map((s) => ({
			id: s.service_id,
			name: s.service_name || '',
			description: s.service_description || '',
			price: s.override_price ?? s.price ?? 0,
			duration_minutes: s.override_duration_minutes ?? s.duration_minutes ?? 0,
			patch_test_duration_hours: 0,
			minimum_age_required: 0
		}));
	}

	function initializeHours(): void {
		workingHours = null;
		availableHours = null;
		loadingHours = false;
		workingHoursCache.clear();
		availableHoursCache.clear();
		userNavigatedCalendar = false;
	}

	$effect(() => {
		if (open) {
			initializeServices();
			editMode = 'select';
			newDate = undefined;
			newTime = '';
			originalNotes = booking.notes || '';
			notes = originalNotes;
			submitting = false;
			initializeHours();
			editRequestAutoSelectDone = false;
			placeholderDate = minDate;
			fetchHoursRange(minDate, 3);
		}
	});

	$effect(() => {
		if (editMode === 'services' || editMode === 'both-services') {
			fetchAvailableServices();
		}
	});

	let servicesHoursFetched = $state(false);
	$effect(() => {
		if (editMode === 'services' && !servicesHoursFetched) {
			servicesHoursFetched = true;
			const bookingDate = new SvelteDate(booking.start_time);
			const calDate = new CalendarDate(
				bookingDate.getFullYear(),
				bookingDate.getMonth() + 1,
				bookingDate.getDate()
			);
			fetchHoursForMonth(calDate);
		}
		if (editMode !== 'services') {
			servicesHoursFetched = false;
		}
	});

	// ─── Auto-select ────────────────────────────────────────
	$effect(() => {
		if (
			!workingHours ||
			!availableHours ||
			newDate ||
			userNavigatedCalendar ||
			editRequestAutoSelectDone
		)
			return;
		editRequestAutoSelectDone = 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 = 0; 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)) {
					newDate = calDate;
					if (!userNavigatedCalendar) {
						placeholderDate = new CalendarDate(nextDate.getFullYear(), nextDate.getMonth() + 1, 1);
					}
					return;
				}
			}
		}

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

	// ─── API calls ──────────────────────────────────────────
	async function fetchHoursRange(startDate: CalendarDate, months: number) {
		loadingHours = true;
		try {
			// 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')}`;

			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) {
				const whData: WorkingHoursDay[] = await whRes.json();
				const ahData: 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.set(key, whMap);
					availableHoursCache.set(key, ahMap);
				}

				// MERGE instead of replace — preserves data from previously loaded months
				workingHours = { ...(workingHours || {}), ...whMap };
				availableHours = { ...(availableHours || {}), ...ahMap };
			}
		} catch (err) {
			console.error('Failed to fetch hours:', err);
		} finally {
			loadingHours = false;
		}
	}

	async function fetchHoursForMonth(date: CalendarDate) {
		const monthKey = `${date.year}-${String(date.month).padStart(2, '0')}`;
		if (workingHoursCache.has(monthKey) && availableHoursCache.has(monthKey)) {
			const cachedWH = workingHoursCache.get(monthKey)!;
			const cachedAH = availableHoursCache.get(monthKey)!;
			// MERGE instead of replace — preserves data from other loaded months
			workingHours = { ...(workingHours || {}), ...cachedWH };
			availableHours = { ...(availableHours || {}), ...cachedAH };
			return;
		}

		loadingHours = 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();

			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) {
				const whData: WorkingHoursDay[] = await whRes.json();
				const ahData: 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 };
				});

				workingHoursCache.set(monthKey, whMap);
				availableHoursCache.set(monthKey, ahMap);
				// MERGE instead of replace — preserves data from other loaded months
				workingHours = { ...(workingHours || {}), ...whMap };
				availableHours = { ...(availableHours || {}), ...ahMap };
			}
		} catch (err) {
			console.error('Failed to fetch hours:', err);
		} finally {
			loadingHours = false;
		}
	}

	async function fetchAvailableServices() {
		loadingServices = true;
		try {
			const response = await apiFetch('/api/services');
			if (response.ok) {
				availableServices = (await response.json()) as Service[];
			}
		} catch (err) {
			console.error('Failed to fetch services:', err);
		} finally {
			loadingServices = false;
		}
	}

	function calculateRemainingTime(): number {
		if (!workingHours || !availableHours) return 0;

		const bookingDate = new SvelteDate(booking.start_time);
		const dateStr = bookingDate.toLocaleDateString('en-CA', { timeZone: 'Europe/London' });

		const dayWH = workingHours[dateStr];
		const dayAH = availableHours[dateStr];

		if (!dayWH?.isOpen || !dayAH?.slots) return 0;

		const [startH, startM] = [bookingDate.getHours(), bookingDate.getMinutes()];
		const bookingEndMinutes = startH * 60 + startM + selectedServicesDuration;
		const workingEndMinutes = timeToMinutes(dayWH.endTime);

		// Find next booking after current booking's end
		const existingBookings = extractBookedSlots(dayWH.startTime, dayWH.endTime, dayAH.slots);
		let nextBookingStart = workingEndMinutes;
		for (const eb of existingBookings) {
			const ebStart = timeToMinutes(eb.startTime);
			if (ebStart >= bookingEndMinutes && ebStart < nextBookingStart) {
				nextBookingStart = ebStart;
			}
		}

		return Math.max(0, Math.min(nextBookingStart, workingEndMinutes) - bookingEndMinutes);
	}

	const availableAdditionalServices = $derived(() => {
		if (editMode !== 'services') return [];
		const remaining = calculateRemainingTime();
		if (remaining <= 0) return [];
		return availableServices.filter(
			(avail) =>
				!selectedServices.some((selected) => selected.id === avail.id) &&
				avail.duration_minutes <= remaining
		);
	});

	async function submitEdit() {
		if (!canSubmit) return;

		submitting = true;
		try {
			// Re-check available hours to handle race conditions
			if (newDate && newTime) {
				const dateStr = newDate.toString();
				await fetchHoursForMonth(newDate);
				const dayAvailable = availableHours?.[dateStr]?.slots;
				if (!dayAvailable || dayAvailable.length === 0) {
					toast.error('This time slot is no longer available. Please choose a different time.');
					submitting = false;
					return;
				}

				const duration = slotDuration;
				const [selHour, selMinute] = newTime.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('This slot was just taken. Please choose a different time.');
					submitting = false;
					return;
				}
			}

			const body: Record<string, unknown> = {};

			if (editMode === 'time' || editMode === 'both-time') {
				const [hours, minutes] = newTime.split(':').map(Number);
				const bookingDate = newDate!.toDate(getLocalTimeZone());
				bookingDate.setHours(hours || 0, minutes || 0, 0, 0);
				body.new_start_time = formatLocalDateTime(bookingDate);
			}

			if (editMode === 'services' || editMode === 'both-time') {
				body.new_services = selectedServices.map((s) => s.id);
			}

			if (notes.trim()) {
				body.notes = notes.trim();
			}

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

			if (response.ok) {
				const noShowWarning = response.headers.get('X-No-Show-Warning');
				if (noShowWarning) {
					toast.warning(noShowWarning, { duration: 8000 });
				}
				toast.success("Edit/reschedule request sent — we'll confirm shortly");
				open = false;
				onSubmitted();
			} else {
				const text = await response.text();
				toast.error(text || 'Failed to submit edit/reschedule request');
			}
		} catch {
			toast.error('Network error');
		} finally {
			submitting = false;
		}
	}

	function selectTime(time: string) {
		newTime = time;
	}

	function goBack() {
		if (editMode === 'both-time') {
			editMode = 'both-services';
		} else {
			editMode = 'select';
		}
	}

	function selectMode(mode: EditMode) {
		newDate = undefined;
		newTime = '';
		editMode = mode;
		if (mode === 'time' || mode === 'both-time') {
			// Fetch hours for the current month + 2 more when entering time selection
			fetchHoursRange(minDate, 3);
		}
	}
</script>

<Modal.Root bind:open>
	<Modal.Content
		class="!z-[70] max-h-[90vh] max-w-[calc(100%-2rem)] overflow-y-auto sm:max-w-md md:max-w-3xl"
	>
		<Modal.Header>
			<div class="flex items-center justify-between">
				<Modal.Title class="text-lg font-semibold">{modalTitle()}</Modal.Title>
			</div>
		</Modal.Header>

		<div class="space-y-4 px-4 pb-4">
			{#if editMode === 'select'}
				<!-- ─── Mode Selection ────────────────────────── -->
				<div class="flex flex-col gap-3">
					<p class="text-sm text-gray-600">What would you like to change?</p>

					{#if noticePeriodBlocked}
						<div class="rounded-md border border-amber-200 bg-amber-50 p-3 text-sm text-amber-800">
							<p class="font-medium text-amber-900">Cannot Reschedule Online</p>
							<p class="mt-1">{noticeBlockedMessage}</p>
							<p class="mt-1">
								<a href="/contact" target="_blank" rel="noopener noreferrer external" class="underline">Contact us</a>
								to discuss options, or
								<button
									type="button"
									onclick={() => (open = false)}
									class="inline cursor-pointer underline">cancel this booking</button
								>
								and rebook — note that cancellation fees may apply based on our
								<PolicyPopover>
									{#snippet trigger()}
										<span class="underline">deposit policy</span>
									{/snippet}
								</PolicyPopover>.
							</p>
						</div>
					{:else if noticePeriodWarning}
						<div class="rounded-md border border-amber-200 bg-amber-50 p-3 text-sm text-amber-800">
							<p class="font-medium text-amber-900">Rescheduling Within 24h</p>
							<p class="mt-1">
								Rescheduling within 24h counts as a no-show towards your deposit obligations. Two
								no-shows within 6 months will require deposits on future bookings.
								<PolicyPopover>
									{#snippet trigger()}
										<span class="underline">Full policy</span>
									{/snippet}
								</PolicyPopover>
							</p>
						</div>
					{/if}

					{#if booking.discounts && booking.discounts.length > 0}
						<div class="rounded-md border border-amber-200 bg-amber-50 p-3 text-sm text-amber-800">
							<p class="font-medium text-amber-900">Discounts Applied to This Booking</p>
							<p class="mt-1">
								Your booking has £{discountTotal.toFixed(2)} in savings from loyalty stamps or promotional
								offers. A time change requires admin approval. If denied, you can cancel (standard refund
								policy applies) and rebook at full price.
							</p>
						</div>
					{/if}

					<button
						type="button"
						disabled={noticePeriodBlocked}
						onclick={() => selectMode('time')}
						class="flex w-full items-center gap-4 rounded-lg border border-gray-200 bg-white p-4 text-left transition-colors hover:border-fuchsia-200 hover:bg-fuchsia-50 disabled:cursor-not-allowed disabled:opacity-50"
					>
						<div
							class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-gray-100 text-gray-600"
						>
							<ClockIcon class="size-5" />
						</div>
						<div class="flex-1">
							<div class="font-medium">Change Time</div>
							<div class="text-sm text-gray-500">Pick a new date and time</div>
						</div>
						<ArrowRightIcon class="size-4 text-gray-400" />
					</button>

					<button
						type="button"
						onclick={() => selectMode('services')}
						disabled={hasOverrides}
						class="flex w-full items-center gap-4 rounded-lg border border-gray-200 bg-white p-4 text-left transition-colors hover:border-fuchsia-200 hover:bg-fuchsia-50 disabled:cursor-not-allowed disabled:opacity-50"
						title={hasOverrides
							? 'This booking has custom pricing. To change services, please contact the salon.'
							: ''}
					>
						<div
							class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-gray-100 text-gray-600"
						>
							<RefreshCwIcon class="size-5" />
						</div>
						<div class="flex-1">
							<div class="font-medium">Change Services</div>
							<div class="text-sm text-gray-500">Add or remove services</div>
						</div>
						<ArrowRightIcon class="size-4 text-gray-400" />
					</button>

					<button
						type="button"
						onclick={() => selectMode('both-services')}
						disabled={hasOverrides || noticePeriodBlocked}
						class="flex w-full items-center gap-4 rounded-lg border border-gray-200 bg-white p-4 text-left transition-colors hover:border-fuchsia-200 hover:bg-fuchsia-50 disabled:cursor-not-allowed disabled:opacity-50"
						title={hasOverrides
							? 'This booking has custom pricing. To change services, please contact the salon.'
							: ''}
					>
						<div
							class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-gray-100 text-gray-600"
						>
							<ClockIcon class="size-5" />
							<RefreshCwIcon class="-ml-2 size-5" />
						</div>
						<div class="flex-1">
							<div class="font-medium">Change Both</div>
							<div class="text-sm text-gray-500">New time and services</div>
						</div>
						<ArrowRightIcon class="size-4 text-gray-400" />
					</button>

					{#if hasOverrides}
						<div class="rounded-md border border-amber-200 bg-amber-50 p-3 text-sm text-amber-800">
							This booking has custom pricing. To change services, please contact the salon. You can
							still request a time change.
						</div>
					{/if}
				</div>
			{:else if editMode === 'time' || editMode === 'both-time'}
				<!-- ─── Time Selection ────────────────────────── -->
				{#if loadingHours && !workingHours}
					<div class="flex items-center justify-center p-6">
						<p class="text-sm text-gray-500">Loading available dates...</p>
					</div>
				{:else}
					<div class="flex items-center justify-center">
						<DatePicker
							date={newDate}
							placeholder={placeholderDate}
							minValue={minDate}
							maxValue={maxCalendarDate}
							{isDateUnavailable}
							onchange={(d) => {
								newDate = d;
								newTime = '';
							}}
							onPlaceholderChange={(p) => {
								userNavigatedCalendar = true;
								placeholderDate = p;
								fetchHoursForMonth(p);
							}}
						/>
					</div>
				{/if}

				{#if newDate}
					{#if loadingHours}
						<div class="flex items-center justify-center border-t p-6">
							<p class="text-sm text-gray-500">Loading times...</p>
						</div>
					{:else}
						<div
							class="mt-2 no-scrollbar flex max-h-40 min-h-25 w-full scroll-pb-6 flex-col gap-4 overflow-y-auto border-t pt-4"
						>
							<div class="grid justify-center gap-2 text-sm text-gray-600">
								{newDate.toDate(getLocalTimeZone()).toLocaleDateString('en-GB', {
									weekday: 'long',
									day: 'numeric',
									month: 'short'
								})}
							</div>
							{#if workingHours && !workingHours[newDate.toString()]?.isOpen}
								<p class="text-center text-sm text-gray-500">We're closed on this day</p>
							{:else}
								{@const grouped = generateGroupedTimeSlots(
									slotDuration,
									newDate,
									lunchProtection()
								)}
								{#if grouped.length > 0}
									<div class="grid gap-2">
										{#each grouped as slot (slot.type + '-' + slot.startTime + '-' + slot.endTime)}
											{#if slot.type === 'available'}
												<Button
													variant="outline"
													onclick={() => selectTime(slot.startTime)}
													class="w-full hover:bg-fuchsia-50 {newTime === slot.startTime
														? 'border-fuchsia-200 bg-fuchsia-100'
														: ''}"
												>
													{formatTime(slot.startTime)}
													<span class="text-gray-500">- {formatTime(slot.endTime)}</span>
												</Button>
											{:else}
												<Button
													variant="outline"
													class="w-full cursor-not-allowed opacity-50 hover:bg-gray-100"
													disabled
												>
													{formatTime(slot.startTime)}
													<span class="text-gray-500">- {formatTime(slot.endTime)}</span>
												</Button>
											{/if}
										{/each}
									</div>
								{:else}
									<p class="text-center text-sm text-gray-500">No available slots</p>
								{/if}
							{/if}
						</div>
					{/if}
				{/if}

				<div class="mt-4 space-y-2">
					<Label.Root for="edit-notes">Reason (optional)</Label.Root>
					<Textarea.Root
						id="edit-notes"
						bind:value={notes}
						placeholder="Tell us why you need to make changes"
						rows={2}
					/>
					<CharCounter text={notes} />
				</div>
			{:else if editMode === 'services'}
				<!-- ─── Services Only (with time restriction) ── -->
				{#if hasOverrides}
					<div class="rounded-md border border-amber-200 bg-amber-50 p-3 text-sm text-amber-800">
						This booking has custom pricing. To change services, please contact the salon.
					</div>
				{:else if loadingServices}
					<div class="flex items-center justify-center p-6">
						<p class="text-sm text-gray-500">Loading services...</p>
					</div>
				{:else}
					{@const remaining = calculateRemainingTime()}

					<div class="space-y-3">
						<div>
							<h4 class="mb-2 text-sm font-semibold tracking-wide text-gray-600 uppercase">
								Current Services
								{#if selectedServices.length > 0}
									<span class="ml-1 text-xs font-normal text-gray-400"> (tap to remove) </span>
								{/if}
							</h4>
							{#if selectedServices.length === 0}
								<p class="text-sm text-gray-500">No services selected</p>
							{:else}
								<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
									{#each selectedServices as service (service.id)}
										<button
											type="button"
											onclick={() => {
												selectedServices = selectedServices.filter((s) => s.id !== service.id);
											}}
											class="cursor-pointer rounded-lg border border-input bg-fuchsia-100 p-4 text-left transition-colors hover:bg-fuchsia-50"
										>
											<div class="flex h-full min-h-24 flex-col justify-between">
												<div>
													<h3 class="font-semibold">{service.name}</h3>
													<p class="text-sm text-gray-600">{service.description || ''}</p>
												</div>
												<div class="mt-2 flex items-center justify-between text-sm text-gray-500">
													<span>{service.duration_minutes} mins</span>
													<span class="font-semibold text-foreground"
														>£{service.price.toFixed(2)}</span
													>
												</div>
											</div>
										</button>
									{/each}
								</div>
							{/if}
						</div>

						{#if remaining > 0}
							{@const fittingServices = availableAdditionalServices()}
							{#if fittingServices.length > 0}
								<div>
									<h4 class="mb-2 text-sm font-semibold tracking-wide text-gray-600 uppercase">
										Add Services
										<span class="ml-1 text-xs font-normal text-gray-400">
											({remaining} min remaining)
										</span>
									</h4>
									<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
										{#each fittingServices as service (service.id)}
											<button
												type="button"
												onclick={() => {
													selectedServices = [...selectedServices, service];
												}}
												class="cursor-pointer rounded-lg border border-input bg-background p-4 text-left transition-colors hover:bg-fuchsia-50"
											>
												<div class="flex h-full min-h-24 flex-col justify-between">
													<div>
														<h3 class="font-semibold">{service.name}</h3>
														<p class="text-sm text-gray-600">{service.description || ''}</p>
													</div>
													<div class="mt-2 flex items-center justify-between text-sm text-gray-500">
														<span>{service.duration_minutes} mins</span>
														<span class="font-semibold text-foreground"
															>£{service.price.toFixed(2)}</span
														>
													</div>
												</div>
											</button>
										{/each}
									</div>
								</div>
							{:else}
								<div class="rounded-md border border-gray-200 bg-gray-50 p-3 text-sm text-gray-500">
									No additional services can fit in the remaining time.
								</div>
							{/if}
						{:else}
							<div
								class="rounded-md border border-amber-200 bg-amber-50 p-3 text-sm text-amber-800"
							>
								No remaining time available. Remove a service to free up time for additions.
							</div>
						{/if}

						{#if selectedServices.length === 0}
							<p class="text-xs text-amber-600">Select at least one service to continue.</p>
						{/if}
					</div>
				{/if}

				<div class="mt-4 space-y-2">
					<div class="flex items-center justify-between">
						<Label.Root for="edit-notes-services">Special Requests</Label.Root>
						{#if notesChanged}
							<span class="text-xs text-amber-600">changed</span>
						{/if}
					</div>
					{#if originalNotes}
						<div class="rounded-md bg-gray-50 p-2 text-xs text-gray-500">
							<span class="font-medium">Original:</span>
							{originalNotes}
						</div>
					{/if}
					<Textarea.Root
						id="edit-notes-services"
						bind:value={notes}
						placeholder="Any special requests or notes for your appointment"
						rows={2}
					/>
					<CharCounter text={notes} />
				</div>
			{:else if editMode === 'both-services'}
				<!-- ─── Both Step 1: Service Selection (no time restriction) ── -->
				{#if hasOverrides}
					<div class="rounded-md border border-amber-200 bg-amber-50 p-3 text-sm text-amber-800">
						This booking has custom pricing. To change services, please contact the salon.
					</div>
				{:else if loadingServices}
					<div class="flex items-center justify-center p-6">
						<p class="text-sm text-gray-500">Loading services...</p>
					</div>
				{:else}
					{@const unselected = availableServices.filter(
						(avail) => !selectedServices.some((selected) => selected.id === avail.id)
					)}

					<div class="space-y-3">
						<div>
							<h4 class="mb-2 text-sm font-semibold tracking-wide text-gray-600 uppercase">
								Selected Services
								{#if selectedServices.length > 0}
									<span class="ml-1 text-xs font-normal text-gray-400"> (tap to remove) </span>
								{/if}
							</h4>
							{#if selectedServices.length === 0}
								<p class="text-sm text-gray-500">No services selected</p>
							{:else}
								<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
									{#each selectedServices as service (service.id)}
										<button
											type="button"
											onclick={() => {
												selectedServices = selectedServices.filter((s) => s.id !== service.id);
											}}
											class="cursor-pointer rounded-lg border border-input bg-fuchsia-100 p-4 text-left transition-colors hover:bg-fuchsia-50"
										>
											<div class="flex h-full min-h-24 flex-col justify-between">
												<div>
													<h3 class="font-semibold">{service.name}</h3>
													<p class="text-sm text-gray-600">{service.description || ''}</p>
												</div>
												<div class="mt-2 flex items-center justify-between text-sm text-gray-500">
													<span>{service.duration_minutes} mins</span>
													<span class="font-semibold text-foreground"
														>£{service.price.toFixed(2)}</span
													>
												</div>
											</div>
										</button>
									{/each}
								</div>
							{/if}
						</div>

						{#if unselected.length > 0}
							<div>
								<h4 class="mb-2 text-sm font-semibold tracking-wide text-gray-600 uppercase">
									Add Services
								</h4>
								<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
									{#each unselected as service (service.id)}
										<button
											type="button"
											onclick={() => {
												selectedServices = [...selectedServices, service];
											}}
											class="cursor-pointer rounded-lg border border-input bg-background p-4 text-left transition-colors hover:bg-fuchsia-50"
										>
											<div class="flex h-full min-h-24 flex-col justify-between">
												<div>
													<h3 class="font-semibold">{service.name}</h3>
													<p class="text-sm text-gray-600">{service.description || ''}</p>
												</div>
												<div class="mt-2 flex items-center justify-between text-sm text-gray-500">
													<span>{service.duration_minutes} mins</span>
													<span class="font-semibold text-foreground"
														>£{service.price.toFixed(2)}</span
													>
												</div>
											</div>
										</button>
									{/each}
								</div>
							</div>
						{/if}

						{#if selectedServices.length === 0}
							<p class="text-xs text-amber-600">Select at least one service to continue.</p>
						{/if}
					</div>

					<div class="mt-4 space-y-2">
						<div class="flex items-center justify-between">
							<Label.Root for="edit-notes-both-services">Special Requests</Label.Root>
							{#if notesChanged}
								<span class="text-xs text-amber-600">changed</span>
							{/if}
						</div>
						{#if originalNotes}
							<div class="rounded-md bg-gray-50 p-2 text-xs text-gray-500">
								<span class="font-medium">Original:</span>
								{originalNotes}
							</div>
						{/if}
						<Textarea.Root
							id="edit-notes-both-services"
							bind:value={notes}
							placeholder="Any special requests or notes for your appointment"
							rows={2}
						/>
						<CharCounter text={notes} />
					</div>
				{/if}
			{/if}
		</div>

		<!-- ─── Footer ─────────────────────────────────────── -->
		<div class="flex flex-col gap-2 border-t px-4 py-3">
			<div class="flex gap-2">
				{#if editMode === 'select'}
					<Button variant="ghost" size="sm" class="flex-1" onclick={() => (open = false)}>
						Cancel
					</Button>
				{:else if editMode === 'time' || editMode === 'services'}
					<Button variant="outline" size="sm" onclick={goBack}>
						<ArrowLeftIcon class="size-4" />
						Back
					</Button>
					<Button
						variant="outline"
						size="sm"
						class="flex-1 hover:bg-fuchsia-50"
						disabled={!canSubmit}
						loading={submitting}
						onclick={submitEdit}
					>
						{submitting ? 'Submitting...' : 'Submit Request'}
					</Button>
				{:else if editMode === 'both-services'}
					<Button variant="outline" size="sm" onclick={goBack}>
						<ArrowLeftIcon class="size-4" />
						Back
					</Button>
					<Button
						size="sm"
						class="flex-1"
						disabled={selectedServices.length === 0}
						onclick={() => {
							editMode = 'both-time';
							newDate = undefined;
							newTime = '';
						}}
					>
						Next: Choose Time
						<ArrowRightIcon class="size-4" />
					</Button>
				{:else if editMode === 'both-time'}
					<Button variant="outline" size="sm" onclick={goBack}>
						<ArrowLeftIcon class="size-4" />
						Back
					</Button>
					<Button
						variant="outline"
						size="sm"
						class="flex-1 hover:bg-fuchsia-50"
						disabled={!canSubmit}
						loading={submitting}
						onclick={submitEdit}
					>
						{submitting ? 'Submitting...' : 'Submit Request'}
					</Button>
				{/if}
			</div>
		</div>
	</Modal.Content>
</Modal.Root>
