"use client"

import { useEffect, useMemo, useRef, useState } from "react"
import { AlertTriangle, Ban, CheckCircle2, PackageCheck, Play, Send, Undo2 } from "lucide-react"

import { cn } from "@/lib/utils"
import { isStaleStageError, productionRunsApi } from "@/lib/api/production-runs"
import { errorMessage } from "@/lib/error-map"
import { CustomFieldForm, missingRequiredFields } from "./CustomFieldForm"
import { STAGE_STATUS_COLOR, STAGE_STATUS_LABEL, type StageStatus } from "@/lib/production-status-colors"
import {
  CompleteOutputLine,
  ProductionRunGraph,
  RunStage,
  RunStageInput,
  RunStageOutput,
  StageQuantityLine,
  TransferLine,
} from "@/types/production"
import { ItemListItem, Uom } from "@/types/master-data"
import { baseUomLabel, contentUnitLabel, uomLabel } from "@/lib/uom-label"
import { ReasonCode } from "@/types/stock"

import { AlertDialog, AlertDialogContent } from "@/components/ui/alert-dialog"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet"
import { Separator } from "@/components/ui/separator"

/**
 * The stage drawer (docs/21-FRONTEND-PHASE2.md §5) — the only place a run is actually driven.
 *
 * One component rather than the seven the plan sketched: each per-status panel is ~30 lines and
 * they all share the same lookup helpers, submit wrapper, and error handling, so splitting them
 * would mean threading that shared context through seven prop lists for no isolation benefit.
 *
 * The body is switched on `stage.status`, which is deliberately the *server's* status and never
 * a local guess — every action re-fetches the whole run through `onActed`, so what is rendered
 * is always what the server last said. Two consequences worth knowing:
 *
 *   * A 409 carrying a stage-status code means someone else moved first. `submit` refreshes
 *     silently instead of showing an error (docs/21 §6); that is also what makes the server's
 *     accept-and-ignore `Idempotency-Key` posture feel right — a double-click just refreshes.
 *   * Local form state is keyed off `stage.runStageId` and reset whenever the stage changes, so
 *     a refresh mid-edit can never post figures from a stage the user is no longer looking at.
 */

function fmt(n: number): string {
  return Number(n.toFixed(4)).toLocaleString(undefined, { maximumFractionDigits: 4 })
}

function money(n: number): string {
  return n.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 4 })
}

/** Minutes between two instants, floored — the same unit the server reports `actualMinutes` in. */
function minutesSince(iso: string, now: number): number {
  return Math.max(0, Math.floor((now - new Date(iso).getTime()) / 60_000))
}

function StatusPill({ status }: { status: StageStatus }) {
  const color = STAGE_STATUS_COLOR[status]
  return (
    <span
      className="inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-semibold"
      style={{ backgroundColor: `${color}1a`, color }}
    >
      <span className="size-2 rounded-full" style={{ backgroundColor: color }} />
      {STAGE_STATUS_LABEL[status]}
    </span>
  )
}

function Row({ label, children }: { label: string; children: React.ReactNode }) {
  return (
    <div className="flex items-baseline justify-between gap-3 text-sm">
      <span className="text-muted-foreground">{label}</span>
      <span className="text-right font-medium text-foreground">{children}</span>
    </div>
  )
}

export function StageDrawer({
  run,
  stage,
  items,
  uoms,
  reasonCodes,
  onClose,
  onActed,
}: {
  run: ProductionRunGraph
  stage: RunStage | null
  items: ItemListItem[]
  uoms: Uom[]
  reasonCodes: ReasonCode[]
  onClose: () => void
  onActed: () => void
}) {
  const [busy, setBusy] = useState(false)
  const [error, setError] = useState<string | null>(null)

  // Editable planned quantities (Ready only) keyed by run input/output id.
  const [plannedInputs, setPlannedInputs] = useState<Record<number, string>>({})
  const [plannedOutputs, setPlannedOutputs] = useState<Record<number, string>>({})

  // Complete-form state.
  const [produced, setProduced] = useState<Record<number, string>>({})
  const [scrapped, setScrapped] = useState<Record<number, string>>({})
  const [scrapReason, setScrapReason] = useState<Record<number, number | null>>({})
  const [fieldValues, setFieldValues] = useState<Record<string, unknown>>({})

  // Transfer state — the amount to push per output, defaulted to everything available.
  const [transferQty, setTransferQty] = useState<Record<number, string>>({})

  const [confirm, setConfirm] = useState<null | "rejectIntake" | "rejectTerminal">(null)

  // A per-action Idempotency-Key, minted fresh for each stage the drawer opens on. Same shape as
  // app/dashboard/receiving/grn/[id]/page.tsx.
  const idempotencyKey = useRef(crypto.randomUUID())

  const stageId = stage?.runStageId ?? null

  // Reseed every form whenever the drawer switches stage *or* the server sends new figures for
  // the one it is on. Without the second half, a refresh after an action would leave the inputs
  // showing pre-action numbers.
  useEffect(() => {
    if (!stage) return
    setPlannedInputs(Object.fromEntries(stage.inputs.map((i) => [i.runInputId, String(i.plannedQty)])))
    setPlannedOutputs(Object.fromEntries(stage.outputs.map((o) => [o.runOutputId, String(o.plannedQty)])))
    setProduced(Object.fromEntries(stage.outputs.map((o) => [o.runOutputId, String(o.producedQty || o.plannedQty)])))
    setScrapped(Object.fromEntries(stage.outputs.map((o) => [o.runOutputId, String(o.scrappedQty || 0)])))
    setScrapReason(Object.fromEntries(stage.outputs.map((o) => [o.runOutputId, o.scrapReasonCodeId])))
    setFieldValues((stage.fieldValues ?? {}) as Record<string, unknown>)
    setTransferQty(Object.fromEntries(stage.outputs.map((o) => [o.runOutputId, String(o.availableToTransfer)])))
    setError(null)
    idempotencyKey.current = crypto.randomUUID()
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [stageId, stage?.status, stage?.outputs, stage?.inputs])

  // Live elapsed while the stage is running (FR-MFG-19). One tick a minute is enough — the
  // figure is rendered in whole minutes, so a faster interval would just re-render for nothing.
  const [now, setNow] = useState(() => Date.now())
  const running = stage?.actualStartAt != null && stage?.actualEndAt == null
  useEffect(() => {
    if (!running) return
    const timer = setInterval(() => setNow(Date.now()), 60_000)
    return () => clearInterval(timer)
  }, [running])

  const itemName = useMemo(() => {
    const byId = new Map(items.map((i) => [i.itemId, i.name]))
    return (id: number | null) => (id === null ? "—" : byId.get(id) ?? `Item #${id}`)
  }, [items])

  // Nothing on a run line carries a unit of its own any more. An output that references a
  // real item shows that item's base UOM; only intermediate WIP falls back to its own label.
  const outputUnit = useMemo(
    () => (output: RunStageOutput) =>
      output.itemId !== null ? baseUomLabel(items, uoms, output.itemId) : uomLabel(uoms, output.uomId),
    [items, uoms],
  )

  // An input's unit follows how its quantity was expressed: ml/g for a content formula,
  // otherwise a count of the item's packs. Upstream inputs are WIP from a parent stage.
  const inputUnit = useMemo(
    () => (input: RunStageInput) => {
      if (input.source === "Upstream") return "WIP"
      if (input.qtyUnit === "Content") return contentUnitLabel(items, input.itemId) ?? "—"
      return baseUomLabel(items, uoms, input.itemId)
    },
    [items, uoms],
  )

  const stageName = useMemo(() => {
    const byId = new Map(run.stages.map((s) => [s.runStageId, s.name]))
    return (id: number) => byId.get(id) ?? `Stage #${id}`
  }, [run.stages])

  if (!stage) return null

  const isEditable = stage.status === "Ready"
  const upstreamInputs = stage.inputs.filter((i) => i.source === "Upstream")
  const deliveredSoFar = upstreamInputs.reduce((sum, i) => sum + i.deliveredQty, 0)
  const canRejectIntake = (stage.status === "Ready" || stage.status === "Waiting") && deliveredSoFar > 0
  const availableTotal = stage.outputs.reduce((sum, o) => sum + o.availableToTransfer, 0)
  const stageEvents = run.events.filter((e) => e.runStageId === stage.runStageId)

  /**
   * One wrapper for every action: busy flag, error surfacing, and the docs/21 §6 rule that a
   * stage-status 409 refreshes silently rather than shouting at the user.
   */
  async function submit(action: () => Promise<unknown>) {
    setBusy(true)
    setError(null)
    try {
      await action()
      idempotencyKey.current = crypto.randomUUID()
      onActed()
    } catch (err) {
      if (isStaleStageError(err)) {
        onActed()
        return
      }
      setError(errorMessage(err))
    } finally {
      setBusy(false)
    }
  }

  function saveQuantities() {
    const inputs: StageQuantityLine[] = stage!.inputs
      .map((i) => ({ id: i.runInputId, plannedQty: Number(plannedInputs[i.runInputId] ?? i.plannedQty) }))
      .filter((line) => Number.isFinite(line.plannedQty) && line.plannedQty > 0)
    const outputs: StageQuantityLine[] = stage!.outputs
      .map((o) => ({ id: o.runOutputId, plannedQty: Number(plannedOutputs[o.runOutputId] ?? o.plannedQty) }))
      .filter((line) => Number.isFinite(line.plannedQty) && line.plannedQty > 0)

    return submit(() => productionRunsApi.updateStageQuantities(run.runId, stage!.runStageId, { inputs, outputs }))
  }

  function completeStage() {
    const outputs: CompleteOutputLine[] = stage!.outputs.map((o) => ({
      runOutputId: o.runOutputId,
      producedQty: Number(produced[o.runOutputId] ?? 0) || 0,
      scrappedQty: Number(scrapped[o.runOutputId] ?? 0) || 0,
      scrapReasonCodeId: Number(scrapped[o.runOutputId] ?? 0) > 0 ? scrapReason[o.runOutputId] : null,
    }))

    return submit(() =>
      productionRunsApi.complete(
        run.runId,
        stage!.runStageId,
        { outputs, fieldValues: Object.keys(fieldValues).length > 0 ? fieldValues : null },
        idempotencyKey.current
      )
    )
  }

  /**
   * Approve. An empty `transfers` array tells the server to push every output in full, which is
   * the common case; a line is only sent when the user has dialled it down below what's
   * available. On the terminal stage the server ignores transfers entirely and posts the receipt.
   */
  function approveStage() {
    const transfers: TransferLine[] = stage!.outputs
      .filter((o) => {
        const wanted = Number(transferQty[o.runOutputId] ?? o.availableToTransfer)
        return Number.isFinite(wanted) && wanted !== o.availableToTransfer
      })
      .map((o) => ({ runOutputId: o.runOutputId, qty: Number(transferQty[o.runOutputId]) }))

    return submit(() =>
      productionRunsApi.approve(
        run.runId,
        stage!.runStageId,
        transfers.length > 0 ? { transfers } : {},
        idempotencyKey.current
      )
    )
  }

  function transferRemainder(output: RunStageOutput) {
    const qty = Number(transferQty[output.runOutputId] ?? 0)
    return submit(() =>
      productionRunsApi.transfer(
        run.runId,
        stage!.runStageId,
        { runOutputId: output.runOutputId, qty },
        idempotencyKey.current
      )
    )
  }

  // --- terminal receipt preview ---------------------------------------------
  // Mirrors the server's arithmetic (docs/30 §D.3 terminal approve) so the operator sees the
  // layer they are about to create *before* creating it. Deliberately recomputed here rather
  // than requested: there is no preview endpoint, and every input is already on this page.
  const terminalOutput = stage.isTerminal ? stage.outputs[0] : undefined
  const goodQty = terminalOutput ? terminalOutput.producedQty - terminalOutput.scrappedQty : 0
  const previewUnitCost = goodQty > 0 ? run.costPool.net / goodQty : null

  const scrapReasons = reasonCodes.filter((r) => r.context === "Production")

  const missingFields = missingRequiredFields(stage.fieldDefs, fieldValues)
  const missingScrapReasons = stage.outputs
    .filter((o) => Number(scrapped[o.runOutputId] ?? 0) > 0 && !scrapReason[o.runOutputId])
    .map((o) => o.name)

  return (
    <>
      <Sheet open onOpenChange={(open) => !open && onClose()}>
        <SheetContent className="w-full overflow-y-auto sm:max-w-lg!">
          <SheetHeader>
            <div className="flex flex-wrap items-center gap-2">
              <SheetTitle className="text-base">{stage.name}</SheetTitle>
              <StatusPill status={stage.status} />
              {stage.roleLabel && (
                <Badge variant="outline" className="border-transparent bg-primary/10 text-primary">
                  {stage.roleLabel}
                </Badge>
              )}
              {stage.isTerminal && (
                <Badge variant="outline" className="border-transparent bg-muted text-muted-foreground">
                  Final stage
                </Badge>
              )}
            </div>
            <SheetDescription>
              Estimated {stage.estimatedMinutes} min
              {stage.actualMinutes !== null
                ? ` · actual ${stage.actualMinutes} min`
                : running && stage.actualStartAt
                  ? ` · running for ${minutesSince(stage.actualStartAt, now)} min`
                  : ""}
            </SheetDescription>
          </SheetHeader>

          <div className="flex flex-col gap-4 px-4 pb-6">
            {error && (
              <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>{error}</span>
              </div>
            )}

            {/* ---------------------------------------------------------- inputs */}
            <section className="flex flex-col gap-2">
              <h3 className="text-sm font-bold text-foreground">Inputs</h3>
              {stage.inputs.length === 0 && <p className="text-sm text-muted-foreground">This stage consumes nothing.</p>}
              {stage.inputs.map((input) => (
                <InputCard
                  key={input.runInputId}
                  input={input}
                  editable={isEditable}
                  value={plannedInputs[input.runInputId] ?? String(input.plannedQty)}
                  onValueChange={(v) => setPlannedInputs((prev) => ({ ...prev, [input.runInputId]: v }))}
                  itemName={itemName}
                  unitLabel={inputUnit}
                />
              ))}
            </section>

            <Separator />

            {/* --------------------------------------------------------- outputs */}
            <section className="flex flex-col gap-2">
              <h3 className="text-sm font-bold text-foreground">Outputs</h3>
              {stage.outputs.map((output) => (
                <div key={output.runOutputId} className="rounded-lg border border-border p-3">
                  <div className="flex items-baseline justify-between gap-2">
                    <p className="min-w-0 truncate text-sm font-medium text-foreground">{output.name}</p>
                    <span className="shrink-0 text-xs text-muted-foreground">{outputUnit(output)}</span>
                  </div>
                  {output.itemId !== null && (
                    <p className="text-xs text-muted-foreground">Finished good: {itemName(output.itemId)}</p>
                  )}

                  <div className="mt-2 flex flex-col gap-1">
                    {isEditable ? (
                      <label className="flex items-center justify-between gap-3 text-sm">
                        <span className="text-muted-foreground">Planned</span>
                        <Input
                          type="number"
                          min={0}
                          step="any"
                          value={plannedOutputs[output.runOutputId] ?? String(output.plannedQty)}
                          onChange={(e) => setPlannedOutputs((prev) => ({ ...prev, [output.runOutputId]: e.target.value }))}
                          className="h-8 w-28 text-sm"
                        />
                      </label>
                    ) : (
                      <Row label="Planned">{fmt(output.plannedQty)}</Row>
                    )}
                    {output.producedQty > 0 && <Row label="Produced">{fmt(output.producedQty)}</Row>}
                    {output.scrappedQty > 0 && (
                      <Row label="Scrapped">
                        <span className="text-destructive">{fmt(output.scrappedQty)}</span>
                      </Row>
                    )}
                    {output.transferredQty > 0 && <Row label="Transferred">{fmt(output.transferredQty)}</Row>}
                    {output.availableToTransfer > 0 && (
                      <Row label="Available to transfer">
                        <span className="text-info">{fmt(output.availableToTransfer)}</span>
                      </Row>
                    )}
                  </div>
                </div>
              ))}
            </section>

            <Separator />

            {/* ------------------------------------------------ status-specific */}
            {stage.status === "Waiting" && (
              <p className="text-sm text-muted-foreground">
                Waiting on upstream deliveries. This stage becomes Ready once every upstream input has received its
                planned quantity.
              </p>
            )}

            {stage.status === "Ready" && (
              <section className="flex flex-col gap-3">
                <p className="text-sm text-muted-foreground">
                  Adjust planned quantities if needed, then start. Starting consumes the stock inputs above FIFO —
                  quantities are locked from that point on.
                </p>
                <div className="flex flex-wrap gap-2">
                  <Button variant="outline" onClick={saveQuantities} disabled={busy}>
                    Save quantities
                  </Button>
                  <Button
                    onClick={() =>
                      submit(() => productionRunsApi.start(run.runId, stage.runStageId, idempotencyKey.current))
                    }
                    disabled={busy}
                  >
                    <Play className="size-4" />
                    Start stage
                  </Button>
                </div>
              </section>
            )}

            {stage.status === "InProgress" && (
              <section className="flex flex-col gap-3">
                <h3 className="text-sm font-bold text-foreground">Record output</h3>
                {stage.outputs.map((output) => {
                  const scrapQty = Number(scrapped[output.runOutputId] ?? 0)
                  return (
                    <div key={output.runOutputId} className="flex flex-col gap-2 rounded-lg border border-border p-3">
                      <p className="text-sm font-medium text-foreground">{output.name}</p>
                      <label className="flex items-center justify-between gap-3 text-sm">
                        <span className="text-muted-foreground">Produced</span>
                        <Input
                          type="number"
                          min={0}
                          step="any"
                          value={produced[output.runOutputId] ?? ""}
                          onChange={(e) => setProduced((prev) => ({ ...prev, [output.runOutputId]: e.target.value }))}
                          className="h-8 w-28 text-sm"
                        />
                      </label>
                      <label className="flex items-center justify-between gap-3 text-sm">
                        <span className="text-muted-foreground">Scrapped</span>
                        <Input
                          type="number"
                          min={0}
                          step="any"
                          value={scrapped[output.runOutputId] ?? ""}
                          onChange={(e) => setScrapped((prev) => ({ ...prev, [output.runOutputId]: e.target.value }))}
                          className="h-8 w-28 text-sm"
                        />
                      </label>
                      {scrapQty > 0 && (
                        <Select<number>
                          value={scrapReason[output.runOutputId] ?? null}
                          onValueChange={(v) => setScrapReason((prev) => ({ ...prev, [output.runOutputId]: v }))}
                        >
                          <SelectTrigger className="h-9! w-full text-sm">
                            <SelectValue placeholder="Scrap reason (required)" />
                          </SelectTrigger>
                          <SelectContent>
                            {scrapReasons.map((r) => (
                              <SelectItem key={r.reasonCodeId} value={r.reasonCodeId} className="text-sm">
                                {r.code} — {r.description}
                              </SelectItem>
                            ))}
                          </SelectContent>
                        </Select>
                      )}
                    </div>
                  )
                })}

                <CustomFields stage={stage} values={fieldValues} onChange={setFieldValues} />

                {/*
                  Blocked client-side as well as server-side. The server's 400
                  REQUIRED_FIELD_MISSING rolls the whole transaction back, so letting it through
                  would cost a round trip and — worse — look to the operator like the completion
                  half-applied. `missingRequiredFields` deliberately mirrors the server's rule.
                */}
                {missingScrapReasons.length > 0 && (
                  <p className="text-sm text-destructive">
                    Pick a scrap reason for: {missingScrapReasons.join(", ")}.
                  </p>
                )}
                {missingFields.length > 0 && (
                  <p className="text-sm text-destructive">Fill in: {missingFields.join(", ")}.</p>
                )}

                <Button
                  onClick={completeStage}
                  disabled={busy || missingFields.length > 0 || missingScrapReasons.length > 0}
                >
                  <CheckCircle2 className="size-4" />
                  Complete stage
                </Button>
              </section>
            )}

            {stage.status === "Done" && !stage.isTerminal && (
              <section className="flex flex-col gap-3">
                <h3 className="text-sm font-bold text-foreground">Approve &amp; transfer</h3>
                <p className="text-sm text-muted-foreground">
                  Approving pushes work downstream. Leave the amounts as they are to transfer everything, or lower one to
                  hold some back — you can transfer the remainder later.
                </p>
                {stage.outputs.map((output) => (
                  <label key={output.runOutputId} className="flex items-center justify-between gap-3 text-sm">
                    <span className="min-w-0 truncate text-muted-foreground">{output.name}</span>
                    <span className="flex shrink-0 items-center gap-2">
                      <Input
                        type="number"
                        min={0}
                        max={output.availableToTransfer}
                        step="any"
                        value={transferQty[output.runOutputId] ?? ""}
                        onChange={(e) => setTransferQty((prev) => ({ ...prev, [output.runOutputId]: e.target.value }))}
                        className="h-8 w-28 text-sm"
                      />
                      <span className="text-xs text-muted-foreground">of {fmt(output.availableToTransfer)}</span>
                    </span>
                  </label>
                ))}
                <Button onClick={approveStage} disabled={busy}>
                  <Send className="size-4" />
                  Approve &amp; transfer
                </Button>
              </section>
            )}

            {stage.status === "Done" && stage.isTerminal && terminalOutput && (
              <section className="flex flex-col gap-3">
                <h3 className="text-sm font-bold text-foreground">Finish the run</h3>
                <div className="flex flex-col gap-1 rounded-lg border border-info/30 bg-info/5 p-3">
                  <Row label="Good quantity">{fmt(goodQty)} {outputUnit(terminalOutput)}</Row>
                  <Row label="Materials consumed">{money(run.costPool.consumed)}</Row>
                  <Row label="Leftovers returned">−{money(run.costPool.returned)}</Row>
                  <Separator className="my-1" />
                  <Row label="Cost pool">{money(run.costPool.net)}</Row>
                  <Row label="Unit cost">{previewUnitCost === null ? "—" : money(previewUnitCost)}</Row>
                </div>
                <p className="text-sm text-muted-foreground">
                  Approving creates a costed stock layer of {fmt(goodQty)} {itemName(terminalOutput.itemId)} and completes
                  the run. Its costs close at that point — return any leftover materials first.
                </p>
                <div className="flex flex-wrap gap-2">
                  <Button onClick={approveStage} disabled={busy || goodQty <= 0}>
                    <PackageCheck className="size-4" />
                    Approve &amp; receive
                  </Button>
                  <Button variant="destructive" onClick={() => setConfirm("rejectTerminal")} disabled={busy}>
                    <Undo2 className="size-4" />
                    Reject for rework
                  </Button>
                </div>
              </section>
            )}

            {stage.status === "Approved" && availableTotal > 0 && (
              <section className="flex flex-col gap-3">
                <h3 className="text-sm font-bold text-foreground">Transfer remainder</h3>
                {stage.outputs
                  .filter((o) => o.availableToTransfer > 0)
                  .map((output) => (
                    <div key={output.runOutputId} className="flex items-center gap-2">
                      <span className="min-w-0 flex-1 truncate text-sm text-muted-foreground">{output.name}</span>
                      <Input
                        type="number"
                        min={0}
                        max={output.availableToTransfer}
                        step="any"
                        value={transferQty[output.runOutputId] ?? ""}
                        onChange={(e) => setTransferQty((prev) => ({ ...prev, [output.runOutputId]: e.target.value }))}
                        className="h-8 w-24 text-sm"
                      />
                      <Button size="sm" onClick={() => transferRemainder(output)} disabled={busy}>
                        Transfer
                      </Button>
                    </div>
                  ))}
              </section>
            )}

            {stage.status === "Approved" && availableTotal === 0 && (
              <p className="text-sm text-muted-foreground">Approved — everything this stage produced has moved on.</p>
            )}

            {canRejectIntake && (
              <>
                <Separator />
                <section className="flex flex-col gap-2">
                  <h3 className="text-sm font-bold text-foreground">Reject what was delivered</h3>
                  <p className="text-sm text-muted-foreground">
                    Sends {fmt(deliveredSoFar)} back to the stage(s) that delivered it and reopens them for correction.
                    No stock moves — this is work in progress, not inventory.
                  </p>
                  <Button variant="outline" className="w-fit text-destructive" onClick={() => setConfirm("rejectIntake")} disabled={busy}>
                    <Ban className="size-4" />
                    Reject intake
                  </Button>
                </section>
              </>
            )}

            {/* --------------------------------------------------------- history */}
            {stageEvents.length > 0 && (
              <>
                <Separator />
                <section className="flex flex-col gap-2">
                  <h3 className="text-sm font-bold text-foreground">History</h3>
                  <ol className="flex flex-col gap-2">
                    {stageEvents.map((event) => (
                      <li key={event.eventId} className="flex gap-2 text-sm">
                        <span className="mt-1.5 size-1.5 shrink-0 rounded-full bg-muted-foreground" />
                        <div className="flex min-w-0 flex-col">
                          <span className="font-medium text-foreground">{event.eventType}</span>
                          <span className="text-xs text-muted-foreground">
                            {new Date(event.createdAt).toLocaleString()}
                          </span>
                          {event.note && <span className="text-xs text-muted-foreground">{event.note}</span>}
                        </div>
                      </li>
                    ))}
                  </ol>
                </section>
              </>
            )}
          </div>
        </SheetContent>
      </Sheet>

      <AlertDialog open={confirm === "rejectIntake"} onOpenChange={(open) => !open && setConfirm(null)}>
        <AlertDialogContent
          variant="destructive"
          title="Reject the delivered work?"
          description={`${stageName(stage.runStageId)} will go back to Waiting and every stage that delivered to it reopens for correction. Their recorded output is cleared, so it has to be re-entered.`}
          confirmLabel="Reject intake"
          onConfirm={() =>
            submit(() => productionRunsApi.rejectIntake(run.runId, stage.runStageId, {}, idempotencyKey.current))
          }
        />
      </AlertDialog>

      <AlertDialog open={confirm === "rejectTerminal"} onOpenChange={(open) => !open && setConfirm(null)}>
        <AlertDialogContent
          variant="destructive"
          title="Reject and rework the whole run?"
          description="Every stage resets and the run starts over as a rework pass. Materials already consumed stay in the cost pool — no stock is returned — so raise planned quantities before restarting if more will be needed."
          confirmLabel="Reject for rework"
          onConfirm={() =>
            submit(() => productionRunsApi.rejectTerminal(run.runId, stage.runStageId, {}, idempotencyKey.current))
          }
        />
      </AlertDialog>
    </>
  )
}

/** Split out only because the input card has three mutually exclusive quantity presentations. */
function InputCard({
  input,
  editable,
  value,
  onValueChange,
  itemName,
  unitLabel,
}: {
  input: RunStageInput
  editable: boolean
  value: string
  onValueChange: (value: string) => void
  itemName: (id: number | null) => string
  unitLabel: (input: RunStageInput) => string
}) {
  const isUpstream = input.source === "Upstream"
  const short = isUpstream && input.deliveredQty < input.plannedQty

  return (
    <div className="rounded-lg border border-border p-3">
      <div className="flex items-baseline justify-between gap-2">
        <p className="min-w-0 truncate text-sm font-medium text-foreground">
          {isUpstream ? "Upstream work in progress" : itemName(input.itemId)}
        </p>
        <span className="shrink-0 text-xs text-muted-foreground">{unitLabel(input)}</span>
      </div>

      <div className="mt-2 flex flex-col gap-1">
        {editable ? (
          <label className="flex items-center justify-between gap-3 text-sm">
            <span className="text-muted-foreground">Planned</span>
            <Input
              type="number"
              min={0}
              step="any"
              value={value}
              onChange={(e) => onValueChange(e.target.value)}
              className="h-8 w-28 text-sm"
            />
          </label>
        ) : (
          <Row label="Planned">{fmt(input.plannedQty)}</Row>
        )}

        {isUpstream && (
          <Row label="Delivered">
            <span className={cn(short ? "text-warning" : "text-success")}>{fmt(input.deliveredQty)}</span>
          </Row>
        )}

        {/*
          Consumed/returned figures are always a count of the item's packs, while `plannedQty`
          above is in the input's declared unit — a formula written as 2000 ml against a 500 ml
          bottle shows planned 2000 and consumed 4. Labelled explicitly so the two are never
          read as the same unit.
        */}
        {input.consumedQty > 0 && (
          <>
            <Row label="Consumed (base)">
              {fmt(input.consumedQty)} · {money(input.consumedValue)}
            </Row>
            {input.returnedQty > 0 && (
              <Row label="Returned (base)">
                {fmt(input.returnedQty)} · {money(input.returnedValue)}
              </Row>
            )}
          </>
        )}
      </div>
    </div>
  )
}

/** Thin wrapper so the drawer body stays readable; the renderer itself lives in its own file. */
function CustomFields({
  stage,
  values,
  onChange,
}: {
  stage: RunStage
  values: Record<string, unknown>
  onChange: (next: Record<string, unknown>) => void
}) {
  if (stage.fieldDefs.length === 0) return null
  return (
    <div className="flex flex-col gap-2">
      <h3 className="text-sm font-bold text-foreground">Checks</h3>
      <CustomFieldForm defs={stage.fieldDefs} values={values} onChange={onChange} />
    </div>
  )
}
