import { router } from '@inertiajs/react';
import {
    ArrowRight,
    BriefcaseBusiness,
    Clock,
    HardHat,
    LifeBuoy,
    Loader2,
    Search,
    UserRound,
    X,
} from 'lucide-react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { cn } from '@/lib/utils';
import { search as searchLookup } from '@/routes/lookups';
import type { GlobalSearchResponse, SearchGroup, SearchResult } from '@/types';

/** The icon a group is drawn with; unknown groups get the magnifying glass. */
const GROUP_ICONS: Record<string, typeof UserRound> = {
    customers: UserRound,
    jobs: BriefcaseBusiness,
    tickets: LifeBuoy,
    staff: HardHat,
};

const RECENT_KEY = 'crm.search.recent';
const RECENT_LIMIT = 5;

type FlatResult = SearchResult & { groupLabel: string };

/**
 * One field that asks the whole system a question.
 *
 * The desk normally holds a fragment of something - a surname, a street, an
 * NMI, a phone number, a job number - and needs the record it belongs to, so
 * this searches customers (including their sites), jobs, tickets and staff in
 * one request and shows the answers grouped, each naming the field that
 * matched. Results open with the keyboard alone, recent searches are kept, and
 * the groups a person may not see are never sent.
 */
export function GlobalSearch() {
    const [term, setTerm] = useState('');
    const [groups, setGroups] = useState<SearchGroup[]>([]);
    const [total, setTotal] = useState(0);
    const [open, setOpen] = useState(false);
    const [searching, setSearching] = useState(false);
    const [failure, setFailure] = useState<string | null>(null);
    const [cursor, setCursor] = useState(0);
    const [recent, setRecent] = useState<string[]>([]);

    const input = useRef<HTMLInputElement>(null);
    const wrapper = useRef<HTMLDivElement>(null);

    useEffect(() => {
        try {
            const stored = window.localStorage.getItem(RECENT_KEY);

            setRecent(stored === null ? [] : (JSON.parse(stored) as string[]));
        } catch {
            // A browser with storage turned off simply has no history.
        }
    }, []);

    const remember = useCallback((value: string) => {
        setRecent((current) => {
            const next = [
                value,
                ...current.filter((item) => item !== value),
            ].slice(0, RECENT_LIMIT);

            try {
                window.localStorage.setItem(RECENT_KEY, JSON.stringify(next));
            } catch {
                // Storage being unavailable is not worth interrupting anybody.
            }

            return next;
        });
    }, []);

    const trimmed = term.trim();

    // One request per pause in typing, cancelled as soon as the term changes
    // again, so a slow answer cannot land on top of a newer question.
    useEffect(() => {
        if (trimmed.length < 2) {
            setGroups([]);
            setTotal(0);
            setSearching(false);
            setFailure(null);

            return;
        }

        const controller = new AbortController();
        setSearching(true);

        const timer = window.setTimeout(async () => {
            try {
                const url = new URL(searchLookup().url, window.location.origin);
                url.searchParams.set('search', trimmed);

                const response = await fetch(url, {
                    headers: { Accept: 'application/json' },
                    signal: controller.signal,
                });

                if (!response.ok) {
                    throw new Error(
                        `Search failed with status ${response.status}`,
                    );
                }

                const payload = (await response.json()) as GlobalSearchResponse;

                if (!controller.signal.aborted) {
                    setGroups(payload.groups ?? []);
                    setTotal(payload.total ?? 0);
                    setFailure(null);
                    setCursor(0);
                }
            } catch (error) {
                if (!controller.signal.aborted) {
                    setGroups([]);
                    setTotal(0);
                    setFailure(
                        error instanceof Error &&
                            error.message.startsWith('Search failed')
                            ? 'Search is not available right now.'
                            : 'Could not reach the server.',
                    );
                }
            } finally {
                if (!controller.signal.aborted) {
                    setSearching(false);
                }
            }
        }, 200);

        return () => {
            window.clearTimeout(timer);
            controller.abort();
        };
    }, [trimmed]);

    const flat = useMemo<FlatResult[]>(
        () =>
            groups.flatMap((group) =>
                group.results.map((result) => ({
                    ...result,
                    groupLabel: group.label,
                })),
            ),
        [groups],
    );

    const visit = useCallback(
        (result: FlatResult) => {
            remember(trimmed);
            setOpen(false);
            input.current?.blur();
            router.visit(result.url);
        },
        [remember, trimmed],
    );

    // ⌘K / Ctrl+K from anywhere on the page, which is how people expect a
    // search box in the corner to behave.
    useEffect(() => {
        const onKeyDown = (event: KeyboardEvent) => {
            if (
                (event.metaKey || event.ctrlKey) &&
                event.key.toLowerCase() === 'k'
            ) {
                event.preventDefault();
                input.current?.focus();
                input.current?.select();
                setOpen(true);
            }
        };

        window.addEventListener('keydown', onKeyDown);

        return () => window.removeEventListener('keydown', onKeyDown);
    }, []);

    // Clicking away is a dismissal, and leaving the field restores the bar to
    // its resting state.
    useEffect(() => {
        if (!open) {
            return;
        }

        const onPointerDown = (event: MouseEvent) => {
            if (
                wrapper.current &&
                !wrapper.current.contains(event.target as Node)
            ) {
                setOpen(false);
            }
        };

        document.addEventListener('mousedown', onPointerDown);

        return () => document.removeEventListener('mousedown', onPointerDown);
    }, [open]);

    useEffect(() => {
        if (!open) {
            return;
        }

        document
            .getElementById(`global-search-result-${cursor}`)
            ?.scrollIntoView({ block: 'nearest' });
    }, [open, cursor]);

    const onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
        if (event.key === 'ArrowDown') {
            event.preventDefault();
            setOpen(true);
            setCursor((position) =>
                Math.min(position + 1, Math.max(flat.length - 1, 0)),
            );

            return;
        }

        if (event.key === 'ArrowUp') {
            event.preventDefault();
            setCursor((position) => Math.max(position - 1, 0));

            return;
        }

        if (event.key === 'Enter') {
            const result = flat[cursor];

            if (result !== undefined) {
                event.preventDefault();
                visit(result);
            }

            return;
        }

        if (event.key === 'Escape') {
            event.preventDefault();
            setOpen(false);
            input.current?.blur();
        }
    };

    const showPanel = open && (trimmed.length > 0 || recent.length > 0);
    const showRecent = trimmed.length < 2;

    return (
        <div
            ref={wrapper}
            className="relative ml-auto hidden sm:block"
            role="search"
        >
            <label className="relative block">
                <span className="sr-only">
                    Search customers, addresses, NMI, jobs, installers, phone
                    numbers and tickets
                </span>

                <Search
                    aria-hidden
                    className="pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2 text-muted-foreground"
                />

                <input
                    ref={input}
                    value={term}
                    onChange={(event) => {
                        setTerm(event.target.value);
                        setOpen(true);
                    }}
                    onFocus={() => setOpen(true)}
                    onKeyDown={onKeyDown}
                    placeholder="Search customers, NMI, jobs…"
                    aria-expanded={showPanel}
                    aria-controls="global-search-panel"
                    autoComplete="off"
                    spellCheck={false}
                    className="h-8 w-56 rounded-lg border border-hairline bg-card/70 pr-14 pl-8 text-xs transition-[width,box-shadow] outline-none placeholder:text-muted-foreground/70 focus-visible:w-72 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/30 lg:w-64"
                />

                {searching ? (
                    <Loader2
                        aria-hidden
                        className="absolute top-1/2 right-2.5 size-3.5 -translate-y-1/2 animate-spin text-muted-foreground"
                    />
                ) : term !== '' ? (
                    <button
                        type="button"
                        onClick={() => {
                            setTerm('');
                            input.current?.focus();
                        }}
                        aria-label="Clear search"
                        className="absolute top-1/2 right-2.5 flex size-4 -translate-y-1/2 items-center justify-center text-muted-foreground hover:text-foreground"
                    >
                        <X className="size-3.5" aria-hidden />
                    </button>
                ) : (
                    <kbd className="pointer-events-none absolute top-1/2 right-2.5 hidden -translate-y-1/2 font-sans text-[10px] text-muted-foreground/70 lg:block">
                        ⌘K
                    </kbd>
                )}
            </label>

            {showPanel ? (
                <div
                    id="global-search-panel"
                    className="absolute top-full right-0 z-50 mt-2 max-h-[70vh] w-[26rem] overflow-y-auto rounded-xl border border-hairline bg-popover p-1 text-popover-foreground shadow-lg"
                >
                    {showRecent ? (
                        <div className="p-2">
                            <p className="flex items-center gap-1.5 px-1 pb-2 text-[11px] font-medium tracking-wide text-muted-foreground uppercase">
                                <Clock className="size-3" aria-hidden />
                                Recent searches
                            </p>

                            <div className="flex flex-wrap gap-1.5">
                                {recent.map((item) => (
                                    <button
                                        key={item}
                                        type="button"
                                        onClick={() => {
                                            setTerm(item);
                                            input.current?.focus();
                                        }}
                                        className="rounded-full border border-hairline px-2.5 py-1 text-xs hover:bg-accent"
                                    >
                                        {item}
                                    </button>
                                ))}
                            </div>
                        </div>
                    ) : null}

                    {!showRecent && failure !== null ? (
                        <p className="px-3 py-3 text-xs text-muted-foreground">
                            {failure} Type again to retry.
                        </p>
                    ) : null}

                    {!showRecent && failure === null && flat.length === 0 ? (
                        <p className="px-3 py-3 text-xs text-muted-foreground">
                            {searching
                                ? 'Searching…'
                                : `Nothing matches “${trimmed}”. Try a surname, a street, an NMI, a job number or a phone number.`}
                        </p>
                    ) : null}

                    {!showRecent && flat.length > 0 ? (
                        <>
                            {groups.map((group) => {
                                const Icon = GROUP_ICONS[group.key] ?? Search;

                                return (
                                    <div key={group.key} className="pb-1">
                                        <div className="flex items-center justify-between px-3 py-1.5">
                                            <p className="text-[11px] font-medium tracking-wide text-muted-foreground uppercase">
                                                {group.label} · {group.total}
                                            </p>
                                            <a
                                                href={group.more_url}
                                                className="text-[11px] text-muted-foreground underline underline-offset-2 hover:text-foreground"
                                            >
                                                View all
                                            </a>
                                        </div>

                                        <ul>
                                            {group.results.map((result) => {
                                                const index = flat.findIndex(
                                                    (item) =>
                                                        item.url ===
                                                            result.url &&
                                                        item.groupLabel ===
                                                            group.label,
                                                );

                                                return (
                                                    <li
                                                        key={`${group.key}-${result.id}`}
                                                    >
                                                        <button
                                                            id={`global-search-result-${index}`}
                                                            type="button"
                                                            onMouseEnter={() =>
                                                                setCursor(index)
                                                            }
                                                            onClick={() =>
                                                                visit({
                                                                    ...result,
                                                                    groupLabel:
                                                                        group.label,
                                                                })
                                                            }
                                                            className={cn(
                                                                'flex w-full items-start gap-2.5 rounded-lg px-3 py-2 text-left',
                                                                index === cursor
                                                                    ? 'bg-accent text-accent-foreground'
                                                                    : 'hover:bg-accent/60',
                                                            )}
                                                        >
                                                            <Icon
                                                                className="mt-0.5 size-4 shrink-0 text-muted-foreground"
                                                                aria-hidden
                                                            />

                                                            <span className="min-w-0 flex-1">
                                                                <span className="flex items-center gap-2">
                                                                    <span className="truncate text-sm font-medium">
                                                                        {
                                                                            result.title
                                                                        }
                                                                    </span>
                                                                    {result.badge ? (
                                                                        <span className="shrink-0 rounded-full bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground">
                                                                            {
                                                                                result.badge
                                                                            }
                                                                        </span>
                                                                    ) : null}
                                                                </span>

                                                                {result.subtitle ? (
                                                                    <span className="block truncate text-xs text-muted-foreground">
                                                                        {
                                                                            result.subtitle
                                                                        }
                                                                    </span>
                                                                ) : null}
                                                            </span>

                                                            <ArrowRight
                                                                className="mt-1 size-3.5 shrink-0 text-muted-foreground/60"
                                                                aria-hidden
                                                            />
                                                        </button>
                                                    </li>
                                                );
                                            })}
                                        </ul>
                                    </div>
                                );
                            })}

                            <p className="border-t border-hairline px-3 py-2 text-[11px] text-muted-foreground">
                                {total} result{total === 1 ? '' : 's'} · ↑↓ to
                                move, ↵ to open, esc to close
                            </p>
                        </>
                    ) : null}
                </div>
            ) : null}
        </div>
    );
}
