import { createSignal, createMemo, untrack, Show, For, JSXElement } from "solid-js";
import { Icon } from "./Icons.tsx";

export const GRID_HEADER_CLS = "border-b border-r border-neutral-300 bg-neutral-50 px-1.5 py-1.5 text-left text-xs font-bold uppercase text-black whitespace-nowrap last:border-r-0";

interface SortableHeaderProps {
    label: string;
    sortKey: string;
    width?: string;
    minWidth?: string;
    current: string | null;
    desc: boolean;
    onSort?: (key: string) => void;
}

function columnSizeClass(width?: string, minWidth?: string): string {
    return width || minWidth || "";
}

export function SortableHeader(props: SortableHeaderProps) {
    const isActive = () => {
        const cur = typeof props.current === "function" ? (props.current as () => string | null)() : props.current;
        return cur === props.sortKey;
    };
    const descending = () => {
        const d = typeof props.desc === "function" ? (props.desc as () => boolean)() : props.desc;
        return !!d;
    };
    const cls = () => GRID_HEADER_CLS + " cursor-pointer select-none hover:bg-neutral-200"
        + (columnSizeClass(props.width, props.minWidth) ? " " + columnSizeClass(props.width, props.minWidth) : "");
    return (
        <th class={cls()} onclick={() => props.onSort?.(props.sortKey)}>
            <div class="flex items-center gap-0.5 min-w-0">
                <span class="truncate min-w-0 flex-1">{props.label}</span>
                <Show when={isActive()}>
                    <span class="shrink-0"><Icon icon={descending() ? "caret-down" : "caret-up"} size={10}/></span>
                </Show>
            </div>
        </th>
    );
}

export function compareRowsGeneric(a: any, b: any, key: string, sortType?: string): number {
    const av = a[key];
    const bv = b[key];
    const aEmpty = av === "" || av == null;
    const bEmpty = bv === "" || bv == null;
    if (aEmpty && bEmpty) return 0;
    if (aEmpty) return 1;
    if (bEmpty) return -1;
    if (sortType === "numeric") {
        const as = String(av);
        const bs = String(bv);
        const am = /^(\d+)/.exec(as);
        const bm = /^(\d+)/.exec(bs);
        const an = am ? parseInt(am[1], 10) : NaN;
        const bn = bm ? parseInt(bm[1], 10) : NaN;
        if (!isNaN(an) && !isNaN(bn)) {
            if (an !== bn) return an - bn;
            return as.localeCompare(bs);
        }
        if (!isNaN(an)) return -1;
        if (!isNaN(bn)) return 1;
        return as.localeCompare(bs);
    }
    if (sortType === "money") {
        return parseFloat(av) - parseFloat(bv);
    }
    return String(av).localeCompare(String(bv));
}

export interface CellGridColumn {
    key: string;
    label: string;
    sortKey?: string;
    sortType?: string;
    sortValue?: (row: any) => unknown;
    width?: string;
    minWidth?: string;
    headerClass?: string;
    editable?: boolean;
    readOnly?: boolean;
    render?: (row: any) => JSXElement;
    cellClass?: string | ((row: any) => string);
    onclick?: (row: any) => void;
    inputMode?: "decimal" | "email" | "none" | "numeric" | "search" | "tel" | "text" | "url" | undefined;
    placeholder?: string;
    parse?: (value: string) => unknown;
}

export interface CellGridApi {
    dirty: () => boolean;
    selected: () => { id: unknown; field: string } | null;
    focusCell: (id: unknown, field: string) => void;
    displayedRows: () => any[];
    snapshotRowPositions: () => Map<unknown, DOMRect>;
    animateRows: (before: Map<unknown, DOMRect>) => void;
}

interface CellGridProps {
    columns: CellGridColumn[];
    rows: any[];
    initialRows: any[];
    idField?: string;
    onCellChange: (rowId: unknown, field: string, value: unknown) => void;
    sortKey: string | null;
    setSortKey: (key: string) => void;
    sortDesc: boolean;
    setSortDesc: (desc: boolean) => void;
    conflictFields?: string[];
    dense?: boolean;
    ref?: (api: CellGridApi) => void;
}

export function CellGrid(props: CellGridProps) {
    const getSortKey = () => typeof props.sortKey === "function" ? (props.sortKey as () => string | null)() : props.sortKey;
    const getSortDesc = () => typeof props.sortDesc === "function" ? (props.sortDesc as () => boolean)() : props.sortDesc;

    const idField = () => props.idField || "id";
    const editableFields = createMemo(() => props.columns.filter((c) => c.editable).map((c) => c.key));
    const columnsByKey = createMemo(() => {
        const m = new Map<string, CellGridColumn>();
        for (const col of props.columns) m.set(col.key, col);
        return m;
    });

    const [selected, setSelected] = createSignal<{ id: unknown; field: string } | null>(null);
    const [sortStamp, setSortStamp] = createSignal(0);
    const [blurStamp, setBlurStamp] = createSignal(0);

    const rowRefs = new Map<unknown, HTMLElement>();
    const inputRefs = new Map<string, HTMLInputElement>();

    const setRowRef = (id: unknown) => (el: HTMLElement) => {
        if (el) rowRefs.set(id, el);
    };
    const setInputRef = (id: unknown, field: string) => (el: HTMLInputElement) => {
        const key = id + "::" + field;
        if (el) inputRefs.set(key, el);
        else inputRefs.delete(key);
    };

    const sortColMap = createMemo(() => {
        const m = new Map<string, CellGridColumn>();
        for (const col of props.columns) {
            if (col.sortKey) m.set(col.sortKey, col);
        }
        return m;
    });

    const sortedOrder = createMemo(() => {
        sortStamp();
        props.initialRows;
        props.rows.length;
        const sk = getSortKey();
        const desc = getSortDesc();
        const col = sortColMap().get(sk || "");
        const st = col?.sortType || "string";
        const getVal = typeof col?.sortValue === "function" ? col.sortValue : (r: any) => r[sk || ""];
        return untrack(() => {
            const idf = idField();
            const snap = props.rows.map((r) => ({ id: r[idf], sortVal: getVal(r) }));
            snap.sort((a, b) => compareRowsGeneric(a, b, "sortVal", st));
            if (desc) snap.reverse();
            return snap.map((s) => s.id);
        });
    });

    const displayedRows = createMemo(() => {
        const order = sortedOrder();
        const idf = idField();
        const byId = new Map();
        for (let i = 0; i < props.rows.length; i++) {
            byId.set(props.rows[i][idf], props.rows[i]);
        }
        const out: any[] = [];
        for (const id of order) {
            const r = byId.get(id);
            if (r) out.push(r);
        }
        return out;
    });

    const dirty = createMemo(() => {
        const current = props.rows;
        const initial = props.initialRows;
        if (!initial || current.length !== initial.length) return true;
        const fields = editableFields();
        for (let i = 0; i < current.length; i++) {
            for (const f of fields) {
                if (current[i][f] !== initial[i][f]) return true;
            }
        }
        return false;
    });

    const conflictSets = createMemo(() => {
        blurStamp();
        props.initialRows;
        return untrack(() => {
            const result: Record<string, Set<unknown>> = {};
            if (!props.conflictFields) return result;
            for (const field of props.conflictFields) {
                const counts = new Map<unknown, number>();
                for (let i = 0; i < props.rows.length; i++) {
                    const v = props.rows[i][field];
                    if (!v) continue;
                    counts.set(v, (counts.get(v) || 0) + 1);
                }
                const conflicts = new Set<unknown>();
                counts.forEach((c, v) => { if (c > 1) conflicts.add(v); });
                result[field] = conflicts;
            }
            return result;
        });
    });

    const isConflict = (field: string, value: unknown): boolean => {
        if (!value) return false;
        const sets = conflictSets();
        return !!sets[field] && sets[field].has(value);
    };

    const animateReorder = (prevPositions: Map<unknown, DOMRect>) => {
        rowRefs.forEach((el, id) => {
            const prev = prevPositions.get(id);
            if (!prev || !el.isConnected) return;
            const next = el.getBoundingClientRect();
            const dy = prev.top - next.top;
            if (dy === 0) return;
            el.animate(
                [{ transform: `translateY(${dy}px)` }, { transform: "translateY(0)" }],
                { duration: 300, easing: "cubic-bezier(0.22, 0.61, 0.36, 1)" }
            );
        });
    };

    const handleSort = (key: string) => {
        const positions = new Map<unknown, DOMRect>();
        rowRefs.forEach((el, id) => {
            if (el.isConnected) positions.set(id, el.getBoundingClientRect());
        });
        if (getSortKey() === key) {
            props.setSortDesc(!getSortDesc());
        } else {
            props.setSortKey(key);
            props.setSortDesc(false);
        }
        setSortStamp((s) => s + 1);
        animateReorder(positions);
    };

    const focusCell = (id: unknown, field: string) => {
        const el = inputRefs.get(id + "::" + field);
        if (el) {
            el.focus();
            try { el.select(); } catch {}
        }
    };

    const moveSelection = (dCol: number, dRow: number) => {
        const cur = selected();
        const rows = displayedRows();
        const fields = editableFields();
        if (rows.length === 0 || fields.length === 0) return;
        const idf = idField();
        let rowIdx = cur ? rows.findIndex((r) => r[idf] === cur.id) : 0;
        let colIdx = cur ? fields.indexOf(cur.field) : 0;
        if (rowIdx < 0) rowIdx = 0;
        if (colIdx < 0) colIdx = 0;
        const newRowIdx = Math.max(0, Math.min(rows.length - 1, rowIdx + dRow));
        const newColIdx = Math.max(0, Math.min(fields.length - 1, colIdx + dCol));
        const newId = rows[newRowIdx][idf];
        const newField = fields[newColIdx];
        setSelected({ id: newId, field: newField });
        focusCell(newId, newField);
    };

    const shouldNavigateHorizontal = (input: HTMLInputElement | null): boolean => {
        if (!input) return false;
        if (!input.value) return true;
        return typeof input.selectionStart === "number" && input.selectionStart !== input.selectionEnd;
    };

    const handleKeyDown = (e: KeyboardEvent) => {
        const key = e.key;
        const input = e.target as HTMLInputElement;

        if (key === "Enter") {
            e.preventDefault();
            moveSelection(0, e.shiftKey ? -1 : 1);
            return;
        }
        if (key === "Tab") {
            e.preventDefault();
            moveSelection(e.shiftKey ? -1 : 1, 0);
            return;
        }
        if (key === "Escape") {
            if (input && typeof input.setSelectionRange === "function") {
                const pos = input.selectionEnd || 0;
                try { input.setSelectionRange(pos, pos); } catch {}
            }
            e.preventDefault();
            return;
        }
        if (key === "ArrowUp" || key === "ArrowDown") {
            e.preventDefault();
            moveSelection(0, key === "ArrowDown" ? 1 : -1);
            return;
        }
        if (key === "ArrowLeft" || key === "ArrowRight") {
            if (shouldNavigateHorizontal(input)) {
                e.preventDefault();
                moveSelection(key === "ArrowRight" ? 1 : -1, 0);
            }
            return;
        }
    };

    const handleCellFocus = (id: unknown, field: string) => {
        setSelected({ id, field });
    };

    const handleCellMouseDown = (id: unknown, field: string) => {
        setSelected({ id, field });
    };

    const snapshotRowPositions = (): Map<unknown, DOMRect> => {
        const m = new Map<unknown, DOMRect>();
        rowRefs.forEach((el, id) => {
            if (el.isConnected) m.set(id, el.getBoundingClientRect());
        });
        return m;
    };

    props.ref?.({
        dirty,
        selected,
        focusCell,
        displayedRows,
        snapshotRowPositions,
        animateRows: animateReorder,
    });

    const dense = () => !!props.dense;
    const rowHCls = () => dense() ? "h-6" : "h-8";
    const readonlyTdCls = () => "border-b border-r border-neutral-300 bg-black/5 px-2 text-neutral-700 align-middle " + rowHCls();
    const editableTdCls = () => "border-b border-r border-neutral-300 p-0 relative align-middle";
    const inputCls = "absolute inset-0 w-full border-none outline-none bg-transparent px-2 placeholder:text-neutral-400 focus:bg-red-50 focus:shadow-[inset_0_0_0_2px_var(--color-red-500)]";

    const colSizeCls = (col: CellGridColumn) => columnSizeClass(col.width, col.minWidth);

    const renderHeader = (col: CellGridColumn) => {
        const widthCls = colSizeCls(col) ? " " + colSizeCls(col) : "";
        if (col.sortKey) {
            return (
                <SortableHeader label={col.label} sortKey={col.sortKey} width={col.width} minWidth={col.minWidth} current={getSortKey()} desc={getSortDesc()} onSort={handleSort}/>
            );
        }
        const cls = col.headerClass
            ? GRID_HEADER_CLS + " " + col.headerClass + widthCls
            : GRID_HEADER_CLS + widthCls;
        return <th class={cls}>{col.label}</th>;
    };

    const renderCell = (row: any, col: CellGridColumn) => {
        const idf = idField();
        const rowId = row[idf];
        const sizeCls = colSizeCls(col) ? " " + colSizeCls(col) : "";

        if (col.render && !col.editable) {
            const cellCls = () => {
                if (typeof col.cellClass === "function") return col.cellClass(row) + sizeCls;
                return (col.cellClass || readonlyTdCls()) + sizeCls;
            };
            return <td class={cellCls()} onclick={col.onclick ? (_: MouseEvent) => col.onclick!(row) : undefined}>{col.render!(row)}</td>;
        }

        if (col.readOnly) {
            const roCls = () => {
                if (typeof col.cellClass === "function") return col.cellClass(row) + sizeCls;
                return (col.cellClass || readonlyTdCls()) + sizeCls;
            };
            return <td class={roCls()} onclick={col.onclick ? (_: MouseEvent) => col.onclick!(row) : undefined}>{col.render ? col.render(row) : row[col.key]}</td>;
        }

        const hasConflict = () => isConflict(col.key, row[col.key]);
        const tdClass = () => {
            let base = editableTdCls() + sizeCls;
            if (props.conflictFields && props.conflictFields.includes(col.key)) {
                base += " relative";
                if (hasConflict()) base += " bg-amber-100";
            }
            return base;
        };

        return (
            <td class={tdClass()}>
                <input ref={setInputRef(rowId, col.key)} class={inputCls} type="text" inputmode={col.inputMode || "text"} placeholder={col.placeholder || ""} value={row[col.key]} oninput={(e: InputEvent) => {
                        const target = e.currentTarget as HTMLInputElement;
                        const val = col.parse ? col.parse(target.value) : target.value;
                        props.onCellChange(rowId, col.key, val);
                    }} onFocus={() => handleCellFocus(rowId, col.key)} onBlur={() => setBlurStamp((s) => s + 1)} onMouseDown={() => handleCellMouseDown(rowId, col.key)}/>
                <Show when={props.conflictFields && props.conflictFields.includes(col.key) && hasConflict()}>
                    <span class="pointer-events-none absolute right-0.5 top-1/2 -translate-y-1/2 text-amber-600" title="Duplicate value">
                        <Icon icon="triangle-exclamation" size={12}/>
                    </span>
                </Show>
            </td>
        );
    };

    const tableCls = () => "min-w-full w-max border-collapse " + (dense() ? "text-xs" : "text-sm");

    return (
        <div class="relative max-w-full overflow-x-auto border border-neutral-300 rounded-default bg-white tabular-nums">
            <table class={tableCls()}>
                <thead>
                    <tr>
                        <For each={props.columns}>{(col) => renderHeader(col)}</For>
                    </tr>
                </thead>
                <tbody onKeyDown={handleKeyDown}>
                    <For each={displayedRows()}>{(row) => (
                        <tr ref={setRowRef(row[idField()])} class="odd:bg-white even:bg-neutral-100">
                            <For each={props.columns}>{(col) => renderCell(row, col)}</For>
                        </tr>
                    )}</For>
                </tbody>
            </table>
        </div>
    );
}
