import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { __ } from "@wordpress/i18n";
import { decodeEntities } from "@wordpress/html-entities";
import { ChevronDown, Loader2, Search, X } from "lucide-react";

/**
 * Searchable AJAX select for the Subscription Box admin editor.
 *
 * Backed by the shared select-options REST endpoint
 * (arraysubs/v1/members-access/select-options). Search starts at 3 typed
 * characters and is debounced; saved values are label-resolved via the
 * `include` parameter.
 *
 * Props:
 * - value: number | number[] (multiple)
 * - onChange(next)
 * - endpoint: e.g. "products" or "terms?taxonomy=product_cat"
 * - multiple: bool
 * - placeholder: string
 */
const MIN_SEARCH_CHARS = 3;
const SEARCH_DEBOUNCE_MS = 250;

const optionCache = new Map();

const AjaxSelect = ({
	value,
	onChange,
	endpoint,
	multiple = false,
	placeholder = "",
	schedule = null,
}) => {
	const env = window.arraySubsBox || {};
	const [isOpen, setIsOpen] = useState(false);
	const [searchTerm, setSearchTerm] = useState("");
	const [options, setOptions] = useState([]);
	const [labels, setLabels] = useState({});
	const [isLoading, setIsLoading] = useState(false);
	// A failed request must not look like an empty result set: a bad nonce or
	// a missing capability would otherwise read as "No results found."
	const [hasError, setHasError] = useState(false);
	const wrapRef = useRef(null);
	const abortRef = useRef(null);

	const selectedIds = useMemo(() => {
		if (multiple) {
			return Array.isArray(value) ? value.map(Number).filter(Boolean) : [];
		}
		return value ? [Number(value)] : [];
	}, [value, multiple]);

	// Results depend on the box schedule, so it is part of the cache identity.
	const cacheScope = useMemo(
		() => (schedule ? `${endpoint}|${schedule.period}:${schedule.interval}` : endpoint),
		[endpoint, schedule]
	);

	const buildUrl = useCallback(
		(params) => {
			const [type, extraQuery] = endpoint.split("?");
			// Schedule-scoped endpoints only offer products/categories that can
			// share the box's billing cycle; other endpoints use the shared
			// select-options route.
			const scoped = type === "box-products" || type === "box-categories";
			const path = scoped
				? `subscription-box/${type === "box-products" ? "products" : "categories"}`
				: "members-access/select-options";
			const url = new URL(`${env.apiUrl}${path}`, window.location.origin);

			if (!scoped) {
				url.searchParams.set("type", type);
			} else if (schedule) {
				url.searchParams.set("period", schedule.period);
				url.searchParams.set("interval", schedule.interval);
			}

			if (extraQuery) {
				new URLSearchParams(extraQuery).forEach((v, k) => url.searchParams.set(k, v));
			}
			Object.entries(params).forEach(([k, v]) => {
				if (v !== undefined && v !== "") {
					url.searchParams.set(k, v);
				}
			});
			return url.toString();
		},
		[endpoint, env.apiUrl, schedule]
	);

	const fetchOptions = useCallback(
		async (params, cacheKey) => {
			if (cacheKey && optionCache.has(cacheKey)) {
				return optionCache.get(cacheKey);
			}
			if (abortRef.current) {
				abortRef.current.abort();
			}
			const controller = new AbortController();
			abortRef.current = controller;

			const response = await fetch(buildUrl(params), {
				headers: { "X-WP-Nonce": env.nonce },
				signal: controller.signal,
			});
			if (!response.ok) {
				// Never cache an error — a transient failure must not stick as
				// a permanent "No results" for this query.
				throw new Error(`Request failed: ${response.status}`);
			}
			const json = await response.json();
			const rows = Array.isArray(json?.data) ? json.data : Array.isArray(json) ? json : [];
			const normalized = rows.map((row) => ({
				value: Number(row.value),
				label: decodeEntities(String(row.label)),
			}));
			// Only cache non-empty successful result sets.
			if (cacheKey && normalized.length > 0) {
				optionCache.set(cacheKey, normalized);
			}
			return normalized;
		},
		[buildUrl, env.nonce]
	);

	// Resolve labels for saved ids.
	useEffect(() => {
		const missing = selectedIds.filter((id) => !labels[id]);
		if (missing.length === 0) {
			return;
		}
		fetchOptions({ include: missing.join(",") }, `${cacheScope}|include:${missing.join(",")}`)
			.then((rows) => {
				setLabels((prev) => {
					const next = { ...prev };
					rows.forEach((row) => {
						next[row.value] = row.label;
					});
					return next;
				});
			})
			.catch(() => {});
		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, [selectedIds.join(",")]);

	// Debounced search — requires MIN_SEARCH_CHARS.
	useEffect(() => {
		if (!isOpen) {
			return undefined;
		}
		if (searchTerm.length < MIN_SEARCH_CHARS) {
			setOptions([]);
			setIsLoading(false);
			return undefined;
		}
		setIsLoading(true);
		setHasError(false);
		const timer = setTimeout(() => {
			fetchOptions({ search: searchTerm }, `${cacheScope}|search:${searchTerm}`)
				.then((rows) => {
					setOptions(rows);
					setIsLoading(false);
				})
				.catch((error) => {
					if (error.name !== "AbortError") {
						setIsLoading(false);
						setHasError(true);
					}
				});
		}, SEARCH_DEBOUNCE_MS);
		return () => clearTimeout(timer);
	}, [searchTerm, isOpen, endpoint, fetchOptions]);

	// Close on outside click.
	useEffect(() => {
		if (!isOpen) {
			return undefined;
		}
		const onDocClick = (event) => {
			if (wrapRef.current && !wrapRef.current.contains(event.target)) {
				setIsOpen(false);
				setSearchTerm("");
			}
		};
		document.addEventListener("mousedown", onDocClick);
		return () => document.removeEventListener("mousedown", onDocClick);
	}, [isOpen]);

	const toggleValue = (id) => {
		if (multiple) {
			const next = selectedIds.includes(id)
				? selectedIds.filter((v) => v !== id)
				: [...selectedIds, id];
			onChange(next);
		} else {
			onChange(selectedIds.includes(id) ? 0 : id);
			setIsOpen(false);
			setSearchTerm("");
		}
	};

	const removeValue = (id, event) => {
		event.stopPropagation();
		if (multiple) {
			onChange(selectedIds.filter((v) => v !== id));
		} else {
			onChange(0);
		}
	};

	return (
		<div className="arraysubs-box-ajax-select" ref={wrapRef}>
			<button
				type="button"
				className="arraysubs-box-ajax-select__trigger"
				onClick={() => setIsOpen((open) => !open)}
				aria-expanded={isOpen}
			>
				<span className="arraysubs-box-ajax-select__values">
					{selectedIds.length === 0 && (
						<span className="arraysubs-box-ajax-select__placeholder">
							{placeholder || __("Search…", "arraysubs")}
						</span>
					)}
					{selectedIds.map((id) => (
						<span key={id} className="arraysubs-box-ajax-select__tag">
							{labels[id] || `#${id}`}
							<span
								role="button"
								tabIndex={0}
								className="arraysubs-box-ajax-select__tag-remove"
								onClick={(event) => removeValue(id, event)}
								onKeyDown={(event) => {
									if (event.key === "Enter" || event.key === " ") {
										removeValue(id, event);
									}
								}}
								aria-label={__("Remove", "arraysubs")}
							>
								<X size={12} />
							</span>
						</span>
					))}
				</span>
				<ChevronDown size={14} className="arraysubs-box-ajax-select__chevron" />
			</button>

			{isOpen && (
				<div className="arraysubs-box-ajax-select__dropdown">
					<div className="arraysubs-box-ajax-select__search">
						<Search size={14} />
						<input
							type="text"
							value={searchTerm}
							onChange={(event) => setSearchTerm(event.target.value)}
							placeholder={__("Type at least 3 characters…", "arraysubs")}
							// eslint-disable-next-line jsx-a11y/no-autofocus
							autoFocus
						/>
						{isLoading && <Loader2 size={14} className="arraysubs-box-spin" />}
					</div>
					<div className="arraysubs-box-ajax-select__options">
						{searchTerm.length < MIN_SEARCH_CHARS && (
							<div className="arraysubs-box-ajax-select__note">
								{__("Type at least 3 characters to search.", "arraysubs")}
							</div>
						)}
						{searchTerm.length >= MIN_SEARCH_CHARS && hasError && (
							<div className="arraysubs-box-ajax-select__note arraysubs-box-ajax-select__note--error">
								{__("Could not load results. Reload the page and try again.", "arraysubs")}
							</div>
						)}
						{searchTerm.length >= MIN_SEARCH_CHARS && !isLoading && !hasError && options.length === 0 && (
							<div className="arraysubs-box-ajax-select__note">
								{__("No results found.", "arraysubs")}
							</div>
						)}
						{options.map((option) => (
							<button
								type="button"
								key={option.value}
								className={`arraysubs-box-ajax-select__option${
									selectedIds.includes(option.value) ? " is-selected" : ""
								}`}
								onClick={() => toggleValue(option.value)}
							>
								{option.label}
							</button>
						))}
					</div>
				</div>
			)}
		</div>
	);
};

export default AjaxSelect;
