---
/**
 * LiveSearch - A live search component for EmDash content
 *
 * ```astro
 * ---
 * import LiveSearch from "emdash/ui/search";
 * ---
 * <LiveSearch placeholder="Search articles..." collections={["posts", "pages"]} />
 * ```
 *
 * Customize the result rendering with slots:
 *
 * ```astro
 * <LiveSearch>
 *   <span slot="no-results">Nothing found</span>
 *   <a slot="result" let:result href={`/${result.collection}/${result.slug}`}>
 *     {result.title}
 *   </a>
 * </LiveSearch>
 * ```
 */

export interface Props {
	/** Placeholder text for the search input */
	placeholder?: string;
	/** Collections to search (defaults to all searchable collections) */
	collections?: string[];
	/** Minimum characters before searching (defaults to 2) */
	minChars?: number;
	/** Debounce delay in milliseconds (defaults to 300) */
	debounce?: number;
	/** Maximum results to show (defaults to 10) */
	limit?: number;
	/** CSS class for the container */
	class?: string;
	/** CSS class for the input */
	inputClass?: string;
	/** CSS class for the results container */
	resultsClass?: string;
	/** CSS class for each result item */
	resultClass?: string;
	/** Show snippets in results (defaults to true) */
	showSnippets?: boolean;
	/** Autofocus the input */
	autofocus?: boolean;
	/** Use suggestions endpoint for autocomplete (defaults to false for full search) */
	suggestMode?: boolean;
	/** Expand input width on focus (provide collapsed and expanded widths) */
	expandOnFocus?: { collapsed: string; expanded: string };
}

const {
	placeholder = "Search...",
	collections,
	minChars = 2,
	debounce = 300,
	limit = 10,
	class: className = "",
	inputClass = "",
	resultsClass = "",
	resultClass = "",
	showSnippets = true,
	autofocus = false,
	suggestMode = false,
	expandOnFocus,
} = Astro.props;

const config = {
	collections: collections?.join(",") ?? "",
	minChars,
	debounce,
	limit,
	showSnippets,
	suggestMode,
	expandOnFocus: expandOnFocus ?? null,
};
---

<emdash-live-search
	class:list={["emdash-live-search", className]}
	data-config={JSON.stringify(config)}
>
	<input
		type="search"
		placeholder={placeholder}
		class:list={["emdash-live-search-input", inputClass]}
		autocomplete="off"
		autofocus={autofocus}
	/>
	<div class:list={["emdash-live-search-results", resultsClass]} hidden>
		<slot name="loading">
			<div class="emdash-live-search-loading">Searching...</div>
		</slot>
		<slot name="no-results">
			<div class="emdash-live-search-no-results">No results found</div>
		</slot>
		<template class="emdash-live-search-result-template">
			<slot name="result">
				<a class:list={["emdash-live-search-result", resultClass]} href="">
					<span class="emdash-live-search-result-title"></span>
					<span class="emdash-live-search-result-collection"></span>
					<span class="emdash-live-search-result-snippet"></span>
				</a>
			</slot>
		</template>
		<div class="emdash-live-search-results-list"></div>
	</div>
</emdash-live-search>

<script>
	interface SearchResult {
		collection: string;
		id: string;
		slug: string | null;
		title?: string;
		snippet?: string;
		score: number;
	}

	interface Suggestion {
		collection: string;
		id: string;
		title: string;
	}

	interface Config {
		collections: string;
		minChars: number;
		debounce: number;
		limit: number;
		showSnippets: boolean;
		suggestMode: boolean;
		expandOnFocus: { collapsed: string; expanded: string } | null;
	}

	class EmDashLiveSearch extends HTMLElement {
		private input: HTMLInputElement | null = null;
		private resultsContainer: HTMLElement | null = null;
		private resultsList: HTMLElement | null = null;
		private loadingEl: HTMLElement | null = null;
		private noResultsEl: HTMLElement | null = null;
		private template: HTMLTemplateElement | null = null;
		private config: Config = {
			collections: "",
			minChars: 2,
			debounce: 300,
			limit: 10,
			showSnippets: true,
			suggestMode: false,
			expandOnFocus: null,
		};
		private debounceTimer: ReturnType<typeof setTimeout> | null = null;
		private abortController: AbortController | null = null;

		connectedCallback() {
			// Parse config
			const configStr = this.dataset.config;
			if (configStr) {
				try {
					this.config = { ...this.config, ...JSON.parse(configStr) };
				} catch {
					// Use defaults
				}
			}

			// Get elements
			this.input = this.querySelector(".emdash-live-search-input");
			this.resultsContainer = this.querySelector(
				".emdash-live-search-results"
			);
			this.resultsList = this.querySelector(
				".emdash-live-search-results-list"
			);
			this.loadingEl = this.querySelector(".emdash-live-search-loading");
			this.noResultsEl = this.querySelector(".emdash-live-search-no-results");
			this.template = this.querySelector(
				".emdash-live-search-result-template"
			);

			if (!this.input) return;

			// Event listeners
			this.input.addEventListener("input", this.handleInput.bind(this));
			this.input.addEventListener("keydown", this.handleKeydown.bind(this));

			// Close on click outside
			document.addEventListener("click", (e) => {
				if (!this.contains(e.target as Node)) {
					this.hideResults();
				}
			});

			// Show results on focus if we have a query
			this.input.addEventListener("focus", () => {
				if (this.input && this.input.value.length >= this.config.minChars) {
					this.showResults();
				}
				// Handle expand on focus
				if (this.config.expandOnFocus && this.input) {
					this.input.style.width = this.config.expandOnFocus.expanded;
				}
			});

			// Handle collapse on blur
			this.input.addEventListener("blur", () => {
				// Delay to allow clicking on results
				setTimeout(() => {
					if (
						this.config.expandOnFocus &&
						this.input &&
						document.activeElement !== this.input
					) {
						// Only collapse if not focused on a result
						const activeInResults = this.resultsContainer?.contains(
							document.activeElement
						);
						if (!activeInResults) {
							this.input.style.width = this.config.expandOnFocus.collapsed;
						}
					}
				}, 150);
			});

			// Set initial width if expandOnFocus is enabled
			if (this.config.expandOnFocus) {
				this.input.style.width = this.config.expandOnFocus.collapsed;
				// Apply transition after initial width is set so the collapsed
				// width doesn't animate in on page load
				requestAnimationFrame(() => {
					if (this.input) {
						this.input.style.transition = "width 0.2s ease";
					}
				});
			}
		}

		private handleInput() {
			if (!this.input) return;

			const query = this.input.value.trim();

			// Clear any pending request
			if (this.debounceTimer) {
				clearTimeout(this.debounceTimer);
			}

			if (query.length < this.config.minChars) {
				this.hideResults();
				return;
			}

			// Debounce the search
			this.debounceTimer = setTimeout(() => {
				this.search(query);
			}, this.config.debounce);
		}

		private handleKeydown(e: KeyboardEvent) {
			if (e.key === "Escape") {
				this.hideResults();
				this.input?.blur();
			} else if (e.key === "ArrowDown" || e.key === "ArrowUp") {
				e.preventDefault();
				this.navigateResults(e.key === "ArrowDown" ? 1 : -1);
			} else if (e.key === "Enter") {
				const focused = this.resultsList?.querySelector(
					".emdash-live-search-result:focus, .emdash-live-search-result.focused"
				) as HTMLAnchorElement | null;
				if (focused?.href) {
					window.location.href = focused.href;
				}
			}
		}

		private navigateResults(direction: number) {
			if (!this.resultsList) return;

			const results = [
				...this.resultsList.querySelectorAll(".emdash-live-search-result"),
			] as HTMLElement[];
			if (results.length === 0) return;

			const currentIndex = results.findIndex(
				(r) => r === document.activeElement || r.classList.contains("focused")
			);

			// Remove current focus
			results.forEach((r) => r.classList.remove("focused"));

			let nextIndex: number;
			if (currentIndex === -1) {
				nextIndex = direction > 0 ? 0 : results.length - 1;
			} else {
				nextIndex = currentIndex + direction;
				if (nextIndex < 0) nextIndex = results.length - 1;
				if (nextIndex >= results.length) nextIndex = 0;
			}

			const nextResult = results[nextIndex];
			if (nextResult) {
				nextResult.classList.add("focused");
				nextResult.focus();
			}
		}

		private async search(query: string) {
			// Cancel any in-flight request
			if (this.abortController) {
				this.abortController.abort();
			}
			this.abortController = new AbortController();

			this.showLoading();

			try {
				const endpoint = this.config.suggestMode
					? "/_emdash/api/search/suggest"
					: "/_emdash/api/search";

				const params = new URLSearchParams({
					q: query,
					limit: String(this.config.limit),
				});

				if (this.config.collections) {
					params.set("collections", this.config.collections);
				}

				const response = await fetch(`${endpoint}?${params}`, {
					signal: this.abortController.signal,
				});

				if (!response.ok) {
					throw new Error("Search failed");
				}

				const responseData = await response.json();
				// Handle API response envelope: { data: { items: [...] } }
				const data = responseData.data || responseData;

				if (this.config.suggestMode) {
					this.renderSuggestions(data.items || data.suggestions || []);
				} else {
					this.renderResults(data.items || data.results || []);
				}
			} catch (error) {
				if ((error as Error).name === "AbortError") {
					// Request was cancelled, ignore
					return;
				}
				console.error("Search error:", error);
				this.showNoResults();
			}
		}

		private renderResults(results: SearchResult[]) {
			if (!this.resultsList || !this.template) return;

			this.resultsList.innerHTML = "";

			if (results.length === 0) {
				this.showNoResults();
				return;
			}

			for (const result of results) {
				const clone = this.template.content.cloneNode(true) as DocumentFragment;
				const link = clone.querySelector("a");

				if (link) {
					// Build URL - use slug if available, otherwise id
					const path = result.slug ?? result.id;
					link.href = `/${result.collection}/${path}`;

					// Fill in title
					const titleEl = link.querySelector(
						".emdash-live-search-result-title"
					);
					if (titleEl) {
						titleEl.textContent = result.title ?? result.slug ?? result.id;
					}

					// Fill in collection
					const collectionEl = link.querySelector(
						".emdash-live-search-result-collection"
					);
					if (collectionEl) {
						collectionEl.textContent = result.collection;
					}

					// Snippets returned by /api/search are already sanitised
					// server-side by sanitizeSnippet() — they contain only
					// HTML-escaped text plus literal <mark>...</mark> tags
					// around matched terms.
					const snippetEl = link.querySelector(
						".emdash-live-search-result-snippet"
					);
					if (snippetEl && this.config.showSnippets && result.snippet) {
						snippetEl.innerHTML = result.snippet;
					} else if (snippetEl) {
						snippetEl.remove();
					}

					// Store data on element for custom handling
					link.dataset.id = result.id;
					link.dataset.collection = result.collection;
					link.dataset.slug = result.slug ?? "";
					link.dataset.score = String(result.score);
				}

				this.resultsList.appendChild(clone);
			}

			this.showResultsList();
		}

		private renderSuggestions(suggestions: Suggestion[]) {
			if (!this.resultsList || !this.template) return;

			this.resultsList.innerHTML = "";

			if (suggestions.length === 0) {
				this.showNoResults();
				return;
			}

			for (const suggestion of suggestions) {
				const clone = this.template.content.cloneNode(true) as DocumentFragment;
				const link = clone.querySelector("a");

				if (link) {
					link.href = `/${suggestion.collection}/${suggestion.id}`;

					const titleEl = link.querySelector(
						".emdash-live-search-result-title"
					);
					if (titleEl) {
						titleEl.textContent = suggestion.title;
					}

					const collectionEl = link.querySelector(
						".emdash-live-search-result-collection"
					);
					if (collectionEl) {
						collectionEl.textContent = suggestion.collection;
					}

					// Remove snippet for suggestions
					const snippetEl = link.querySelector(
						".emdash-live-search-result-snippet"
					);
					if (snippetEl) {
						snippetEl.remove();
					}

					link.dataset.id = suggestion.id;
					link.dataset.collection = suggestion.collection;
				}

				this.resultsList.appendChild(clone);
			}

			this.showResultsList();
		}

		private showResults() {
			if (this.resultsContainer) {
				this.resultsContainer.hidden = false;
			}
		}

		private hideResults() {
			if (this.resultsContainer) {
				this.resultsContainer.hidden = true;
			}
		}

		private showLoading() {
			this.showResults();
			if (this.loadingEl) this.loadingEl.hidden = false;
			if (this.noResultsEl) this.noResultsEl.hidden = true;
			if (this.resultsList) this.resultsList.hidden = true;
		}

		private showNoResults() {
			this.showResults();
			if (this.loadingEl) this.loadingEl.hidden = true;
			if (this.noResultsEl) this.noResultsEl.hidden = false;
			if (this.resultsList) this.resultsList.hidden = true;
		}

		private showResultsList() {
			this.showResults();
			if (this.loadingEl) this.loadingEl.hidden = true;
			if (this.noResultsEl) this.noResultsEl.hidden = true;
			if (this.resultsList) this.resultsList.hidden = false;
		}
	}

	customElements.define("emdash-live-search", EmDashLiveSearch);
</script>

<style>
	/*
	 * LiveSearch uses CSS custom properties for theming.
	 * Override these in your site's CSS to match your design:
	 *
	 * --emdash-search-bg: Background color for the results dropdown
	 * --emdash-search-text: Text color
	 * --emdash-search-muted: Muted/secondary text color
	 * --emdash-search-border: Border color
	 * --emdash-search-hover: Hover/focus background color
	 * --emdash-search-highlight: Highlighted match text color
	 */

	.emdash-live-search {
		position: relative;
		display: inline-block;
	}

	.emdash-live-search-input {
		width: 100%;
		padding: 0.5rem 1rem;
		font-size: 1rem;
		border: 1px solid var(--emdash-search-border, #ccc);
		border-radius: 0.25rem;
		background: var(--emdash-search-bg, white);
		color: var(--emdash-search-text, inherit);
	}

	.emdash-live-search-input:focus {
		outline: none;
		border-color: var(--emdash-search-border-focus, #666);
	}

	.emdash-live-search-results {
		position: absolute;
		top: 100%;
		left: 0;
		right: 0;
		margin-top: 0.25rem;
		background: var(--emdash-search-bg, white);
		border: 1px solid var(--emdash-search-border, #ccc);
		border-radius: 0.25rem;
		box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
		max-height: 400px;
		overflow-y: auto;
		z-index: 1000;
	}

	.emdash-live-search-loading,
	.emdash-live-search-no-results {
		padding: 1rem;
		text-align: center;
		color: var(--emdash-search-muted, #666);
	}

	.emdash-live-search-result {
		display: block;
		padding: 0.75rem 1rem;
		text-decoration: none;
		color: var(--emdash-search-text, inherit);
		border-bottom: 1px solid var(--emdash-search-border, #eee);
	}

	.emdash-live-search-result:last-child {
		border-bottom: none;
	}

	.emdash-live-search-result:hover,
	.emdash-live-search-result:focus,
	.emdash-live-search-result.focused {
		background: var(--emdash-search-hover, #f5f5f5);
		outline: none;
	}

	.emdash-live-search-result-title {
		display: block;
		font-weight: 500;
	}

	.emdash-live-search-result-collection {
		display: block;
		font-size: 0.75rem;
		color: var(--emdash-search-muted, #888);
		text-transform: uppercase;
		letter-spacing: 0.05em;
		margin-top: 0.125rem;
	}

	.emdash-live-search-result-snippet {
		display: block;
		font-size: 0.875rem;
		color: var(--emdash-search-muted, #666);
		margin-top: 0.25rem;
	}

	/* Highlight matches in snippets (FTS5 uses <mark> tags) */
	.emdash-live-search-result-snippet :global(mark) {
		font-weight: 600;
		background: none;
		color: var(--emdash-search-highlight, var(--emdash-search-text, #000));
	}
</style>
