"use client"

import { Suspense, useCallback, useEffect, useMemo, useState } from "react"
import { useParams, useRouter, useSearchParams } from "next/navigation"
import Link from "next/link"
import {
  addEdge,
  applyEdgeChanges,
  applyNodeChanges,
  Background,
  Controls,
  MiniMap,
  ReactFlow,
  type Connection,
  type Edge,
  type Node,
  type NodeChange,
  type EdgeChange,
  type NodeMouseHandler,
} from "@xyflow/react"
import "@xyflow/react/dist/style.css"
import { useTheme } from "next-themes"
import { AlertTriangle, Lock, Minus, Plus, Save, Square } from "lucide-react"

import { cn } from "@/lib/utils"
import { productionTemplatesApi } from "@/lib/api/production-templates"
import { itemsApi } from "@/lib/api/items"
import { uomsApi } from "@/lib/api/uoms"
import { errorMessage } from "@/lib/error-map"
import { CanvasAnnotation, ProductionTemplateGraph, SaveTemplateRequest, TemplateStatus } from "@/types/production"
import { ItemListItem, Uom } from "@/types/master-data"

import { Badge } from "@/components/ui/badge"
import { Button, buttonVariants } from "@/components/ui/button"
import { Field, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Skeleton } from "@/components/ui/skeleton"
import { toast } from "@/components/ui/toast"
import StageNode from "./StageNode"
import AnnotationBoxNode, { LineNodeComponent } from "./AnnotationNodes"
import { StageEditorPanel, type UpstreamOutputOption } from "./StageEditorPanel"
import { AnnotationData, StageNodeData, newKey, newLocalId } from "./types"

const nodeTypes = { stage: StageNode, box: AnnotationBoxNode, line: LineNodeComponent }

const DEFAULT_BOX = { width: 320, height: 220 }
const DEFAULT_LINE = { width: 220, height: 4 }

/**
 * Server graph -> React Flow.
 *
 * A stage's node id **is** its server key, which is why edges need no translation in either
 * direction: `edge.source`/`edge.target` are already the `parentKey`/`childKey` the save
 * payload wants. Annotations come first in the array because React Flow paints later entries
 * on top, and a grouping box belongs behind the stage cards it groups.
 */
function graphToFlow(graph: ProductionTemplateGraph): { nodes: Node[]; edges: Edge[] } {
  const annotations: Node[] = graph.annotations.map((a) => ({
    id: `ann-${newLocalId()}`,
    type: a.kind,
    position: { x: a.posX, y: a.posY },
    width: a.width,
    height: a.height,
    data: { label: a.label ?? "", rotation: a.rotation ?? undefined } satisfies AnnotationData,
  }))

  const stages: Node[] = graph.stages.map((s) => ({
    id: s.key,
    type: "stage",
    position: { x: s.posX, y: s.posY },
    data: {
      name: s.name,
      roleLabel: s.roleLabel ?? "",
      estimatedMinutes: s.estimatedMinutes,
      inputs: s.inputs.map((i) => ({
        localId: newLocalId(),
        source: i.source,
        itemId: i.itemId,
        fromOutputKey: i.fromOutputKey,
        qtyUnit: i.qtyUnit,
        qtyPerBatch: i.qtyPerBatch,
      })),
      outputs: s.outputs.map((o) => ({
        key: o.key,
        itemId: o.itemId,
        name: o.name,
        uomId: o.uomId,
        qtyPerBatch: o.qtyPerBatch,
      })),
      fieldDefs: s.fieldDefs.map((f) => ({
        localId: newLocalId(),
        key: f.key,
        label: f.label,
        type: f.type,
        options: f.options ?? [],
        required: f.required,
      })),
    } satisfies StageNodeData,
  }))

  const edges: Edge[] = graph.edges.map((e) => ({
    id: `e${e.edgeId}`,
    source: e.parentKey,
    target: e.childKey,
  }))

  return { nodes: [...annotations, ...stages], edges }
}

/** Kahn's algorithm — true when the toposort can't reach every node, i.e. there's a cycle. */
function detectCycle(nodes: Node[], edges: Edge[]): boolean {
  const inDegree = new Map(nodes.map((n) => [n.id, 0]))
  for (const e of edges) inDegree.set(e.target, (inDegree.get(e.target) ?? 0) + 1)
  const queue = nodes.filter((n) => inDegree.get(n.id) === 0).map((n) => n.id)
  let visited = 0
  while (queue.length > 0) {
    const id = queue.shift()!
    visited++
    for (const e of edges.filter((e) => e.source === id)) {
      const next = (inDegree.get(e.target) ?? 0) - 1
      inDegree.set(e.target, next)
      if (next === 0) queue.push(e.target)
    }
  }
  return visited !== nodes.length
}

/**
 * Best-effort mapping from a `422 GRAPH_*` detail back to the stages it names.
 *
 * The validator's messages quote real stage names ("...contains a cycle involving: Cut frame,
 * Assemble"), so a substring match finds them without the server having to return keys. It is
 * deliberately advisory: the full message is always shown in the banner too, so a stage renamed
 * to something ambiguous costs a highlight, never the explanation.
 */
function stagesNamedIn(detail: string | undefined, stageNodes: Node[]): Set<string> {
  if (!detail) return new Set()
  const named = stageNodes.filter((n) => {
    const name = (n.data as StageNodeData).name.trim()
    return name.length > 0 && detail.includes(name)
  })
  return new Set(named.map((n) => n.id))
}

function TemplateBuilderContent() {
  const params = useParams<{ id: string }>()
  const searchParams = useSearchParams()
  const router = useRouter()
  const { resolvedTheme } = useTheme()

  // "new" is a draft that exists only in this page until the first successful Save. A template
  // cannot be created from a name alone — the server requires at least one stage and a terminal
  // output naming a real finished item (FR-MFG-02/05) — so there is nothing to POST up front.
  const isNew = params.id === "new"
  const templateId = Number(params.id)

  const [graph, setGraph] = useState<ProductionTemplateGraph | null>(null)
  const [etag, setEtag] = useState<string | null>(null)
  const [loadError, setLoadError] = useState<string | null>(null)

  // Derived rather than its own state: a draft is ready immediately, and a saved template is
  // ready as soon as the GET resolves either way.
  const loaded = isNew || graph !== null || loadError !== null

  const [code, setCode] = useState("")
  const [name, setName] = useState("")
  const [description, setDescription] = useState("")

  const [nodes, setNodes] = useState<Node[]>([])
  const [edges, setEdges] = useState<Edge[]>([])
  const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null)

  const [items, setItems] = useState<ItemListItem[]>([])
  const [uoms, setUoms] = useState<Uom[]>([])

  const [saving, setSaving] = useState(false)
  const [saveError, setSaveError] = useState<string | null>(null)
  const [conflict, setConflict] = useState(false)
  const [lockedByServer, setLockedByServer] = useState(false)
  const [focusedKeys, setFocusedKeys] = useState<Set<string>>(new Set())
  const [togglingStatus, setTogglingStatus] = useState(false)

  // `lockedByServer` covers the edit-lock TOCTOU: a run can start between our GET and our PUT,
  // in which case the 409 is the first we hear of it (FR-MFG-06).
  const activeRunCount = graph?.activeRunCount ?? 0
  const locked = activeRunCount > 0 || lockedByServer

  // `resolvedTheme` is unknown on the server (and on the client's first paint, before
  // next-themes reads localStorage), so `colorMode` would differ between the SSR markup and the
  // first client render — the same hydration-mismatch class theme-toggle.tsx guards against.
  const [mounted, setMounted] = useState(false)
  useEffect(() => setMounted(true), [])

  const applyGraph = useCallback((data: ProductionTemplateGraph, tag: string | null) => {
    const flow = graphToFlow(data)
    setGraph(data)
    setEtag(tag)
    setCode(data.code)
    setName(data.name)
    setDescription(data.description ?? "")
    setNodes(flow.nodes)
    setEdges(flow.edges)
    setSelectedNodeId(null)
    setConflict(false)
    setLockedByServer(false)
    setFocusedKeys(new Set())
    setSaveError(null)
  }, [])

  const load = useCallback(() => {
    setLoadError(null)
    productionTemplatesApi
      .get(templateId)
      .then(({ data, etag: tag }) => applyGraph(data, tag))
      .catch((err) => setLoadError(errorMessage(err)))
  }, [templateId, applyGraph])

  useEffect(() => {
    if (isNew) {
      // Seeded from the overview's "New Template" dialog.
      setCode(searchParams.get("code") ?? "")
      setName(searchParams.get("name") ?? "")
      return
    }
    if (Number.isFinite(templateId)) load()
    // searchParams is read once for the draft seed; re-running on every query change would
    // overwrite what the user has typed since.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isNew, templateId, load])

  useEffect(() => {
    Promise.all([itemsApi.list({ pageSize: 200, status: "Active" }), uomsApi.list({ pageSize: 200 })])
      .then(([itemRes, uomRes]) => {
        setItems(itemRes.items)
        setUoms(uomRes.items)
      })
      .catch((err) => toast.error("Could not load items and UOMs", errorMessage(err)))
  }, [])

  const onNodesChange = useCallback(
    (changes: NodeChange[]) => setNodes((nds) => applyNodeChanges(changes, nds)),
    []
  )
  const onEdgesChange = useCallback(
    (changes: EdgeChange[]) => setEdges((eds) => applyEdgeChanges(changes, eds)),
    []
  )
  const onConnect = useCallback(
    (connection: Connection) => {
      if (locked) return
      if (connection.source === connection.target) {
        toast.error("Can't connect a stage to itself")
        return
      }
      const duplicate = edges.some((e) => e.source === connection.source && e.target === connection.target)
      if (duplicate) {
        toast.error("These stages are already connected")
        return
      }
      setEdges((eds) => addEdge(connection, eds))
    },
    [edges, locked]
  )

  const onNodeClick: NodeMouseHandler = useCallback((_, node) => setSelectedNodeId(node.id), [])
  const onPaneClick = useCallback(() => setSelectedNodeId(null), [])

  // Guarded here, not just by hiding the toolbar/panel controls: `elementsSelectable` stays
  // true even when locked (so a locked template can still be inspected), and these two setters
  // go straight to setNodes/setEdges — they don't route through onNodesChange, which is what
  // actually gets set to `undefined` when locked.
  function updateNodeData(nodeId: string, patch: Partial<StageNodeData> | Partial<AnnotationData>) {
    if (locked) return
    setNodes((nds) => nds.map((n) => (n.id === nodeId ? { ...n, data: { ...n.data, ...patch } } : n)))
  }

  function addStage() {
    const id = newKey()
    const existingStages = nodes.filter((n) => n.type === "stage")
    const maxX = existingStages.reduce((max, n) => Math.max(max, n.position.x), 0)
    const y = existingStages.length > 0 ? existingStages[existingStages.length - 1].position.y : 120
    setNodes((nds) => [
      ...nds,
      {
        id,
        type: "stage",
        position: { x: existingStages.length > 0 ? maxX + 280 : 40, y },
        data: {
          name: "New stage",
          roleLabel: "",
          estimatedMinutes: 15,
          inputs: [],
          outputs: [],
          fieldDefs: [],
        } satisfies StageNodeData,
      },
    ])
    setSelectedNodeId(id)
  }

  // Generic across every node type — stage cards, boxes and lines all use this (the inline ×
  // buttons on the nodes themselves, plus the stage editor's "Delete stage"). Box/line nodes
  // never have edges, so the edge filter is a no-op for them, not a special case.
  function deleteNode(nodeId: string) {
    if (locked) return
    setNodes((nds) => nds.filter((n) => n.id !== nodeId))
    setEdges((eds) => eds.filter((e) => e.source !== nodeId && e.target !== nodeId))
    setSelectedNodeId((id) => (id === nodeId ? null : id))
  }

  function addAnnotation(kind: "box" | "line") {
    const size = kind === "box" ? DEFAULT_BOX : DEFAULT_LINE
    setNodes((nds) => [
      {
        id: `ann-${newLocalId()}`,
        type: kind,
        position: kind === "box" ? { x: 40, y: 40 } : { x: 60, y: 300 },
        ...size,
        data: { label: "" } satisfies AnnotationData,
      },
      ...nds,
    ])
  }

  // React Flow's built-in keyboard delete (Backspace/Delete on a selected node) goes through
  // this callback, not through deleteNode() above — stage edges need cleaning up either way.
  const onNodesDelete = useCallback((deleted: Node[]) => {
    const deletedIds = new Set(deleted.map((n) => n.id))
    setEdges((eds) => eds.filter((e) => !deletedIds.has(e.source) && !deletedIds.has(e.target)))
  }, [])

  // Boxes and lines are pure annotations — never part of the stage graph, so every graph check
  // below operates on stage nodes only (docs/21 §2 "Client-side graph checks (UX only — server
  // re-validates on save)").
  const stageNodes = useMemo(() => nodes.filter((n) => n.type === "stage"), [nodes])

  const analysis = useMemo(() => {
    const terminalIds = new Set(stageNodes.filter((n) => !edges.some((e) => e.source === n.id)).map((n) => n.id))
    const entryIds = new Set(stageNodes.filter((n) => !edges.some((e) => e.target === n.id)).map((n) => n.id))
    const disconnectedIds = new Set(
      stageNodes.length > 1
        ? stageNodes.filter((n) => !edges.some((e) => e.source === n.id || e.target === n.id)).map((n) => n.id)
        : []
    )
    const hasCycle = detectCycle(stageNodes, edges)
    return { terminalIds, entryIds, disconnectedIds, hasCycle }
  }, [stageNodes, edges])

  /** Output keys a stage may legally draw from — its *direct* parents' outputs (FR-MFG-04). */
  const allowedUpstreamKeys = useCallback(
    (stageId: string) => {
      const parentIds = new Set(edges.filter((e) => e.target === stageId).map((e) => e.source))
      return new Set(
        nodes
          .filter((n) => n.type === "stage" && parentIds.has(n.id))
          .flatMap((n) => (n.data as StageNodeData).outputs.map((o) => o.key))
      )
    },
    [edges, nodes]
  )

  // Clear stale Upstream references after an edge is deleted, with a warning toast — docs/21 §2
  // "re-check after edge deletions and clear broken references with a warning toast".
  useEffect(() => {
    for (const node of stageNodes) {
      const data = node.data as StageNodeData
      const allowed = allowedUpstreamKeys(node.id)
      const stale = data.inputs.filter(
        (i) => i.source === "Upstream" && i.fromOutputKey && !allowed.has(i.fromOutputKey)
      )
      if (stale.length > 0) {
        updateNodeData(node.id, {
          inputs: data.inputs.map((i) => (stale.includes(i) ? { ...i, fromOutputKey: null } : i)),
        })
        toast.warning("Input reference cleared", `"${data.name}" drew from a stage that no longer feeds it.`)
      }
    }
    // Only re-run when the edge set changes — re-running on every node data edit would loop.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [edges])

  const issues = useMemo(() => {
    const list: string[] = []
    if (!code.trim()) list.push("Code is required.")
    if (!name.trim()) list.push("Name is required.")
    if (stageNodes.length === 0) list.push("Add at least one stage.")

    if (analysis.hasCycle) list.push("Cycle detected — stages must form a one-directional flow.")
    if (stageNodes.length > 0 && analysis.terminalIds.size !== 1) {
      list.push(
        analysis.terminalIds.size === 0
          ? "No terminal stage — connect stages so the line converges to a single final stage."
          : `${analysis.terminalIds.size} terminal stages found — connect stages so the line converges to a single final stage.`
      )
    }
    if (stageNodes.length > 0 && analysis.entryIds.size === 0) {
      list.push("No entry stage — at least one stage must have no inputs from other stages.")
    }
    if (analysis.disconnectedIds.size > 0) {
      const names = stageNodes.filter((n) => analysis.disconnectedIds.has(n.id)).map((n) => (n.data as StageNodeData).name)
      list.push(`Disconnected stage${names.length > 1 ? "s" : ""}: ${names.join(", ")}.`)
    }

    // Field-completeness. These mirror the server's own requirements, and they are what make
    // the non-null assertions in buildRequest() below sound — a row is never sent half-filled.
    for (const node of stageNodes) {
      const data = node.data as StageNodeData
      const label = data.name.trim() || "Untitled stage"
      const isTerminal = analysis.terminalIds.has(node.id)

      if (!data.name.trim()) list.push("Every stage needs a name.")

      data.inputs.forEach((input, i) => {
        const where = `Input ${i + 1} of "${label}"`
        if (input.source === "Stock" && input.itemId === null) list.push(`${where} needs an item.`)
        if (input.source === "Upstream" && !input.fromOutputKey) list.push(`${where} needs an upstream output.`)
        // Content quantities divide by the item's content size, so the item must declare one.
        if (input.qtyUnit === "Content") {
          const item = items.find((candidate) => candidate.itemId === input.itemId)
          if (item && !item.contentBaseQty) {
            list.push(`${where} is in content units, but ${item.sku} has no content size.`)
          }
        }
        if (input.qtyPerBatch <= 0) list.push(`${where} needs a quantity greater than zero.`)
      })

      data.outputs.forEach((output, i) => {
        const where = `Output ${i + 1} of "${label}"`
        if (!isTerminal && !output.name.trim()) list.push(`${where} needs a name.`)
        // Only work-in-progress declares its own unit; a finished good takes its item's.
        if (!isTerminal && output.uomId === null) list.push(`${where} needs a UOM.`)
        if (output.qtyPerBatch <= 0) list.push(`${where} needs a quantity greater than zero.`)
      })

      if (isTerminal) {
        if (data.outputs.length !== 1) {
          list.push(`Terminal stage "${label}" must have exactly one output (the finished good).`)
        } else if (data.outputs[0].itemId === null) {
          list.push(`Terminal stage "${label}" needs its output linked to a finished-good item.`)
        }
      }
    }

    return list
  }, [analysis, stageNodes, code, name])

  const selectedNode = stageNodes.find((n) => n.id === selectedNodeId)
  const upstreamOptions: UpstreamOutputOption[] = useMemo(() => {
    if (!selectedNode) return []
    const parentIds = edges.filter((e) => e.target === selectedNode.id).map((e) => e.source)
    return parentIds.flatMap((parentId) => {
      const parent = nodes.find((n) => n.id === parentId)
      if (!parent) return []
      const parentData = parent.data as StageNodeData
      return parentData.outputs.map((o) => ({
        stageKey: parent.id,
        stageName: parentData.name,
        outputKey: o.key,
        outputName: o.name || "(unnamed output)",
      }))
    })
  }, [selectedNode, edges, nodes])

  const displayNodes = useMemo(
    () =>
      nodes.map((n) =>
        n.type === "stage"
          ? {
              ...n,
              data: {
                ...n.data,
                disconnected: analysis.disconnectedIds.has(n.id),
                focused: focusedKeys.has(n.id),
                onDelete: locked ? undefined : () => deleteNode(n.id),
              },
            }
          : {
              ...n,
              data: {
                ...n.data,
                onLabelChange: locked ? undefined : (label: string) => updateNodeData(n.id, { label }),
                onRotationChange: locked ? undefined : (rotation: number) => updateNodeData(n.id, { rotation }),
                onDelete: locked ? undefined : () => deleteNode(n.id),
              },
            }
      ),
    [nodes, analysis.disconnectedIds, focusedKeys] // eslint-disable-line react-hooks/exhaustive-deps
  )

  function buildRequest(): SaveTemplateRequest {
    const stages = stageNodes.map((n) => {
      const data = n.data as StageNodeData
      const isTerminal = analysis.terminalIds.has(n.id)
      return {
        key: n.id,
        name: data.name.trim(),
        roleLabel: data.roleLabel.trim() || null,
        estimatedMinutes: data.estimatedMinutes,
        posX: Math.round(n.position.x),
        posY: Math.round(n.position.y),
        fieldDefs: data.fieldDefs.map((f) => ({
          key: f.key,
          label: f.label,
          type: f.type,
          options: f.type === "Select" ? f.options : null,
          required: f.required,
        })),
        inputs: data.inputs.map((i) => ({
          source: i.source,
          itemId: i.source === "Stock" ? i.itemId : null,
          fromOutputKey: i.source === "Upstream" ? i.fromOutputKey : null,
          qtyUnit: i.source === "Upstream" ? "Pack" : i.qtyUnit,
          qtyPerBatch: i.qtyPerBatch,
        })),
        // Only the terminal stage's output may name an item (FR-MFG-05). A stage that *was*
        // terminal and then got a child keeps its picked itemId in local state with the item
        // field no longer rendered, so dropping it here is the only way the user can recover —
        // an issue-list message about an invisible field would be unactionable.
        outputs: data.outputs.map((o) => ({
          key: o.key,
          itemId: isTerminal ? o.itemId : null,
          name: o.name.trim(),
          // An item-bearing output takes its unit from the item, so it must send none.
          uomId: isTerminal ? null : o.uomId,
          qtyPerBatch: o.qtyPerBatch,
        })),
      }
    })

    const annotations: CanvasAnnotation[] = nodes
      .filter((n) => n.type === "box" || n.type === "line")
      .map((n) => {
        const data = n.data as AnnotationData
        const fallback = n.type === "box" ? DEFAULT_BOX : DEFAULT_LINE
        return {
          kind: n.type as "box" | "line",
          posX: Math.round(n.position.x),
          posY: Math.round(n.position.y),
          // `width`/`height` are set on creation and updated by NodeResizer; `measured` is what
          // React Flow fills in after layout for nodes sized purely by CSS.
          width: Math.round(n.width ?? n.measured?.width ?? fallback.width),
          height: Math.round(n.height ?? n.measured?.height ?? fallback.height),
          label: data.label.trim() || null,
          rotation: data.rotation ?? null,
        }
      })

    return {
      code: code.trim(),
      name: name.trim(),
      description: description.trim() || null,
      stages,
      edges: edges.map((e) => ({ parentKey: e.source, childKey: e.target })),
      annotations,
    }
  }

  async function handleSave() {
    if (issues.length > 0) {
      toast.error("Can't save yet", `${issues.length} issue${issues.length > 1 ? "s" : ""} to fix first.`)
      return
    }

    setSaving(true)
    setSaveError(null)
    setFocusedKeys(new Set())

    try {
      if (isNew) {
        const result = await productionTemplatesApi.create(buildRequest())
        toast.success("Template created", `${result.data.code} — ${result.data.name}`)
        // Swap the draft URL for the real one. The load effect re-runs on the new id and
        // rehydrates from the server, so keys minted here are replaced by real ones.
        router.replace(`/dashboard/production/templates/${result.data.templateId}`)
        return
      }

      if (!etag) return
      const result = await productionTemplatesApi.update(templateId, buildRequest(), etag)
      applyGraph(result.data, result.etag)
      toast.success("Template saved", `${result.data.code} — ${result.data.stages.length} stage(s)`)
    } catch (err) {
      const errorCode = (err as { code?: string })?.code
      const detail = (err as { detail?: string })?.detail

      if (errorCode === "CONCURRENCY_CONFLICT" || errorCode === "PRECONDITION_REQUIRED") {
        setConflict(true)
        setSaveError(errorMessage(err))
        return
      }
      if (errorCode === "TEMPLATE_IN_USE") {
        // A run started between our GET and this PUT. Lock the canvas rather than reloading,
        // so nothing the user just drew is thrown away without them seeing why.
        setLockedByServer(true)
        setSaveError(errorMessage(err))
        return
      }
      if (errorCode?.startsWith("GRAPH_") || errorCode === "TERMINAL_OUTPUT_ITEM_REQUIRED") {
        setFocusedKeys(stagesNamedIn(detail, stageNodes))
      }
      setSaveError(errorMessage(err))
      toast.error("Could not save template", errorMessage(err))
    } finally {
      setSaving(false)
    }
  }

  async function handleToggleStatus() {
    if (!graph) return
    const next: TemplateStatus = graph.status === "Active" ? "Inactive" : "Active"
    setTogglingStatus(true)
    try {
      await productionTemplatesApi.updateStatus(templateId, next)
      // PATCH /status bumps the row's xmin, which invalidates the ETag we hold. Re-read it (and
      // only it) so an unsaved canvas edit can still be saved afterwards — a full reload here
      // would silently discard the user's work.
      const refreshed = await productionTemplatesApi.get(templateId)
      setEtag(refreshed.etag)
      setGraph((g) =>
        g ? { ...g, status: refreshed.data.status, activeRunCount: refreshed.data.activeRunCount } : g
      )
      toast.success(next === "Active" ? "Template activated" : "Template deactivated")
    } catch (err) {
      toast.error("Could not update status", errorMessage(err))
    } finally {
      setTogglingStatus(false)
    }
  }

  if (!loaded) {
    return (
      <div className="flex flex-col gap-3">
        <Skeleton className="h-10 w-72" />
        <Skeleton className="h-[60vh] w-full rounded-2xl" />
      </div>
    )
  }

  if (loadError) {
    return (
      <div className="flex flex-col gap-4">
        <div className="rounded-lg border border-destructive/30 bg-destructive/5 p-5 text-base text-destructive">{loadError}</div>
        <Link href="/dashboard/production/templates" className={cn(buttonVariants({ variant: "outline" }))}>
          Back to templates
        </Link>
      </div>
    )
  }

  return (
    <div className="flex h-[calc(100vh-8rem)] flex-col gap-4">
      <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
        <div className="flex items-center gap-3">
          <div>
            <div className="flex items-center gap-3">
              <h1 className="text-2xl font-bold text-foreground">{name || "Untitled template"}</h1>
              {graph ? (
                <Badge
                  variant="outline"
                  className={cn(
                    "h-6 w-fit justify-center border-transparent px-2.5 text-sm",
                    graph.status === "Active" ? "bg-success/10 text-success" : "bg-muted text-muted-foreground"
                  )}
                >
                  {graph.status}
                </Badge>
              ) : (
                <Badge variant="outline" className="h-6 w-fit justify-center border-transparent bg-muted px-2.5 text-sm text-muted-foreground">
                  Unsaved draft
                </Badge>
              )}
            </div>
            <p className="text-base text-muted-foreground">
              {stageNodes.length} stage{stageNodes.length === 1 ? "" : "s"} · {edges.length} connection
              {edges.length === 1 ? "" : "s"}
            </p>
          </div>
        </div>
        <div className="flex flex-wrap items-center gap-2">
          {!locked && (
            <>
              <Button type="button" variant="outline" onClick={addStage}>
                <Plus className="size-5" />
                Add Stage
              </Button>
              <Button type="button" variant="outline" onClick={() => addAnnotation("box")}>
                <Square className="size-5" />
                Add Box
              </Button>
              <Button type="button" variant="outline" onClick={() => addAnnotation("line")}>
                <Minus className="size-5" />
                Add Line
              </Button>
            </>
          )}
          {graph && (
            <Button
              type="button"
              variant={graph.status === "Active" ? "destructive" : "success"}
              onClick={handleToggleStatus}
              disabled={togglingStatus}
            >
              {togglingStatus ? "Updating…" : graph.status === "Active" ? "Deactivate" : "Activate"}
            </Button>
          )}
          {!locked && (
            <Button type="button" onClick={handleSave} disabled={saving || conflict}>
              <Save className="size-5" />
              {saving ? "Saving…" : isNew ? "Create" : "Save"}
            </Button>
          )}
        </div>
      </div>

      <div className="grid gap-3 sm:grid-cols-[10rem_1fr_1fr]">
        <Field>
          <FieldLabel htmlFor="tpl-code">Code</FieldLabel>
          <Input id="tpl-code" value={code} disabled={locked} onChange={(e) => setCode(e.target.value)} placeholder="PT-CHAIR" className="h-10" />
        </Field>
        <Field>
          <FieldLabel htmlFor="tpl-name">Name</FieldLabel>
          <Input id="tpl-name" value={name} disabled={locked} onChange={(e) => setName(e.target.value)} placeholder="Aluminium Frame Assembly" className="h-10" />
        </Field>
        <Field>
          <FieldLabel htmlFor="tpl-description">Description</FieldLabel>
          <Input
            id="tpl-description"
            value={description}
            disabled={locked}
            onChange={(e) => setDescription(e.target.value)}
            placeholder="Optional"
            className="h-10"
          />
        </Field>
      </div>

      {locked && (
        <div className="flex items-center gap-2 rounded-lg border border-warning/30 bg-warning/5 p-3 text-sm text-warning">
          <Lock className="size-4 shrink-0" />
          {activeRunCount > 0
            ? `Template locked — ${activeRunCount} run${activeRunCount === 1 ? "" : "s"} in progress. It can be viewed but not edited until they finish.`
            : "Template locked — a run started while you were editing, so this template can no longer be changed."}
        </div>
      )}

      {conflict && (
        <div className="flex items-start gap-3 rounded-lg border border-warning/30 bg-warning/5 p-3 text-sm text-warning">
          <AlertTriangle className="mt-0.5 size-4 shrink-0" />
          <div className="flex flex-col items-start gap-2">
            <span>{saveError ?? "This template was changed by someone else."} Reload before retrying.</span>
            <Button size="sm" variant="outline" onClick={load}>
              Reload
            </Button>
          </div>
        </div>
      )}

      {saveError && !conflict && (
        <div className="flex items-start gap-2 rounded-lg border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive">
          <AlertTriangle className="mt-0.5 size-4 shrink-0" />
          <span>{saveError}</span>
        </div>
      )}

      {issues.length > 0 && (
        <div className="flex max-h-32 flex-col gap-1.5 overflow-y-auto rounded-lg border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive">
          {issues.map((issue, i) => (
            <div key={i} className="flex items-start gap-2">
              <AlertTriangle className="mt-0.5 size-4 shrink-0" />
              <span>{issue}</span>
            </div>
          ))}
        </div>
      )}

      <div className="min-h-0 flex-1 overflow-hidden rounded-2xl bg-muted ring-1 ring-foreground/10">
        {mounted && (
          <ReactFlow
            nodes={displayNodes}
            edges={edges}
            nodeTypes={nodeTypes}
            onNodesChange={locked ? undefined : onNodesChange}
            onEdgesChange={locked ? undefined : onEdgesChange}
            onNodesDelete={locked ? undefined : onNodesDelete}
            onConnect={locked ? undefined : onConnect}
            onNodeClick={onNodeClick}
            onPaneClick={onPaneClick}
            nodesDraggable={!locked}
            nodesConnectable={!locked}
            elementsSelectable
            colorMode={resolvedTheme === "dark" ? "dark" : "light"}
            fitView
          >
            <Background />
            <Controls showInteractive={!locked} />
            <MiniMap pannable zoomable />
          </ReactFlow>
        )}
      </div>

      {selectedNode && (
        <StageEditorPanel
          data={selectedNode.data as StageNodeData}
          isTerminal={analysis.terminalIds.has(selectedNode.id)}
          upstreamOptions={upstreamOptions}
          items={items}
          uoms={uoms}
          readOnly={locked}
          onChange={(patch) => updateNodeData(selectedNode.id, patch)}
          onDelete={() => deleteNode(selectedNode.id)}
          onClose={() => setSelectedNodeId(null)}
        />
      )}
    </div>
  )
}

export default function TemplateBuilderPage() {
  return (
    <Suspense fallback={<Skeleton className="h-[60vh] w-full rounded-2xl" />}>
      <TemplateBuilderContent />
    </Suspense>
  )
}
