import { useEffect, useMemo, useState } from 'react';
import { GripVertical, RotateCcw, Search, Settings2 } from 'lucide-react';
import { Dialog, DialogContent, DialogDescription, DialogTitle } from '@/Components/ui/dialog';
import { Button } from '@/Components/ui/button';
import { Switch } from '@/Components/ui/switch';

// title/subtitle/searchPlaceholder are optional so the SAME modal can present other lists the
// same way (Dashboard v3 uses it for widgets: "Atur Dashboard") — defaults keep every existing
// column call-site unchanged.
/**
 * `renderOption(def)` — OPTIONAL per-column extra control, drawn under the column's own row.
 *
 * Some columns have more than one way to render the same data: Sample Order's "Sample List"
 * can show a compact "N samples" pill you hover, or every product line inline. That choice
 * belongs beside the column it affects, not in the page toolbar — a user looking for it will
 * look where they went to turn the column on.
 *
 * Opt-in: a page that passes nothing renders exactly as before, which is the other ~20
 * callers of this modal.
 */
export function CustomizeColumnsModal({ open, onClose, groups, definitions, state, onApply, onReset, title = 'Customize columns', subtitle = null, searchPlaceholder = 'Cari column...', renderOption = null }) {
    // Local DRAFT state — only commits on Apply
    const [draft, setDraft] = useState(state);
    const [query, setQuery] = useState('');
    const [dragId, setDragId] = useState(null);
    // Sync draft when modal opens (Radix Dialog handles scroll-lock / Esc / focus-trap)
    useEffect(() => {
        if (open) {
            setDraft(state);
            setQuery('');
        }
    }, [open, state]);
    const groupLabelById = useMemo(() => new Map((groups ?? []).map((g) => [g.id, g.label])), [groups]);
    const groupLabel = (id) => groupLabelById.get(id) ?? null;
    const defById = useMemo(() => new Map(definitions.map((d) => [d.id, d])), [definitions]);
    const visibleCount = draft.filter((c) => c.visible).length;
    const matchSearch = (id) => {
        if (!query)
            return true;
        const def = defById.get(id);
        return !!def && def.label.toLowerCase().includes(query.toLowerCase());
    };
    const toggleVisible = (id) => {
        setDraft((d) => d.map((c) => (c.id === id ? { ...c, visible: !c.visible } : c)));
    };
    const handleDrop = (targetId) => {
        if (!dragId || dragId === targetId)
            return;
        const srcDef = defById.get(dragId);
        const tgtDef = defById.get(targetId);
        // Cross-group moves are ALLOWED. This used to bail whenever the two columns came from
        // different groups, which made "drag Product List to the very top" quietly impossible —
        // the row lifted, the drop landed on a column in another group, and nothing happened.
        // The group is only a label; the table's column order is this array's order.
        if (!srcDef || !tgtDef)
            return;
        setDraft((d) => {
            const src = d.findIndex((c) => c.id === dragId);
            const tgt = d.findIndex((c) => c.id === targetId);
            if (src < 0 || tgt < 0)
                return d;
            const next = [...d];
            const [moved] = next.splice(src, 1);
            next.splice(tgt, 0, moved);
            return next;
        });
        setDragId(null);
    };
    const handleApply = () => {
        onApply(draft);
        onClose();
    };
    const handleReset = () => {
        setDraft(onReset());
    };

    const renderItem = (col, i = 0, arr = []) => {
        const def = defById.get(col.id);
        if (!def || !matchSearch(col.id))
            return null;
        const isLocked = !!def.required;
        const prevDef = i > 0 ? defById.get(arr[i - 1]?.id) : null;
        const startsGroupRun = !!def.groupId && def.groupId !== prevDef?.groupId;
        const isOff = !col.visible;
        const row = (
            <div
                key={col.id}
                className={`flex items-center gap-3.5 rounded-md px-2 py-2.5 transition-colors hover:bg-secondary ${dragId === col.id ? 'opacity-40' : ''}`}
                draggable={!isLocked}
                onDragStart={() => !isLocked && setDragId(col.id)}
                onDragEnd={() => setDragId(null)}
                onDragOver={(e) => { e.preventDefault(); }}
                onDrop={() => handleDrop(col.id)}
            >
                <span className={`inline-grid place-items-center text-muted-foreground ${isLocked ? 'invisible' : 'cursor-grab opacity-60'}`} aria-hidden="true">
                    <GripVertical className="size-3.5" />
                </span>
                <div className="min-w-0 flex-1">
                    <div className={`flex items-center gap-2 text-[13px] ${isOff ? 'font-medium text-muted-foreground' : 'font-semibold text-foreground'}`}>
                        {def.label}
                        {isLocked && (
                            <span className="rounded bg-secondary px-1.5 py-px text-[9px] font-extrabold uppercase tracking-[0.06em] text-muted-foreground">
                                Required
                            </span>
                        )}
                    </div>
                    {def.description && (
                        <p className="mt-0.5 text-[11px] font-normal text-muted-foreground">{def.description}</p>
                    )}
                </div>
                {groupLabel(def.groupId) !== null && (
                    // Printed ONCE per run, not on all 15 rows — repeated on every line it was
                    // noise, and the eye only needs to be told where a family starts. Because the
                    // run is computed from the CURRENT order, dragging Product List to the top
                    // moves its label up with it: the grouping stays true instead of becoming a
                    // decoration that contradicts the list. The empty slot keeps the column aligned.
                    //
                    // Plain muted text, NOT a filled chip: a chip is the REQUIRED badge's language,
                    // and "cannot be hidden" is a state while "belongs to Metadata" is context.
                    <span className="ml-auto hidden w-[108px] shrink-0 truncate text-right text-[9.5px] font-semibold uppercase tracking-[0.07em] text-muted-foreground/55 sm:block">
                        {startsGroupRun ? groupLabel(def.groupId) : ''}
                    </span>
                )}
                <Switch
                    className="ml-auto"
                    checked={col.visible}
                    onCheckedChange={() => !isLocked && toggleVisible(col.id)}
                    disabled={isLocked}
                    aria-label={`Toggle ${def.label}`}
                />
            </div>
        );

        // The option is INSIDE the draggable row's wrapper but outside its flex line, and it
        // is hidden while the column is off — an option for something invisible is noise.
        const option = renderOption?.(def);

        return (option && col.visible)
            ? (
                <div key={col.id}>
                    {row}
                    <div className="mb-1 ml-[38px] mr-2">{option}</div>
                </div>
            )
            : row;
    };

    return (
        <Dialog open={open} onOpenChange={(v) => { if (!v) onClose(); }}>
            <DialogContent className="flex max-h-[calc(100vh-3rem)] sm:max-w-[560px] flex-col gap-0 overflow-hidden p-0">
                {/* Header */}
                <header className="grid grid-cols-[auto_1fr] items-start gap-3 border-b border-border px-5 pb-4 pr-12 pt-[18px]">
                    <span className="mt-0.5 inline-grid size-8 place-items-center rounded-md bg-accent text-accent-foreground" aria-hidden="true">
                        <Settings2 className="size-4" />
                    </span>
                    <div>
                        <DialogTitle className="text-[17px] font-bold tracking-tight">{title}</DialogTitle>
                        <DialogDescription className="mt-1 text-[11px] leading-snug">
                            {subtitle ?? 'Drag ke mana saja untuk ubah urutan · toggle untuk show/hide'} ·{' '}
                            <strong className="font-bold text-foreground">{visibleCount} of {draft.length} visible</strong>
                        </DialogDescription>
                    </div>
                </header>

                {/* Search */}
                <div className="border-b border-border px-5 py-3">
                    <label className="flex h-[38px] items-center gap-2 rounded-full bg-secondary px-3.5 text-muted-foreground">
                        <Search className="size-3.5 shrink-0" aria-hidden="true" />
                        <input
                            type="search"
                            value={query}
                            onChange={(e) => setQuery(e.target.value)}
                            placeholder={searchPlaceholder}
                            autoComplete="off"
                            aria-label="Search columns"
                            className="min-w-0 flex-1 border-0 bg-transparent text-[13px] text-foreground outline-none placeholder:text-muted-foreground"
                        />
                    </label>
                </div>

                {/* Body */}
                <div className="flex-1 overflow-y-auto px-4 pb-4 pt-2">
                    {draft.filter((c) => matchSearch(c.id)).length === 0 ? (
                        <div className="px-3 py-6 text-center text-xs text-muted-foreground">No columns match the search</div>
                    ) : (
                        // FLAT list, ordered exactly like the table's columns.
                        //
                        // It used to render one section per group, each listing only its own
                        // columns. Two problems: a column could never be dragged past its group's
                        // subheader, and — because the table reads this array's order while the
                        // modal re-sorted it into groups — the order shown here could differ from
                        // the order on screen. The group now travels as a tag on the row, so it is
                        // still visible without being a wall.
                        draft.filter((c) => matchSearch(c.id)).map(renderItem)
                    )}
                </div>

                {/* Footer */}
                <footer className="flex items-center justify-between gap-3 border-t border-border bg-secondary/40 px-5 py-3.5">
                    <Button type="button" variant="ghost" className="rounded-full" onClick={handleReset}>
                        <RotateCcw className="size-3.5" />
                        Reset to default
                    </Button>
                    <div className="flex gap-2.5">
                        <Button type="button" variant="outline" className="rounded-full" onClick={onClose}>Cancel</Button>
                        <Button type="button" className="rounded-full" onClick={handleApply}>Apply changes</Button>
                    </div>
                </footer>
            </DialogContent>
        </Dialog>
    );
}
