import { useCallback, useEffect, useMemo, useState } from "react"
import {
  ArrowLeft,
  Boxes,
  CheckCircle2,
  Copy,
  Database,
  Eye,
  ExternalLink,
  FolderOpen,
  Grid3x3,
  List as ListIcon,
  Plus,
  RefreshCw,
  Settings as SettingsIcon,
  Shield,
  Trash2,
} from "lucide-react"

import { ArcadiaError, useArcadiaClient } from "@crema/arcadia-client"
import {
  ActionsCell,
  DataTable,
  DateCell,
  Pagination,
  useTable,
  type ActionItem,
  type Column,
} from "@crema/table-ui"
import { SearchInput } from "@crema/search-ui"
import { AlertBanner, ConfirmDialog, EmptyState, LoadingOverlay } from "@crema/feedback-ui"
import { FileGrid, FileList, formatBytes, type FileItem } from "@crema/file-ui"
import { KpiTile, formatCompact } from "@crema/dashboard-ui"

import { AppShell } from "~/components/layout/app-shell"
import { Badge } from "~/components/ui/badge"
import { Button } from "~/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} 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 { Switch } from "~/components/ui/switch"
import { Textarea } from "~/components/ui/textarea"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs"
import {
  configureCors,
  configurePolicy,
  configureVersioning,
  createBucket,
  deleteBucket,
  generateConfirmationCode,
  getCors,
  getPresignedUrl,
  listBuckets,
  listObjects,
  listRegions,
  type Bucket,
  type BucketObject,
  type CorsRule,
} from "~/lib/arcadia/buckets"
import {
  listStorageConfigs,
  type StorageConfig,
} from "~/lib/arcadia/storage-configs"
import { pageTitle } from "~/lib/page-meta"
import { useSession } from "~/lib/session"
import { useRegisterAdminContext } from "~/lib/admin-context"

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

const SELECTED_CONFIG_KEY = "crema.buckets.selected-config"

type View =
  | { kind: "list" }
  | { kind: "objects"; bucket: Bucket }

type Editor =
  | { kind: "create" }
  | { kind: "configure"; bucket: Bucket }
  | null

export default function BucketsRoute() {
  const session = useSession()
  const arcadia = useArcadiaClient()

  const [configs, setConfigs] = useState<StorageConfig[]>([])
  const [configId, setConfigId] = useState<string>(() =>
    typeof window !== "undefined"
      ? (localStorage.getItem(SELECTED_CONFIG_KEY) ?? "")
      : "",
  )
  const [buckets, setBuckets] = useState<Bucket[]>([])
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState<string | null>(null)
  const [info, setInfo] = useState<string | null>(null)
  const [view, setView] = useState<View>({ kind: "list" })
  const [editor, setEditor] = useState<Editor>(null)
  const [pendingDelete, setPendingDelete] = useState<Bucket | null>(null)

  const activeConfig = useMemo(
    () => configs.find((c) => c.id === configId),
    [configs, configId],
  )

  // Load storage configs on mount; pick the default if no choice was persisted.
  useEffect(() => {
    if (!session) return
    let mounted = true
    listStorageConfigs(arcadia)
      .then((rows) => {
        if (!mounted) return
        setConfigs(rows)
        if (!configId) {
          const def = rows.find((r) => r.is_default) ?? rows[0]
          if (def) {
            setConfigId(def.id)
            localStorage.setItem(SELECTED_CONFIG_KEY, def.id)
          }
        } else if (!rows.find((r) => r.id === configId)) {
          // Stored config no longer exists.
          const def = rows.find((r) => r.is_default) ?? rows[0]
          setConfigId(def?.id ?? "")
        }
      })
      .catch((err) =>
        setError(
          err instanceof ArcadiaError ? err.message : "Failed to load storage configs.",
        ),
      )
    return () => {
      mounted = false
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [session, arcadia])

  useEffect(() => {
    if (configId) localStorage.setItem(SELECTED_CONFIG_KEY, configId)
  }, [configId])

  const refresh = useCallback(async () => {
    if (!configId) {
      setBuckets([])
      return
    }
    setError(null)
    setLoading(true)
    try {
      setBuckets(await listBuckets(arcadia, configId))
    } catch (err) {
      setError(err instanceof ArcadiaError ? err.message : "Failed to load buckets.")
    } finally {
      setLoading(false)
    }
  }, [arcadia, configId])

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

  const summary = useMemo(
    () => ({
      storage_config: activeConfig
        ? { id: activeConfig.id, name: activeConfig.name, backend: activeConfig.backend_type }
        : null,
      total_buckets: buckets.length,
      total_objects: buckets.reduce((a, b) => a + (b.object_count ?? 0), 0),
      total_size_bytes: buckets.reduce((a, b) => a + (b.size_bytes ?? 0), 0),
      buckets: buckets.map((b) => ({
        name: b.name,
        region: b.region,
        size_bytes: b.size_bytes,
        object_count: b.object_count,
      })),
    }),
    [activeConfig, buckets],
  )
  useRegisterAdminContext("buckets", summary)

  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">
              {view.kind === "list"
                ? "Buckets"
                : `${view.bucket.name}`}
            </h1>
            <p className="text-sm text-muted-foreground">
              {view.kind === "list"
                ? "Storage backends, buckets, and their contents. Configure CORS, versioning, and policies per bucket."
                : `Objects in ${view.bucket.name}.`}
            </p>
          </div>
          <div className="flex items-center gap-2">
            {view.kind === "objects" ? (
              <Button
                variant="outline"
                size="sm"
                onClick={() => setView({ kind: "list" })}
                data-action="buckets-back"
              >
                <ArrowLeft className="size-4" />
                Buckets
              </Button>
            ) : (
              <>
                <Button
                  variant="outline"
                  size="sm"
                  onClick={refresh}
                  disabled={loading || !configId}
                  data-action="buckets-refresh"
                >
                  <RefreshCw className={`size-4 ${loading ? "animate-spin" : ""}`} />
                  Refresh
                </Button>
                <Button
                  size="sm"
                  onClick={() => setEditor({ kind: "create" })}
                  disabled={!configId}
                  data-action="buckets-create"
                >
                  <Plus className="size-4" />
                  New bucket
                </Button>
              </>
            )}
          </div>
        </header>

        {error ? (
          <AlertBanner variant="error" dismissible onDismiss={() => setError(null)}>
            {error}
          </AlertBanner>
        ) : null}
        {info ? (
          <AlertBanner variant="success" dismissible onDismiss={() => setInfo(null)}>
            {info}
          </AlertBanner>
        ) : null}

        {view.kind === "list" ? (
          <>
            <Card>
              <CardHeader className="flex flex-row flex-wrap items-end gap-3">
                <div className="flex flex-col gap-1.5">
                  <Label htmlFor="buckets-config" className="text-xs">
                    Storage configuration
                  </Label>
                  <Select value={configId} onValueChange={setConfigId}>
                    <SelectTrigger
                      id="buckets-config"
                      className="w-72"
                      data-action="buckets-config-select"
                    >
                      <SelectValue placeholder="Pick a storage config" />
                    </SelectTrigger>
                    <SelectContent>
                      {configs.length === 0 ? (
                        <SelectItem value="__none" disabled>
                          No storage configs yet — create one in Storage
                        </SelectItem>
                      ) : (
                        configs.map((c) => (
                          <SelectItem key={c.id} value={c.id}>
                            {c.name}{" "}
                            <span className="text-xs text-muted-foreground">
                              ({c.backend_type}
                              {c.is_default ? " · default" : ""})
                            </span>
                          </SelectItem>
                        ))
                      )}
                    </SelectContent>
                  </Select>
                </div>
                <div className="ml-auto grid grid-cols-3 gap-3 min-w-0">
                  <KpiTile
                    label="Buckets"
                    value={formatCompact(buckets.length)}
                    size="sm"
                  />
                  <KpiTile
                    label="Objects"
                    value={formatCompact(summary.total_objects)}
                    size="sm"
                  />
                  <KpiTile
                    label="Size"
                    value={formatBytes(summary.total_size_bytes ?? 0)}
                    size="sm"
                  />
                </div>
              </CardHeader>
            </Card>

            <BucketsTable
              buckets={buckets}
              loading={loading}
              hasConfig={!!configId}
              onOpen={(b) => setView({ kind: "objects", bucket: b })}
              onConfigure={(b) => setEditor({ kind: "configure", bucket: b })}
              onDelete={(b) => setPendingDelete(b)}
            />
          </>
        ) : (
          <ObjectBrowser
            storageConfigId={configId}
            bucket={view.bucket}
            onError={setError}
          />
        )}
      </div>

      {/* Create */}
      <CreateBucketDialog
        open={editor?.kind === "create"}
        configId={configId}
        onClose={() => setEditor(null)}
        onCreated={async (msg) => {
          setEditor(null)
          if (msg) setInfo(msg)
          await refresh()
        }}
        onError={setError}
      />

      {/* Configure (versioning / CORS / policy) */}
      <ConfigureBucketDialog
        state={editor?.kind === "configure" ? editor : null}
        configId={configId}
        onClose={() => setEditor(null)}
        onChanged={async (msg) => {
          if (msg) setInfo(msg)
          await refresh()
        }}
        onError={setError}
      />

      {/* Delete */}
      <DeleteBucketFlow
        bucket={pendingDelete}
        configId={configId}
        onClose={() => setPendingDelete(null)}
        onDeleted={async (msg) => {
          setPendingDelete(null)
          if (msg) setInfo(msg)
          await refresh()
        }}
        onError={setError}
      />
    </AppShell>
  )
}

// --- Buckets table -----------------------------------------------------

function BucketsTable({
  buckets,
  loading,
  hasConfig,
  onOpen,
  onConfigure,
  onDelete,
}: {
  buckets: Bucket[]
  loading: boolean
  hasConfig: boolean
  onOpen: (b: Bucket) => void
  onConfigure: (b: Bucket) => void
  onDelete: (b: Bucket) => void
}) {
  const [search, setSearch] = useState("")

  const columns = useMemo<Column<Bucket>[]>(
    () => [
      {
        id: "name",
        header: "Name",
        accessor: "name",
        sortable: true,
        cell: (b) => (
          <button
            type="button"
            onClick={() => onOpen(b)}
            data-action={`bucket-${b.name}-open`}
            className="flex items-center gap-2 font-medium text-left hover:underline"
          >
            <Database className="size-4 text-muted-foreground" />
            {b.name}
          </button>
        ),
      },
      {
        id: "region",
        header: "Region",
        accessor: "region",
        sortable: true,
        cell: (b) =>
          b.region ? (
            <code className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs">{b.region}</code>
          ) : (
            <span className="text-muted-foreground">—</span>
          ),
      },
      {
        id: "objects",
        header: "Objects",
        accessor: "object_count",
        sortable: true,
        cell: (b) =>
          b.object_count != null ? (
            <span className="font-mono text-xs">{b.object_count.toLocaleString()}</span>
          ) : (
            <span className="text-muted-foreground">—</span>
          ),
      },
      {
        id: "size",
        header: "Size",
        accessor: "size_bytes",
        sortable: true,
        cell: (b) =>
          b.size_bytes != null ? (
            <span className="text-xs">{formatBytes(b.size_bytes)}</span>
          ) : (
            <span className="text-muted-foreground">—</span>
          ),
      },
      {
        id: "created",
        header: "Created",
        accessor: "created_at",
        sortable: true,
        cell: (b) =>
          b.created_at ? (
            <DateCell value={b.created_at} format="short" />
          ) : (
            <span className="text-muted-foreground">—</span>
          ),
      },
      {
        id: "actions",
        header: "",
        align: "right",
        cell: (b) => {
          const items: ActionItem[] = [
            {
              id: "open",
              label: "Browse objects",
              icon: <FolderOpen className="size-4" />,
              dataAction: `bucket-${b.name}-browse`,
              onSelect: () => onOpen(b),
            },
            {
              id: "configure",
              label: "Configure",
              icon: <SettingsIcon className="size-4" />,
              dataAction: `bucket-${b.name}-configure`,
              onSelect: () => onConfigure(b),
            },
            {
              id: "delete",
              label: "Delete",
              icon: <Trash2 className="size-4" />,
              destructive: true,
              dataAction: `bucket-${b.name}-delete`,
              onSelect: () => onDelete(b),
            },
          ]
          return (
            <ActionsCell items={items} triggerDataAction={`bucket-${b.name}-actions`} />
          )
        },
      },
    ],
    [onOpen, onConfigure, onDelete],
  )

  const table = useTable<Bucket>({
    data: buckets,
    columns,
    getRowId: (b) => b.name,
    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 name or region"
          data-action="buckets-search"
          className="max-w-sm flex-1"
        />
        <div className="ml-auto text-xs text-muted-foreground">
          {table.total} of {buckets.length}
        </div>
      </CardHeader>

      <CardContent className="relative p-0">
        <LoadingOverlay active={loading && buckets.length === 0} label="Loading buckets…" />
        {!hasConfig ? (
          <EmptyState
            icon={<Boxes className="size-6" />}
            title="Pick a storage configuration"
            description="Buckets are scoped to a credential. Add one under Storage if you don't have any yet."
            className="py-12"
          />
        ) : table.total === 0 && !loading ? (
          <EmptyState
            icon={<Boxes className="size-6" />}
            title={search ? "No buckets match." : "No buckets in this account."}
            description={search ? "Try a different search." : "Create your first bucket."}
            className="py-12"
          />
        ) : (
          <>
            <DataTable
              columns={columns}
              rows={table.pageRows}
              getRowId={(b) => b.name}
              sort={table.sort}
              onSortToggle={table.toggleSort}
              loading={loading && buckets.length > 0}
              stickyHeader
            />
            <Pagination
              page={table.page}
              pageSize={table.pageSize}
              total={table.total}
              onPageChange={table.setPage}
              onPageSizeChange={table.setPageSize}
            />
          </>
        )}
      </CardContent>
    </Card>
  )
}

// --- Object browser ----------------------------------------------------

function ObjectBrowser({
  storageConfigId,
  bucket,
  onError,
}: {
  storageConfigId: string
  bucket: Bucket
  onError: (msg: string | null) => void
}) {
  const arcadia = useArcadiaClient()
  const [objects, setObjects] = useState<BucketObject[]>([])
  const [prefix, setPrefix] = useState("")
  const [loading, setLoading] = useState(true)
  const [layout, setLayout] = useState<"grid" | "list">("list")
  const [previewUrl, setPreviewUrl] = useState<{ url: string; key: string } | null>(null)
  const [search, setSearch] = useState("")

  const refresh = useCallback(async () => {
    setLoading(true)
    onError(null)
    try {
      const res = await listObjects(arcadia, {
        storage_config_id: storageConfigId,
        bucket_name: bucket.name,
        prefix: prefix || undefined,
        max_keys: 1000,
      })
      setObjects(res.objects ?? [])
    } catch (err) {
      onError(err instanceof ArcadiaError ? err.message : "Failed to load objects.")
    } finally {
      setLoading(false)
    }
  }, [arcadia, storageConfigId, bucket.name, prefix, onError])

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

  const filtered = useMemo(() => {
    const q = search.trim().toLowerCase()
    if (!q) return objects
    return objects.filter((o) => o.key.toLowerCase().includes(q))
  }, [objects, search])

  const fileItems = useMemo<FileItem[]>(
    () =>
      filtered.map((o) => ({
        id: o.key,
        name: o.key.split("/").pop() || o.key,
        size: o.size,
        mime: guessMime(o.key),
        modified: o.last_modified ?? undefined,
      })),
    [filtered],
  )

  const openPresigned = useCallback(
    async (key: string) => {
      try {
        const res = await getPresignedUrl(arcadia, {
          storage_config_id: storageConfigId,
          bucket_name: bucket.name,
          key,
          expires_in: 3600,
        })
        setPreviewUrl({ url: res.url, key })
      } catch (err) {
        onError(err instanceof ArcadiaError ? err.message : "Presign failed.")
      }
    },
    [arcadia, storageConfigId, bucket.name, onError],
  )

  return (
    <Card>
      <CardHeader className="flex flex-col gap-3 lg:flex-row lg:flex-wrap lg:items-end">
        <div className="flex flex-col gap-1.5">
          <Label htmlFor="objects-prefix" className="text-xs">
            Prefix / folder
          </Label>
          <Input
            id="objects-prefix"
            value={prefix}
            onChange={(e) => setPrefix(e.target.value)}
            placeholder="path/to/folder/"
            className="w-64 font-mono text-xs"
            data-action="objects-prefix"
          />
        </div>
        <SearchInput
          value={search}
          onValueChange={setSearch}
          placeholder="Filter by name"
          data-action="objects-search"
          className="max-w-sm flex-1"
        />
        <div className="ml-auto flex items-center gap-2">
          <Button
            variant={layout === "list" ? "default" : "outline"}
            size="sm"
            onClick={() => setLayout("list")}
            data-action="objects-layout-list"
            aria-label="List view"
          >
            <ListIcon className="size-4" />
          </Button>
          <Button
            variant={layout === "grid" ? "default" : "outline"}
            size="sm"
            onClick={() => setLayout("grid")}
            data-action="objects-layout-grid"
            aria-label="Grid view"
          >
            <Grid3x3 className="size-4" />
          </Button>
          <Button
            variant="outline"
            size="sm"
            onClick={refresh}
            disabled={loading}
            data-action="objects-refresh"
          >
            <RefreshCw className={`size-4 ${loading ? "animate-spin" : ""}`} />
            Refresh
          </Button>
        </div>
      </CardHeader>

      <CardContent className="relative p-4">
        <LoadingOverlay active={loading && objects.length === 0} label="Loading objects…" />
        {fileItems.length === 0 && !loading ? (
          <EmptyState
            icon={<FolderOpen className="size-6" />}
            title={search || prefix ? "No matches." : "Empty bucket."}
            description={
              search || prefix
                ? "Adjust the filter or prefix."
                : "Upload an object via your application; this view is read-only for now."
            }
            className="py-12"
          />
        ) : layout === "list" ? (
          <FileList
            files={fileItems}
            onItemClick={(f) => openPresigned(f.id)}
            renderAction={(f) => (
              <Button
                size="sm"
                variant="ghost"
                onClick={(e) => {
                  e.stopPropagation()
                  openPresigned(f.id)
                }}
                data-action={`object-${f.id}-presign`}
              >
                <ExternalLink className="size-3.5" />
                Link
              </Button>
            )}
          />
        ) : (
          <FileGrid
            files={fileItems}
            onItemClick={(f) => openPresigned(f.id)}
            minItemWidth={180}
          />
        )}
      </CardContent>

      <PresignDialog reveal={previewUrl} onClose={() => setPreviewUrl(null)} />
    </Card>
  )
}

function PresignDialog({
  reveal,
  onClose,
}: {
  reveal: { url: string; key: string } | null
  onClose: () => void
}) {
  const [copied, setCopied] = useState(false)
  useEffect(() => {
    if (!reveal) setCopied(false)
  }, [reveal])

  if (!reveal) return null

  return (
    <Dialog open onOpenChange={(o) => !o && onClose()}>
      <DialogContent className="sm:max-w-lg">
        <DialogHeader>
          <DialogTitle className="flex items-center gap-2">
            <ExternalLink className="size-5 text-primary" />
            Presigned URL
          </DialogTitle>
          <DialogDescription>
            <code className="font-mono text-xs">{reveal.key}</code> — valid for 1 hour.
          </DialogDescription>
        </DialogHeader>

        <div className="flex flex-col gap-2 rounded-md border bg-muted/30 p-3">
          <code className="select-all break-all font-mono text-xs">{reveal.url}</code>
          <div className="flex gap-2">
            <Button
              size="sm"
              variant="outline"
              onClick={async () => {
                try {
                  await navigator.clipboard.writeText(reveal.url)
                  setCopied(true)
                } catch {}
              }}
              data-action="presign-copy"
            >
              {copied ? <CheckCircle2 className="size-3.5" /> : <Copy className="size-3.5" />}
              {copied ? "Copied" : "Copy"}
            </Button>
            <Button asChild size="sm" data-action="presign-open">
              <a href={reveal.url} target="_blank" rel="noopener noreferrer">
                <ExternalLink className="size-3.5" />
                Open
              </a>
            </Button>
          </div>
        </div>

        <DialogFooter>
          <Button onClick={onClose} variant="outline" data-action="presign-close">
            Close
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}

// --- Create / configure / delete dialogs --------------------------------

function CreateBucketDialog({
  open,
  configId,
  onClose,
  onCreated,
  onError,
}: {
  open: boolean
  configId: string
  onClose: () => void
  onCreated: (msg?: string) => Promise<void>
  onError: (msg: string | null) => void
}) {
  const arcadia = useArcadiaClient()
  const [name, setName] = useState("")
  const [region, setRegion] = useState("")
  const [acl, setAcl] = useState<"private" | "public-read">("private")
  const [versioning, setVersioning] = useState(false)
  const [regions, setRegions] = useState<string[]>([])
  const [saving, setSaving] = useState(false)

  useEffect(() => {
    if (!open) {
      setName("")
      setRegion("")
      setAcl("private")
      setVersioning(false)
      return
    }
    if (configId) {
      listRegions(arcadia, configId)
        .then(setRegions)
        .catch(() => setRegions([]))
    }
  }, [open, arcadia, configId])

  const submit = async () => {
    onError(null)
    setSaving(true)
    try {
      await createBucket(arcadia, {
        storage_config_id: configId,
        bucket_name: name,
        region: region || undefined,
        acl,
        versioning,
      })
      await onCreated(`Bucket ${name} created.`)
    } catch (err) {
      onError(
        err instanceof ArcadiaError
          ? err.message
          : err instanceof Error
            ? err.message
            : "Create failed.",
      )
    } finally {
      setSaving(false)
    }
  }

  return (
    <Dialog open={open} onOpenChange={(o) => !o && onClose()}>
      <DialogContent className="sm:max-w-md">
        <DialogHeader>
          <DialogTitle>New bucket</DialogTitle>
          <DialogDescription>
            Bucket names must be globally unique on the provider and follow DNS rules
            (lowercase, no underscores).
          </DialogDescription>
        </DialogHeader>

        <div className="flex flex-col gap-3">
          <div className="flex flex-col gap-1.5">
            <Label htmlFor="new-bucket-name">Name</Label>
            <Input
              id="new-bucket-name"
              value={name}
              onChange={(e) => setName(e.target.value)}
              placeholder="my-app-uploads"
              data-action="bucket-form-name"
            />
          </div>
          <div className="flex flex-col gap-1.5">
            <Label htmlFor="new-bucket-region">Region</Label>
            {regions.length > 0 ? (
              <Select value={region} onValueChange={setRegion}>
                <SelectTrigger id="new-bucket-region" data-action="bucket-form-region">
                  <SelectValue placeholder="Pick a region" />
                </SelectTrigger>
                <SelectContent>
                  {regions.map((r) => (
                    <SelectItem key={r} value={r}>
                      {r}
                    </SelectItem>
                  ))}
                </SelectContent>
              </Select>
            ) : (
              <Input
                id="new-bucket-region"
                value={region}
                onChange={(e) => setRegion(e.target.value)}
                placeholder="us-east-1"
                data-action="bucket-form-region"
              />
            )}
          </div>
          <div className="flex flex-col gap-1.5">
            <Label>ACL</Label>
            <Select value={acl} onValueChange={(v) => setAcl(v as "private" | "public-read")}>
              <SelectTrigger data-action="bucket-form-acl">
                <SelectValue />
              </SelectTrigger>
              <SelectContent>
                <SelectItem value="private">Private</SelectItem>
                <SelectItem value="public-read">Public read</SelectItem>
              </SelectContent>
            </Select>
          </div>
          <div className="flex items-center justify-between rounded-md border px-3 py-2">
            <div>
              <div className="text-sm font-medium">Versioning</div>
              <div className="text-xs text-muted-foreground">
                Keep object history. Can be enabled later.
              </div>
            </div>
            <Switch
              checked={versioning}
              onCheckedChange={setVersioning}
              data-action="bucket-form-versioning"
            />
          </div>
        </div>

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

function ConfigureBucketDialog({
  state,
  configId,
  onClose,
  onChanged,
  onError,
}: {
  state: { kind: "configure"; bucket: Bucket } | null
  configId: string
  onClose: () => void
  onChanged: (msg?: string) => Promise<void>
  onError: (msg: string | null) => void
}) {
  const arcadia = useArcadiaClient()
  const [versioningOn, setVersioningOn] = useState(false)
  const [versioningSaving, setVersioningSaving] = useState(false)

  const [corsRules, setCorsRules] = useState<CorsRule[]>([])
  const [corsSaving, setCorsSaving] = useState(false)
  const [corsLoading, setCorsLoading] = useState(false)

  const [policyText, setPolicyText] = useState("")
  const [policySaving, setPolicySaving] = useState(false)

  const open = state !== null

  useEffect(() => {
    if (!open || !state) return
    setCorsLoading(true)
    getCors(arcadia, configId, state.bucket.name)
      .then((res) => {
        setCorsRules(res?.rules ?? [])
      })
      .catch(() => setCorsRules([]))
      .finally(() => setCorsLoading(false))
  }, [open, state, arcadia, configId])

  if (!state) return null
  const { bucket } = state

  const saveVersioning = async () => {
    setVersioningSaving(true)
    onError(null)
    try {
      await configureVersioning(arcadia, {
        storage_config_id: configId,
        bucket_name: bucket.name,
        enabled: versioningOn,
      })
      await onChanged(`Versioning ${versioningOn ? "enabled" : "suspended"}.`)
    } catch (err) {
      onError(err instanceof ArcadiaError ? err.message : "Save failed.")
    } finally {
      setVersioningSaving(false)
    }
  }

  const saveCors = async () => {
    setCorsSaving(true)
    onError(null)
    try {
      await configureCors(arcadia, {
        storage_config_id: configId,
        bucket_name: bucket.name,
        rules: corsRules,
      })
      await onChanged("CORS rules saved.")
    } catch (err) {
      onError(err instanceof ArcadiaError ? err.message : "Save failed.")
    } finally {
      setCorsSaving(false)
    }
  }

  const savePolicy = async () => {
    setPolicySaving(true)
    onError(null)
    try {
      const policy = policyText.trim() === "" ? {} : JSON.parse(policyText)
      await configurePolicy(arcadia, {
        storage_config_id: configId,
        bucket_name: bucket.name,
        policy,
      })
      await onChanged("Bucket policy saved.")
    } catch (err) {
      onError(
        err instanceof ArcadiaError
          ? err.message
          : err instanceof Error
            ? `Invalid JSON or save failed: ${err.message}`
            : "Save failed.",
      )
    } finally {
      setPolicySaving(false)
    }
  }

  return (
    <Dialog open onOpenChange={(o) => !o && onClose()}>
      <DialogContent className="sm:max-w-2xl max-h-[90vh] overflow-y-auto">
        <DialogHeader>
          <DialogTitle>Configure {bucket.name}</DialogTitle>
          <DialogDescription>
            Each section saves independently — changes to versioning don't touch CORS, etc.
          </DialogDescription>
        </DialogHeader>

        <Tabs defaultValue="versioning">
          <TabsList>
            <TabsTrigger value="versioning" data-action="configure-tab-versioning">
              Versioning
            </TabsTrigger>
            <TabsTrigger value="cors" data-action="configure-tab-cors">
              CORS
            </TabsTrigger>
            <TabsTrigger value="policy" data-action="configure-tab-policy">
              Policy
            </TabsTrigger>
          </TabsList>

          <TabsContent value="versioning" className="pt-4">
            <div className="flex items-center justify-between rounded-md border px-3 py-2">
              <div>
                <div className="text-sm font-medium">Versioning enabled</div>
                <div className="text-xs text-muted-foreground">
                  When on, overwrites and deletes are kept as previous versions.
                </div>
              </div>
              <Switch
                checked={versioningOn}
                onCheckedChange={setVersioningOn}
                data-action="configure-versioning-toggle"
              />
            </div>
            <div className="mt-3 flex justify-end">
              <Button
                onClick={saveVersioning}
                disabled={versioningSaving}
                data-action="configure-versioning-save"
              >
                {versioningSaving ? (
                  <RefreshCw className="size-4 animate-spin" />
                ) : (
                  <CheckCircle2 className="size-4" />
                )}
                Save versioning
              </Button>
            </div>
            <p className="mt-2 text-xs text-muted-foreground">
              Note: this UI shows your intended state, not the current bucket setting (the
              backend's GET endpoint isn't exposed). Submit to apply.
            </p>
          </TabsContent>

          <TabsContent value="cors" className="pt-4">
            {corsLoading ? (
              <p className="py-4 text-center text-sm text-muted-foreground">
                <RefreshCw className="mr-1 inline size-3.5 animate-spin" /> Loading rules…
              </p>
            ) : (
              <CorsEditor rules={corsRules} onChange={setCorsRules} />
            )}
            <div className="mt-3 flex justify-end gap-2">
              <Button
                variant="outline"
                onClick={() =>
                  setCorsRules([
                    ...corsRules,
                    {
                      allowed_origins: ["*"],
                      allowed_methods: ["GET"],
                      allowed_headers: [],
                      max_age_seconds: 3000,
                    },
                  ])
                }
                data-action="configure-cors-add"
              >
                <Plus className="size-3.5" />
                Add rule
              </Button>
              <Button
                onClick={saveCors}
                disabled={corsSaving}
                data-action="configure-cors-save"
              >
                {corsSaving ? <RefreshCw className="size-4 animate-spin" /> : <CheckCircle2 className="size-4" />}
                Save CORS
              </Button>
            </div>
          </TabsContent>

          <TabsContent value="policy" className="pt-4">
            <div className="flex flex-col gap-1.5">
              <Label htmlFor="bucket-policy" className="text-xs">
                Bucket policy (JSON, AWS S3 format)
              </Label>
              <Textarea
                id="bucket-policy"
                value={policyText}
                onChange={(e) => setPolicyText(e.target.value)}
                rows={14}
                placeholder='{\n  "Version": "2012-10-17",\n  "Statement": [...]\n}'
                className="font-mono text-xs"
                data-action="configure-policy-text"
                spellCheck={false}
              />
            </div>
            <div className="mt-3 flex justify-end">
              <Button
                onClick={savePolicy}
                disabled={policySaving}
                data-action="configure-policy-save"
              >
                {policySaving ? (
                  <RefreshCw className="size-4 animate-spin" />
                ) : (
                  <Shield className="size-4" />
                )}
                Save policy
              </Button>
            </div>
          </TabsContent>
        </Tabs>

        <DialogFooter>
          <Button variant="outline" onClick={onClose} data-action="configure-close">
            Close
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}

function CorsEditor({
  rules,
  onChange,
}: {
  rules: CorsRule[]
  onChange: (next: CorsRule[]) => void
}) {
  if (rules.length === 0) {
    return (
      <p className="rounded-md border bg-muted/30 p-4 text-center text-sm text-muted-foreground">
        No CORS rules. Click "Add rule" to allow a browser origin to call this bucket.
      </p>
    )
  }
  return (
    <ul className="flex flex-col gap-3">
      {rules.map((r, i) => (
        <li key={i} className="flex flex-col gap-2 rounded-md border p-3">
          <div className="flex items-center justify-between">
            <Badge variant="secondary" className="font-mono text-xs">
              Rule {i + 1}
            </Badge>
            <Button
              variant="ghost"
              size="sm"
              onClick={() => onChange(rules.filter((_, idx) => idx !== i))}
              data-action={`configure-cors-rule-${i}-delete`}
            >
              <Trash2 className="size-3.5" />
            </Button>
          </div>
          <div className="grid grid-cols-2 gap-2">
            <div>
              <Label className="text-xs">Allowed origins (comma-separated)</Label>
              <Input
                value={r.allowed_origins.join(", ")}
                onChange={(e) =>
                  onChange(
                    rules.map((rr, idx) =>
                      idx === i ? { ...rr, allowed_origins: csv(e.target.value) } : rr,
                    ),
                  )
                }
                placeholder="https://example.com"
                data-action={`configure-cors-rule-${i}-origins`}
              />
            </div>
            <div>
              <Label className="text-xs">Allowed methods</Label>
              <Input
                value={r.allowed_methods.join(", ")}
                onChange={(e) =>
                  onChange(
                    rules.map((rr, idx) =>
                      idx === i ? { ...rr, allowed_methods: csv(e.target.value) } : rr,
                    ),
                  )
                }
                placeholder="GET, PUT, POST"
                data-action={`configure-cors-rule-${i}-methods`}
              />
            </div>
            <div>
              <Label className="text-xs">Allowed headers</Label>
              <Input
                value={(r.allowed_headers ?? []).join(", ")}
                onChange={(e) =>
                  onChange(
                    rules.map((rr, idx) =>
                      idx === i ? { ...rr, allowed_headers: csv(e.target.value) } : rr,
                    ),
                  )
                }
                placeholder="*"
                data-action={`configure-cors-rule-${i}-headers`}
              />
            </div>
            <div>
              <Label className="text-xs">Max age (seconds)</Label>
              <Input
                type="number"
                min={0}
                value={r.max_age_seconds ?? ""}
                onChange={(e) =>
                  onChange(
                    rules.map((rr, idx) =>
                      idx === i
                        ? {
                            ...rr,
                            max_age_seconds:
                              e.target.value === "" ? undefined : Number(e.target.value),
                          }
                        : rr,
                    ),
                  )
                }
                data-action={`configure-cors-rule-${i}-max-age`}
              />
            </div>
          </div>
        </li>
      ))}
    </ul>
  )
}

function DeleteBucketFlow({
  bucket,
  configId,
  onClose,
  onDeleted,
  onError,
}: {
  bucket: Bucket | null
  configId: string
  onClose: () => void
  onDeleted: (msg?: string) => Promise<void>
  onError: (msg: string | null) => void
}) {
  const arcadia = useArcadiaClient()
  const [code, setCode] = useState("")
  const [forceEmpty, setForceEmpty] = useState(false)
  const [issuingCode, setIssuingCode] = useState(false)
  const [deleting, setDeleting] = useState(false)

  useEffect(() => {
    if (!bucket) {
      setCode("")
      setForceEmpty(false)
    }
  }, [bucket])

  const requestCode = async () => {
    if (!bucket) return
    setIssuingCode(true)
    onError(null)
    try {
      const res = await generateConfirmationCode(arcadia, configId, bucket.name)
      setCode(res.code ?? "")
    } catch (err) {
      onError(
        err instanceof ArcadiaError ? err.message : "Failed to generate confirmation code.",
      )
    } finally {
      setIssuingCode(false)
    }
  }

  const doDelete = async () => {
    if (!bucket) return
    setDeleting(true)
    onError(null)
    try {
      await deleteBucket(arcadia, {
        storage_config_id: configId,
        bucket_name: bucket.name,
        confirmation_code: code,
        force_empty: forceEmpty,
        dry_run: false,
      })
      await onDeleted(`${bucket.name} deleted.`)
    } catch (err) {
      onError(err instanceof ArcadiaError ? err.message : "Delete failed.")
    } finally {
      setDeleting(false)
    }
  }

  return (
    <Dialog open={bucket !== null} onOpenChange={(o) => !o && onClose()}>
      <DialogContent className="sm:max-w-md">
        <DialogHeader>
          <DialogTitle className="text-destructive">
            {bucket ? `Delete ${bucket.name}?` : ""}
          </DialogTitle>
          <DialogDescription>
            Destructive action. Generate the 6-digit confirmation code and paste it back to
            authorize the delete.
          </DialogDescription>
        </DialogHeader>

        <div className="flex flex-col gap-3">
          <div className="flex items-end gap-2">
            <div className="flex-1">
              <Label htmlFor="delete-code" className="text-xs">
                Confirmation code
              </Label>
              <Input
                id="delete-code"
                value={code}
                onChange={(e) => setCode(e.target.value)}
                placeholder="6 digits"
                className="font-mono"
                data-action="bucket-delete-code"
              />
            </div>
            <Button
              variant="outline"
              onClick={requestCode}
              disabled={issuingCode}
              data-action="bucket-delete-issue-code"
            >
              {issuingCode ? (
                <RefreshCw className="size-4 animate-spin" />
              ) : (
                <Shield className="size-4" />
              )}
              Issue code
            </Button>
          </div>
          <div className="flex items-center justify-between rounded-md border px-3 py-2">
            <div>
              <div className="text-sm font-medium">Force empty</div>
              <div className="text-xs text-destructive">
                Delete all objects first. Cannot be undone.
              </div>
            </div>
            <Switch
              checked={forceEmpty}
              onCheckedChange={setForceEmpty}
              data-action="bucket-delete-force-empty"
            />
          </div>
        </div>

        <DialogFooter>
          <Button variant="outline" onClick={onClose} disabled={deleting}>
            Cancel
          </Button>
          <Button
            variant="destructive"
            onClick={doDelete}
            disabled={deleting || code.length === 0}
            data-action="bucket-delete-confirm"
          >
            {deleting ? <RefreshCw className="size-4 animate-spin" /> : <Trash2 className="size-4" />}
            Delete bucket
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}

// --- helpers --------------------------------------------------------------

function csv(s: string): string[] {
  return s
    .split(",")
    .map((x) => x.trim())
    .filter(Boolean)
}

function guessMime(key: string): string {
  const ext = key.split(".").pop()?.toLowerCase() ?? ""
  const m: Record<string, string> = {
    jpg: "image/jpeg",
    jpeg: "image/jpeg",
    png: "image/png",
    gif: "image/gif",
    webp: "image/webp",
    svg: "image/svg+xml",
    pdf: "application/pdf",
    mp4: "video/mp4",
    mov: "video/quicktime",
    mp3: "audio/mpeg",
    wav: "audio/wav",
    json: "application/json",
    csv: "text/csv",
    txt: "text/plain",
    md: "text/markdown",
    html: "text/html",
    zip: "application/zip",
  }
  return m[ext] ?? "application/octet-stream"
}
