// Lalaluxe booking — components/steps/calendar.jsx
// Page: "calendar" — month grid + time slots, backed by live /api/availability.
// One screen per file. Load before app.jsx (see CLAUDE.md "multi-file Babel contract").
// Humanize a minute count for the booking-length blurb (e.g. 135 → "2 hr 15 min").
function humanDuration(min) {
	const m = Math.max(0, Math.round(Number(min) || 0));
	if (m < 60) return `${m} min`;
	const h = Math.floor(m / 60);
	const r = m % 60;
	return r ? `${h} hr ${r} min` : `${h} hr`;
}

function CalendarStep({ cfg, s, set, sq, dur }) {
	const today = useMemo(() => {
		const d = new Date();
		return new Date(d.getFullYear(), d.getMonth(), d.getDate());
	}, []);
	const initialMonth = useMemo(() => {
		if (s.date) {
			const d = parseYmd(s.date);
			return { y: d.getFullYear(), m: d.getMonth() };
		}
		return { y: today.getFullYear(), m: today.getMonth() };
	}, [today, s.date]);
	const [view, setView] = useState(initialMonth);

	const year = view.y;
	const m = view.m;
	const first = new Date(year, m, 1);
	const startDow = first.getDay();
	const daysInMonth = new Date(year, m + 1, 0).getDate();
	const cells = [];
	for (let i = 0; i < startDow; i++) cells.push(null);
	for (let d = 1; d <= daysInMonth; d++) cells.push(d);
	while (cells.length % 7 !== 0) cells.push(null);

	const atMonthStart =
		view.y === today.getFullYear() && view.m === today.getMonth();

	const goPrev = () => {
		if (atMonthStart) return;
		setView((v) =>
			v.m === 0 ? { y: v.y - 1, m: 11 } : { y: v.y, m: v.m - 1 },
		);
	};
	const goNext = () => {
		setView((v) =>
			v.m === 11 ? { y: v.y + 1, m: 0 } : { y: v.y, m: v.m + 1 },
		);
	};

	// Live availability for the visible month [first .. last], cached per fetched range and
	// refetched on month change. `avail.days` = { "YYYY-MM-DD": ["10:00",…] } of OPEN slots.
	const fromKey = ymd(year, m, 1);
	const toKey = ymd(year, m, daysInMonth);
	const [avail, setAvail] = useState({
		days: null,
		loading: true,
		error: false,
	});
	const [nonce, setNonce] = useState(0); // "Try again" bumps this to refetch after an error
	useEffect(() => {
		let live = true;
		setAvail({ days: null, loading: true, error: false });
		fetch(`/api/availability?from=${fromKey}&to=${toKey}&dur=${dur || 0}`)
			.then((r) => r.json())
			.then((d) => {
				if (live)
					setAvail({
						days: (d && d.ok && d.days) || {},
						loading: false,
						error: false,
					});
			})
			.catch(() => {
				if (live) setAvail({ days: null, loading: false, error: true });
			});
		return () => {
			live = false;
		};
	}, [fromKey, toKey, dur, nonce]);

	// The API is authoritative when Square is configured: a valid response (even an empty one) is
	// followed exactly — empty day → closed, no phantom slots — so we never take a deposit on a slot
	// that doesn't exist. The deterministic Tue–Sat fallback fires ONLY when the fetch genuinely
	// errored, OR when Square isn't configured (prototype mode with no seeded availability).
	const useFallback = avail.error || !(sq && sq.configured);
	const openFor = useCallback(
		(key) => {
			if (avail.loading) return [];
			if (useFallback) return fallbackSlots(key); // errored OR prototype mode → soft fallback
			return (avail.days && avail.days[key]) || []; // configured + valid response → API authoritative
		},
		[avail.loading, avail.days, useFallback],
	);

	// A pick made in another month must not bleed its date/slots into the month now on screen —
	// the grid shows no highlight there, so its "Available times · <date>" reads as a desync.
	// The pick itself is preserved (To checkout stays enabled); only its off-month UI is hidden.
	const pickedInView =
		!!s.date &&
		(() => {
			const d = parseYmd(s.date);
			return d.getFullYear() === year && d.getMonth() === m;
		})();
	const slots = pickedInView ? openFor(s.date) : [];

	return (
		<div className="screen">
			<h2 className="h-question">
				When are you <span className="swirl">free?</span>
			</h2>
			<p className="h-sub">
				{s.visit === "subscription" ? (
					s.subMode === "existing" ? (
						"Pick a day for your next included visit."
					) : (
						"Pick a day for your first visit — you'll book the other two later with your code."
					)
				) : (
					<>
						Pick any open day.{" "}
						{dur > 0 ? `Your session runs about ${humanDuration(dur)}.` : "Sessions run about 90 min."}
					</>
				)}
			</p>

			<div className="body cal-body">
				<div className="cal-card">
					<div className="cal-head">
						<div className="cal-month">
							{MONTHS_LONG[m]} <em>{year}</em>
						</div>
						<div className="cal-nav">
							<button
								onClick={goPrev}
								disabled={atMonthStart}
								aria-label="previous month"
							>
								<span className="chev"></span>
							</button>
							<button className="next" onClick={goNext} aria-label="next month">
								<span className="chev"></span>
							</button>
						</div>
					</div>

					<div className="cal-dow-row">
						{DOW_SHORT.map((d, i) => (
							<div key={i} className="cal-dow">
								{d}
							</div>
						))}
					</div>

					<div className="cal-grid">
						{cells.map((d, i) => {
							if (d === null)
								return <div key={i} className="cal-cell empty"></div>;
							const dt = new Date(year, m, d);
							const past = dt < today;
							const isToday = sameDay(dt, today);
							const key = ymd(year, m, d);
							// Open/closed is DRIVEN BY availability: a day is open iff it has ≥1 slot. While the
							// month loads, days render dimmed "wait" (not struck-through closed, and no flicker
							// into the fallback) until data lands.
							const closed =
								!past && !avail.loading && openFor(key).length === 0;
							const sel = s.date === key;
							const cls = ["cal-cell"];
							if (past) cls.push("past");
							else if (avail.loading) cls.push("wait");
							else if (closed) cls.push("closed");
							else cls.push("avail");
							if (isToday) cls.push("today");
							if (sel) cls.push("sel");
							const disabled = past || avail.loading || closed;
							return (
								<button
									key={i}
									className={cls.join(" ")}
									disabled={disabled}
									aria-label={`${MONTHS_LONG[m]} ${d}, ${year}`}
									aria-pressed={sel}
									onClick={() => {
										if (disabled) return;
										set({ date: key, time: null });
										// Short/mid viewports can render the slot list below the fold;
										// "nearest" makes this a no-op when it is already visible.
										requestAnimationFrame(() =>
											document
												.querySelector(".slot-section")
												?.scrollIntoView({ block: "nearest", behavior: "smooth" }),
										);
									}}
								>
									<span>{d}</span>
									{!disabled && <span className="ddot"></span>}
								</button>
							);
						})}
					</div>

					<div className="cal-legend">
						<span className="l-item">
							<span className="swatch"></span> Open
						</span>
						<span className="l-item">
							<span className="swatch closed"></span> Closed
						</span>
						{pickedInView && (
							<span className="l-item">
								<span className="swatch sel"></span> Picked
							</span>
						)}
					</div>
				</div>

				<div className="slot-section">
					<div className="slabel" role="status">
						<span>{pickedInView ? "Available times" : "Pick a day first"}</span>
						{pickedInView && (
							<span className="picked">{prettyDateShort(s.date)}</span>
						)}
					</div>
					{/* One message box at a time: loading (grid intentionally shows no open
					    days yet), error (with a refetch path back to REAL hours), then the
					    pick states. error+picked keeps the fallback slot list below. */}
					{avail.loading ? (
						<div className="empty-slots" role="status">
							{pickedInView ? "Loading times…" : "Loading open days…"}
						</div>
					) : avail.error ? (
						<div className="empty-slots" role="status">
							Couldn't load live times — showing our usual hours ♡
							<button
								className="cal-retry"
								onClick={() => setNonce((n) => n + 1)}
							>
								Try again
							</button>
						</div>
					) : null}
					{!avail.loading &&
						(!pickedInView ? (
							avail.error ? null : (
								<div className="empty-slots" role="status">
									Tap any open day above.
								</div>
							)
						) : slots.length === 0 ? (
							<div className="empty-slots" role="status">
								Fully booked — try another day ♡
							</div>
						) : (
							<div className="slots">
								{slots.map((t) => {
									const sel = s.time === t;
									return (
										<button
											key={t}
											className={"slot " + (sel ? "sel" : "")}
											onClick={() => set({ time: t })}
										>
											{fmtTime(t)}
											<small>{sel ? "Picked" : "Open"}</small>
										</button>
									);
								})}
							</div>
						))}
				</div>
			</div>
		</div>
	);
}
