"use client";

import "@xyflow/react/dist/style.css";
import clsx from "clsx";
import Link from "next/link";
import { useParams, useRouter } from "next/navigation";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
  Background,
  BackgroundVariant,
  Controls,
  MarkerType,
  MiniMap,
  ReactFlow,
  ReactFlowProvider,
  useEdgesState,
  useNodesState,
  useReactFlow,
  useUpdateNodeInternals,
  type Connection,
  type Edge,
  type EdgeChange,
  type NodeChange,
} from "@xyflow/react";
import { Activity, AlertCircle, AlertTriangle, Play, ArrowLeft, CheckCircle2, ChevronDown, History, Plus, Rocket, Save, Settings2, ShieldCheck } from "lucide-react";
import { Badge, Button, ConfirmDialog, Dialog, Field, Input, Select, Skeleton } from "@/components/ui/kit";
import { useToast } from "@/components/ui/toast";
import { useSession } from "@/components/session";
import { TRIGGERS } from "@/components/bots/bot-reply-dialog";
import { api, ApiError } from "@/lib/api";
import { formatDateTime } from "@/lib/format";
import { useApi } from "@/lib/use-api";
import type { BotAsset, BotFlow, CustomField, FlowDraft, FlowIssue, FlowNodeType, FlowVersion, Group, Tag, TeamMember, TriggerType } from "@/lib/types";
import { BuilderContext, FlowNodeCard, type BuilderNode } from "./flow-node";
import { ADDABLE, defaultData, ensureIds, NODE_META, newNodeId, outputsOf, waitsForReply } from "./node-meta";
import { PropertiesPanel, type Lookups } from "./properties-panel";
import { PreviewPanel } from "./preview-panel";
import { RunsDialog } from "./runs-dialog";

const nodeTypes = Object.fromEntries((Object.keys(NODE_META) as FlowNodeType[]).map((t) => [t, FlowNodeCard]));
const edgeStyle = { stroke: "#0e6b5c", strokeWidth: 2 };
const defaultEdgeOptions = { type: "default", style: edgeStyle, markerEnd: { type: MarkerType.ArrowClosed, color: "#0e6b5c", width: 16, height: 16 } };

const TRIGGER_PHRASE: Record<TriggerType, string> = { welcome: "", is: "is exactly", starts_with: "starts with", ends_with: "ends with", contains_word: "contains the word", contains: "contains" };

const toNodes = (d: FlowDraft): BuilderNode[] => d.nodes.map((n) => ({ id: n.id, type: n.type, position: n.position, data: n.data, deletable: n.type !== "start" }));
const toEdges = (d: FlowDraft): Edge[] => d.edges.map((e) => ({ id: e.id, source: e.source, sourceHandle: e.sourceHandle, target: e.target, targetHandle: "in" }));

/** Drops connections whose output handle no longer exists (e.g. a deleted button). */
function pruneEdges(edges: Edge[], nodes: BuilderNode[]) {
  const outs = new Map(nodes.map((n) => [n.id, new Set(outputsOf(n.type as FlowNodeType, n.data).map((o) => o.id))]));
  return edges.filter((e) => outs.get(e.source)?.has(e.sourceHandle ?? "next") && outs.has(e.target));
}

function stripIds(data: Record<string, unknown>): Record<string, unknown> {
  const copy = structuredClone(data) as Record<string, unknown> & { action?: { buttons?: { id?: string }[]; sections?: { rows: { id?: string }[] }[] }; options?: { id?: string }[] };
  copy.action?.buttons?.forEach((b) => delete b.id);
  copy.action?.sections?.forEach((s) => s.rows.forEach((r) => delete r.id));
  copy.options?.forEach((o) => delete o.id);
  return copy;
}

function SettingsDialog({ flow, onClose, onSaved }: { flow: BotFlow; onClose: () => void; onSaved: (f: BotFlow, warnings: string[]) => void }) {
  const toast = useToast();
  const [title, setTitle] = useState(flow.title);
  const [tt, setTt] = useState<TriggerType>(flow.trigger_type);
  const [subject, setSubject] = useState(flow.trigger_subject ?? "");
  const [errors, setErrors] = useState<Record<string, string>>({});
  const [busy, setBusy] = useState(false);
  async function save() {
    setBusy(true);
    try {
      const r = await api.patch<{ data: BotFlow; warnings: string[] }>(`/bot-flows/${flow.id}`, { title, trigger_type: tt, trigger_subject: tt === "welcome" ? undefined : subject, whatsapp_account_id: flow.whatsapp_account_id });
      onSaved(r.data, r.warnings ?? []);
    } catch (e) {
      if (e instanceof ApiError && Object.keys(e.fields).length) setErrors(e.fields);
      else toast("error", (e as Error).message);
    } finally {
      setBusy(false);
    }
  }
  return (
    <Dialog
      open
      onClose={onClose}
      title="Flow settings"
      footer={
        <>
          <Button onClick={onClose}>Cancel</Button>
          <Button variant="primary" loading={busy} onClick={save}>
            Save
          </Button>
        </>
      }
    >
      <div className="space-y-4">
        <Field label="Title" htmlFor="fs-title" error={errors.title}>
          <Input id="fs-title" value={title} maxLength={120} onChange={(e) => setTitle(e.target.value)} invalid={!!errors.title} />
        </Field>
        <Field label="Starts when the message" htmlFor="fs-tt" error={errors.trigger_type}>
          <Select id="fs-tt" value={tt} onChange={(e) => setTt(e.target.value as TriggerType)}>
            {TRIGGERS.map((t) => (
              <option key={t.value} value={t.value}>
                {t.label}
              </option>
            ))}
          </Select>
        </Field>
        {tt !== "welcome" && (
          <Field label="Start trigger text" htmlFor="fs-ts" error={errors.trigger_subject}>
            <Input id="fs-ts" value={subject} maxLength={255} onChange={(e) => setSubject(e.target.value)} invalid={!!errors.trigger_subject} />
          </Field>
        )}
        <p className="text-[13px] text-muted">The trigger is part of each published version, so publish again after changing it.</p>
      </div>
    </Dialog>
  );
}

function VersionsDialog({ flowId, dirty, onClose, onRestored }: { flowId: number; dirty: boolean; onClose: () => void; onRestored: (f: BotFlow) => void }) {
  const toast = useToast();
  const { data } = useApi<{ data: FlowVersion[] }>(`/bot-flows/${flowId}/versions`);
  const [confirm, setConfirm] = useState<number | null>(null);
  const [busy, setBusy] = useState(false);
  async function restore(v: number) {
    setBusy(true);
    try {
      const r = await api.post<{ data: BotFlow }>(`/bot-flows/${flowId}/versions/${v}/restore`);
      toast("success", `Version ${v} copied into the draft`);
      onRestored(r.data);
    } catch (e) {
      toast("error", (e as Error).message);
    } finally {
      setBusy(false);
      setConfirm(null);
    }
  }
  const list = data?.data ?? [];
  return (
    <>
      <Dialog open onClose={onClose} title="Version history" description="Each publish is kept unchanged. Restoring copies a version into the draft; publish to make it live.">
        {!data ? (
          <Skeleton className="h-20 w-full" />
        ) : list.length === 0 ? (
          <p className="text-sm text-muted">Nothing published yet.</p>
        ) : (
          <ul className="divide-y divide-line">
            {list.map((v, i) => (
              <li key={v.version} className="flex items-center gap-3 py-3">
                <span className="tabular grid size-9 shrink-0 place-items-center rounded-full bg-teal-soft text-sm font-semibold text-teal">v{v.version}</span>
                <div className="min-w-0 flex-1 text-sm">
                  <p className="font-medium">
                    {v.note || `Version ${v.version}`} {i === 0 && <Badge tone="teal">Latest</Badge>}
                  </p>
                  <p className="text-[13px] text-muted">
                    {formatDateTime(v.published_at)}
                    {v.published_by_name ? `, ${v.published_by_name}` : ""}, {v.step_count} steps
                  </p>
                </div>
                <Button size="sm" onClick={() => (dirty ? setConfirm(v.version) : restore(v.version))} loading={busy && confirm === v.version}>
                  Restore
                </Button>
              </li>
            ))}
          </ul>
        )}
      </Dialog>
      <ConfirmDialog open={confirm !== null} title="Replace unsaved changes?" message="Your unsaved edits on the canvas will be lost." confirmLabel="Restore" loading={busy} onConfirm={() => confirm && restore(confirm)} onClose={() => setConfirm(null)} />
    </>
  );
}

function Builder({ initial, initialAssets, lookups }: { initial: BotFlow; initialAssets: BotAsset[]; lookups: Lookups }) {
  const toast = useToast();
  const router = useRouter();
  const { can } = useSession();
  const readOnly = !can("bots.manage");
  const rf = useReactFlow<BuilderNode, Edge>();
  const updateInternals = useUpdateNodeInternals();
  const wrapper = useRef<HTMLDivElement>(null);

  const [flow, setFlow] = useState(initial);
  const [nodes, setNodes, onNodesChangeBase] = useNodesState<BuilderNode>(toNodes(initial.draft!));
  const [edges, setEdges, onEdgesChangeBase] = useEdgesState<Edge>(toEdges(initial.draft!));
  const [dirty, setDirty] = useState(false);
  const [selectedId, setSelectedId] = useState<string | null>(null);
  const [issues, setIssues] = useState<FlowIssue[] | null>(null);
  const [assets, setAssets] = useState<Record<number, BotAsset>>(() => Object.fromEntries(initialAssets.map((a) => [a.id, a])));
  const [busy, setBusy] = useState<"save" | "validate" | "publish" | null>(null);
  const [dialog, setDialog] = useState<"settings" | "versions" | "publish" | "leave" | "runs" | "off" | null>(null);
  const [previewing, setPreviewing] = useState(false);
  const [previewAt, setPreviewAt] = useState<string | null>(null);
  const [toggling, setToggling] = useState<"active" | "inactive" | null>(null);
  const [note, setNote] = useState("");
  const [addOpen, setAddOpen] = useState(false);
  const [triggerWarnings, setTriggerWarnings] = useState<string[]>([]);

  const issueMap = useMemo(() => {
    const m = new Map<string, FlowIssue[]>();
    for (const i of issues ?? []) if (i.node_id) m.set(i.node_id, [...(m.get(i.node_id) ?? []), i]);
    return m;
  }, [issues]);
  const errorCount = (issues ?? []).filter((i) => i.severity === "error").length;
  const warningCount = (issues ?? []).filter((i) => i.severity === "warning").length;
  const triggerText = flow.trigger_type === "welcome" ? "someone messages for the first time" : `the message ${TRIGGER_PHRASE[flow.trigger_type]} “${flow.trigger_subject ?? ""}”`;

  // Warn before closing the tab with unsaved work.
  useEffect(() => {
    if (!dirty) return;
    const h = (e: BeforeUnloadEvent) => e.preventDefault();
    window.addEventListener("beforeunload", h);
    return () => window.removeEventListener("beforeunload", h);
  }, [dirty]);

  const onNodesChange = useCallback(
    (changes: NodeChange<BuilderNode>[]) => {
      if (readOnly) changes = changes.filter((c) => c.type === "select" || c.type === "dimensions");
      if (changes.some((c) => c.type === "remove" || c.type === "add" || (c.type === "position" && c.dragging === false))) setDirty(true);
      const removed = new Set(changes.filter((c) => c.type === "remove").map((c) => (c as { id: string }).id));
      if (removed.size) setSelectedId((s) => (s && removed.has(s) ? null : s));
      onNodesChangeBase(changes);
    },
    [onNodesChangeBase, readOnly],
  );
  const onEdgesChange = useCallback(
    (changes: EdgeChange<Edge>[]) => {
      if (readOnly) changes = changes.filter((c) => c.type === "select");
      if (changes.some((c) => c.type === "remove")) setDirty(true);
      onEdgesChangeBase(changes);
    },
    [onEdgesChangeBase, readOnly],
  );

  const isValidConnection = useCallback(
    (c: Connection | Edge) => {
      const target = rf.getNode(c.target);
      const source = rf.getNode(c.source);
      if (!target || !source || target.type === "start") return false;
      if (c.source === c.target) return waitsForReply(source.type as FlowNodeType, source.data);
      return true;
    },
    [rf],
  );

  const onConnect = useCallback(
    (c: Connection) => {
      const handle = c.sourceHandle ?? "next";
      // Each output leads to exactly one step: a new connection replaces the old one.
      setEdges((es) => [...es.filter((e) => !(e.source === c.source && (e.sourceHandle ?? "next") === handle)), { id: `e_${c.source}_${handle}_${c.target}`, source: c.source, sourceHandle: handle, target: c.target, targetHandle: "in" }]);
      setDirty(true);
    },
    [setEdges],
  );

  const updateData = useCallback(
    (id: string, data: Record<string, unknown>) => {
      const next = nodes.map((n) => (n.id === id ? { ...n, data: ensureIds(n.type as FlowNodeType, data) } : n));
      setNodes(next);
      setEdges((es) => pruneEdges(es, next));
      setDirty(true);
      requestAnimationFrame(() => updateInternals(id));
    },
    [nodes, setNodes, setEdges, updateInternals],
  );

  function addNode(type: FlowNodeType) {
    setAddOpen(false);
    const box = wrapper.current?.getBoundingClientRect();
    const center = rf.screenToFlowPosition({ x: (box?.left ?? 0) + (box?.width ?? 800) / 2 - 130, y: (box?.top ?? 0) + (box?.height ?? 600) / 2 - 60 });
    const offset = (nodes.length % 5) * 24;
    const node: BuilderNode = { id: newNodeId(type), type, position: { x: center.x + offset, y: center.y + offset }, data: ensureIds(type, defaultData(type)), selected: true };
    setNodes((ns) => [...ns.map((n) => ({ ...n, selected: false })), node]);
    setSelectedId(node.id);
    setDirty(true);
  }

  const duplicate = useCallback(
    (id: string) => {
      const src = rf.getNode(id);
      if (!src || src.type === "start") return;
      const node: BuilderNode = { id: newNodeId(src.type!), type: src.type, position: { x: src.position.x + 40, y: src.position.y + 40 }, data: ensureIds(src.type as FlowNodeType, stripIds(src.data)), selected: true };
      setNodes((ns) => [...ns.map((n) => ({ ...n, selected: false })), node]);
      setSelectedId(node.id);
      setDirty(true);
    },
    [rf, setNodes],
  );
  const remove = useCallback(
    (id: string) => {
      setNodes((ns) => ns.filter((n) => n.id !== id || n.type === "start"));
      setEdges((es) => es.filter((e) => e.source !== id && e.target !== id));
      setSelectedId((s) => (s === id ? null : s));
      setDirty(true);
    },
    [setNodes, setEdges],
  );

  const currentDraft = useCallback((): FlowDraft => {
    const vp = rf.getViewport();
    return {
      nodes: rf.getNodes().map((n) => ({ id: n.id, type: n.type as FlowNodeType, position: { x: Math.round(n.position.x), y: Math.round(n.position.y) }, data: n.data })),
      edges: rf.getEdges().map((e) => ({ id: e.id, source: e.source, sourceHandle: e.sourceHandle ?? "next", target: e.target })),
      viewport: { x: Math.round(vp.x), y: Math.round(vp.y), zoom: Number(vp.zoom.toFixed(3)) },
    };
  }, [rf]);

  function load(f: BotFlow) {
    setFlow(f);
    if (f.draft) {
      setNodes(toNodes(f.draft).map((n) => ({ ...n, selected: n.id === selectedId })));
      setEdges(toEdges(f.draft));
    }
    setDirty(false);
  }

  async function save(): Promise<boolean> {
    setBusy("save");
    try {
      const r = await api.put<{ data: BotFlow }>(`/bot-flows/${flow.id}/draft`, { revision: flow.draft_revision, draft: currentDraft() });
      setFlow((f) => ({ ...f, ...r.data }));
      setDirty(false);
      return true;
    } catch (e) {
      toast("error", (e as Error).message);
      return false;
    } finally {
      setBusy(null);
    }
  }

  async function validate() {
    setBusy("validate");
    try {
      const r = await api.post<{ data: { issues: FlowIssue[]; errors: number; warnings: number } }>(`/bot-flows/${flow.id}/validate`, { draft: currentDraft() });
      setIssues(r.data.issues);
      setSelectedId(null);
      if (!r.data.issues.length) toast("success", "No problems found");
    } catch (e) {
      toast("error", (e as Error).message);
    } finally {
      setBusy(null);
    }
  }

  async function publish() {
    if (dirty && !(await save())) return;
    setBusy("publish");
    try {
      const r = await api.post<{ data: BotFlow; issues: FlowIssue[] }>(`/bot-flows/${flow.id}/publish`, { note: note.trim() || undefined });
      load(r.data);
      setIssues(r.issues);
      setDialog(null);
      setNote("");
      toast("success", `Published version ${r.data.published_version}`);
    } catch (e) {
      if (e instanceof ApiError && Array.isArray(e.details?.issues)) {
        setIssues(e.details.issues as FlowIssue[]);
        setSelectedId(null);
        setDialog(null);
        toast("error", e.message);
      } else toast("error", (e as Error).message);
    } finally {
      setBusy(null);
    }
  }

  function focusNode(id: string) {
    const n = rf.getNode(id);
    if (!n) return;
    setNodes((ns) => ns.map((x) => ({ ...x, selected: x.id === id })));
    setSelectedId(id);
    rf.setCenter(n.position.x + 130, n.position.y + 60, { zoom: Math.max(rf.getZoom(), 0.9), duration: 300 });
  }

  // Ctrl/Cmd+S saves.
  const saveRef = useRef(save);
  useEffect(() => {
    saveRef.current = save;
  });
  useEffect(() => {
    if (readOnly) return;
    const h = (e: KeyboardEvent) => {
      if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "s") {
        e.preventDefault();
        saveRef.current();
      }
    };
    window.addEventListener("keydown", h);
    return () => window.removeEventListener("keydown", h);
  }, [readOnly]);

  const selected = nodes.find((n) => n.id === selectedId) ?? null;
  const ctx = useMemo(
    () => ({ issues: issueMap, trigger: flow.trigger_type === "welcome" ? "a first message" : flow.trigger_subject, readOnly, onDuplicate: duplicate, onDelete: remove, activeNodeId: previewing ? previewAt : null }),
    [issueMap, flow.trigger_type, flow.trigger_subject, readOnly, duplicate, remove, previewing, previewAt],
  );
  const showPanel = previewing || Boolean(selected) || issues !== null;

  async function setStatus(status: "active" | "inactive") {
    setToggling(status);
    try {
      const r = await api.post<{ data: BotFlow }>(`/bot-flows/${flow.id}/status`, { status });
      setFlow((f) => ({ ...f, ...r.data }));
      toast("success", status === "active" ? `“${flow.title}” is on and answering customers` : "Flow switched off");
      setDialog(null);
    } catch (e) {
      toast("error", (e as Error).message);
    } finally {
      setToggling(null);
    }
  }

  return (
    <div className="flex h-[calc(100dvh-4rem)] min-h-0 flex-col">
      {/* Toolbar */}
      <div className="flex flex-wrap items-center gap-2 border-b border-line bg-white px-3 py-2 sm:px-4">
        <Link
          href="/bot-flows"
          onClick={(e) => {
            if (dirty) {
              e.preventDefault();
              setDialog("leave");
            }
          }}
          className="rounded p-1.5 text-muted hover:bg-paper hover:text-teal"
          aria-label="Back to bot flows"
        >
          <ArrowLeft className="size-5" />
        </Link>
        <div className="min-w-0 flex-1 basis-[calc(100%-3rem)] md:basis-0">
          <div className="flex items-center gap-2">
            <h1 className="truncate text-[15px] font-semibold text-ink">{flow.title}</h1>
            {flow.published_version ? <Badge tone="teal">v{flow.published_version}</Badge> : <Badge>Not published</Badge>}
            {(dirty || (flow.published_version && flow.has_unpublished_changes)) && <span className="hidden whitespace-nowrap text-xs text-amber sm:inline">{dirty ? "Unsaved changes" : "Draft differs from published"}</span>}
          </div>
          <p className="truncate text-xs text-muted">Starts when {triggerText}</p>
        </div>
        <label className="flex items-center gap-2 text-[13px] font-medium" title={flow.published_version ? "Runs the published version for incoming messages" : "Publish the flow first"}>
          <input
            type="checkbox"
            role="switch"
            aria-label="Flow on"
            className="size-4 accent-teal"
            checked={(toggling ?? flow.status) === "active"}
            disabled={readOnly || toggling !== null || (!flow.published_version && flow.status !== "active")}
            onChange={(e) => (e.target.checked ? setStatus("active") : setDialog("off"))}
          />
          <span className={flow.status === "active" ? "text-teal" : "text-muted"}>{flow.status === "active" ? "On" : "Off"}</span>
        </label>
        <Button
          icon={<Play className="size-4" />}
          aria-label="Preview"
          onClick={() => {
            setSelectedId(null);
            setIssues(null);
            setPreviewing(true);
          }}
        >
          <span className="hidden sm:inline">Preview</span>
        </Button>
        <Button icon={<Activity className="size-4" />} onClick={() => setDialog("runs")} aria-label="Runs">
          <span className="hidden 2xl:inline">Runs</span>
        </Button>
        {!readOnly && (
          <div className="relative">
            <Button icon={<Plus className="size-4" />} onClick={() => setAddOpen((o) => !o)} aria-expanded={addOpen} aria-haspopup="menu">
              Add step <ChevronDown className="-mr-1 size-4" />
            </Button>
            {addOpen && (
              <>
                <div className="fixed inset-0 z-30" onClick={() => setAddOpen(false)} />
                <div role="menu" className="absolute right-0 z-40 mt-2 w-72 rounded-lg border border-line bg-white p-1 shadow-lg" onKeyDown={(e) => e.key === "Escape" && setAddOpen(false)}>
                  {ADDABLE.map((g) => (
                    <div key={g.heading} className="py-1">
                      <p className="px-3 pb-1 text-[11px] font-semibold uppercase tracking-wide text-muted">{g.heading}</p>
                      {g.types.map((t) => (
                        <button key={t} role="menuitem" onClick={() => addNode(t)} className="flex w-full items-start gap-3 rounded-md px-3 py-1.5 text-left hover:bg-paper focus:bg-paper focus:outline-none">
                          <span className={clsx("mt-0.5 grid size-6 shrink-0 place-items-center rounded [&_svg]:size-3.5", NODE_META[t].accent)}>{NODE_META[t].icon}</span>
                          <span>
                            <span className="block text-sm font-medium">{NODE_META[t].label}</span>
                            <span className="block text-xs text-muted">{NODE_META[t].hint}</span>
                          </span>
                        </button>
                      ))}
                    </div>
                  ))}
                </div>
              </>
            )}
          </div>
        )}
        <Button icon={<ShieldCheck className="size-4" />} loading={busy === "validate"} onClick={validate} aria-label="Validate">
          <span className="hidden sm:inline">Validate</span>
        </Button>
        <Button icon={<History className="size-4" />} onClick={() => setDialog("versions")} aria-label="Version history">
          <span className="hidden 2xl:inline">Versions</span>
        </Button>
        {!readOnly && (
          <>
            <Button icon={<Settings2 className="size-4" />} onClick={() => setDialog("settings")} aria-label="Flow settings">
              <span className="hidden 2xl:inline">Settings</span>
            </Button>
            <Button icon={<Save className="size-4" />} loading={busy === "save"} disabled={!dirty} onClick={save}>
              Save
            </Button>
            <Button variant="primary" icon={<Rocket className="size-4" />} loading={busy === "publish"} onClick={() => setDialog("publish")}>
              Publish
            </Button>
          </>
        )}
      </div>
      {triggerWarnings.length > 0 && (
        <div className="flex items-start gap-2 border-b border-amber/30 bg-amber-soft px-4 py-2 text-[13px] text-amber">
          <AlertTriangle className="mt-0.5 size-4 shrink-0" />
          <div className="flex-1">{triggerWarnings.join(" ")}</div>
          <button className="font-medium underline" onClick={() => setTriggerWarnings([])}>
            Dismiss
          </button>
        </div>
      )}

      <div className="relative flex min-h-0 flex-1">
        <div ref={wrapper} className="min-w-0 flex-1" aria-label="Flow canvas">
          <BuilderContext.Provider value={ctx}>
            <ReactFlow
              nodes={nodes}
              edges={edges}
              nodeTypes={nodeTypes}
              onNodesChange={onNodesChange}
              onEdgesChange={onEdgesChange}
              onConnect={onConnect}
              isValidConnection={isValidConnection}
              onNodeClick={(_, n) => {
                setPreviewing(false);
                setSelectedId(n.id);
              }}
              onPaneClick={() => setSelectedId(null)}
              defaultEdgeOptions={defaultEdgeOptions}
              defaultViewport={initial.draft?.viewport ?? { x: 80, y: 40, zoom: 0.9 }}
              minZoom={0.2}
              maxZoom={2}
              deleteKeyCode={readOnly ? null : ["Backspace", "Delete"]}
              nodesDraggable={!readOnly}
              nodesConnectable={!readOnly}
              proOptions={{ hideAttribution: false }}
              className="bg-paper"
            >
              <Background variant={BackgroundVariant.Dots} gap={18} size={1.2} color="#c9d4d0" />
              <Controls showInteractive={false} position="bottom-left" />
              <MiniMap pannable zoomable position="bottom-right" className="!hidden md:!block" nodeColor={(n) => (n.type === "start" ? "#0f2a2b" : n.type === "handoff" ? "#b93c2b" : "#2fbf8f")} maskColor="rgba(244,247,246,0.7)" />
            </ReactFlow>
          </BuilderContext.Provider>
        </div>

        {showPanel && (
          <aside className="absolute inset-0 z-20 flex flex-col border-l border-line bg-white sm:static sm:w-[380px] sm:shrink-0">
            {previewing ? (
              <PreviewPanel
                flowId={flow.id}
                draft={currentDraft}
                assets={assets}
                onAt={setPreviewAt}
                onClose={() => {
                  setPreviewing(false);
                  setPreviewAt(null);
                }}
              />
            ) : selected ? (
              <PropertiesPanel
                key={selected.id}
                nodeId={selected.id}
                type={selected.type as FlowNodeType}
                data={selected.data}
                issues={issueMap.get(selected.id) ?? []}
                lookups={lookups}
                assets={assets}
                trigger={triggerText}
                readOnly={readOnly}
                onChange={(d) => updateData(selected.id, d)}
                onAsset={(a) => setAssets((m) => ({ ...m, [a.id]: a }))}
                onClose={() => setSelectedId(null)}
                onEditSettings={() => setDialog("settings")}
              />
            ) : (
              issues && (
                <div className="flex h-full min-h-0 flex-col">
                  <header className="flex items-center gap-2 border-b border-line px-4 py-3">
                    <h2 className="flex-1 font-semibold">Check results</h2>
                    <button onClick={() => setIssues(null)} className="rounded px-2 py-1 text-[13px] text-muted hover:bg-paper">
                      Close
                    </button>
                  </header>
                  <div className="min-h-0 flex-1 overflow-y-auto p-4">
                    {issues.length === 0 ? (
                      <p className="flex items-center gap-2 text-sm text-teal">
                        <CheckCircle2 className="size-5" /> Everything checks out.
                      </p>
                    ) : (
                      <>
                        <p className="mb-3 text-sm text-muted">
                          {errorCount} {errorCount === 1 ? "problem" : "problems"} to fix, {warningCount} {warningCount === 1 ? "warning" : "warnings"}. Problems block publishing.
                        </p>
                        <ul className="space-y-2">
                          {issues.map((i, k) => {
                            const n = i.node_id ? nodes.find((x) => x.id === i.node_id) : null;
                            return (
                              <li key={k}>
                                <button
                                  onClick={() => i.node_id && focusNode(i.node_id)}
                                  disabled={!i.node_id}
                                  className={clsx("flex w-full items-start gap-2 rounded-md px-3 py-2 text-left text-[13px]", i.severity === "error" ? "bg-brick-soft text-brick" : "bg-amber-soft text-amber", i.node_id && "hover:ring-1 hover:ring-current")}
                                >
                                  {i.severity === "error" ? <AlertCircle className="mt-0.5 size-4 shrink-0" /> : <AlertTriangle className="mt-0.5 size-4 shrink-0" />}
                                  <span>
                                    {n && <span className="block font-semibold">{(n.data.label as string) || NODE_META[n.type as FlowNodeType].label}</span>}
                                    {i.message}
                                  </span>
                                </button>
                              </li>
                            );
                          })}
                        </ul>
                      </>
                    )}
                  </div>
                </div>
              )
            )}
          </aside>
        )}
      </div>

      {dialog === "settings" && (
        <SettingsDialog
          flow={flow}
          onClose={() => setDialog(null)}
          onSaved={(f, w) => {
            setFlow((cur) => ({ ...cur, ...f }));
            setTriggerWarnings(w);
            setDialog(null);
            toast("success", "Settings saved");
          }}
        />
      )}
      {dialog === "runs" && <RunsDialog flowId={flow.id} canManage={!readOnly} onClose={() => setDialog(null)} />}
      <ConfirmDialog
        open={dialog === "off"}
        title="Switch this flow off?"
        message="It stops answering its start message, and conversations in the middle of it stop where they are."
        confirmLabel="Switch off"
        loading={toggling !== null}
        onConfirm={() => setStatus("inactive")}
        onClose={() => setDialog(null)}
      />
      {dialog === "versions" && (
        <VersionsDialog
          flowId={flow.id}
          dirty={dirty}
          onClose={() => setDialog(null)}
          onRestored={(f) => {
            load(f);
            setIssues(null);
            setDialog(null);
          }}
        />
      )}
      {dialog === "publish" && (
        <Dialog
          open
          onClose={() => setDialog(null)}
          title={`Publish version ${(flow.published_version ?? 0) + 1}`}
          description="Publishing saves your draft, checks it, and keeps an unchangeable copy you can restore later."
          footer={
            <>
              <Button onClick={() => setDialog(null)}>Cancel</Button>
              <Button variant="primary" loading={busy === "publish" || busy === "save"} onClick={publish}>
                Publish
              </Button>
            </>
          }
        >
          <Field label="What changed? (optional)" htmlFor="pub-note">
            <Input id="pub-note" value={note} maxLength={200} onChange={(e) => setNote(e.target.value)} placeholder="e.g. Added refund answers" />
          </Field>
          {flow.status === "active" && <p className="mt-3 text-[13px] text-muted">The flow is on: new conversations use the new version right away. Conversations already in progress finish on the version they started with.</p>}
        </Dialog>
      )}
      <ConfirmDialog open={dialog === "leave"} title="Leave without saving?" message="Your changes to this flow will be lost." confirmLabel="Leave" onConfirm={() => router.push("/bot-flows")} onClose={() => setDialog(null)} />
    </div>
  );
}

export function FlowBuilderPage() {
  const { id } = useParams<{ id: string }>();
  const flowReq = useApi<{ data: BotFlow & { assets: BotAsset[] } }>(`/bot-flows/${id}`);
  const fields = useApi<{ data: CustomField[] }>("/custom-fields");
  const groups = useApi<{ data: Group[] }>("/groups");
  const tags = useApi<{ data: Tag[] }>("/tags");
  const team = useApi<{ data: TeamMember[] }>("/team");
  const lookups = useMemo<Lookups>(
    () => ({ customFields: fields.data?.data ?? [], groups: groups.data?.data ?? [], tags: tags.data?.data ?? [], team: team.data?.data ?? [] }),
    [fields.data, groups.data, tags.data, team.data],
  );
  if (flowReq.error) return <p className="m-6 rounded-md bg-brick-soft px-4 py-3 text-sm text-brick">{flowReq.error}</p>;
  if (!flowReq.data) return <Skeleton className="m-6 h-[60vh]" />;
  return (
    <ReactFlowProvider>
      <Builder key={flowReq.data.data.id} initial={flowReq.data.data} initialAssets={flowReq.data.data.assets} lookups={lookups} />
    </ReactFlowProvider>
  );
}
