package main

import (
	"fmt"
	"strconv"
)

// CalcomBookingWidget renders the main booking widget container.
//
// The widget is divided into four regions, each with a stable ID so HTMX can
// swap them independently:
//
//   - #calcom-steps-{id}      step indicator (OOB-updated on every transition)
//   - #calcom-date-grid-{id}  the date picker
//   - #calcom-slots-{id}      time slots, loaded after a date click
//   - #calcom-form-{id}       booking form, loaded after a slot click
//   - #calcom-confirm-{id}    success panel, populated after submit
//
// Each partial returned by the server emits hx-swap-oob directives to keep
// the step indicator in sync with the user's progress, and to clear stale
// regions when the user changes their mind (Change date / Change time) or
// finishes (post-confirmation).
templ CalcomBookingWidget(config BookingConfig) {
	<div
		id={ fmt.Sprintf("calcom-widget-%s", config.BlockID) }
		class="calcom-booking-widget bg-card border border-border rounded-xl p-4 sm:p-6 shadow-sm"
		data-username={ config.Username }
		data-event-type={ config.EventTypeSlug }
	>
		// Booking times render server-side in the MEETING's timezone (the
		// event's Cal.com availability schedule) — the visitor's browser
		// timezone is deliberately never detected or sent. Client-side
		// timezone plumbing is what caused the 2026-06-10 incident: a hidden
		// input's "UTC" default never got updated, every hx-include sent
		// timezone=UTC, and visitors saw UTC slot times.
		//
		// This script only:
		//   - captures the browser locale for phone dial-code inference
		//   - wires the date-grid selection highlight
		//   - drives the phone country combobox
		//
		// Init is deferred to DOMContentLoaded: the locale input sits after
		// this script in the widget and doesn't exist while it parses.
		<script>
			(function(){
				var s = document.currentScript;
				if (!s) return;
				var w = s.parentElement;
				if (!w || !w.classList.contains("calcom-booking-widget")) return;
				function init(){
				// Capture the browser locale (e.g. "en-AU") so the server can infer the
				// phone dial code and convert national numbers to E.164 — Cal.com rejects
				// national-format attendee phone numbers as invalid_number.
				var loc = "";
				try { loc = navigator.language || (navigator.languages && navigator.languages[0]) || ""; } catch(e) {}
				var locInput = w.querySelector("input[name=\"locale\"]");
				if (locInput) locInput.value = loc;
				// Click delegate: mark the clicked date cell aria-pressed=true,
				// clearing any previously pressed cell. Visual styling is handled
				// by aria-pressed Tailwind variants on the button class.
				w.addEventListener("click", function(ev){
					var btn = ev.target.closest("[role=\"grid\"][aria-label=\"Booking date picker\"] button[role=\"gridcell\"]");
					if (!btn || btn.disabled) return;
					w.querySelectorAll("[role=\"grid\"][aria-label=\"Booking date picker\"] button[role=\"gridcell\"][aria-pressed=\"true\"]").forEach(function(b){
						b.setAttribute("aria-pressed", "false");
					});
					btn.setAttribute("aria-pressed", "true");
				});
				// Country combobox (phone field). Delegated on the widget root so it works
				// for the form partial HTMX swaps in after this script first runs.
				function ccReset(box){
					var hidden = box.querySelector("input[name=\"phoneCountry\"]");
					var search = box.querySelector(".cc-search");
					if (!hidden || !search) return;
					var opt = box.querySelector(".cc-option[data-code=\"" + (hidden.value || "") + "\"]");
					search.value = opt ? opt.textContent.trim() : "";
				}
				function ccClose(except){
					w.querySelectorAll("[data-cc-combobox]").forEach(function(box){
						var ul = box.querySelector(".cc-options");
						if (!ul || ul === except) return;
						if (!ul.classList.contains("hidden")) { ul.classList.add("hidden"); ccReset(box); }
						var s = box.querySelector(".cc-search"); if (s) s.setAttribute("aria-expanded", "false");
					});
				}
				function ccOpen(box, showAll){
					var ul = box.querySelector(".cc-options"); var s = box.querySelector(".cc-search");
					if (!ul || !s) return;
					if (showAll) ul.querySelectorAll(".cc-option").forEach(function(li){ li.classList.remove("hidden"); });
					ul.classList.remove("hidden"); s.setAttribute("aria-expanded", "true");
				}
				w.addEventListener("input", function(ev){
					var s = ev.target.closest(".cc-search"); if (!s) return;
					var box = s.closest("[data-cc-combobox]"); var ul = box.querySelector(".cc-options");
					var q = s.value.trim().toLowerCase();
					ul.querySelectorAll(".cc-option").forEach(function(li){
						li.classList.toggle("hidden", q !== "" && li.textContent.toLowerCase().indexOf(q) === -1);
					});
					ccOpen(box, false);
				});
				w.addEventListener("click", function(ev){
					var opt = ev.target.closest(".cc-option");
					if (opt){
						var box = opt.closest("[data-cc-combobox]");
						box.querySelector("input[name=\"phoneCountry\"]").value = opt.getAttribute("data-code");
						box.querySelector(".cc-search").value = opt.textContent.trim();
						ccClose(); return;
					}
					var s = ev.target.closest(".cc-search");
					if (s){ var b = s.closest("[data-cc-combobox]"); ccClose(b.querySelector(".cc-options")); ccOpen(b, true); s.select(); return; }
					if (!ev.target.closest("[data-cc-combobox]")) ccClose();
				});
				w.addEventListener("keydown", function(ev){
					var s = ev.target.closest(".cc-search"); if (!s) return;
					if (ev.key === "Escape") { ccClose(); return; }
					if (ev.key === "Enter"){
						ev.preventDefault();
						var box = s.closest("[data-cc-combobox]");
						var first = box.querySelector(".cc-option:not(.hidden)");
						if (first){
							box.querySelector("input[name=\"phoneCountry\"]").value = first.getAttribute("data-code");
							s.value = first.textContent.trim(); ccClose();
						}
					}
				});

				// ── "Manage your booking" (localStorage-backed) ───────────────
				// After a successful booking, an inline script in the confirmation
				// partial persists {uid, when, email, savedAt} to localStorage under
				// a STABLE key derived from this widget's username + event type (NOT
				// the per-render blockId, which changes each page render). On a later
				// visit we read it back here and show a panel offering Cancel / redo,
				// hiding the date grid so the visitor manages the existing booking
				// instead of accidentally double-booking.
				//
				// SECURITY: the UID is Cal.com's own unguessable booking identifier
				// and lives only in THIS visitor's localStorage — the same capability
				// model as Cal.com's emailed cancel link. The server rate-limits the
				// cancel POST by IP as the abuse guard.
				function bookingKey(){
					return "calcom:booking:" + (w.getAttribute("data-username") || "") + ":" + (w.getAttribute("data-event-type") || "");
				}
				function readSavedBooking(){
					try {
						var raw = window.localStorage.getItem(bookingKey());
						if (!raw) return null;
						var b = JSON.parse(raw);
						return (b && b.uid) ? b : null;
					} catch(e) { return null; }
				}
				function clearSavedBooking(){
					try { window.localStorage.removeItem(bookingKey()); } catch(e) {}
				}
				var savedPanel = w.querySelector("[data-calcom-saved]");
				var dateGrid = w.querySelector(".calcom-date-picker");
				function showGrid(){
					if (dateGrid) dateGrid.classList.remove("hidden");
					if (savedPanel) savedPanel.classList.add("hidden");
				}
				function showSavedPanel(b){
					if (!savedPanel) return;
					var whenEl = savedPanel.querySelector("[data-calcom-saved-when]");
					if (whenEl) whenEl.textContent = b.when || "";
					var emailEl = savedPanel.querySelector("[data-calcom-saved-email]");
					if (emailEl) emailEl.textContent = b.email || "";
					// Populate the Cancel form's hidden uid so its HTMX POST carries it.
					var uidInput = savedPanel.querySelector("input[name=\"uid\"]");
					if (uidInput) uidInput.value = b.uid || "";
					savedPanel.classList.remove("hidden");
					if (dateGrid) dateGrid.classList.add("hidden");
				}
				function refreshSavedPanel(){
					var b = readSavedBooking();
					if (b) { showSavedPanel(b); } else { showGrid(); }
				}
				// Reschedule / redo: cancel the existing booking (so the visitor
				// can't double-book), then reveal the grid to book afresh. The
				// Cancel form already POSTs /cancel and its afterRequest handler
				// clears storage + restores the grid on success, so "redo" just
				// triggers that same cancel — the restored grid IS the rebook UI.
				var redoBtn = savedPanel ? savedPanel.querySelector("[data-calcom-redo]") : null;
				if (redoBtn) {
					redoBtn.addEventListener("click", function(){
						var cancelBtn = savedPanel.querySelector("[data-calcom-cancel-submit]");
						if (cancelBtn) cancelBtn.click();
					});
				}
				// When the Cancel POST succeeds, the server swaps a "cancelled"
				// message into #calcom-confirm. Clear the localStorage key, hide the
				// saved panel, and re-show the date grid (only CSS-hidden while the
				// panel was up — never removed — so the visitor can rebook) so a
				// reload no longer shows the saved panel.
				w.addEventListener("htmx:afterRequest", function(ev){
					var d = ev.detail || {};
					var path = (d.requestConfig && d.requestConfig.path) || "";
					if (path.indexOf("/api/plugins/calcomblock/cancel") === -1) return;
					if (d.successful) {
						clearSavedBooking();
						showGrid(); // re-show the grid + hide the saved panel
					}
				});
				refreshSavedPanel();
				}
				// init() queries elements parsed after this script — defer until
				// the document is complete. (w is captured outside the callback:
				// document.currentScript is null by the time it fires.)
				if (document.readyState === "loading") {
					document.addEventListener("DOMContentLoaded", init);
				} else {
					init();
				}
			})();
		</script>
		<input type="hidden" name="locale" value=""/>
		// Admin-authored fallback copy for a non-fixable booking failure
		// (Tier-2). Stamped once from block content here and hx-included into
		// the POST /book request (like locale) so HandleCreateBooking can read
		// it back. templ auto-escapes the value. Defaults to
		// DefaultUnavailableMessage when the admin leaves it blank (resolved in
		// block.go), so it is never empty.
		<input type="hidden" name="unavailableMessage" value={ config.UnavailableMessage }/>
		if config.Title != "" {
			<h3 class="text-2xl font-bold text-foreground mb-2">{ config.Title }</h3>
		}
		if config.Description != "" {
			<p class="text-muted-foreground mb-6">{ config.Description }</p>
		}
		// Step indicator — wrapped in a stable container so partial responses
		// can replace its contents via hx-swap-oob.
		<div id={ fmt.Sprintf("calcom-steps-%s", config.BlockID) } class="mb-6">
			@calcomStepIndicator(1)
		</div>
		if config.ShowTimezone {
			<div class="text-xs text-muted-foreground mb-4 flex items-center gap-1.5 flex-wrap">
				<svg class="h-3.5 w-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true">
					<circle cx="12" cy="12" r="9"></circle>
					<path d="M3 12h18M12 3a14 14 0 010 18M12 3a14 14 0 000 18"></path>
				</svg>
				<span>All times shown in</span>
				<span class="font-medium text-foreground">{ config.TimeZone }</span>
			</div>
		}
		// Saved-booking panel — normally hidden. The widget script reveals it on
		// DOMContentLoaded when this visitor's localStorage holds a booking for
		// this username/event, hiding the date grid below so they manage the
		// existing booking (cancel / redo) instead of double-booking.
		@calcomSavedBooking(config.BlockID)
		// Date picker section — wrapped so confirmation can OOB-clear it.
		<div
			id={ fmt.Sprintf("calcom-date-grid-%s", config.BlockID) }
			class="calcom-date-picker"
		>
			@calcomDateGrid(config)
		</div>
		// Time slots section — populated after a date click.
		<div
			id={ fmt.Sprintf("calcom-slots-%s", config.BlockID) }
			class="calcom-time-slots mt-6"
			aria-live="polite"
			aria-busy="false"
		></div>
		// Booking form section — populated after a slot click.
		<div
			id={ fmt.Sprintf("calcom-form-%s", config.BlockID) }
			class="calcom-booking-form"
			aria-live="polite"
			aria-busy="false"
		></div>
		// Confirmation section — populated after successful booking.
		<div
			id={ fmt.Sprintf("calcom-confirm-%s", config.BlockID) }
			class="calcom-confirmation"
			aria-live="polite"
			aria-busy="false"
		></div>
		// Central loading indicator shown during HTMX requests.
		<div
			id={ fmt.Sprintf("calcom-loading-%s", config.BlockID) }
			class="htmx-indicator flex items-center justify-center py-8"
			role="status"
			aria-label="Loading"
		>
			<svg class="animate-spin h-8 w-8 text-primary" fill="none" viewBox="0 0 24 24" aria-hidden="true">
				<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
				<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
			</svg>
		</div>
	</div>
}

// calcomStepIndicator renders the three-step progress indicator. `active` is
// the user's current step (1 = Select Date, 2 = Select Time, 3 = Confirm,
// 4 = Done — all three checkmarked). When active > n the step is rendered as
// completed; when active == n it is current; when active < n it is upcoming.
templ calcomStepIndicator(active int) {
	<div class="flex items-center gap-2 flex-wrap text-sm">
		@calcomStep(1, "Select Date", active)
		<div class="h-px flex-1 min-w-[1rem] bg-border"></div>
		@calcomStep(2, "Select Time", active)
		<div class="h-px flex-1 min-w-[1rem] bg-border"></div>
		@calcomStep(3, "Confirm", active)
	</div>
}

// calcomStep renders one step pill inside the progress indicator.
templ calcomStep(n int, label string, active int) {
	<div class={
		"flex items-center gap-2",
		templ.KV("opacity-50", active < n),
	}>
		<span class={
			"flex items-center justify-center w-6 h-6 rounded-full text-xs font-medium shrink-0",
			templ.KV("bg-primary text-primary-foreground", active >= n),
			templ.KV("bg-muted text-muted-foreground", active < n),
		}>
			if active > n {
				<svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3" aria-hidden="true">
					<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"></path>
				</svg>
			} else {
				{ strconv.Itoa(n) }
			}
		</span>
		<span class={
			"hidden sm:inline",
			templ.KV("text-foreground font-medium", active == n),
			templ.KV("text-muted-foreground", active != n),
		}>{ label }</span>
	</div>
}

// calcomStepIndicatorOOB wraps calcomStepIndicator in the OOB-swap envelope
// so partial responses can update the indicator out-of-band.
templ calcomStepIndicatorOOB(blockID string, active int) {
	<div id={ fmt.Sprintf("calcom-steps-%s", blockID) } class="mb-6" hx-swap-oob="true">
		@calcomStepIndicator(active)
	</div>
}

// calcomClearRegion emits an empty container with the given ID and an OOB
// swap directive, used to wipe stale regions when the user transitions
// between flow steps (Change date / Change time / post-confirmation).
templ calcomClearRegion(id, extraClasses string) {
	<div id={ id } class={ extraClasses } aria-live="polite" aria-busy="false" hx-swap-oob="innerHTML"></div>
}

// calcomDateGrid renders the calendar date picker. The prev/next buttons fire
// hx-get against /api/plugins/calcomblock/date-grid, which returns a fresh
// instance of this template scoped to a new WeekStart — innerHTML-swapped
// into the #calcom-date-grid-{id} container in the widget root.
templ calcomDateGrid(config BookingConfig) {
	<div class="space-y-4">
		<div class="flex items-center justify-between gap-2">
			if config.CanGoBack {
				<button
					type="button"
					aria-label="Previous dates"
					class="p-1.5 rounded-md text-muted-foreground hover:bg-muted hover:text-foreground transition-colors focus:outline-none focus:ring-2 focus:ring-ring"
					hx-get={ fmt.Sprintf("/api/plugins/calcomblock/date-grid?blockId=%s&username=%s&eventType=%s&weeks=%d&weekStart=%s",
						config.BlockID, config.Username, config.EventTypeSlug, config.WeeksToShow, prevWeekStart(config.WeekStart, config.WeeksToShow, config.Today)) }
					hx-target={ fmt.Sprintf("#calcom-date-grid-%s", config.BlockID) }
					hx-swap="innerHTML"
				>
					<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7"/></svg>
				</button>
			} else {
				<span class="p-1.5 w-7 h-7" aria-hidden="true"></span>
			}
			<span class="text-sm font-medium text-foreground" aria-live="polite">{ config.RangeLabel }</span>
			<button
				type="button"
				aria-label="Next dates"
				class="p-1.5 rounded-md text-muted-foreground hover:bg-muted hover:text-foreground transition-colors focus:outline-none focus:ring-2 focus:ring-ring"
				hx-get={ fmt.Sprintf("/api/plugins/calcomblock/date-grid?blockId=%s&username=%s&eventType=%s&weeks=%d&weekStart=%s",
					config.BlockID, config.Username, config.EventTypeSlug, config.WeeksToShow, nextWeekStart(config.WeekStart, config.WeeksToShow)) }
				hx-target={ fmt.Sprintf("#calcom-date-grid-%s", config.BlockID) }
				hx-swap="innerHTML"
			>
				<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7"/></svg>
			</button>
		</div>
		<div class="grid grid-cols-7 gap-1 text-center" role="row">
			for _, day := range []string{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"} {
				<div class="text-xs font-medium text-muted-foreground py-2" role="columnheader">
					{ day }
				</div>
			}
		</div>
		<div class="grid grid-cols-7 gap-1" role="grid" aria-label="Booking date picker">
			// Offset the first date to its weekday column. Without these pad
			// cells a mid-week window pours in at the "Sun" column and every
			// date renders under the wrong header — the second bidbuddy
			// incident (2026-06-10): tapped the cell under "Tue", got Friday.
			for i, pad := 0, leadingBlanks(config.Dates); i < pad; i++ {
				<div class="calcom-date-pad" aria-hidden="true"></div>
			}
			for _, date := range config.Dates {
				if date.IsPast {
					<button
						type="button"
						disabled
						aria-disabled="true"
						class="flex aspect-square w-full items-center justify-center rounded-lg font-medium text-muted-foreground opacity-40 cursor-not-allowed"
						role="gridcell"
					>
						<span class="text-base sm:text-lg">{ date.DayOfMonth }</span>
					</button>
				} else {
					<button
						type="button"
						role="gridcell"
						class={
							// aspect-square + flex centring (not fixed p-3): cells track
							// the column width, so a narrow phone container yields small
							// squares instead of 25×52 stretched pills (iOS report,
							// 2026-06-10 21:52).
							"flex aspect-square w-full items-center justify-center rounded-lg font-medium transition-all",
							"hover:bg-primary hover:text-primary-foreground",
							"focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
							"aria-pressed:bg-primary aria-pressed:text-primary-foreground aria-pressed:ring-2 aria-pressed:ring-primary",
							templ.KV("bg-primary/10 text-primary ring-1 ring-primary", date.IsToday && !date.IsSelected),
							templ.KV("text-foreground", !date.IsToday && !date.IsSelected),
						}
						aria-pressed={ fmt.Sprintf("%t", date.IsSelected) }
						hx-get={ fmt.Sprintf("/api/plugins/calcomblock/slots?username=%s&eventType=%s&date=%s&blockId=%s",
							config.Username, config.EventTypeSlug, date.Date, config.BlockID) }
						hx-target={ fmt.Sprintf("#calcom-slots-%s", config.BlockID) }
						hx-swap="innerHTML"
						hx-indicator={ fmt.Sprintf("#calcom-loading-%s", config.BlockID) }
					>
						<span class="text-base sm:text-lg">{ date.DayOfMonth }</span>
					</button>
				}
			}
		</div>
	</div>
}

// CalcomTimeSlots renders available time slots for a selected date. Returned
// as the body of GET /slots; also emits OOB updates for the step indicator
// (active=2) and an OOB clear of the form region so a previously-rendered
// form for an earlier slot doesn't linger.
templ CalcomTimeSlots(slots []TimeSlot, config SlotConfig) {
	<div class="border-t border-border pt-6">
		<div class="flex items-center justify-between mb-4 flex-wrap gap-2">
			<h4 class="text-lg font-semibold text-foreground">
				Available times for { config.SelectedDate }
			</h4>
			<button
				type="button"
				class="text-sm text-muted-foreground hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring rounded px-1"
				hx-get={ fmt.Sprintf("/api/plugins/calcomblock/reset?blockId=%s", config.BlockID) }
				hx-target={ fmt.Sprintf("#calcom-slots-%s", config.BlockID) }
				hx-swap="innerHTML"
			>
				← Change date
			</button>
		</div>
		if len(slots) == 0 {
			<p class="text-muted-foreground py-4">No available times for this date. Please select another date.</p>
		} else {
			<div class="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 gap-2" role="grid" aria-label="Available time slots">
				for _, slot := range slots {
					<button
						type="button"
						role="gridcell"
						class="px-4 py-4 border border-border rounded-lg text-sm font-medium text-foreground hover:bg-primary hover:text-primary-foreground hover:border-primary transition-all focus:outline-none focus:ring-2 focus:ring-ring"
						hx-get={ fmt.Sprintf("/api/plugins/calcomblock/form?blockId=%s&start=%s&date=%s&username=%s&eventType=%s",
							config.BlockID, slot.Start, config.SelectedDate, config.Username, config.EventType) }
						hx-target={ fmt.Sprintf("#calcom-form-%s", config.BlockID) }
						hx-swap="innerHTML"
						hx-include={ fmt.Sprintf("#calcom-widget-%s input[name='locale']", config.BlockID) }
					>
						{ slot.DisplayTime }
					</button>
				}
			</div>
		}
	</div>
	// OOB: advance step indicator to "Select Time" + clear any stale form
	// from a previous slot selection.
	@calcomStepIndicatorOOB(config.BlockID, 2)
	@calcomClearRegion(fmt.Sprintf("calcom-form-%s", config.BlockID), "calcom-booking-form")
}

// CalcomBookingForm renders the contact form for booking. When the event type
// has custom bookingFields configured, each is rendered via bookingField. When
// the list is empty (typical for default event types or a transient fetch
// error), a built-in name + email pair is rendered so the flow still works.
//
// Returned as the body of GET /form. Also emits an OOB update advancing the
// step indicator to "Confirm".
templ CalcomBookingForm(formConfig FormConfig) {
	<div class="border-t border-border pt-6 mt-6">
		<div class="flex items-center justify-between mb-4 flex-wrap gap-2">
			<div>
				<h4 class="text-lg font-semibold text-foreground" id={ fmt.Sprintf("booking-summary-%s", formConfig.BlockID) }>Your Details</h4>
				<p class="text-sm text-muted-foreground" id={ fmt.Sprintf("booking-when-%s", formConfig.BlockID) }>
					Booking for { formConfig.SelectedDate } at { formConfig.SelectedTime }
				</p>
			</div>
			<button
				type="button"
				class="text-sm text-muted-foreground hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring rounded px-1"
				hx-get={ fmt.Sprintf("/api/plugins/calcomblock/slots?username=%s&eventType=%s&date=%s&blockId=%s",
					formConfig.Username, formConfig.EventType, formConfig.SelectedDate, formConfig.BlockID) }
				hx-target={ fmt.Sprintf("#calcom-slots-%s", formConfig.BlockID) }
				hx-swap="innerHTML"
			>
				← Change time
			</button>
		</div>
		// hx-include pulls the widget-level hidden inputs into the POST: locale
		// (phone dial-code inference) and unavailableMessage (admin-authored
		// Tier-2 copy). Both live in the widget root, outside this form partial.
		<form
			hx-post="/api/plugins/calcomblock/book"
			hx-target={ fmt.Sprintf("#calcom-confirm-%s", formConfig.BlockID) }
			hx-swap="innerHTML"
			hx-include={ fmt.Sprintf("#calcom-widget-%s input[name='locale'], #calcom-widget-%s input[name='unavailableMessage']", formConfig.BlockID, formConfig.BlockID) }
			hx-indicator={ fmt.Sprintf("#calcom-loading-%s", formConfig.BlockID) }
			hx-disabled-elt="find button[type='submit']"
			class="space-y-4"
		>
			<input type="hidden" name="blockId" value={ formConfig.BlockID }/>
			<input type="hidden" name="username" value={ formConfig.Username }/>
			<input type="hidden" name="eventType" value={ formConfig.EventType }/>
			<input type="hidden" name="start" value={ formConfig.StartTime }/>
			// Honeypot: a real user can't see this off-screen input; bots and
			// over-eager autofill heuristics still hit it. The name is
			// deliberately non-semantic so Chrome's autofill doesn't fire on
			// "company" / "address" / "website".
			<input
				type="text"
				name="bn_message_extra"
				tabindex="-1"
				autocomplete="off"
				aria-hidden="true"
				class="absolute -left-[9999px] h-0 w-0 overflow-hidden"
				value=""
			/>
			if len(formConfig.BookingFields) > 0 {
				for _, f := range formConfig.BookingFields {
					@bookingField(f, formConfig.BlockID, formConfig.DefaultCountryCode)
				}
			} else {
				@bookingField(BookingField{Name: "name", Label: "Name", Type: "name", Required: true, Placeholder: "Your name"}, formConfig.BlockID, "")
				@bookingField(BookingField{Name: "email", Label: "Email", Type: "email", Required: true, Placeholder: "your@email.com"}, formConfig.BlockID, "")
			}
			// Cap proof-of-work widget — rendered only when the block enables
			// captcha. On solve it injects a hidden cap-token field into this form,
			// which HandleCreateBooking verifies (server-authoritative: same flag).
			if formConfig.CaptchaEnabled {
				<div class="pt-1">
					@CaptchaWidget()
				</div>
			}
			<button
				type="submit"
				aria-describedby={ fmt.Sprintf("booking-when-%s", formConfig.BlockID) }
				class="w-full bg-primary text-primary-foreground px-6 py-3 rounded-lg font-medium hover:bg-primary/90 transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:opacity-60 disabled:cursor-wait"
			>
				Confirm Booking
			</button>
		</form>
	</div>
	// OOB: advance step indicator to "Confirm".
	@calcomStepIndicatorOOB(formConfig.BlockID, 3)
}

// bookingField renders a single Cal.com bookingField in the correct widget for
// its type. Unknown types fall back to a text input. defaultCountry is the ISO
// region pre-selected in the phone-field country combobox ("" = none).
templ bookingField(f BookingField, blockID string, defaultCountry string) {
	<div class="space-y-2">
		<label for={ fmt.Sprintf("%s-%s", f.Name, blockID) } class="block text-sm font-medium text-foreground">
			{ f.Label }
			if f.Required {
				<span class="text-destructive" aria-label="required">*</span>
			}
		</label>
		switch f.Type {
			case "textarea":
				<textarea
					id={ fmt.Sprintf("%s-%s", f.Name, blockID) }
					name={ f.Name }
					rows="3"
					placeholder={ f.Placeholder }
					if f.Required {
						required
						aria-required="true"
					}
					class="w-full px-3 py-2 border border-border rounded-lg bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent resize-none"
				>{ f.DefaultValue }</textarea>
			case "boolean", "checkbox":
				<div class="flex items-center gap-2">
					<input
						type="checkbox"
						id={ fmt.Sprintf("%s-%s", f.Name, blockID) }
						name={ f.Name }
						value="true"
						if f.Required {
							required
							aria-required="true"
						}
						class="h-4 w-4 rounded border-border text-primary focus:ring-2 focus:ring-ring"
					/>
					<span class="text-sm text-muted-foreground">{ f.Placeholder }</span>
				</div>
			case "select":
				<select
					id={ fmt.Sprintf("%s-%s", f.Name, blockID) }
					name={ f.Name }
					if f.Required {
						required
						aria-required="true"
					}
					class="w-full px-3 py-2 border border-border rounded-lg bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
				>
					if f.Placeholder != "" {
						<option value="">{ f.Placeholder }</option>
					}
					for _, opt := range f.Options {
						<option value={ opt.Value } selected?={ opt.Value == f.DefaultValue }>{ opt.Label }</option>
					}
				</select>
			case "multiselect":
				<div class="space-y-1" role="group" aria-label={ f.Label }>
					for _, opt := range f.Options {
						<label class="flex items-center gap-2 text-sm text-foreground">
							<input
								type="checkbox"
								name={ f.Name }
								value={ opt.Value }
								class="h-4 w-4 rounded border-border text-primary focus:ring-2 focus:ring-ring"
							/>
							{ opt.Label }
						</label>
					}
				</div>
			case "radio":
				<div class="space-y-1" role="radiogroup" aria-label={ f.Label }>
					for _, opt := range f.Options {
						<label class="flex items-center gap-2 text-sm text-foreground">
							<input
								type="radio"
								name={ f.Name }
								value={ opt.Value }
								checked?={ opt.Value == f.DefaultValue }
								if f.Required {
									required
									aria-required="true"
								}
								class="h-4 w-4 border-border text-primary focus:ring-2 focus:ring-ring"
							/>
							{ opt.Label }
						</label>
					}
				</div>
			case "phone":
				<div class="flex gap-2">
					<div class="relative w-40 sm:w-48 shrink-0" data-cc-combobox>
						<input type="hidden" name="phoneCountry" value={ defaultCountry }/>
						<input
							type="text"
							class="cc-search w-full px-3 py-2 border border-border rounded-lg bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
							role="combobox"
							aria-expanded="false"
							aria-label="Phone country code"
							autocomplete="off"
							placeholder="Country"
							value={ countryLabel(defaultCountry) }
						/>
						<ul class="cc-options hidden absolute z-50 mt-1 max-h-60 w-64 overflow-auto rounded-lg border border-border bg-popover text-popover-foreground shadow-lg" role="listbox" aria-label="Select country">
							for _, c := range countries {
								<li
									class="cc-option px-3 py-2 text-sm cursor-pointer hover:bg-accent hover:text-accent-foreground aria-selected:bg-accent aria-selected:text-accent-foreground"
									role="option"
									data-code={ c.Code }
									data-dial={ c.Dial }
									aria-selected={ fmt.Sprintf("%t", c.Code == defaultCountry) }
								>
									{ countryFlag(c.Code) } { c.Name } ({ c.Dial })
								</li>
							}
						</ul>
					</div>
					<input
						type="tel"
						id={ fmt.Sprintf("%s-%s", f.Name, blockID) }
						name={ f.Name }
						value={ f.DefaultValue }
						placeholder={ f.Placeholder }
						if f.Required {
							required
							aria-required="true"
						}
						class="flex-1 px-3 py-2 border border-border rounded-lg bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent"
					/>
				</div>
			default:
				<input
					type={ inputTypeFor(f.Type) }
					id={ fmt.Sprintf("%s-%s", f.Name, blockID) }
					name={ f.Name }
					value={ f.DefaultValue }
					placeholder={ f.Placeholder }
					if f.Required {
						required
						aria-required="true"
					}
					class="w-full px-3 py-2 border border-border rounded-lg bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent"
				/>
		}
	</div>
}

// CalcomConfirmation renders the success panel after a booking is created.
// Returned as the body of POST /book; also emits OOB clears that wipe the
// date grid + slots + form so the visitor is presented with only the
// confirmation (preventing accidental double-submits) and an OOB step-indicator
// update marking all three steps complete.
templ CalcomConfirmation(result BookingResult, blockID string) {
	<div class="mt-6 text-center p-8 bg-success/10 border border-success/20 rounded-xl">
		<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-success/15 mb-4">
			<svg class="h-8 w-8 text-success" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
				<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
			</svg>
		</div>
		<h4 class="text-xl font-semibold text-foreground mb-2">Booking Confirmed!</h4>
		<p class="text-muted-foreground mb-4">
			Your appointment has been scheduled for { result.FormattedDateTime }.
		</p>
		<p class="text-sm text-muted-foreground">
			A confirmation email has been sent to <span class="font-medium">{ result.Email }</span>.
		</p>
		if result.MeetingLink != "" {
			<a
				href={ templ.SafeURL(result.MeetingLink) }
				target="_blank"
				rel="noopener"
				class="inline-flex items-center gap-2 mt-4 text-primary hover:underline"
			>
				<span>Join meeting</span>
				<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
					<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path>
				</svg>
			</a>
		}
		// Machine-readable booking identity for the "manage your booking" feature.
		// Hidden from view; the script below reads it and persists the booking to
		// this visitor's localStorage so they can cancel / redo it on a later
		// visit. templ auto-escapes every attribute value.
		<div
			data-calcom-booking
			data-uid={ result.BookingID }
			data-when={ result.FormattedDateTime }
			data-email={ result.Email }
			hidden
		></div>
		// Persist {uid, when, email, savedAt} under a STABLE key derived from the
		// widget root's data-username + data-event-type (NOT the per-render
		// blockId). HTMX evaluates this script when it swaps the confirmation in.
		// Skips when uid is empty — the honeypot path renders a fake confirmation
		// with no UID, which must never poison the saved-booking panel.
		<script>
			(function(){
				var s = document.currentScript;
				if (!s) return;
				var holder = s.parentElement ? s.parentElement.querySelector("[data-calcom-booking]") : null;
				var w = s.closest ? s.closest(".calcom-booking-widget") : null;
				if (!holder || !w) return;
				var uid = holder.getAttribute("data-uid") || "";
				if (!uid) return; // honeypot / no real booking — do not persist
				var key = "calcom:booking:" + (w.getAttribute("data-username") || "") + ":" + (w.getAttribute("data-event-type") || "");
				try {
					window.localStorage.setItem(key, JSON.stringify({
						uid: uid,
						when: holder.getAttribute("data-when") || "",
						email: holder.getAttribute("data-email") || "",
						savedAt: new Date().toISOString()
					}));
				} catch(e) {}
			})();
		</script>
	</div>
	// OOB: mark all three steps complete and clear the upstream regions so
	// the visitor cannot re-submit the form (prevents the double-booking
	// footgun the adversarial review caught).
	@calcomStepIndicatorOOB(blockID, 4)
	@calcomClearRegion(fmt.Sprintf("calcom-date-grid-%s", blockID), "calcom-date-picker")
	@calcomClearRegion(fmt.Sprintf("calcom-slots-%s", blockID), "calcom-time-slots mt-6")
	@calcomClearRegion(fmt.Sprintf("calcom-form-%s", blockID), "calcom-booking-form")
}

// calcomSavedBooking renders the normally-hidden "manage your booking" panel.
// The widget script un-hides it (and hides the date grid) on DOMContentLoaded
// when this visitor's localStorage holds a booking for this username/event,
// filling in the date/time + email from storage. It offers two actions:
//
//   - Cancel: an HTMX form POSTing /cancel with a hidden `uid` the script
//     populates from localStorage; on a successful swap the script clears the
//     storage key (htmx:afterRequest listener in the widget script).
//   - Reschedule (redo): cancels the existing booking then reveals the grid to
//     rebook — wired in the widget script to trigger the same Cancel POST, so
//     the visitor can't end up double-booked.
//
// Cancellation is authorized by the Cal.com booking UID alone (the same
// capability model as Cal.com's emailed cancel link); the server rate-limits
// the POST by IP as the abuse guard.
templ calcomSavedBooking(blockID string) {
	<div
		id={ fmt.Sprintf("calcom-saved-%s", blockID) }
		data-calcom-saved
		class="calcom-saved-booking hidden mt-2 p-4 sm:p-6 bg-muted/40 border border-border rounded-xl"
	>
		<div class="flex items-start gap-3">
			<div class="inline-flex items-center justify-center w-10 h-10 shrink-0 rounded-full bg-primary/10 text-primary">
				<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true">
					<path stroke-linecap="round" stroke-linejoin="round" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
				</svg>
			</div>
			<div class="min-w-0">
				<h4 class="text-lg font-semibold text-foreground">You have a booking</h4>
				<p class="text-sm text-muted-foreground mt-0.5">
					<span data-calcom-saved-when class="font-medium text-foreground"></span>
				</p>
				<p class="text-xs text-muted-foreground mt-0.5">
					Confirmation sent to <span data-calcom-saved-email class="font-medium"></span>
				</p>
			</div>
		</div>
		<div class="flex flex-wrap gap-3 mt-4">
			// Cancel: HTMX form whose hidden uid the widget script fills from
			// localStorage. Targets the confirm region; on success the script
			// clears the localStorage key.
			<form
				hx-post="/api/plugins/calcomblock/cancel"
				hx-target={ fmt.Sprintf("#calcom-confirm-%s", blockID) }
				hx-swap="innerHTML"
				hx-indicator={ fmt.Sprintf("#calcom-loading-%s", blockID) }
				hx-disabled-elt="find button[type='submit']"
			>
				<input type="hidden" name="blockId" value={ blockID }/>
				<input type="hidden" name="uid" value=""/>
				<button
					type="submit"
					data-calcom-cancel-submit
					data-action="cancel"
					data-entity="booking"
					class="inline-flex items-center gap-2 bg-destructive text-destructive-foreground px-4 py-2 rounded-lg text-sm font-medium hover:bg-destructive/90 transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:opacity-60 disabled:cursor-wait"
				>
					Cancel booking
				</button>
			</form>
			// Reschedule / redo: cancels the existing booking then reveals the
			// grid to book again (handled in the widget script).
			<button
				type="button"
				data-calcom-redo
				data-action="reschedule"
				data-entity="booking"
				class="inline-flex items-center gap-2 border border-border text-foreground px-4 py-2 rounded-lg text-sm font-medium hover:bg-muted transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
			>
				Reschedule
			</button>
		</div>
	</div>
}

// CalcomCancelled is the body returned by POST /cancel on success. It swaps a
// "your booking was cancelled" message into #calcom-confirm-{id} and rewinds
// the step indicator to step 1. The date grid is restored client-side: when
// cancel fires from the saved panel the grid is only CSS-hidden (never removed),
// so the widget script's htmx:afterRequest listener re-shows it (showGrid) —
// avoiding a server-side grid re-render that would need the booking config the
// cancel POST doesn't carry. That same listener also clears the saved booking
// from localStorage and hides the saved panel.
templ CalcomCancelled(blockID string) {
	<div class="mt-6 text-center p-8 bg-muted/40 border border-border rounded-xl" role="status">
		<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-muted mb-4">
			<svg class="h-8 w-8 text-muted-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true">
				<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"></path>
			</svg>
		</div>
		<h4 class="text-xl font-semibold text-foreground mb-2">Booking cancelled</h4>
		<p class="text-muted-foreground">
			Your booking has been cancelled. You can book another time below whenever you're ready.
		</p>
	</div>
	// OOB: rewind the step indicator to step 1 (the grid is re-shown client-side).
	@calcomStepIndicatorOOB(blockID, 1)
}

// CalcomReset is the body returned by GET /reset — empty slots region with
// an OOB clear of the form (so the user pressing "Change date" loses any
// stale form rendered from an earlier slot click) and an OOB rewind of the
// step indicator to step 1.
templ CalcomReset(blockID string) {
	// Main response body — empty, replaces #calcom-slots-{id}.
	// OOB: clear the form region and rewind the step indicator.
	@calcomClearRegion(fmt.Sprintf("calcom-form-%s", blockID), "calcom-booking-form")
	@calcomStepIndicatorOOB(blockID, 1)
}

// CalcomError renders an error message.
templ CalcomError(message string, blockID string, canRetry bool) {
	<div class="mt-6 p-4 bg-destructive/10 border border-destructive/20 rounded-lg" role="alert">
		<p class="text-sm text-destructive">{ message }</p>
		if canRetry {
			<button
				type="button"
				class="mt-3 text-sm text-destructive hover:underline focus:outline-none focus:ring-2 focus:ring-ring rounded px-1"
				hx-get={ fmt.Sprintf("/api/plugins/calcomblock/reset?blockId=%s", blockID) }
				hx-target={ fmt.Sprintf("#calcom-widget-%s", blockID) }
				hx-swap="outerHTML"
			>
				Try again
			</button>
		}
	</div>
}
