import React, { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState } from "react";
import axios from "axios";
import { notifyError, notifySuccess } from "../shared/toast";
import { useConfirm } from "../shared/ConfirmDialog";
import { TemplateShell, TemplateRow, Cell, TemplateDialog } from "./shared/TemplateShell";

// The third template screen. Message templates carry the WORDS a flow says, form templates the
// QUESTIONS it asks, and these carry the SUMS it works out — a real spreadsheet, formulas and all,
// authored here rather than in Google.
//
// LAZY, and this is the whole reason the canvas lives in its own file: Univer is a spreadsheet
// engine and the largest thing in this bundle. Imported statically it would be downloaded by every
// org staffer who opens the Dashboard and never touches a workbook.
const SheetCanvas = lazy(() => import("./workbook/SheetCanvas"));

const blankPort = () => ({ cell: "", var: "" });

// "OUTPUT_SEVERANCE" → "severance". The variable is what a later step reads, so it has to be an
// identifier; deriving it from the cell name saves the author typing the same thing twice in two
// different shapes.
function varFrom(cell) {
    return String(cell || "").toLowerCase()
        .replace(/^(input|output)_/, "")
        .replace(/[^a-z0-9]+/g, "_")
        .replace(/^_+|_+$/g, "")
        .slice(0, 64);
}

export default function WorkbookTemplates() {
    const [templates, setTemplates] = useState([]);
    const [loading, setLoading] = useState(true);
    const [selectedId, setSelectedId] = useState(null);
    const [draft, setDraft] = useState(null);
    const [saving, setSaving] = useState(false);
    const [query, setQuery] = useState("");
    // The names the sheet actually defines, refreshed from the canvas on demand. Offered rather
    // than typed: a mapping has to match the sheet exactly, and a typo would surface as a wrong
    // number in somebody's conversation rather than as an error here.
    const [names, setNames] = useState([]);
    // The sheet, and only the sheet, blown up to the window. A calculation is a wide thing and the
    // dialog is not; the alternative was making the dialog itself full-screen, which would hand a
    // whole screen to two text inputs and a mapping table as well. (Arfu, 10/09/2026)
    const [full, setFull] = useState(false);
    // The live workbook facade — snapshot() and names(). A ref, not state: it is a handle on
    // something Univer owns, and putting it in state would re-render the canvas that produced it.
    const sheetRef = useRef(null);
    const [confirmEl, confirm] = useConfirm();

    const refresh = useCallback(async () => {
        try {
            const res = await axios.get("/workbook_template/list");
            setTemplates(res.data?.data?.templates || []);
        } catch (err) {
            notifyError(err?.response?.data?.message || "Could not load workbooks.");
        } finally {
            setLoading(false);
        }
    }, []);

    useEffect(() => { refresh(); }, [refresh]);

    // The list carries no snapshot — five workbooks would be five whole spreadsheets — so opening
    // one fetches it.
    const open = async (id) => {
        setSelectedId(id);
        setNames([]);
        sheetRef.current = null;
        try {
            const res = await axios.get("/workbook_template/get", { params: { id } });
            const t = res.data?.data?.template || {};
            setDraft({
                id,
                name: t.name || "",
                description: t.description || "",
                snapshot: t.snapshot || null,
                inputs: (t.inputs || []).map(p => ({ ...p })),
                outputs: (t.outputs || []).map(p => ({ ...p })),
            });
        } catch (err) {
            notifyError(err?.response?.data?.message || "Could not open that workbook.");
            setSelectedId(null);
        }
    };

    const createNew = () => {
        setSelectedId(null);
        setNames([]);
        sheetRef.current = null;
        setDraft({ id: null, name: "", description: "", snapshot: null, inputs: [], outputs: [] });
    };

    const onSheetReady = (api) => {
        sheetRef.current = api;
        setNames(api.names());
    };

    const readNames = () => {
        if (!sheetRef.current) return;
        const found = sheetRef.current.names();
        setNames(found);
        notifySuccess(found.length
            ? `${found.length} name${found.length === 1 ? "" : "s"} in the sheet.`
            : "This sheet has no named cells yet — name one in the sheet first, then map it here.");
    };

    const patchPort = (side, i, patch) => setDraft(d => ({
        ...d,
        [side]: d[side].map((p, k) => k === i ? { ...p, ...patch } : p),
    }));
    const addPort = (side) => setDraft(d => ({ ...d, [side]: [...d[side], blankPort()] }));
    const removePort = (side, i) => setDraft(d => ({ ...d, [side]: d[side].filter((_, k) => k !== i) }));

    const save = async () => {
        if (!draft || !sheetRef.current) return;
        setSaving(true);
        try {
            // Pulled at SAVE, never watched: a half-typed formula is not a state worth storing.
            const payload = {
                name: draft.name.trim(),
                description: draft.description.trim() || null,
                snapshot: sheetRef.current.snapshot(),
                inputs: draft.inputs.filter(p => p.cell && p.var),
                outputs: draft.outputs.filter(p => p.cell && p.var),
            };
            const res = draft.id
                ? await axios.put("/workbook_template/edit", { id: draft.id, ...payload })
                : await axios.post("/workbook_template/create", payload);
            const saved = res.data?.data?.template;
            notifySuccess(res.data?.message || "Saved.");
            await refresh();
            if (saved?.id && !draft.id) setDraft(d => ({ ...d, id: String(saved.id) }));
            if (saved?.id) setSelectedId(String(saved.id));
        } catch (err) {
            notifyError(fieldErrors(err) || err?.response?.data?.message || "Could not save the workbook.");
        } finally {
            setSaving(false);
        }
    };

    const remove = async () => {
        if (!draft?.id) { setDraft(null); return; }
        const ok = await confirm({
            title: `Delete “${draft.name || "this workbook"}”?`,
            body: "Chatflows already published keep the copy they were saved with. A flow that still binds it will refuse the delete.",
            confirmLabel: "Delete",
            destructive: true,
        });
        if (!ok) return;
        try {
            await axios.post("/workbook_template/delete", { id: draft.id });
            notifySuccess("Workbook deleted.");
            setDraft(null);
            setSelectedId(null);
            refresh();
        } catch (err) {
            notifyError(err?.response?.data?.message || "Could not delete the workbook.");
        }
    };

    // Univer sizes its canvas to the container it was given, so a container that changes size has
    // to be told. The same DOM node stays mounted throughout — only its styles change — because
    // moving it in the tree would rebuild the instance and throw away unsaved edits.
    useEffect(() => {
        const id = requestAnimationFrame(() => window.dispatchEvent(new Event("resize")));

        return () => cancelAnimationFrame(id);
    }, [full]);

    // Esc is what people press, and there is no visible dialog chrome to click while full-screen.
    useEffect(() => {
        if (!full) return;
        const onKey = (e) => { if (e.key === "Escape") { e.stopPropagation(); setFull(false); } };
        window.addEventListener("keydown", onKey, true);

        return () => window.removeEventListener("keydown", onKey, true);
    }, [full]);

    const shown = useMemo(() => templates.filter(t =>
        !query.trim() || (t.name || "").toLowerCase().includes(query.trim().toLowerCase())
    ), [templates, query]);

    const canSave = draft && draft.name.trim() && draft.outputs.some(p => p.cell && p.var) && !saving;

    const COLUMNS = [
        { key: "name",  label: "Workbook" },
        { key: "about", label: "What it works out" },
        { key: "ports", label: "In / out", align: "right", width: 110 },
    ];

    return (
        <>
            <TemplateShell
                title="Workbook templates"
                blurb="Spreadsheets a chatflow fills in and reads results back from — the sums, kept where the org can edit them."
                newLabel="+ New workbook"
                onNew={createNew}
                onRefresh={refresh}
                search={query}
                onSearch={setQuery}
                searchPlaceholder="Search workbooks…"
                columns={COLUMNS}
                rows={shown}
                loading={loading}
                empty={{
                    title: "No workbooks yet",
                    body: "A workbook is a spreadsheet with formulas in it. A chatflow writes answers into the cells you name, the sheet works out the rest, and the results come back as variables the conversation can use.",
                }}
            >
                {shown.map(t => (
                    <TemplateRow key={t.id} onOpen={() => open(t.id)}>
                        <Cell><b style={{ fontWeight: 600 }}>{t.name || "Untitled"}</b></Cell>
                        <Cell muted>{t.description || "—"}</Cell>
                        <Cell align="right" mono nowrap>
                            {(t.inputs || []).length} / {(t.outputs || []).length}
                        </Cell>
                    </TemplateRow>
                ))}
            </TemplateShell>

            {draft && (
                <TemplateDialog
                    size="xl"
                    title={draft.id ? (draft.name || "Workbook") : "New workbook"}
                    subtitle="Write the calculation as a spreadsheet, then name the cells a chatflow fills in and the cells it reads back."
                    onClose={() => { setFull(false); setDraft(null); }}
                    onSave={save}
                    saveLabel={draft.id ? "Save" : "Create"}
                    saveDisabled={!canSave}
                    saving={saving}
                    onDelete={draft.id ? remove : null}
                >
                    <div style={{ display: "flex", gap: 10, alignItems: "flex-end", marginBottom: 14 }}>
                        <div style={{ flex: 1 }}>
                            <label style={{ fontSize: 11.5, color: "var(--muted)" }}>Name</label>
                            <input className="pt-input" value={draft.name} placeholder="Severance calculator"
                                onChange={e => setDraft(d => ({ ...d, name: e.target.value }))} />
                        </div>
                        <div style={{ flex: 2 }}>
                            <label style={{ fontSize: 11.5, color: "var(--muted)" }}>What it works out</label>
                            <input className="pt-input" value={draft.description} placeholder="Statutory severance, notice pay and a pro-rata month"
                                onChange={e => setDraft(d => ({ ...d, description: e.target.value }))} />
                        </div>
                    </div>

                    <div style={full
                        ? { position: "fixed", inset: 0, zIndex: 1200, background: "var(--surface)", display: "flex", flexDirection: "column" }
                        : { border: "1px solid var(--line)", borderRadius: 8, overflow: "hidden", height: "56vh", minHeight: 380, display: "flex", flexDirection: "column" }}>
                        <div style={{ display: "flex", alignItems: "center", gap: 8, padding: full ? "8px 12px" : "6px 8px",
                                      borderBottom: full ? "1px solid var(--line)" : "0" }}>
                            {full && <b style={{ fontSize: 13 }}>{draft.name || "New workbook"}</b>}
                            <button type="button" className="pt-btn pt-btn-sm" style={{ marginLeft: "auto" }}
                                onClick={() => setFull(f => !f)}>
                                {full ? "↙ Exit full screen  (Esc)" : "⛶ Full screen"}
                            </button>
                        </div>
                        <div style={{ flex: 1, minHeight: 0 }}>
                            <Suspense fallback={<div style={{ padding: 32, textAlign: "center", color: "var(--muted)", fontSize: 13 }}>Loading the spreadsheet…</div>}>
                                {/* KEYED on the template, so opening a different workbook builds a new
                                    instance rather than asking Univer to swallow another document. */}
                                <SheetCanvas key={draft.id || "new"} snapshot={draft.snapshot} onReady={onSheetReady} />
                            </Suspense>
                        </div>
                    </div>

                    <div style={{ display: "flex", alignItems: "center", gap: 10, margin: "16px 0 8px" }}>
                        <div style={{ fontSize: 12, fontWeight: 700, color: "var(--muted)", textTransform: "uppercase", letterSpacing: ".06em" }}>
                            What a chatflow puts in and takes out
                        </div>
                        <button className="pt-btn pt-btn-sm" onClick={readNames}>Read names from the sheet</button>
                    </div>
                    <div style={{ fontSize: 11.5, color: "var(--muted)", marginBottom: 12, lineHeight: 1.55, maxWidth: "72ch" }}>
                        Name the cells inside the sheet, then point at those names here — never a cell like
                        <b> B17</b>. Inserting a row moves what B17 means, and a figure that quietly starts
                        reading the wrong cell is the one mistake nobody catches.
                    </div>

                    <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 20 }}>
                        <PortTable side="inputs" label="Filled in from the conversation" rows={draft.inputs}
                            names={names} onAdd={() => addPort("inputs")}
                            onPatch={(i, patch) => patchPort("inputs", i, patch)}
                            onRemove={i => removePort("inputs", i)} />

                        <PortTable side="outputs" label="Read back into the conversation" rows={draft.outputs}
                            names={names} onAdd={() => addPort("outputs")}
                            onPatch={(i, patch) => patchPort("outputs", i, patch)}
                            onRemove={i => removePort("outputs", i)} />
                    </div>

                    <div style={{ fontSize: 11.5, color: "var(--muted)", marginTop: 14, lineHeight: 1.55, maxWidth: "72ch" }}>
                        Saving stores the sheet as it stands. A chatflow that already uses this workbook keeps
                        the copy it was saved with, so editing here cannot change a figure a published flow
                        has already quoted.
                    </div>
                </TemplateDialog>
            )}

            {confirmEl}
        </>
    );
}

// One side of the contract. Inputs and outputs are the same shape and deliberately the same
// editor — what differs is only which direction the value travels.
function PortTable({ side, label, rows, names, onAdd, onPatch, onRemove }) {
    return (
        <div style={{ marginBottom: 14 }}>
            <div style={{ fontSize: 12, fontWeight: 600, marginBottom: 6 }}>{label}</div>
            {rows.length === 0 && (
                <div style={{ fontSize: 12, color: "var(--muted)", marginBottom: 6 }}>
                    {side === "outputs"
                        ? "Nothing yet — a workbook with no outputs tells the chatflow nothing."
                        : "Nothing yet. A workbook with no inputs is a rate table, which is a fair thing to want."}
                </div>
            )}
            {rows.map((p, i) => (
                <div key={i} style={{ display: "flex", gap: 6, alignItems: "center", marginBottom: 6 }}>
                    <input className="pt-input" list={`wb-names-${side}`} value={p.cell} placeholder="INPUT_LAST_WAGE"
                        onChange={e => {
                            const cell = e.target.value;
                            // The variable follows the name until the author types their own.
                            onPatch(i, p.var ? { cell } : { cell, var: varFrom(cell) });
                        }}
                        style={{ flex: 1, fontFamily: "var(--mono)", fontSize: 12.5 }} />
                    <span style={{ color: "var(--muted)", fontSize: 12 }}>{side === "inputs" ? "←" : "→"}</span>
                    <input className="pt-input" value={p.var} placeholder="last_wage"
                        onChange={e => onPatch(i, { var: e.target.value })}
                        style={{ flex: 1, fontFamily: "var(--mono)", fontSize: 12.5 }} />
                    <button className="pt-btn" onClick={() => onRemove(i)} title="Remove">×</button>
                </div>
            ))}
            <datalist id={`wb-names-${side}`}>
                {names.map(n => <option key={n.name} value={n.name}>{n.ref}</option>)}
            </datalist>
            <button className="pt-btn pt-btn-sm" onClick={onAdd}>+ Add</button>
        </div>
    );
}

// Laravel answers a Form Request with {errors: {field: [messages]}}. Showing the first is enough:
// they are all about the same contract and a toast is not a form.
function fieldErrors(err) {
    const errors = err?.response?.data?.errors;
    if (!errors || typeof errors !== "object") return null;
    const first = Object.values(errors).flat()[0];

    return typeof first === "string" ? first : null;
}
