import { cn } from '@/lib/cn';
import {
    addCustomFont,
    extractFontFamilyFromInput,
    getCustomFonts,
    GOOGLE_FONTS,
    removeCustomFont,
    subscribeFonts,
    verifyGoogleFont,
    type CustomFont,
    type FontCategory,
    type FontEntry,
} from '@/editor/fonts';
import type { Script } from '@/editor/locale-fonts';
import { ChevronDown, Plus, X } from 'lucide-react';
import React, {
    useEffect,
    useMemo,
    useRef,
    useState,
    useSyncExternalStore,
} from 'react';

/**
 * Shared font picker used by both the editor's text properties panel and
 * the admin Appearance editor. Filters a curated Google Fonts list, lets
 * the user paste a Google Fonts URL or family name, verifies via
 * `verifyGoogleFont` against the live API, and persists adds to per-browser
 * `customFonts` storage so a re-add is instant on the next visit.
 */
export type FontFamilyPickerProps = {
    value: string;
    onCommit: (family: string) => void;
    onPreview?: (family: string) => void;
    /** Stable testid prefix for the trigger + search input. */
    testIdPrefix?: string;
    /**
     * Optional "Recommended for {label}" group rendered at the top of the
     * dropdown. Surfaces script-aware font suggestions for non-Latin locales
     * (Arabic, Devanagari, CJK, etc.). Falls through to the main list when
     * `families` is empty or the same as the script's default ranking.
     */
    recommended?: {
        label: string;
        families: string[];
    };
    /**
     * Restrict the picker to families whose `FontEntry.scripts` covers this
     * script. `'Latin'` and `undefined` are no-ops (Latin locales see every
     * font). Non-Latin values hide every Latin-only family so authors can't
     * pick a font that'll render their text in browser-fallback glyphs.
     *
     * Custom-added fonts are always allowed (we don't know their script
     * coverage; user explicitly added them).
     */
    requiredScript?: Script;
};

const CATEGORY_ORDER: FontCategory[] = [
    'sans-serif',
    'serif',
    'display',
    'monospace',
    'handwriting',
];

const FONTS_BY_CATEGORY: Record<FontCategory, typeof GOOGLE_FONTS> =
    CATEGORY_ORDER.reduce(
        (acc, cat) => {
            acc[cat] = GOOGLE_FONTS.filter((f) => f.category === cat);
            return acc;
        },
        {} as Record<FontCategory, typeof GOOGLE_FONTS>,
    );

type FlatFont = {
    family: string;
    custom: boolean;
    /** Scripts the family has dedicated glyph coverage for. Undefined for
     *  custom-added fonts (we can't introspect Google's subset list); the
     *  picker treats undefined as "always allow". */
    scripts?: FontEntry['scripts'];
};

const BUILTIN_FLAT_FONTS: FlatFont[] = CATEGORY_ORDER.flatMap((cat) =>
    FONTS_BY_CATEGORY[cat].map((f) => ({
        family: f.family,
        custom: false,
        scripts: f.scripts,
    })),
);

const EMPTY_CUSTOM_FONTS: ReadonlyArray<CustomFont> = [];

function useCustomFonts(): ReadonlyArray<CustomFont> {
    return useSyncExternalStore(
        subscribeFonts,
        getCustomFonts,
        () => EMPTY_CUSTOM_FONTS,
    );
}

export function FontFamilyPicker({
    value,
    onCommit,
    onPreview,
    testIdPrefix = 'font-family',
    recommended,
    requiredScript,
}: FontFamilyPickerProps) {
    const customFonts = useCustomFonts();
    const allFonts = useMemo<FlatFont[]>(
        () => [
            ...BUILTIN_FLAT_FONTS,
            ...customFonts.map((f) => ({ family: f.family, custom: true })),
        ],
        [customFonts],
    );

    const [open, setOpen] = useState(false);
    const [query, setQuery] = useState('');
    const [highlightIndex, setHighlightIndex] = useState(0);
    const [addState, setAddState] = useState<
        | { status: 'idle' }
        | { status: 'verifying'; family: string }
        | { status: 'error'; family: string; message: string }
    >({ status: 'idle' });
    const originalRef = useRef(value);
    const rootRef = useRef<HTMLDivElement>(null);
    const listRef = useRef<HTMLDivElement>(null);
    const onPreviewRef = useRef(onPreview);
    useEffect(() => {
        onPreviewRef.current = onPreview;
    }, [onPreview]);

    const trimmedQuery = query.trim();
    const effectiveQuery = useMemo(() => {
        const extracted = extractFontFamilyFromInput(query);
        return extracted ?? trimmedQuery;
    }, [query, trimmedQuery]);

    type Item = FlatFont & { recommended: boolean };

    const filtered = useMemo<Item[]>(() => {
        const q = effectiveQuery.toLowerCase();
        const matches = (f: FlatFont) =>
            !effectiveQuery || f.family.toLowerCase().includes(q);

        // Script gate: only relevant when the active locale's script is
        // non-Latin. Built-in fonts without an explicit `scripts` tag are
        // treated as Latin-only (matches the FontEntry comment). Custom
        // fonts (user-added at runtime) always pass — we can't inspect
        // their subset list, and the user explicitly opted in.
        const needsScript: Script | null =
            requiredScript && requiredScript !== 'Latin' ? requiredScript : null;
        const passesScript = (f: FlatFont): boolean => {
            if (!needsScript) return true;
            if (f.custom) return true;
            const tags = f.scripts ?? ['Latin'];
            return tags.includes(needsScript);
        };

        // Order: recommended-for-this-locale fonts first (in their listed
        // order), then every other font in catalog order. Both lists are
        // search-filtered with the same query. A custom font that matches
        // the recommended slot reuses its custom flag.
        const byFamily = new Map<string, FlatFont>();
        for (const f of allFonts) byFamily.set(f.family, f);

        const rec: Item[] = [];
        if (recommended) {
            for (const family of recommended.families) {
                const f = byFamily.get(family);
                const base: FlatFont = f ?? { family, custom: false };
                // Recommended list bypasses passesScript — recommended-for-X
                // fonts are by definition glyph-complete for X. Saves us
                // from having to keep the script tagging on built-in fonts
                // in lockstep with the recommended map.
                if (matches(base)) rec.push({ ...base, recommended: true });
            }
        }
        const seen = new Set(rec.map((r) => r.family));
        const rest: Item[] = allFonts
            .filter((f) => !seen.has(f.family) && matches(f) && passesScript(f))
            .map((f) => ({ ...f, recommended: false }));

        return [...rec, ...rest];
    }, [effectiveQuery, allFonts, recommended, requiredScript]);

    const addTarget = useMemo(() => {
        if (!effectiveQuery) return null;
        const lower = effectiveQuery.toLowerCase();
        if (allFonts.some((f) => f.family.toLowerCase() === lower)) return null;
        return effectiveQuery;
    }, [effectiveQuery, allFonts]);

    const canAddQuery = addTarget !== null;

    useEffect(() => {
        if (!open) return;
        const item = filtered[highlightIndex];
        if (!item) return;
        onPreviewRef.current?.(item.family);
    }, [highlightIndex, open, filtered]);

    useEffect(() => {
        if (highlightIndex >= filtered.length) {
            setHighlightIndex(Math.max(0, filtered.length - 1));
        }
    }, [filtered, highlightIndex]);

    useEffect(() => {
        setAddState((s) =>
            s.status !== 'idle' && s.family !== addTarget
                ? { status: 'idle' }
                : s,
        );
    }, [addTarget]);

    useEffect(() => {
        if (!open || !listRef.current) return;
        const el = listRef.current.querySelector<HTMLElement>(
            `[data-index="${highlightIndex}"]`,
        );
        el?.scrollIntoView({ block: 'nearest' });
    }, [highlightIndex, open]);

    useEffect(() => {
        if (!open) return;
        const onDown = (e: MouseEvent) => {
            if (rootRef.current?.contains(e.target as Node)) return;
            onPreviewRef.current?.(originalRef.current);
            setOpen(false);
        };
        document.addEventListener('mousedown', onDown);
        return () => document.removeEventListener('mousedown', onDown);
    }, [open]);

    const openPicker = () => {
        originalRef.current = value;
        setQuery('');
        const i = allFonts.findIndex((f) => f.family === value);
        setHighlightIndex(i >= 0 ? i : 0);
        setOpen(true);
    };

    const cancel = () => {
        onPreviewRef.current?.(originalRef.current);
        setOpen(false);
    };

    const commit = (family: string) => {
        // Revert the live preview FIRST. `onPreview` mutated the display layout
        // to the new font; if we commit on top of that, `updateLayer` diffs
        // against the already-previewed layout, produces zero patches, and the
        // change is never written to baseLayout (→ never autosaved/synced).
        // Reverting to the pre-preview value makes the commit a real edit.
        onPreviewRef.current?.(originalRef.current);
        onCommit(family);
        setOpen(false);
    };

    const addAndCommit = async (family: string) => {
        const clean = family.trim();
        if (!clean) return;
        if (addState.status === 'verifying') return;
        setAddState({ status: 'verifying', family: clean });
        const canonical = await verifyGoogleFont(clean);
        if (!canonical) {
            setAddState({
                status: 'error',
                family: clean,
                message: `"${clean}" isn't on Google Fonts. Check the spelling.`,
            });
            return;
        }
        setAddState({ status: 'idle' });
        addCustomFont(canonical);
        commit(canonical);
    };

    const removeCustom = (family: string) => {
        removeCustomFont(family);
    };

    const onInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
        if (e.key === 'ArrowDown') {
            e.preventDefault();
            setHighlightIndex((i) => Math.min(filtered.length - 1, i + 1));
        } else if (e.key === 'ArrowUp') {
            e.preventDefault();
            setHighlightIndex((i) => Math.max(0, i - 1));
        } else if (e.key === 'Enter') {
            e.preventDefault();
            if (filtered.length === 0 && addTarget) {
                addAndCommit(addTarget);
                return;
            }
            const item = filtered[highlightIndex];
            if (item) commit(item.family);
        } else if (e.key === 'Escape') {
            e.preventDefault();
            cancel();
        }
    };

    const onButtonKeyDown = (e: React.KeyboardEvent<HTMLButtonElement>) => {
        if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {
            e.preventDefault();
            openPicker();
        }
    };

    return (
        <div ref={rootRef} className="relative">
            <button
                type="button"
                onClick={() => (open ? cancel() : openPicker())}
                onKeyDown={onButtonKeyDown}
                data-testid={testIdPrefix}
                className="flex h-9 w-full items-center justify-between gap-2 rounded-md border border-border bg-background px-2 text-xs text-foreground hover:border-accent/60 focus:border-accent focus:outline-none"
                style={{ fontFamily: value }}
            >
                <span className="truncate">{value}</span>
                <ChevronDown className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
            </button>
            {open && (
                <div className="absolute left-0 right-0 top-full z-20 mt-1 overflow-hidden rounded-md border border-border bg-card shadow-lg">
                    <input
                        type="text"
                        value={query}
                        onChange={(e) => setQuery(e.target.value)}
                        onKeyDown={onInputKeyDown}
                        autoFocus
                        placeholder="Search or paste Google Fonts URL"
                        data-testid={`${testIdPrefix}-search`}
                        className="block w-full border-b border-border bg-background px-2 py-1.5 text-xs text-foreground placeholder:text-muted-foreground focus:outline-none"
                    />
                    <div ref={listRef} className="max-h-64 overflow-y-auto">
                        {filtered.length === 0 && !canAddQuery && (
                            <div className="px-2 py-2 text-xs text-muted-foreground">
                                {effectiveQuery
                                    ? `No fonts match "${effectiveQuery}"`
                                    : 'No fonts'}
                            </div>
                        )}
                        {filtered.map((item, i) => {
                            const prev = filtered[i - 1];
                            const recommendedLabel = recommended?.label;
                            // Emit a group header when we transition from
                            // recommended → other, OR at index 0 if the row
                            // is recommended. No header when the list has
                            // no recommended group at all (Latin locales).
                            const showRecommendedHeader =
                                item.recommended && (i === 0 || !prev?.recommended);
                            const showOthersHeader =
                                !item.recommended &&
                                recommendedLabel &&
                                prev?.recommended === true;
                            return (
                                <React.Fragment key={item.family}>
                                    {showRecommendedHeader && recommendedLabel && (
                                        <div
                                            data-testid={`${testIdPrefix}-recommended-header`}
                                            className="border-b border-border/60 bg-muted/30 px-2 py-1 text-[10px] font-medium uppercase tracking-wide text-muted-foreground"
                                        >
                                            Recommended for {recommendedLabel}
                                        </div>
                                    )}
                                    {showOthersHeader && (
                                        <div
                                            className="border-y border-border/60 bg-muted/30 px-2 py-1 text-[10px] font-medium uppercase tracking-wide text-muted-foreground"
                                        >
                                            All fonts
                                        </div>
                                    )}
                                    <div
                                        data-index={i}
                                        className={cn(
                                            'group flex items-center',
                                            i === highlightIndex
                                                ? 'bg-accent/10 text-accent'
                                                : 'text-foreground hover:bg-muted',
                                        )}
                                        onMouseEnter={() => setHighlightIndex(i)}
                                    >
                                <button
                                    type="button"
                                    data-testid={`${testIdPrefix}-option-${item.family}`}
                                    onClick={() => commit(item.family)}
                                    style={{ fontFamily: item.family }}
                                    className="flex min-w-0 flex-1 items-center gap-2 truncate px-2 py-1.5 text-left text-xs"
                                >
                                    <span className="truncate">{item.family}</span>
                                    {item.custom && (
                                        <span
                                            className="shrink-0 rounded-sm bg-muted px-1 py-0.5 font-mono text-[9px] uppercase tracking-wider text-muted-foreground group-hover:bg-background"
                                            style={{ fontFamily: 'inherit' }}
                                        >
                                            custom
                                        </span>
                                    )}
                                </button>
                                {item.custom && (
                                    <button
                                        type="button"
                                        onClick={(e) => {
                                            e.stopPropagation();
                                            removeCustom(item.family);
                                        }}
                                        data-testid={`${testIdPrefix}-remove-${item.family}`}
                                        aria-label={`Remove ${item.family}`}
                                        className="mr-1 rounded p-1 text-muted-foreground opacity-0 transition-opacity hover:bg-destructive/10 hover:text-destructive group-hover:opacity-100"
                                    >
                                        <X className="h-3 w-3" strokeWidth={2} />
                                    </button>
                                )}
                                    </div>
                                </React.Fragment>
                            );
                        })}
                        {addTarget &&
                            (() => {
                                const verifying =
                                    addState.status === 'verifying' &&
                                    addState.family === addTarget;
                                const error =
                                    addState.status === 'error' &&
                                    addState.family === addTarget
                                        ? addState.message
                                        : null;
                                return (
                                    <div className="border-t border-border">
                                        <button
                                            type="button"
                                            onClick={() => addAndCommit(addTarget)}
                                            disabled={verifying}
                                            data-testid={`${testIdPrefix}-add`}
                                            className="flex w-full items-center gap-2 px-2 py-1.5 text-left text-xs text-accent hover:bg-accent/10 disabled:opacity-60"
                                        >
                                            {verifying ? (
                                                <span className="inline-block h-3.5 w-3.5 shrink-0 animate-spin rounded-full border-2 border-accent border-t-transparent" />
                                            ) : (
                                                <Plus
                                                    className="h-3.5 w-3.5 shrink-0"
                                                    strokeWidth={2}
                                                />
                                            )}
                                            <span className="truncate">
                                                {verifying
                                                    ? `Checking Google Fonts for "${addTarget}"…`
                                                    : `Add "${addTarget}" as Google Font`}
                                            </span>
                                        </button>
                                        {error && (
                                            <div
                                                data-testid={`${testIdPrefix}-add-error`}
                                                className="border-t border-border bg-destructive/5 px-2 py-1.5 text-[11px] text-destructive"
                                            >
                                                {error}
                                            </div>
                                        )}
                                    </div>
                                );
                            })()}
                    </div>
                    <div className="border-t border-border bg-muted/30 px-2 py-1.5 text-[10px] leading-snug text-muted-foreground">
                        Type any Google Fonts family (e.g.{' '}
                        <span className="font-mono text-foreground/80">
                            Caveat Brush
                        </span>
                        ) or paste a URL from{' '}
                        <span className="font-mono text-foreground/80">
                            fonts.google.com
                        </span>{' '}
                        to add it.
                    </div>
                </div>
            )}
        </div>
    );
}
