<script lang="ts">
	import { SvelteDate, SvelteSet } from 'svelte/reactivity';
	import { toast } from 'svelte-sonner';
	import { apiFetch } from '$lib/utils/api';
	import { CalendarDate, getLocalTimeZone, type DateValue } from '@internationalized/date';
	import { Checkbox } from '$lib/components/ui/checkbox';
	import PolicyPopover from '$lib/components/ui/policyPopover.svelte';
	import { formatUserName } from '$lib/utils/nameDisplay';

	import * as Modal from '$lib/components/ui/dialog';
	import { Button } from '$lib/components/ui/button';
	import { Separator } from '$lib/components/ui/separator';
	import * as Card from '$lib/components/ui/card';
	import DatePicker from '$lib/components/booking/DatePicker.svelte';
	import TimeSlotList from '$lib/components/booking/TimeSlotList.svelte';
	import SelectedTimeSummary from '$lib/components/booking/SelectedTimeSummary.svelte';
	import {
		buildLunchProtection,
		generateAvailableTimeSlots,
		generateGroupedTimeSlots,
		formatTime,
		formatLocalDateTime,
		calculateEndTime,
		getDayWithOrdinal,
		getLondonTodayCalendarDate,
		parseWallClockDate,
		type DayHours,
		type DayAvailability
	} from '$lib/utils/timeSlots';
	import type { Booking, WorkingHoursDay, AvailableHoursDay } from '$lib/types/booking';

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

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

	let saving = $state(false);
	let placeholder = $state<CalendarDate>(getLondonTodayCalendarDate());
	let selectedDate = $state<CalendarDate | undefined>(undefined);
	let selectedTime = $state<string | null>(null);
	let workingHours = $state<Record<string, DayHours> | null>(null);
	let availableHours = $state<Record<string, DayAvailability> | null>(null);
	let loadingHours = $state(false);
	let hoursRangeGeneration = $state(0);
	let hoursMonthGeneration = $state(0);
	let userNavigatedCalendar = $state(false);

	let workingHoursCache: Record<string, Record<string, DayHours>> = {};
	let availableHoursCache: Record<string, Record<string, DayAvailability>> = {};
	let loadingMonths: Record<string, boolean> = {};
	let loadingMonthKeys = new SvelteSet<string>();
	let initialLoadDone = $state(false);
	let rescheduleAutoSelectDone = $state(false);

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

	const hoursUntilAppointment = $derived(
		(new SvelteDate(booking.start_time).getTime() - new SvelteDate().getTime()) / (1000 * 60 * 60)
	);
	const hasPayments = $derived((booking.amount_paid ?? 0) > 0);
	const showNoticeWarning = $derived(
		hasPayments ? hoursUntilAppointment < 72 : hoursUntilAppointment < 24
	);
	const showNoShowWarning = $derived(
		!hasPayments && hoursUntilAppointment < 24 && hoursUntilAppointment >= 0
	);
	let forgiveFees = $state(false);
	let forgiveNoShow = $state(false);

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

	const lunchProtection = $derived(
		selectedDate
			? buildLunchProtection(selectedDate, workingHours, availableHours, bookingDuration, true)
			: new Map()
	);

	const groupedTimeSlots = $derived(
		selectedDate
			? generateGroupedTimeSlots(
					selectedDate,
					workingHours,
					availableHours,
					bookingDuration,
					lunchProtection
				)
			: []
	);

	const formattedSelectedDate = $derived(
		selectedDate ? getDayWithOrdinal(selectedDate) : undefined
	);

	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();
		if (!workingHours[dateStr]?.isOpen) return true;
		// No available hours data for this date = data not yet 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 booking duration is 0 (data missing), no valid slots can exist — mark unavailable
		if (bookingDuration <= 0) return true;
		// Build lunch protection specifically for the date being checked (not the selected date)
		const dayProtection = buildLunchProtection(
			date as CalendarDate,
			workingHours,
			availableHours,
			bookingDuration,
			true
		);
		const slots = generateAvailableTimeSlots(
			date as CalendarDate,
			workingHours,
			availableHours,
			bookingDuration,
			dayProtection
		);
		if (slots.length === 0) return true;
		return false;
	}

	// =============== Auto-Select ===============
	// Handled reactively via $effect below

	async function fetchHoursRange(startDate: CalendarDate, months: number) {
		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')}`;

		hoursRangeGeneration++;
		const gen = hoursRangeGeneration;

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

				if (gen !== hoursRangeGeneration) return;

				const whMap: Record<string, DayHours> = {};
				const ahMap: Record<string, DayAvailability> = {};

				whData.forEach(
					(d) => (whMap[d.date] = { isOpen: d.isOpen, startTime: d.startTime, endTime: d.endTime })
				);
				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 (err) {
			console.error('Failed to fetch hours:', err);
			toast.error('Failed to load availability');
			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];
			}
		} finally {
			loadingHours = false;
		}
	}

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

		if (monthKey in workingHoursCache && monthKey in availableHoursCache) {
			// MERGE instead of replace — preserves data from other loaded months
			workingHours = { ...workingHours, ...workingHoursCache[monthKey] };
			availableHours = { ...availableHours, ...availableHoursCache[monthKey] };
			return;
		}

		if (loadingMonths[monthKey]) return;
		if (loadingMonthKeys.has(monthKey)) return;
		loadingMonthKeys.add(monthKey);
		loadingMonths[monthKey] = true;

		hoursMonthGeneration++;
		const gen = hoursMonthGeneration;

		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 [whRes, ahRes] = await Promise.all([
				apiFetch(`/api/scheduling/working-hours?start=${startOfMonth}&end=${endOfMonth}`),
				apiFetch(`/api/scheduling/available-hours?start=${startOfMonth}&end=${endOfMonth}`)
			]);
			if (whRes.ok && ahRes.ok) {
				const whData: WorkingHoursDay[] = await whRes.json();
				const ahData: AvailableHoursDay[] = await ahRes.json();
				if (gen !== hoursMonthGeneration) return;
				const whMap: Record<string, DayHours> = {};
				const ahMap: Record<string, DayAvailability> = {};
				whData.forEach(
					(d) => (whMap[d.date] = { isOpen: d.isOpen, startTime: d.startTime, endTime: d.endTime })
				);
				ahData.forEach((d) => (ahMap[d.date] = { isOpen: d.isOpen, slots: d.slots }));

				workingHoursCache[monthKey] = whMap;
				availableHoursCache[monthKey] = 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);
			toast.error('Failed to load availability');
		} finally {
			delete loadingMonths[monthKey];
			loadingMonthKeys.delete(monthKey);
			loadingHours = false;
		}
	}

	function resetForm() {
		selectedDate = undefined;
		selectedTime = null;
		workingHours = null;
		availableHours = null;
		userNavigatedCalendar = false;
		initialLoadDone = false;
		workingHoursCache = {};
		availableHoursCache = {};
		loadingMonths = {};
		loadingMonthKeys = new SvelteSet<string>();
		rescheduleAutoSelectDone = false;
		placeholder = getLondonTodayCalendarDate();
	}

	async function reschedule() {
		if (!selectedDate || !selectedTime) {
			toast.error('Please select a date and time');
			return;
		}
		const localDate = selectedDate.toDate(getLocalTimeZone());
		const [hours, minutes] = selectedTime.split(':').map(Number);
		localDate.setHours(hours, minutes, 0, 0);
		const newStartTime = formatLocalDateTime(localDate);
		if (new Date(newStartTime).getTime() <= Date.now()) {
			toast.error('Start time must be in the future');
			return;
		}
		saving = true;
		const loadingToast = toast.loading('Rescheduling booking...');
		try {
			const body: Record<string, unknown> = { start_time: newStartTime };
			if (forgiveFees) body.forgive_fees = true;
			if (forgiveNoShow) body.forgive_noshow = true;
			const response = await apiFetch(`/api/admin/bookings/${booking.id}/reschedule`, {
				method: 'PUT',
				headers: {
					'Content-Type': 'application/json'
				},
				body: JSON.stringify(body)
			});
			if (response.ok) {
				toast.success('Booking rescheduled!', { id: loadingToast });
				open = false;
				resetForm();
				onSaved?.();
			} else {
				const text = await response.text();
				toast.error('Failed to reschedule: ' + text, { id: loadingToast });
			}
		} catch (err) {
			console.error('Error rescheduling booking:', err);
			toast.error('Network error rescheduling booking', { id: loadingToast });
		} finally {
			saving = false;
		}
	}

	let wasOpen = false;
	$effect(() => {
		if (open && !wasOpen) {
			resetForm();
			// 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, DayHours>;
					availableHoursCache[key] = null as unknown as Record<string, DayAvailability>;
					loadingMonths[key] = true;
				}
			}
			fetchHoursRange(placeholder, 2);
			initialLoadDone = true;
		}
		wasOpen = open;
	});

	// Fetch additional months when navigating beyond preloaded range
	$effect(() => {
		if (open && initialLoadDone) {
			const monthKey = `${placeholder.year}-${String(placeholder.month).padStart(2, '0')}`;
			if (!(monthKey in workingHoursCache) && !loadingMonthKeys.has(monthKey)) {
				fetchHoursForMonth(placeholder);
			}
		}
	});

	// Auto-select first available date once data loads (timing-safe, data-driven)
	$effect(() => {
		if (
			open &&
			workingHours &&
			availableHours &&
			!selectedDate &&
			!userNavigatedCalendar &&
			!rescheduleAutoSelectDone
		) {
			rescheduleAutoSelectDone = true;
			const now = new SvelteDate(getLondonTodayCalendarDate().toString() + 'T00:00:00');
			const maxDateJs = new SvelteDate(
				maxCalendarDate.year,
				maxCalendarDate.month - 1,
				maxCalendarDate.day
			);
			const daysDifference = Math.floor(
				(maxDateJs.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)
			);
			const daysToCheck = Math.min(daysDifference, 180);
			for (let i = 1; i <= daysToCheck; i++) {
				const checkDate = new SvelteDate(now);
				checkDate.setDate(now.getDate() + i);
				const dateStr = checkDate.toLocaleDateString('en-CA', { timeZone: 'Europe/London' });
				const calDate = new CalendarDate(
					checkDate.getFullYear(),
					checkDate.getMonth() + 1,
					checkDate.getDate()
				);
				if (workingHours[dateStr]?.isOpen && !isDateUnavailable(calDate)) {
					selectedDate = calDate;
					if (!userNavigatedCalendar) {
						placeholder = new CalendarDate(checkDate.getFullYear(), checkDate.getMonth() + 1, 1);
					}
					break;
				}
			}
		}
	});
</script>

<Modal.Root bind:open>
	<Modal.Content class="!z-[70] max-h-[90vh] max-w-[calc(100%-2rem)] overflow-y-auto sm:max-w-4xl">
		<Modal.Header>
			<Modal.Title class="text-lg font-semibold">Reschedule Booking</Modal.Title>
			<Modal.Description>
				Move this booking to a new time. This change takes effect immediately.
			</Modal.Description>
		</Modal.Header>

		<div class="px-6 pb-4">
			{#if showNoticeWarning || showNoShowWarning}
				<div class="mb-4 rounded-md border border-amber-200 bg-amber-50 p-3 text-sm text-amber-800">
					<p class="font-medium text-amber-900">Short Notice Reschedule</p>

					{#if hasPayments}
						<p class="mt-1">Rescheduling may forfeit deposit protection on payments made.</p>
						<details class="mt-1 text-xs text-gray-500">
							<summary class="cursor-pointer hover:text-gray-700">What this means</summary>
							<p class="mt-1">
								"Rescheduling within {Math.round(hoursUntilAppointment)}h of the original time with
								payments present means deposit protection applies — up to 50% of the total (up to £{(
									booking.total_amount * 0.5
								).toFixed(2)}) could be retained depending on notice period."
							</p>
						</details>
						<label class="mt-2 flex cursor-pointer items-center gap-2">
							<Checkbox bind:checked={forgiveFees} />
							<span class="text-xs">Forgive fees — refund fully (overrides deposit protection)</span
							>
						</label>
						<details class="ml-6 text-xs text-gray-500">
							<summary class="cursor-pointer hover:text-gray-700"
								>What happens with forgiveness</summary
							>
							<p class="mt-1">
								"We've waived deposit protection on this reschedule as a goodwill gesture. The full
								amount moves to the new appointment instead of having up to 50% retained as
								deposit."
							</p>
						</details>
					{/if}

					<p class="mt-1">This time change counts as a no-show toward deposit obligations.</p>
					<details class="mt-1 text-xs text-gray-500">
						<summary class="cursor-pointer hover:text-gray-700">What this means</summary>
						<p class="mt-1">
							"This time change will count as a no-show toward your booking history. Two no-shows
							within 6 months would require a deposit on future bookings."
						</p>
					</details>
					<label class="mt-2 flex cursor-pointer items-center gap-2">
						<Checkbox bind:checked={forgiveNoShow} />
						<span class="text-xs"
							>Forgive no-show — this reschedule will <strong>not</strong> count toward deposit obligations</span
						>
					</label>
					<details class="ml-6 text-xs text-gray-500">
						<summary class="cursor-pointer hover:text-gray-700"
							>What happens with forgiveness</summary
						>
						<p class="mt-1">
							"We've waived the no-show record for this reschedule so your deposit obligations are
							unaffected."
						</p>
					</details>
				</div>
				<p class="mt-2 text-xs text-gray-500">
					<PolicyPopover>
						{#snippet trigger()}
							<span class="underline">View full cancellation policy →</span>
						{/snippet}
					</PolicyPopover>
				</p>
			{/if}

			<Card.Root class="mb-4">
				<Card.Content class="pt-4">
					<div class="text-sm font-medium">
						Current: {parseWallClockDate(booking.start_time).toLocaleDateString('en-GB', {
							weekday: 'short',
							day: 'numeric',
							month: 'short',
							year: 'numeric'
						})} at {parseWallClockDate(booking.start_time).toLocaleTimeString('en-GB', {
							hour: 'numeric',
							minute: '2-digit',
							hour12: true
						})}
					</div>
					<div class="mt-1 text-sm text-gray-500">
						{formatUserName(
							booking.user?.full_name || 'Unknown',
							booking.user?.previous_first_name,
							booking.user?.previous_last_name
						)} · {booking.services?.map((s) => s.service_name).join(', ') || 'No services'} · {bookingDuration}
						min
					</div>
				</Card.Content>
			</Card.Root>

			<Separator class="mb-4" />

			<div class="flex items-center justify-center p-6">
				<DatePicker
					date={selectedDate}
					{placeholder}
					minValue={minDate}
					maxValue={maxCalendarDate}
					{isDateUnavailable}
					onchange={(newDate) => {
						selectedDate = newDate;
						selectedTime = null;
					}}
					onPlaceholderChange={(newPlaceholder) => {
						placeholder = newPlaceholder;
						userNavigatedCalendar = true;
					}}
				/>
			</div>

			{#if !loadingHours}
				{#if selectedDate}
					<div class="border-t">
						<div class="max-h-64 overflow-y-auto p-6">
							{#if formattedSelectedDate}
								<div class="mb-3 grid justify-center gap-2 text-sm font-medium">
									{formattedSelectedDate}
								</div>
							{/if}
							<TimeSlotList
								slots={groupedTimeSlots}
								{selectedTime}
								duration={bookingDuration}
								protection={lunchProtection}
								onSelect={(time) => {
									selectedTime = time;
								}}
							/>
						</div>
					</div>
				{:else}
					<div class="flex items-center justify-center border-t p-6">
						<p class="text-center text-sm text-gray-500">Select a date to see available times</p>
					</div>
				{/if}
			{/if}

			{#if selectedDate && selectedTime}
				<SelectedTimeSummary
					selectedDate={formattedSelectedDate || ''}
					selectedTime={formatTime(selectedTime)}
					endTime={formatTime(calculateEndTime(selectedTime, bookingDuration))}
					duration={bookingDuration}
					protection={lunchProtection.get(selectedTime)}
				/>
			{/if}
		</div>

		<Modal.Footer class="flex items-center justify-end gap-2">
			<Button
				variant="outline"
				onclick={() => {
					open = false;
					resetForm();
				}}
				disabled={saving}
			>
				Cancel
			</Button>
			<Button onclick={reschedule} disabled={saving || !selectedDate || !selectedTime}>
				{saving ? 'Rescheduling…' : 'Confirm Reschedule'}
			</Button>
		</Modal.Footer>
	</Modal.Content>
</Modal.Root>
