"use client";
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { useCallback, useEffect, useId, useMemo, useRef, useState, } from "react";
import { cn } from "@stigmer/theme";
import { useWorkspaceFileSearch, } from "./useWorkspaceFileSearch.js";
/** Cap on rendered rows — keeps the DOM bounded on huge repositories. */
const MAX_RESULTS = 200;
/**
 * Workspace-wide filename/path search surface.
 *
 * Searches the already-loaded listings across every workspace entry (via
 * {@link useWorkspaceFileSearch}) and opens a hit in the viewer through
 * `onOpenFile` — the same seam a file-tree click uses. Filename-first by design;
 * content search is a separate substrate-specific capability (DD-09).
 *
 * Keyboard/a11y follows the platform's combobox+listbox pattern (see
 * `mcp-server/McpServerPicker`): the input is a `role="combobox"` driving
 * `aria-activedescendant`, results are a `role="listbox"` of `role="option"`
 * rows, and ArrowUp/Down move a virtual focus with Enter to open — focus stays
 * in the input. All visual properties flow through `--stgm-*` tokens (DD-005).
 */
export function WorkspaceFileSearch({ entries, lister, onOpenFile, selectedFile, className, }) {
    const [query, setQuery] = useState("");
    const [focusIndex, setFocusIndex] = useState(-1);
    const inputRef = useRef(null);
    const listboxId = useId();
    const optionId = useCallback((index) => `${listboxId}-opt-${index}`, [listboxId]);
    const { groups, totalMatches, isLoading, isUnsupported } = useWorkspaceFileSearch({ entries, lister, query });
    // Flat, capped view of every rendered match — the index space the keyboard
    // and `aria-activedescendant` share with the grouped render below.
    const flatResults = useMemo(() => {
        const flat = [];
        for (const group of groups) {
            for (const match of group.matches) {
                if (flat.length >= MAX_RESULTS)
                    return flat;
                flat.push({ entryId: group.entry.id, entry: group.entry, match });
            }
        }
        return flat;
    }, [groups]);
    // A new query invalidates the previous virtual focus.
    useEffect(() => {
        setFocusIndex(-1);
    }, [query]);
    // Autofocus on mount — the user switched into Search intending to type.
    useEffect(() => {
        inputRef.current?.focus();
    }, []);
    // Keep the virtually-focused option in view (no-op where unsupported, e.g. tests).
    useEffect(() => {
        if (focusIndex < 0)
            return;
        const el = typeof document !== "undefined"
            ? document.getElementById(optionId(focusIndex))
            : null;
        el?.scrollIntoView?.({ block: "nearest" });
    }, [focusIndex, optionId]);
    const handleKeyDown = useCallback((e) => {
        if (e.key === "ArrowDown") {
            e.preventDefault();
            setFocusIndex((prev) => prev < flatResults.length - 1 ? prev + 1 : prev);
        }
        else if (e.key === "ArrowUp") {
            e.preventDefault();
            setFocusIndex((prev) => (prev > 0 ? prev - 1 : -1));
        }
        else if (e.key === "Enter") {
            e.preventDefault();
            const target = flatResults[focusIndex];
            if (target)
                onOpenFile(target.entryId, target.match.path);
        }
    }, [flatResults, focusIndex, onOpenFile]);
    const trimmed = query.trim();
    const showGroupHeaders = groups.length > 1;
    // A concise, screen-reader-only summary of the current result state. It lives
    // in a persistent polite live region (below) so status changes are announced
    // even as the visible states — which live in separate, unmounting branches —
    // swap. Deliberately terse: never the result list itself, only its shape.
    const statusMessage = isUnsupported
        ? ""
        : trimmed.length === 0
            ? ""
            : isLoading && totalMatches === 0 && groups.length === 0
                ? "Searching files…"
                : groups.length === 0
                    ? `No files matching ${trimmed}.`
                    : totalMatches > flatResults.length
                        ? `Showing the first ${flatResults.length} of ${totalMatches} matching files.`
                        : `${flatResults.length} matching ${flatResults.length === 1 ? "file" : "files"}.`;
    return (_jsxs("div", { className: cn("flex h-full flex-col", className), children: [_jsx("div", { role: "status", "aria-live": "polite", className: "sr-only", children: statusMessage }), _jsxs("div", { className: "flex items-center gap-1.5 border-b border-border px-2 py-1.5", children: [_jsx(SearchIcon, {}), _jsx("input", { ref: inputRef, type: "text", role: "combobox", "aria-expanded": true, "aria-controls": listboxId, "aria-activedescendant": focusIndex >= 0 ? optionId(focusIndex) : undefined, value: query, onChange: (e) => setQuery(e.target.value), onKeyDown: handleKeyDown, placeholder: "Search files by name\u2026", disabled: isUnsupported, className: cn("min-w-0 flex-1 bg-transparent text-xs text-foreground", "placeholder:text-muted-foreground", "focus-visible:outline-none disabled:cursor-not-allowed"), "aria-label": "Search workspace files by name" }), trimmed.length > 0 && (_jsx("button", { type: "button", onClick: () => {
                            setQuery("");
                            inputRef.current?.focus();
                        }, className: "shrink-0 text-muted-foreground transition-colors hover:text-foreground", "aria-label": "Clear search", children: _jsx(ClearIcon, {}) }))] }), _jsx("div", { className: "min-h-0 flex-1 overflow-y-auto", children: isUnsupported ? (_jsx(MessageState, { children: "File search isn\u2019t available here." })) : trimmed.length === 0 ? (_jsx(MessageState, { children: "Search files by name across the whole workspace." })) : isLoading && totalMatches === 0 && groups.length === 0 ? (_jsx(MessageState, { children: "Searching\u2026" })) : groups.length === 0 ? (_jsxs(MessageState, { children: ["No files matching \u201C", trimmed, "\u201D."] })) : (_jsxs(_Fragment, { children: [_jsx("ul", { id: listboxId, role: "listbox", "aria-label": "Search results", className: "py-0.5", children: renderRows({
                                groups,
                                showGroupHeaders,
                                selectedFile: selectedFile ?? null,
                                focusIndex,
                                optionId,
                                onOpenFile,
                            }) }), totalMatches > flatResults.length && (_jsxs("p", { className: "border-t border-border px-3 py-1.5 text-[0.65rem] text-muted-foreground", children: ["Showing the first ", flatResults.length, " of ", totalMatches, " matches \u2014 refine your search to narrow the list."] }))] })) })] }));
}
// ---------------------------------------------------------------------------
// Result rows — a single pass over groups that keeps the flat match index
// aligned with `flatResults` (and thus keyboard focus / aria-activedescendant).
// ---------------------------------------------------------------------------
function renderRows({ groups, showGroupHeaders, selectedFile, focusIndex, optionId, onOpenFile, }) {
    const rows = [];
    let matchIndex = 0;
    for (const group of groups) {
        if (showGroupHeaders) {
            rows.push(_jsx(GroupHeader, { name: group.entry.name }, `h:${group.entry.id}`));
        }
        for (const match of group.matches) {
            if (matchIndex >= MAX_RESULTS)
                break;
            const index = matchIndex++;
            const isOpen = selectedFile?.entryId === group.entry.id &&
                selectedFile.path === match.path;
            rows.push(_jsx(ResultRow, { id: optionId(index), match: match, isFocused: index === focusIndex, isOpen: isOpen, onOpen: () => onOpenFile(group.entry.id, match.path) }, `${group.entry.id}:${match.path}`));
        }
        if (group.truncated) {
            rows.push(_jsx(GroupNotice, { children: "Partial listing \u2014 too many files to search in full." }, `t:${group.entry.id}`));
        }
        if (group.error) {
            rows.push(_jsxs(GroupNotice, { isError: true, children: ["Couldn\u2019t list files: ", group.error.message] }, `e:${group.entry.id}`));
        }
        if (matchIndex >= MAX_RESULTS)
            break;
    }
    return rows;
}
function ResultRow({ id, match, isFocused, isOpen, onOpen, }) {
    const lastSlash = match.path.lastIndexOf("/");
    const basename = match.path.slice(lastSlash + 1);
    const dir = lastSlash >= 0 ? match.path.slice(0, lastSlash) : "";
    // The `role="option"` IS the interactive leaf of the listbox (keyboard
    // activation flows through the combobox input's Enter handler +
    // aria-activedescendant). It must not nest another interactive control — a
    // button inside would violate WCAG 4.1.2 (axe `nested-interactive`). So the
    // click handler and row styling live on the option itself.
    return (_jsxs("li", { id: id, role: "option", "aria-selected": isFocused, "aria-current": isOpen ? "true" : undefined, onClick: onOpen, className: cn("flex cursor-pointer items-baseline gap-1.5 px-3 py-1 text-xs transition-colors", isFocused && "bg-muted", isOpen && "bg-muted font-medium text-foreground", !isFocused && !isOpen && "hover:bg-muted"), children: [_jsx("span", { className: "truncate text-foreground", children: _jsx(HighlightedPath, { text: basename, offset: lastSlash + 1, matchStart: match.matchStart, matchEnd: match.matchEnd }) }), dir && (_jsx("span", { className: "truncate text-[0.65rem] text-muted-foreground", children: _jsx(HighlightedPath, { text: dir, offset: 0, matchStart: match.matchStart, matchEnd: match.matchEnd }) }))] }));
}
/**
 * Renders a slice of the path (`text`, starting at `offset` within the full
 * path), emphasizing the intersection with the match range `[matchStart,
 * matchEnd)`. Non-overlapping slices render plain.
 */
function HighlightedPath({ text, offset, matchStart, matchEnd, }) {
    const localStart = Math.max(0, matchStart - offset);
    const localEnd = Math.min(text.length, matchEnd - offset);
    if (localStart >= localEnd)
        return _jsx(_Fragment, { children: text });
    return (_jsxs(_Fragment, { children: [text.slice(0, localStart), _jsx("span", { className: "font-semibold text-foreground", children: text.slice(localStart, localEnd) }), text.slice(localEnd)] }));
}
function GroupHeader({ name }) {
    return (_jsx("li", { role: "presentation", className: "truncate px-3 pb-0.5 pt-2 text-[0.6rem] font-medium uppercase tracking-wide text-muted-foreground", children: name }));
}
function GroupNotice({ children, isError, }) {
    return (_jsx("li", { role: "presentation", className: cn("px-3 py-1 text-[0.65rem]", isError ? "text-destructive" : "text-muted-foreground"), children: children }));
}
function MessageState({ children }) {
    return (_jsx("div", { className: "flex h-full items-center justify-center p-8 text-center text-xs text-muted-foreground", children: children }));
}
// ---------------------------------------------------------------------------
// Inline SVG icons (SDK independence — no lucide dependency)
// ---------------------------------------------------------------------------
function SearchIcon() {
    return (_jsxs("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", className: "shrink-0 text-muted-foreground", "aria-hidden": "true", children: [_jsx("circle", { cx: "5.25", cy: "5.25", r: "3.5" }), _jsx("path", { d: "M7.75 7.75L10.5 10.5" })] }));
}
function ClearIcon() {
    return (_jsx("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("path", { d: "M3 3L9 9M9 3L3 9" }) }));
}
//# sourceMappingURL=WorkspaceFileSearch.js.map