import React, { useEffect, useMemo, useRef, useState } from 'react';
import { __, sprintf } from '@wordpress/i18n';
import { Alert, Button, Calendar, EmptyState, Icon, Skeleton, SkeletonGroup, TimeSlots, addDays, addMonths, monthOf } from '../../../shared/ui';
import { api } from '../api';
import { browserTimezone, maxBookingDays, siteTimezone, siteToday, weekStartsOn } from '../config';
import { LOCALE, formatDateWithWeekday, formatMonth, timeFormat, tzLabel, tzOffsetMinutes } from '../format';

/** How many months ahead we look for the first free day before giving up. */
const LOOKAHEAD_MONTHS = 3;

export default function DateTimeStep({ flow, headingId }) {
	const { sel, slotsVersion, notice } = flow;
	const today = siteToday();
	const maxDate = addDays(today, maxBookingDays());
	const [month, setMonth] = useState(() => monthOf(sel.date && sel.date >= today ? sel.date : today));
	const [days, setDays] = useState({}); // month → { ymd: { has_slots, count } }
	const [monthError, setMonthError] = useState('');
	const [slots, setSlots] = useState(null); // null = loading
	const [slotsError, setSlotsError] = useState('');
	const monthCache = useRef(new Map());
	const dayCache = useRef(new Map());
	const lookahead = useRef(sel.date ? LOOKAHEAD_MONTHS : 0);
	const slotsHeadingId = `${headingId}-slots`;

	const base = { serviceId: sel.serviceId, agentId: sel.agentId || 0, locationId: sel.locationId || 0 };
	const baseKey = `${base.serviceId}|${base.agentId}|${base.locationId}|${slotsVersion}`;

	// A new service/staff member (or a slot that was just taken) invalidates everything.
	useEffect(() => {
		monthCache.current.clear();
		dayCache.current.clear();
		setDays({});
	}, [baseKey]);

	/* Month availability → calendar day states. */
	useEffect(() => {
		const key = `${baseKey}|${month}`;
		if (monthCache.current.has(key)) {
			setDays((d) => ({ ...d, [month]: monthCache.current.get(key) }));
			return undefined;
		}
		const ctrl = new AbortController();
		setMonthError('');
		api.month({ ...base, month }, ctrl.signal)
			.then((rows) => {
				const map = {};
				rows.forEach((r) => {
					map[r.date] = r;
				});
				monthCache.current.set(key, map);
				setDays((d) => ({ ...d, [month]: map }));
			})
			.catch((e) => {
				if (e.name !== 'AbortError') setMonthError(e.message);
			});
		return () => ctrl.abort();
		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, [baseKey, month]);

	const monthDays = days[month];
	const firstFree = useMemo(() => {
		if (!monthDays) return null;
		return (
			Object.keys(monthDays)
				.sort()
				.find((d) => d >= today && d <= maxDate && monthDays[d].has_slots) || null
		);
	}, [monthDays, today, maxDate]);

	/* Pre-select the first free day; look ahead a few months when this one is full. */
	useEffect(() => {
		if (!monthDays) return;
		if (sel.date && monthOf(sel.date) === month) return;
		if (firstFree) {
			if (!sel.date) flow.actions.update({ date: firstFree });
			return;
		}
		if (lookahead.current < LOOKAHEAD_MONTHS && addMonths(month, 1) <= monthOf(maxDate)) {
			lookahead.current += 1;
			setMonth(addMonths(month, 1));
		}
		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, [monthDays, firstFree]);

	/* Times for the selected day. */
	useEffect(() => {
		if (!sel.date) {
			setSlots([]);
			return undefined;
		}
		const key = `${baseKey}|${sel.date}`;
		if (dayCache.current.has(key)) {
			setSlots(dayCache.current.get(key));
			return undefined;
		}
		const ctrl = new AbortController();
		setSlots(null);
		setSlotsError('');
		api.day({ ...base, date: sel.date }, ctrl.signal)
			.then((rows) => {
				const list = rows.map((r) => ({ time: String(r.time || r.start || '').slice(0, 5), end: r.end || '' })).filter((r) => r.time);
				dayCache.current.set(key, list);
				setSlots(list);
			})
			.catch((e) => {
				if (e.name !== 'AbortError') {
					setSlots([]);
					setSlotsError(e.message);
				}
			});
		return () => ctrl.abort();
		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, [baseKey, sel.date]);

	const getDayState = (ymd) => {
		if (!monthDays) return 'loading';
		const d = monthDays[ymd];
		return d && d.has_slots ? 'available' : 'unavailable';
	};

	const siteTz = siteTimezone();
	const visitorTz = browserTimezone();
	const monthEmpty = monthDays && !firstFree;
	const siteOffset = tzOffsetMinutes(siteTz, sel.date);
	const visitorOffset = tzOffsetMinutes(visitorTz, sel.date);
	const differs = siteOffset !== null && visitorOffset !== null && siteOffset !== visitorOffset;

	return (
		<div className="bp-bw-datetime">
			{notice ? (
				<Alert tone={notice.tone} live className="bp-bw-datetime__notice">
					{notice.text}
				</Alert>
			) : null}

			<div className="bp-bw-datetime__grid">
				<div className="bp-bw-datetime__cal">
					<Calendar
						month={month}
						onMonthChange={(m) => {
							lookahead.current = LOOKAHEAD_MONTHS;
							setMonth(m);
						}}
						value={sel.date}
						onChange={(d) => flow.actions.update({ date: d })}
						getDayState={getDayState}
						minDate={today}
						maxDate={maxDate}
						today={today}
						weekStartsOn={weekStartsOn()}
						locale={LOCALE}
						showLegend
					/>
					{monthError ? (
						<Alert tone="danger" className="bp-bw-cal-error">
							{monthError}
						</Alert>
					) : null}
					{monthEmpty ? (
						<p className="bp-bw-hint bp-bw-hint--center" role="status">
							{
								/* translators: %s: month and year, e.g. "October 2026". */
								sprintf(__('No free times in %s.', 'pointly-booking'), formatMonth(month))
							}{' '}
							{addMonths(month, 1) <= monthOf(maxDate) ? (
								<Button variant="link" size="sm" onClick={() => setMonth(addMonths(month, 1))}>
									{__('Check the next month', 'pointly-booking')}
								</Button>
							) : null}
						</p>
					) : null}
				</div>

				<div className="bp-bw-datetime__slots">
					<h3 className="bp-bw-subhead" id={slotsHeadingId}>
						{sel.date ? formatDateWithWeekday(sel.date) : __('Select a date', 'pointly-booking')}
					</h3>

					{!sel.date ? (
						<p className="bp-bw-hint">{__('Choose a day in the calendar to see the available times.', 'pointly-booking')}</p>
					) : slots === null ? (
						<SkeletonGroup label={__('Loading available times…', 'pointly-booking')} className="bp-bw-slot-skel">
							{Array.from({ length: 8 }).map((_, i) => (
								<Skeleton key={i} height={40} />
							))}
						</SkeletonGroup>
					) : slotsError ? (
						<Alert tone="danger">{slotsError}</Alert>
					) : slots.length === 0 ? (
						<EmptyState compact icon="calendar-x" title={__('Fully booked', 'pointly-booking')}>
							{__('There are no free times on this day. Please pick another date.', 'pointly-booking')}
						</EmptyState>
					) : (
						<TimeSlots
							slots={slots}
							value={sel.time}
							format={timeFormat}
							groupLabelledBy={slotsHeadingId}
							onChange={(time, slot, e) =>
								e && e.detail > 0 ? flow.actions.pickAndNext({ time }) : flow.actions.update({ time })
							}
						/>
					)}

					{siteTz ? (
						<p className="bp-bw-tz">
							<Icon name="globe" />
							<span>
								{
									/* translators: %s: time zone name, e.g. "Copenhagen (Europe)". */
									sprintf(__('Times are shown in %s.', 'pointly-booking'), tzLabel(siteTz))
								}
								{visitorTz && differs ? (
									<>
										{' '}
										{
											/* translators: %s: the visitor's own time zone. */
											sprintf(__('Your time zone is %s.', 'pointly-booking'), tzLabel(visitorTz))
										}
									</>
								) : null}
							</span>
						</p>
					) : null}
				</div>
			</div>
		</div>
	);
}
