import { useCallback, useEffect, useMemo, useState } from "react"
import {
  CheckCircle2,
  Database,
  FileText,
  Plus,
  Power,
  RefreshCw,
  Trash2,
} from "lucide-react"

import {
  ActionsCell,
  DataTable,
  Pagination,
  useTable,
  type ActionItem,
  type Column,
} from "@crema/table-ui"
import { SearchInput } from "@crema/search-ui"
import { ConfirmDialog, EmptyState } from "@crema/feedback-ui"
import { useToast } from "@crema/notification-ui"
import { KpiTile, formatCompact } from "@crema/dashboard-ui"

import { AppShell } from "~/components/layout/app-shell"
import { DataState, DialogError, ErrorState } from "~/components/data-state"
import { errorMessage } from "~/lib/errors"
import { Badge } from "~/components/ui/badge"
import { Button } from "~/components/ui/button"
import {
  Card,
  CardContent,
  CardHeader,
} from "~/components/ui/card"
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from "~/components/ui/dialog"
import { Input } from "~/components/ui/input"
import { Label } from "~/components/ui/label"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "~/components/ui/select"
import { Textarea } from "~/components/ui/textarea"
import {
  searchAdmin,
  type CorpusSummary,
  type TenantSummary,
} from "~/lib/search-admin"
import { pageTitle } from "~/lib/page-meta"
import { useSession } from "~/lib/session"
import { useRegisterContext } from "@crema/aifirst-ui/context"

export const meta = () => pageTitle("Search")

type Row = CorpusSummary & { rowId: string }

type EditorState =
  | { kind: "new-tenant" }
  | { kind: "new-corpus"; tenant: string }
  | { kind: "edit-corpus"; tenant: string; corpus: string }
  | null

export default function SearchRoute() {
  const session = useSession()
  const toast = useToast()

  const [tenants, setTenants] = useState<TenantSummary[]>([])
  const [corpora, setCorpora] = useState<Row[]>([])
  const [loading, setLoading] = useState(false)
  // arcadia-search is a separate sidecar, so this is often a plain Error /
  // TypeError (connection refused) rather than an ArcadiaError. Pass it through
  // raw — `describeError` already turns that into "Can't reach arcadia".
  const [error, setError] = useState<unknown>(null)
  const [corporaError, setCorporaError] = useState<unknown>(null)
  const [editor, setEditor] = useState<EditorState>(null)
  const [pendingDeleteTenant, setPendingDeleteTenant] = useState<string | null>(
    null,
  )
  const [pendingDeleteCorpus, setPendingDeleteCorpus] = useState<{
    tenant: string
    corpus: string
  } | null>(null)
  const [restartConfirm, setRestartConfirm] = useState(false)
  const [rebuilding, setRebuilding] = useState<string | null>(null)

  const refresh = useCallback(async () => {
    setLoading(true)
    setError(null)
    setCorporaError(null)
    try {
      const tRes = await searchAdmin.listTenants()
      setTenants(tRes.tenants)

      // Fan out per-tenant corpus lookups in parallel. A tenant whose lookup
      // fails no longer disappears silently: if every lookup failed we surface
      // the failure instead of rendering "No corpora yet."
      const settled = await Promise.allSettled(
        tRes.tenants.map((t) => searchAdmin.listCorpora(t.id)),
      )
      const ok = settled.filter(
        (r): r is PromiseFulfilledResult<{ corpora: CorpusSummary[] }> =>
          r.status === "fulfilled",
      )
      const firstFailure = settled.find((r) => r.status === "rejected")

      setCorpora(
        ok
          .flatMap((r) => r.value.corpora)
          .map((c) => ({ ...c, rowId: `${c.tenant}/${c.corpus}` })),
      )
      if (settled.length > 0 && ok.length === 0 && firstFailure) {
        setCorporaError((firstFailure as PromiseRejectedResult).reason)
      }
    } catch (err) {
      setError(err)
      setTenants([])
      setCorpora([])
    } finally {
      setLoading(false)
    }
  }, [])

  useEffect(() => {
    if (!session) return
    refresh()
  }, [session, refresh])

  const totals = useMemo(() => {
    const indexed = corpora.filter((c) => c.indexed).length
    const docs = corpora.reduce((a, c) => a + (c.num_docs ?? 0), 0)
    return { indexed, docs }
  }, [corpora])

  // Publish a snapshot to the assistant's admin context so the agent
  // can answer "what corpora exist?" / "is the docs corpus indexed?"
  // without having to call list_search_corpora.
  const adminSurface = useMemo(
    () => ({
      endpoint: searchAdmin.baseUrl,
      tenants: tenants.map((t) => ({ id: t.id, corpus_count: t.corpus_count })),
      corpora: corpora.map((c) => ({
        tenant: c.tenant,
        corpus: c.corpus,
        indexed: c.indexed,
        num_docs: c.num_docs,
      })),
    }),
    [tenants, corpora],
  )
  useRegisterContext("search", adminSurface)

  const rebuild = useCallback(
    async (tenant: string, corpus: string) => {
      const id = `${tenant}/${corpus}`
      setRebuilding(id)
      try {
        const out = await searchAdmin.rebuild(tenant, corpus)
        await refresh()
        toast.success(`Rebuilt ${id}`, {
          description: `${out.chunk_count} chunks indexed.`,
        })
      } catch (err) {
        toast.error(errorMessage(err, `rebuild ${id}`))
      } finally {
        setRebuilding(null)
      }
    },
    [refresh, toast],
  )

  return (
    <AppShell>
      <div className="flex flex-col gap-4">
        <header className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
          <div>
            <h1 className="text-2xl font-semibold tracking-tight">Search</h1>
            <p className="text-sm text-muted-foreground">
              Manage arcadia-search tenants and corpora. Trigger rebuilds and
              restart the service after env changes.
            </p>
          </div>
          <div className="flex items-center gap-2">
            <Button
              variant="outline"
              size="sm"
              onClick={refresh}
              disabled={loading}
              data-action="search-refresh"
            >
              <RefreshCw
                className={`size-4 ${loading ? "animate-spin" : ""}`}
              />
              Refresh
            </Button>
            <Button
              variant="outline"
              size="sm"
              onClick={() => setRestartConfirm(true)}
              data-action="search-restart"
            >
              <Power className="size-4" />
              Restart service
            </Button>
            <Button
              variant="outline"
              size="sm"
              onClick={() => setEditor({ kind: "new-tenant" })}
              data-action="search-new-tenant"
            >
              <Plus className="size-4" />
              New tenant
            </Button>
            <Button
              size="sm"
              disabled={tenants.length === 0}
              onClick={() =>
                setEditor({
                  kind: "new-corpus",
                  tenant: tenants[0]?.id ?? "",
                })
              }
              data-action="search-new-corpus"
            >
              <Plus className="size-4" />
              New corpus
            </Button>
          </div>
        </header>

        {/* A standing configuration fact, not a load failure — so it isn't an
            error state and it isn't dismissible. It stays until it's fixed. */}
        {!searchAdmin.hasToken ? (
          <div
            role="note"
            className="rounded-md border bg-muted/40 px-3 py-2.5 text-sm"
            data-action="search-token-missing"
          >
            <p className="font-medium">Admin token not configured</p>
            <p className="text-muted-foreground">
              VITE_ARCADIA_SEARCH_ADMIN_TOKEN is unset, so every call below will
              come back 401. Endpoint:{" "}
              <code className="font-mono">{searchAdmin.baseUrl}</code>
            </p>
          </div>
        ) : null}

        {/* The whole screen hangs off one call to the sidecar. If that call
            failed, nothing here loaded — say so once, rather than rendering
            three cheerful empty states over a dead connection. */}
        {error ? (
          <Card>
            <CardContent className="p-0">
              <ErrorState error={error} onRetry={refresh} />
            </CardContent>
          </Card>
        ) : (
          <>
            <Card>
              <CardHeader className="flex flex-row flex-wrap items-end gap-3">
                <div className="grid grid-cols-3 gap-3 min-w-0">
                  <KpiTile
                    label="Tenants"
                    value={formatCompact(tenants.length)}
                  />
                  <KpiTile
                    label="Corpora indexed"
                    value={`${totals.indexed} / ${corpora.length}`}
                  />
                  <KpiTile label="Docs" value={formatCompact(totals.docs)} />
                </div>
              </CardHeader>
            </Card>

            <TenantsCard
              tenants={tenants}
              loading={loading}
              onRetry={refresh}
              onDelete={(id) => setPendingDeleteTenant(id)}
            />

            <CorporaCard
              corpora={corpora}
              loading={loading}
              error={corporaError}
              onRetry={refresh}
              rebuildingId={rebuilding}
              onRebuild={rebuild}
              onEdit={(t, c) => setEditor({ kind: "edit-corpus", tenant: t, corpus: c })}
              onDelete={(t, c) => setPendingDeleteCorpus({ tenant: t, corpus: c })}
            />
          </>
        )}
      </div>

      {/* New tenant */}
      <NewTenantDialog
        open={editor?.kind === "new-tenant"}
        onClose={() => setEditor(null)}
        onCreated={async (msg) => {
          setEditor(null)
          await refresh()
          toast.success(msg)
        }}
      />

      {/* New / edit corpus */}
      <CorpusEditor
        editor={
          editor?.kind === "new-corpus" || editor?.kind === "edit-corpus"
            ? editor
            : null
        }
        tenants={tenants}
        onClose={() => setEditor(null)}
        onSaved={async (msg) => {
          setEditor(null)
          await refresh()
          toast.success(msg)
        }}
      />

      {/* Delete tenant */}
      <ConfirmDialog
        open={pendingDeleteTenant !== null}
        onOpenChange={(o) => !o && setPendingDeleteTenant(null)}
        title={`Delete tenant ${pendingDeleteTenant ?? ""}?`}
        description="Removes the tenant's config directory AND its entire index directory. This cannot be undone."
        confirmLabel="Delete tenant"
        variant="danger"
        onConfirm={async () => {
          if (!pendingDeleteTenant) return
          const id = pendingDeleteTenant
          try {
            await searchAdmin.deleteTenant(id)
            setPendingDeleteTenant(null)
            await refresh()
            toast.success(`Deleted tenant ${id}`)
          } catch (err) {
            setPendingDeleteTenant(null)
            toast.error(errorMessage(err, `delete tenant ${id}`))
          }
        }}
      />

      {/* Delete corpus */}
      <ConfirmDialog
        open={pendingDeleteCorpus !== null}
        onOpenChange={(o) => !o && setPendingDeleteCorpus(null)}
        title={
          pendingDeleteCorpus
            ? `Delete ${pendingDeleteCorpus.tenant}/${pendingDeleteCorpus.corpus}?`
            : ""
        }
        description="Removes the corpus config and its index directory. The tenant is preserved."
        confirmLabel="Delete corpus"
        variant="danger"
        onConfirm={async () => {
          if (!pendingDeleteCorpus) return
          const { tenant, corpus } = pendingDeleteCorpus
          try {
            await searchAdmin.deleteCorpus(tenant, corpus)
            setPendingDeleteCorpus(null)
            await refresh()
            toast.success(`Deleted ${tenant}/${corpus}`)
          } catch (err) {
            setPendingDeleteCorpus(null)
            toast.error(errorMessage(err, `delete ${tenant}/${corpus}`))
          }
        }}
      />

      {/* Restart confirm */}
      <ConfirmDialog
        open={restartConfirm}
        onOpenChange={(o) => !o && setRestartConfirm(false)}
        title="Restart arcadia-search admin?"
        description="The sidecar will exit and systemd will bring it back up. Active rebuilds will be aborted."
        confirmLabel="Restart"
        variant="danger"
        onConfirm={async () => {
          setRestartConfirm(false)
          try {
            await searchAdmin.restart()
            toast.success("Requested a restart of arcadia-search")
          } catch (err) {
            toast.error(errorMessage(err, "restart arcadia-search"))
          }
        }}
      />
    </AppShell>
  )
}

// --- Tenants card --------------------------------------------------------

function TenantsCard({
  tenants,
  loading,
  onRetry,
  onDelete,
}: {
  tenants: TenantSummary[]
  loading: boolean
  onRetry: () => void
  onDelete: (id: string) => void
}) {
  return (
    <Card>
      <CardHeader>
        <h2 className="text-base font-semibold">Tenants</h2>
      </CardHeader>
      <CardContent className="relative p-4">
        {/* The sidecar's own failure is rendered once at page level, so by the
            time we get here the load succeeded — this empty state is true. */}
        <DataState
          loading={loading}
          error={null}
          isEmpty={tenants.length === 0}
          onRetry={onRetry}
          loadingLabel="Loading tenants…"
          empty={
            <EmptyState
              title="No tenants yet."
              description="Create one to start adding corpora."
              className="py-8"
            />
          }
        >
          <ul className="flex flex-wrap gap-2">
            {tenants.map((t) => (
              <li
                key={t.id}
                className="flex items-center gap-2 rounded-md border bg-card px-3 py-1.5"
              >
                <code className="font-mono text-xs">{t.id}</code>
                <Badge variant="secondary" className="text-xs">
                  {t.corpus_count} corpus{t.corpus_count === 1 ? "" : "es"}
                </Badge>
                <Button
                  variant="ghost"
                  size="icon-sm"
                  onClick={() => onDelete(t.id)}
                  aria-label={`Delete tenant ${t.id}`}
                  data-action={`tenant-${t.id}-delete`}
                >
                  <Trash2 className="size-3.5" />
                </Button>
              </li>
            ))}
          </ul>
        </DataState>
      </CardContent>
    </Card>
  )
}

// --- Corpora table -------------------------------------------------------

function CorporaCard({
  corpora,
  loading,
  error,
  onRetry,
  rebuildingId,
  onRebuild,
  onEdit,
  onDelete,
}: {
  corpora: Row[]
  loading: boolean
  error: unknown
  onRetry: () => void
  rebuildingId: string | null
  onRebuild: (tenant: string, corpus: string) => void
  onEdit: (tenant: string, corpus: string) => void
  onDelete: (tenant: string, corpus: string) => void
}) {
  const [search, setSearch] = useState("")

  const columns = useMemo<Column<Row>[]>(
    () => [
      {
        id: "tenant",
        header: "Tenant",
        accessor: "tenant",
        sortable: true,
        cell: (r) => (
          <code className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs">
            {r.tenant}
          </code>
        ),
      },
      {
        id: "corpus",
        header: "Corpus",
        accessor: "corpus",
        sortable: true,
        cell: (r) => (
          <span className="flex items-center gap-2 font-medium">
            <Database className="size-4 text-muted-foreground" />
            {r.corpus}
          </span>
        ),
      },
      {
        id: "indexed",
        header: "Status",
        sortable: true,
        accessor: (r) => (r.indexed ? 1 : 0),
        cell: (r) =>
          r.indexed ? (
            <Badge variant="default" className="text-xs">
              Indexed
            </Badge>
          ) : (
            <Badge variant="secondary" className="text-xs">
              Not built
            </Badge>
          ),
      },
      {
        id: "docs",
        header: "Docs",
        sortable: true,
        accessor: (r) => r.num_docs ?? -1,
        cell: (r) =>
          r.num_docs != null ? (
            <span className="font-mono text-xs">
              {r.num_docs.toLocaleString()}
            </span>
          ) : (
            <span className="text-muted-foreground">—</span>
          ),
      },
      {
        id: "actions",
        header: "",
        align: "right",
        cell: (r) => {
          const id = `${r.tenant}/${r.corpus}`
          const isRebuilding = rebuildingId === id
          const items: ActionItem[] = [
            {
              id: "rebuild",
              label: isRebuilding ? "Rebuilding…" : "Rebuild",
              icon: (
                <RefreshCw
                  className={`size-4 ${isRebuilding ? "animate-spin" : ""}`}
                />
              ),
              dataAction: `corpus-${r.tenant}-${r.corpus}-rebuild`,
              onSelect: () =>
                isRebuilding ? undefined : onRebuild(r.tenant, r.corpus),
            },
            {
              id: "edit",
              label: "Edit config",
              icon: <FileText className="size-4" />,
              dataAction: `corpus-${r.tenant}-${r.corpus}-edit`,
              onSelect: () => onEdit(r.tenant, r.corpus),
            },
            {
              id: "delete",
              label: "Delete",
              icon: <Trash2 className="size-4" />,
              destructive: true,
              dataAction: `corpus-${r.tenant}-${r.corpus}-delete`,
              onSelect: () => onDelete(r.tenant, r.corpus),
            },
          ]
          return (
            <ActionsCell
              items={items}
              triggerDataAction={`corpus-${r.tenant}-${r.corpus}-actions`}
            />
          )
        },
      },
    ],
    [rebuildingId, onRebuild, onEdit, onDelete],
  )

  const table = useTable<Row>({
    data: corpora,
    columns,
    getRowId: (r) => r.rowId,
    initialPageSize: 25,
    initialSearch: search,
  })
  useEffect(() => {
    table.setSearch(search)
  }, [search, table])

  return (
    <Card>
      <CardHeader className="flex flex-row items-center gap-3">
        <SearchInput
          value={search}
          onValueChange={setSearch}
          placeholder="Search by tenant or corpus"
          data-action="corpora-search"
          className="max-w-sm flex-1"
        />
        <div className="ml-auto text-xs text-muted-foreground">
          {table.total} of {corpora.length}
        </div>
      </CardHeader>
      <CardContent className="relative p-0">
        <DataState
          loading={loading}
          error={error}
          isEmpty={table.total === 0}
          onRetry={onRetry}
          loadingLabel="Loading corpora…"
          empty={
            <EmptyState
              icon={<Database className="size-6" />}
              title={search ? "No matches." : "No corpora yet."}
              description={
                search ? "Try a different search." : "Create one above."
              }
              className="py-12"
            />
          }
        >
          <DataTable
            columns={columns}
            rows={table.pageRows}
            getRowId={(r) => r.rowId}
            sort={table.sort}
            onSortToggle={table.toggleSort}
            loading={loading && corpora.length > 0}
            stickyHeader
          />
          <Pagination
            page={table.page}
            pageSize={table.pageSize}
            total={table.total}
            onPageChange={table.setPage}
            onPageSizeChange={table.setPageSize}
          />
        </DataState>
      </CardContent>
    </Card>
  )
}

// --- Dialogs -------------------------------------------------------------

function NewTenantDialog({
  open,
  onClose,
  onCreated,
}: {
  open: boolean
  onClose: () => void
  onCreated: (msg: string) => Promise<void>
}) {
  const [id, setId] = useState("")
  const [saving, setSaving] = useState(false)
  const [error, setError] = useState<unknown>(null)

  useEffect(() => {
    if (!open) {
      setId("")
      setError(null)
    }
  }, [open])

  const submit = async () => {
    setError(null)
    setSaving(true)
    try {
      await searchAdmin.createTenant(id)
      await onCreated(`Created tenant ${id}`)
    } catch (err) {
      setError(err)
    } finally {
      setSaving(false)
    }
  }

  return (
    <Dialog open={open} onOpenChange={(o) => !o && onClose()}>
      <DialogContent className="sm:max-w-md">
        <DialogHeader>
          <DialogTitle>New tenant</DialogTitle>
          <DialogDescription>
            Creates an empty config dir at{" "}
            <code className="font-mono text-xs">
              $INDEX_CONFIG_DIR/&lt;id&gt;/
            </code>
            . Add corpora separately. Names are alphanumeric, dash, or
            underscore.
          </DialogDescription>
        </DialogHeader>
        <div className="flex flex-col gap-1.5">
          <Label htmlFor="new-tenant-id">Tenant id</Label>
          <Input
            id="new-tenant-id"
            value={id}
            onChange={(e) => setId(e.target.value)}
            placeholder="acme"
            data-action="tenant-form-id"
          />
        </div>

        {error ? <DialogError error={error} context="create the tenant" /> : null}

        <DialogFooter>
          <Button
            variant="outline"
            onClick={onClose}
            disabled={saving}
            data-action="tenant-form-cancel"
          >
            Cancel
          </Button>
          <Button
            onClick={submit}
            disabled={saving || !id}
            data-action="tenant-form-save"
          >
            {saving ? (
              <RefreshCw className="size-4 animate-spin" />
            ) : (
              <CheckCircle2 className="size-4" />
            )}
            Create
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}

const CORPUS_CONFIG_TEMPLATE = `{
  "corpus": "docs",
  "sources": [
    {
      "type": "arcadia",
      "list_url": "/api/v1/files?tenant_id={tenant}",
      "item_url": "/api/v1/files/{id}/content",
      "title_field": "name",
      "id_field": "id",
      "mtime_field": "updated_at",
      "tags": ["uploaded"]
    }
  ]
}`

function CorpusEditor({
  editor,
  tenants,
  onClose,
  onSaved,
}: {
  editor:
    | { kind: "new-corpus"; tenant: string }
    | { kind: "edit-corpus"; tenant: string; corpus: string }
    | null
  tenants: TenantSummary[]
  onClose: () => void
  onSaved: (msg: string) => Promise<void>
}) {
  const [tenant, setTenant] = useState("")
  const [text, setText] = useState("")
  const [saving, setSaving] = useState(false)
  const [loading, setLoading] = useState(false)
  // Covers both the hydrate-on-open failure and the save failure. Either way
  // the operator is looking at this dialog, so this is where it has to speak.
  const [error, setError] = useState<unknown>(null)

  const isEdit = editor?.kind === "edit-corpus"
  const headerCorpus = isEdit ? editor.corpus : ""

  // Hydrate on open: load existing config for edit, template for new.
  useEffect(() => {
    if (!editor) return
    setTenant(editor.tenant)
    setError(null)
    if (editor.kind === "edit-corpus") {
      setLoading(true)
      searchAdmin
        .getCorpus(editor.tenant, editor.corpus)
        .then((res) => {
          setText(JSON.stringify(res.config, null, 2))
        })
        .catch((err) => setError(err))
        .finally(() => setLoading(false))
    } else {
      setText(CORPUS_CONFIG_TEMPLATE)
    }
  }, [editor])

  if (!editor) return null

  const submit = async () => {
    setError(null)
    setSaving(true)
    try {
      const parsed = JSON.parse(text)
      if (typeof parsed !== "object" || parsed === null) {
        throw new Error("config must be a JSON object")
      }
      if (editor.kind === "new-corpus") {
        const corpus = parsed.corpus
        if (typeof corpus !== "string" || !corpus) {
          throw new Error('config must have a string "corpus" field')
        }
        await searchAdmin.createCorpus(tenant, parsed)
        await onSaved(`Created ${tenant}/${corpus}`)
      } else {
        await searchAdmin.updateCorpus(editor.tenant, editor.corpus, parsed)
        await onSaved(`Updated ${editor.tenant}/${editor.corpus}`)
      }
    } catch (err) {
      // The JSON the operator just wrote stays in the textarea.
      setError(err)
    } finally {
      setSaving(false)
    }
  }

  return (
    <Dialog open onOpenChange={(o) => !o && onClose()}>
      <DialogContent className="sm:max-w-2xl max-h-[90vh] overflow-y-auto">
        <DialogHeader>
          <DialogTitle>
            {isEdit ? `Edit ${editor.tenant}/${headerCorpus}` : "New corpus"}
          </DialogTitle>
          <DialogDescription>
            JSON config matching arcadia-search's IndexerConfig schema. The{" "}
            <code className="font-mono text-xs">tenant</code> field is set
            from the URL — your value is overwritten.
          </DialogDescription>
        </DialogHeader>

        <div className="flex flex-col gap-3">
          {!isEdit ? (
            <div className="flex flex-col gap-1.5">
              <Label htmlFor="corpus-tenant">Tenant</Label>
              <Select value={tenant} onValueChange={setTenant}>
                <SelectTrigger
                  id="corpus-tenant"
                  data-action="corpus-form-tenant"
                >
                  <SelectValue placeholder="Pick a tenant" />
                </SelectTrigger>
                <SelectContent>
                  {tenants.map((t) => (
                    <SelectItem key={t.id} value={t.id}>
                      {t.id}
                    </SelectItem>
                  ))}
                </SelectContent>
              </Select>
            </div>
          ) : null}

          <div className="flex flex-col gap-1.5">
            <Label htmlFor="corpus-config">Config JSON</Label>
            <Textarea
              id="corpus-config"
              value={text}
              onChange={(e) => setText(e.target.value)}
              rows={20}
              className="font-mono text-xs"
              spellCheck={false}
              disabled={loading}
              data-action="corpus-form-config"
            />
          </div>
        </div>

        {error ? (
          <DialogError
            error={error}
            context={isEdit ? "save the corpus" : "create the corpus"}
          />
        ) : null}

        <DialogFooter>
          <Button
            variant="outline"
            onClick={onClose}
            disabled={saving}
            data-action="corpus-form-cancel"
          >
            Cancel
          </Button>
          <Button
            onClick={submit}
            disabled={saving || loading || !tenant}
            data-action="corpus-form-save"
          >
            {saving ? (
              <RefreshCw className="size-4 animate-spin" />
            ) : (
              <CheckCircle2 className="size-4" />
            )}
            {isEdit ? "Update" : "Create"}
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}

export { RouteErrorBoundary as ErrorBoundary } from "~/components/route-error"
