"use client"

import * as React from "react"
import Link from "next/link"
import { useTranslation } from "react-i18next"
import {
  RiFileAddLine,
  RiDeleteBin5Line,
  RiDownloadCloud2Line,
  RiRefreshLine,
  RiFolderLine,
  RiFileLine,
  RiEyeLine,
  RiEdit2Line,
  RiArrowUpSLine,
  RiArrowDownSLine,
} from "@remixicon/react"
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { SearchInput } from "@/components/search-input"
import { PageHeader } from "@/components/page-header"
import { DataTable } from "@/components/data-table/data-table"
import { Spinner } from "@/components/ui/spinner"
import { useDataTable } from "@/hooks/use-data-table"
import { useObject } from "@/hooks/use-object"
import { useBucket } from "@/hooks/use-bucket"
import { useLocalStorage } from "@/hooks/use-local-storage"
import { usePermissions } from "@/hooks/use-permissions"
import { useApi } from "@/contexts/api-context"
import { useMessage } from "@/lib/feedback/message"
import { exportFile } from "@/lib/export-file"
import { getContentType } from "@/lib/mime-types"
import { formatBytes, formatDateTime } from "@/lib/functions"
import { normalizeDateToIso } from "@/lib/safe-date"
import { buildBucketPath } from "@/lib/bucket-path"
import {
  createObjectListScope,
  resolveObjectListDisplayState,
  shouldApplyObjectListResponse,
  shouldResetObjectListPagination,
} from "@/lib/object-list-state"
import { OBJECT_LIST_DEFAULT_PAGE_SIZE, resolveObjectListPageSize } from "@/lib/object-list-pagination"
import {
  resolveBucketVersioningState,
  shouldForceDeleteObjects,
  shouldShowDeleteAllVersions,
  type BucketVersioningState,
} from "@/lib/object-delete"
import {
  buildObjectZipDownloadPayload,
  normalizeObjectZipDownloadUrl,
  type CreateObjectZipDownloadResponse,
} from "@/lib/object-zip-download"
import {
  buildRenamedObjectKey,
  getObjectBaseName,
  validateObjectRename,
  type ObjectRenameValidation,
} from "@/lib/object-rename"
import { TaskStatsButton } from "@/components/tasks/stats-button"
import { useAddDeleteKeys, useAddDeleteFolder, useTasks } from "@/contexts/task-context"
import type { ColumnDef } from "@tanstack/react-table"

interface ObjectRow {
  Key: string
  type: "prefix" | "object"
  Size: number
  LastModified: string
}

type ObjectListError = "accessDenied" | "loadFailed"

function isAccessDeniedError(error: unknown): boolean {
  const serviceError = error as {
    $metadata?: { httpStatusCode?: number }
    Code?: string
    name?: string
    message?: string
  }
  if (serviceError?.$metadata?.httpStatusCode === 403) return true

  const code = (serviceError?.Code ?? serviceError?.name ?? serviceError?.message ?? "").toLowerCase()
  return code === "accessdenied" || code === "forbidden" || code.includes("access denied")
}

interface ObjectListProps {
  bucket: string
  path: string
  onOpenInfo: (bucket: string, key: string) => void
  onUploadClick: () => void
  canUpload?: boolean
  pageSize?: number
  onRefresh?: () => void
  refreshTrigger?: number
  onPreview: (params: { key?: string; data?: Record<string, unknown> }) => void
}

export function ObjectList({
  bucket,
  path,
  onOpenInfo,
  onUploadClick,
  canUpload = false,
  pageSize = OBJECT_LIST_DEFAULT_PAGE_SIZE,
  onRefresh,
  refreshTrigger = 0,
  onPreview,
}: ObjectListProps) {
  const { t } = useTranslation()
  const message = useMessage()
  const api = useApi()
  const { listObject, getSignedUrl, renameObject } = useObject(bucket)
  const { getBucketVersioning } = useBucket()
  const { canCapability } = usePermissions()
  const addDeleteKeys = useAddDeleteKeys()
  const addDeleteFolder = useAddDeleteFolder()
  const tasks = useTasks()

  const [searchTerm, setSearchTerm] = React.useState("")
  const [showDeleted, setShowDeleted] = useLocalStorage("object-list-show-deleted", false)
  const [loading, setLoading] = React.useState(false)
  const [listError, setListError] = React.useState<ObjectListError | null>(null)
  const [data, setData] = React.useState<ObjectRow[]>([])
  const [nextToken, setNextToken] = React.useState<string | undefined>()
  const [showScrollShortcuts, setShowScrollShortcuts] = React.useState(false)
  const [bucketVersioningState, setBucketVersioningState] = React.useState<BucketVersioningState>("unknown")
  const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false)
  const [deleteDialogKeys, setDeleteDialogKeys] = React.useState<string[]>([])
  const [deleteAllVersions, setDeleteAllVersions] = React.useState(false)
  const [renameDialogOpen, setRenameDialogOpen] = React.useState(false)
  const [renameSourceKey, setRenameSourceKey] = React.useState("")
  const [renameName, setRenameName] = React.useState("")
  const [renameSubmitting, setRenameSubmitting] = React.useState(false)

  const prefix = decodeURIComponent(path)
  const resolvedPageSize = resolveObjectListPageSize(pageSize)
  const listScope = React.useMemo(
    () =>
      createObjectListScope({
        bucket,
        prefix,
        pageSize: resolvedPageSize,
        showDeleted,
      }),
    [bucket, prefix, resolvedPageSize, showDeleted],
  )
  const canBulkDelete = canCapability("objects.bulkDelete", { bucket, prefix })
  const canBulkDownload = canCapability("objects.download", { bucket, prefix })

  const bucketPath = React.useCallback((p?: string | string[]) => buildBucketPath(bucket, p), [bucket])
  const requestIdRef = React.useRef(0)
  const activeScopeRef = React.useRef(listScope)
  const previousScopeRef = React.useRef(listScope)
  const loadingRef = React.useRef(false)
  const loadMoreRef = React.useRef<HTMLDivElement | null>(null)

  React.useEffect(() => {
    activeScopeRef.current = listScope
  }, [listScope])

  React.useEffect(() => {
    loadingRef.current = loading
  }, [loading])

  const updateScrollShortcutVisibility = React.useCallback(() => {
    setShowScrollShortcuts(document.documentElement.scrollHeight > window.innerHeight)
  }, [])

  React.useEffect(() => {
    updateScrollShortcutVisibility()
  }, [data.length, loading, updateScrollShortcutVisibility])

  React.useEffect(() => {
    updateScrollShortcutVisibility()
    window.addEventListener("resize", updateScrollShortcutVisibility)
    window.addEventListener("scroll", updateScrollShortcutVisibility, { passive: true })

    return () => {
      window.removeEventListener("resize", updateScrollShortcutVisibility)
      window.removeEventListener("scroll", updateScrollShortcutVisibility)
    }
  }, [updateScrollShortcutVisibility])

  const scrollToTop = React.useCallback(() => {
    window.scrollTo({ top: 0, left: 0, behavior: "auto" })
  }, [])

  const scrollToBottom = React.useCallback(() => {
    window.scrollTo({ top: document.documentElement.scrollHeight, left: 0, behavior: "auto" })
  }, [])

  const fetchObjects = React.useCallback(
    async (options?: { token?: string; resetPagination?: boolean; append?: boolean }) => {
      const token = options?.resetPagination ? undefined : options?.token
      const shouldAppend = Boolean(options?.append && token)
      const requestId = requestIdRef.current + 1
      requestIdRef.current = requestId
      const requestScope = activeScopeRef.current
      loadingRef.current = true
      setLoading(true)
      if (!shouldAppend) setListError(null)
      try {
        const response = await listObject(bucket, prefix || undefined, resolvedPageSize, token, {
          includeDeleted: showDeleted,
        })

        const r = response as {
          CommonPrefixes?: Array<{ Prefix?: string }>
          Contents?: Array<{ Key?: string; Size?: number; LastModified?: Date }>
          NextContinuationToken?: string
        }

        if (
          !shouldApplyObjectListResponse({
            requestId,
            activeRequestId: requestIdRef.current,
            requestScope,
            activeScope: activeScopeRef.current,
          })
        ) {
          return
        }

        setNextToken(r.NextContinuationToken)
        setListError(null)

        const prefixItems: ObjectRow[] = (r.CommonPrefixes ?? []).map((item) => ({
          Key: item.Prefix ?? "",
          type: "prefix" as const,
          Size: 0,
          LastModified: "",
        }))

        const objectItems: ObjectRow[] = (r.Contents ?? []).map((item) => ({
          Key: item.Key ?? "",
          type: "object" as const,
          Size: item.Size ?? 0,
          LastModified: normalizeDateToIso(item.LastModified),
        }))

        const rows = [...prefixItems, ...objectItems]
        if (shouldAppend) {
          setData((currentRows) => [...currentRows, ...rows])
        } else {
          setData(rows)
        }
      } catch (error) {
        console.error("Failed to fetch objects:", error)
        message.error((error as Error)?.message ?? t("Failed to load objects"))
        if (
          shouldApplyObjectListResponse({
            requestId,
            activeRequestId: requestIdRef.current,
            requestScope,
            activeScope: activeScopeRef.current,
          })
        ) {
          setNextToken(undefined)
          if (!shouldAppend) {
            setData([])
            setListError(isAccessDeniedError(error) ? "accessDenied" : "loadFailed")
          }
        }
      } finally {
        window.setTimeout(() => {
          if (
            shouldApplyObjectListResponse({
              requestId,
              activeRequestId: requestIdRef.current,
              requestScope,
              activeScope: activeScopeRef.current,
            })
          ) {
            loadingRef.current = false
            setLoading(false)
          }
        }, 200)
      }
    },
    [bucket, prefix, resolvedPageSize, showDeleted, listObject, message, t],
  )

  const resetAndFetchObjects = React.useCallback(() => {
    setNextToken(undefined)
    setData([])
    void fetchObjects({ resetPagination: true })
  }, [fetchObjects])

  const prevRefreshTriggerRef = React.useRef(refreshTrigger)

  React.useEffect(() => {
    const shouldResetPagination = shouldResetObjectListPagination(previousScopeRef.current, listScope)
    previousScopeRef.current = listScope
    const isRefresh = prevRefreshTriggerRef.current !== refreshTrigger
    prevRefreshTriggerRef.current = refreshTrigger

    if (isRefresh || shouldResetPagination) {
      resetAndFetchObjects()
    } else {
      void fetchObjects({ resetPagination: true })
    }
  }, [listScope, refreshTrigger, fetchObjects, resetAndFetchObjects])

  const prevDeleteTaskIdsRef = React.useRef<Set<string>>(new Set())

  React.useEffect(() => {
    const currentDeleteTasks = tasks.filter((t) => t.kind === "delete" || t.kind === "delete-folder")
    const completedDeleteTasks = currentDeleteTasks.filter((t) => t.status === "completed")

    const newlyCompleted = completedDeleteTasks.filter((t) => !prevDeleteTaskIdsRef.current.has(t.id))

    if (newlyCompleted.length > 0) {
      const anyActive = currentDeleteTasks.some((t) => ["pending", "running"].includes(t.status))
      if (!anyActive) {
        // No more active delete tasks, refresh the list
        resetAndFetchObjects()
      }
    }

    prevDeleteTaskIdsRef.current = new Set(completedDeleteTasks.map((t) => t.id))
  }, [tasks, resetAndFetchObjects])

  React.useEffect(() => {
    let cancelled = false

    const loadBucketVersioningStatus = async () => {
      setBucketVersioningState("unknown")

      try {
        const resp = await getBucketVersioning(bucket)
        if (!cancelled) {
          setBucketVersioningState(resolveBucketVersioningState((resp as { Status?: string })?.Status))
        }
      } catch (error) {
        console.error("Failed to load bucket versioning status:", error)
        if (!cancelled) {
          setBucketVersioningState("unknown")
        }
      }
    }

    void loadBucketVersioningStatus()

    return () => {
      cancelled = true
    }
  }, [bucket, getBucketVersioning])

  const displayKey = React.useCallback(
    (key: string) => {
      if (!prefix) return key
      return key.startsWith(prefix) ? key.slice(prefix.length) : key
    },
    [prefix],
  )

  const filteredData = React.useMemo(() => {
    const term = searchTerm.trim().toLowerCase()
    return data.filter((item) => {
      if (term) {
        const key = displayKey(item.Key).toLowerCase()
        return key.includes(term)
      }
      return item.Key !== prefix
    })
  }, [data, searchTerm, prefix, displayKey])
  const displayState = resolveObjectListDisplayState({
    searchTerm,
    filteredCount: filteredData.length,
    loadedCount: data.length,
    hasMore: Boolean(nextToken),
    loading,
  })
  const filteredEmptyState = displayState === "filtered-partial" || displayState === "filtered-empty"
  const emptyTitle = filteredEmptyState
    ? t(displayState === "filtered-partial" ? "No matches in loaded objects" : "No matching objects")
    : t("No Objects")
  const emptyDescription = filteredEmptyState
    ? t(
        displayState === "filtered-partial"
          ? "More objects have not been searched yet."
          : "No loaded objects match this filter.",
      )
    : t("Upload files or create folders to populate this bucket.")

  const downloadFile = React.useCallback(
    async (key: string) => {
      if (!key) return
      const loadingMsg = message.loading(t("Getting URL"), { duration: 0 })
      try {
        const url = await getSignedUrl(key)
        const response = await fetch(url)
        if (!response.ok) throw new Error(t("Download Failed"))
        const filename = key.split("/").pop() ?? ""
        const headers: Record<string, string> = {
          "content-type": getContentType(response.headers, filename),
          filename: response.headers.get("content-disposition")?.split("filename=")[1] ?? "",
        }
        const blob = await response.blob()
        exportFile({ headers, data: blob }, filename)
      } catch (err) {
        message.error((err as Error)?.message ?? t("Download Failed"))
      } finally {
        loadingMsg.destroy()
      }
    },
    [message, t, getSignedUrl],
  )

  const getRenameValidationMessage = React.useCallback(
    (validation: ObjectRenameValidation) => {
      switch (validation) {
        case "empty":
          return t("Object name is required")
        case "containsSlash":
          return t("Object name cannot contain slashes")
        case "sameName":
          return t("New object name must be different")
      }
    },
    [t],
  )

  const renameValidation = renameSourceKey ? validateObjectRename(renameSourceKey, renameName) : "empty"

  const openDeleteDialog = React.useCallback(
    (keys: string[]) => {
      if (bucketVersioningState === "unknown") {
        message.error(t("Failed to get data"))
        return
      }

      setDeleteDialogKeys(keys)
      setDeleteAllVersions(false)
      setDeleteDialogOpen(true)
    },
    [bucketVersioningState, message, t],
  )

  const columns: ColumnDef<ObjectRow>[] = React.useMemo(
    () => [
      {
        id: "object",
        header: () => t("Object"),
        accessorFn: (row) => displayKey(row.Key ?? ""),
        cell: ({ row }) => {
          const key = row.original.Key ?? ""
          const display = displayKey(key) || "/"

          if (row.original.type === "prefix") {
            return (
              <Link
                href={bucketPath(row.original.Key)}
                className="flex min-w-0 max-w-full items-center gap-2 text-primary hover:underline focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring/50"
              >
                <RiFolderLine className="size-4 shrink-0" aria-hidden />
                <span className="min-w-0 truncate">{display}</span>
              </Link>
            )
          }
          return (
            <button
              type="button"
              className="flex min-w-0 max-w-full items-center gap-2 text-start text-primary hover:underline focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring/50"
              onClick={() => onOpenInfo(bucket, row.original.Key)}
            >
              <RiFileLine className="size-4 shrink-0" aria-hidden />
              <span className="min-w-0 truncate">{display}</span>
            </button>
          )
        },
        filterFn: (row, _columnId, filterValue) => {
          if (!filterValue) return true
          const key = displayKey(row.original.Key ?? "").toLowerCase()
          return key.includes(String(filterValue).toLowerCase())
        },
      },
      {
        id: "size",
        header: () => t("Size"),
        accessorFn: (row) => (row.type === "object" ? row.Size : -1),
        cell: ({ row }) => (row.original.type === "object" ? formatBytes(row.original.Size) : "-"),
      },
      {
        id: "lastModified",
        header: () => t("Last Modified"),
        accessorFn: (row) => (row.type === "prefix" || !row.LastModified ? "" : new Date(row.LastModified).getTime()),
        cell: ({ row }) => (row.original.LastModified ? formatDateTime(row.original.LastModified) : "-"),
      },
      {
        id: "actions",
        header: () => t("Actions"),
        enableSorting: false,
        cell: ({ row }) => (
          <div className="flex items-center gap-2">
            {row.original.type === "object" ? (
              <>
                {canCapability("objects.preview", { bucket, objectKey: row.original.Key }) ? (
                  <Button
                    variant="outline"
                    size="sm"
                    onClick={() => {
                      onPreview({ key: row.original.Key })
                    }}
                  >
                    <RiEyeLine className="size-4" aria-hidden />
                    <span>{t("Preview")}</span>
                  </Button>
                ) : null}
                {canCapability("objects.download", { bucket, objectKey: row.original.Key }) ? (
                  <Button variant="outline" size="sm" onClick={() => downloadFile(row.original.Key)}>
                    <RiDownloadCloud2Line className="size-4" aria-hidden />
                    <span>{t("Download")}</span>
                  </Button>
                ) : null}
                {canCapability("objects.rename", { bucket, objectKey: row.original.Key, prefix }) ? (
                  <Button variant="outline" size="sm" onClick={() => openRenameDialog(row.original.Key)}>
                    <RiEdit2Line className="size-4" aria-hidden />
                    <span>{t("Rename")}</span>
                  </Button>
                ) : null}
              </>
            ) : null}
            {canCapability("objects.delete", { bucket, objectKey: row.original.Key }) ? (
              <Button
                variant="outline"
                size="sm"
                onClick={() => openDeleteDialog([row.original.Key])}
                disabled={bucketVersioningState === "unknown"}
              >
                <RiDeleteBin5Line className="size-4" aria-hidden />
                <span>{t("Delete")}</span>
              </Button>
            ) : null}
          </div>
        ),
      },
    ],
    [
      t,
      displayKey,
      bucketPath,
      onOpenInfo,
      bucket,
      downloadFile,
      canCapability,
      onPreview,
      prefix,
      bucketVersioningState,
      openDeleteDialog,
    ],
  )

  const { table, selectedRowIds } = useDataTable<ObjectRow>({
    data: filteredData,
    columns,
    getRowId: (row) => row.Key,
    enableRowSelection: true,
    manualPagination: true,
  })

  const checkedKeys = selectedRowIds

  const downloadMultiple = async () => {
    if (!checkedKeys.length) {
      message.warning(t("Please select at least one item"))
      return
    }

    const selectedRows = data.filter((item) => checkedKeys.includes(item.Key))
    if (!selectedRows.length) {
      message.warning(t("No files to download"))
      return
    }

    const loadingMsg = message.loading(t("Preparing download"), { duration: 0 })

    try {
      const payload = buildObjectZipDownloadPayload({
        bucket,
        basePrefix: prefix,
        rows: selectedRows,
      })
      const response = (await api.post("/object-zip-downloads", payload)) as CreateObjectZipDownloadResponse

      if (!response.download_url) {
        throw new Error(t("Download Failed"))
      }

      window.location.href = normalizeObjectZipDownloadUrl(response.download_url, api.resolveUrl("/"))
      table.resetRowSelection()
      message.success(t("Download ready"))
    } catch (err) {
      message.error((err as Error)?.message ?? t("Download Failed"))
    } finally {
      loadingMsg.destroy()
    }
  }

  const openRenameDialog = (key: string) => {
    setRenameSourceKey(key)
    setRenameName(getObjectBaseName(key))
    setRenameDialogOpen(true)
  }

  const handleRenameOpenChange = (open: boolean) => {
    if (renameSubmitting) return
    setRenameDialogOpen(open)
    if (!open) {
      setRenameSourceKey("")
      setRenameName("")
    }
  }

  const handleConfirmRename = async () => {
    if (!renameSourceKey) return

    const validation = validateObjectRename(renameSourceKey, renameName)
    if (validation) {
      message.warning(getRenameValidationMessage(validation))
      return
    }

    const targetKey = buildRenamedObjectKey(renameSourceKey, renameName)
    const loadingMsg = message.loading(t("Renaming object"), { duration: 0 })
    setRenameSubmitting(true)

    try {
      await renameObject(renameSourceKey, targetKey)
      message.success(t("Object renamed"))
      table.resetRowSelection()
      setRenameDialogOpen(false)
      setRenameSourceKey("")
      setRenameName("")
      resetAndFetchObjects()
    } catch (err) {
      message.error((err as Error)?.message ?? t("Rename Failed"))
    } finally {
      loadingMsg.destroy()
      setRenameSubmitting(false)
    }
  }

  const handleConfirmDelete = async () => {
    if (bucketVersioningState === "unknown") {
      message.error(t("Failed to get data"))
      return
    }

    const keys = [...deleteDialogKeys]
    setDeleteDialogOpen(false)
    if (!keys.length) return

    if (shouldForceDeleteObjects(bucketVersioningState, deleteAllVersions)) {
      await handleDeleteAllVersions(keys)
    } else {
      await handleDelete(keys)
    }
  }

  const handleDelete = async (keys: string[]) => {
    try {
      const rowMap = new Map(data.map((item) => [item.Key, item]))
      const objectKeys: string[] = []
      const folderPrefixes: string[] = []

      for (const key of keys) {
        const row = rowMap.get(key)
        if (row?.type === "prefix") {
          folderPrefixes.push(key)
        } else {
          objectKeys.push(key)
        }
      }

      if (objectKeys.length > 0) {
        addDeleteKeys(objectKeys, bucket, undefined)
      }
      for (const prefix of folderPrefixes) {
        addDeleteFolder(prefix, bucket)
      }

      message.success(t("Delete task created"))
      table.resetRowSelection()
    } catch (err) {
      message.error((err as Error)?.message ?? t("Delete Failed"))
    }
  }

  const handleDeleteAllVersions = async (keys: string[]) => {
    try {
      const rowMap = new Map(data.map((item) => [item.Key, item]))
      const objectKeys: string[] = []
      const folderPrefixes: string[] = []

      for (const key of keys) {
        const row = rowMap.get(key)
        if (row?.type === "prefix") {
          folderPrefixes.push(key)
        } else {
          objectKeys.push(key)
        }
      }

      if (objectKeys.length > 0) {
        addDeleteKeys(objectKeys, bucket, undefined, { forceDelete: true })
      }
      for (const prefix of folderPrefixes) {
        addDeleteFolder(prefix, bucket, { forceDelete: true })
      }

      message.success(t("Delete task created"))
      table.resetRowSelection()
    } catch (err) {
      message.error((err as Error)?.message ?? t("Delete Failed"))
    }
  }

  const handleBatchDelete = () => {
    if (!checkedKeys.length) {
      message.warning(t("Please select at least one item"))
      return
    }
    openDeleteDialog([...checkedKeys])
  }

  React.useEffect(() => {
    const col = table.getColumn("object")
    if (col) col.setFilterValue(searchTerm || undefined)
  }, [searchTerm, table])

  const loadNextBatch = React.useCallback(() => {
    if (!nextToken || loadingRef.current) return
    void fetchObjects({ token: nextToken, append: true })
  }, [fetchObjects, nextToken])

  React.useEffect(() => {
    const node = loadMoreRef.current
    if (!node || !nextToken || typeof IntersectionObserver === "undefined") return

    const observer = new IntersectionObserver(
      (entries) => {
        if (entries.some((entry) => entry.isIntersecting)) {
          loadNextBatch()
        }
      },
      { rootMargin: "320px 0px" },
    )

    observer.observe(node)

    return () => {
      observer.disconnect()
    }
  }, [loadNextBatch, nextToken])

  return (
    <div className="space-y-6">
      <PageHeader
        actions={
          <>
            <TaskStatsButton />
            {canUpload ? (
              <Button variant="outline" onClick={onUploadClick}>
                <RiFileAddLine className="size-4" aria-hidden />
                <span>
                  {t("Upload File")}/{t("Folder")}
                </span>
              </Button>
            ) : null}
            {checkedKeys.length > 0 ? (
              <>
                {canBulkDelete ? (
                  <Button
                    variant="outline"
                    className="border-destructive text-destructive"
                    onClick={handleBatchDelete}
                    disabled={bucketVersioningState === "unknown"}
                  >
                    <RiDeleteBin5Line className="size-4" aria-hidden />
                    <span>{t("Delete Selected")}</span>
                  </Button>
                ) : null}
                {canBulkDownload ? (
                  <Button variant="outline" onClick={downloadMultiple}>
                    <RiDownloadCloud2Line className="size-4" aria-hidden />
                    <span>{t("Download")}</span>
                  </Button>
                ) : null}
              </>
            ) : null}
            <Button variant="outline" onClick={() => (onRefresh ? onRefresh() : resetAndFetchObjects())}>
              <RiRefreshLine className="size-4" aria-hidden />
              <span>{t("Refresh")}</span>
            </Button>
          </>
        }
      >
        <div className="flex flex-wrap items-center gap-4 min-w-[40vw]">
          <SearchInput
            value={searchTerm}
            onChange={setSearchTerm}
            placeholder={t("Filter loaded objects")}
            clearable
            className="lg:max-w-sm"
          />
          <label
            htmlFor="object-list-show-deleted"
            className="flex items-center gap-2 text-sm text-muted-foreground cursor-pointer"
          >
            <Checkbox
              id="object-list-show-deleted"
              checked={showDeleted}
              onCheckedChange={(v) => setShowDeleted(v === true)}
            />
            <span>{t("Show Deleted Objects")}</span>
          </label>
        </div>
      </PageHeader>

      <DataTable
        table={table}
        isLoading={displayState === "loading" || displayState === "filtered-loading"}
        errorTitle={
          listError === "accessDenied"
            ? t("Access Denied")
            : listError === "loadFailed"
              ? t("Failed to load objects")
              : undefined
        }
        emptyTitle={emptyTitle}
        emptyDescription={emptyDescription}
      />

      {!listError ? (
        <div className="flex flex-wrap items-center justify-between gap-3 text-sm text-muted-foreground">
          <span>
            {t("Loaded {count} objects", {
              count: data.length,
            })}
          </span>
          <span>{t("Filtering and sorting apply to loaded objects")}</span>
        </div>
      ) : null}

      {nextToken ? (
        <div ref={loadMoreRef} className="flex min-h-10 items-center justify-center text-sm text-muted-foreground">
          {loading && data.length > 0 ? (
            <span className="inline-flex items-center gap-2">
              <Spinner className="size-4" />
              {t("Loading more objects")}
            </span>
          ) : (
            <span>{t("Scroll to load more objects")}</span>
          )}
        </div>
      ) : null}

      {showScrollShortcuts ? (
        <div className="fixed end-4 bottom-4 z-40 flex flex-col gap-2">
          <Button
            type="button"
            variant="outline"
            size="icon"
            className="bg-background/95 shadow-none backdrop-blur"
            onClick={scrollToTop}
            aria-label={t("Back to top")}
            title={t("Back to top")}
          >
            <RiArrowUpSLine className="size-5" aria-hidden />
          </Button>
          <Button
            type="button"
            variant="outline"
            size="icon"
            className="bg-background/95 shadow-none backdrop-blur"
            onClick={scrollToBottom}
            aria-label={t("Go to bottom")}
            title={t("Go to bottom")}
          >
            <RiArrowDownSLine className="size-5" aria-hidden />
          </Button>
        </div>
      ) : null}

      <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
        <AlertDialogContent className="sm:max-w-md">
          <AlertDialogHeader>
            <AlertDialogTitle>{t("Warning")}</AlertDialogTitle>
            <AlertDialogDescription>
              {t("Are you sure you want to delete the selected objects?")}
              {shouldShowDeleteAllVersions(bucketVersioningState) && (
                <label htmlFor="object-list-delete-all-versions" className="mt-4 flex items-center gap-2">
                  <Checkbox
                    id="object-list-delete-all-versions"
                    checked={deleteAllVersions}
                    onCheckedChange={(v) => setDeleteAllVersions(v === true)}
                  />
                  <span>{t("Delete All Versions")}</span>
                </label>
              )}
            </AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel
              render={
                <Button variant="outline" className="w-full sm:w-auto text-foreground">
                  {t("Cancel")}
                </Button>
              }
            />
            <AlertDialogAction
              render={
                <Button
                  variant="destructive"
                  className="w-full text-white sm:w-auto"
                  onClick={handleConfirmDelete}
                  disabled={bucketVersioningState === "unknown"}
                >
                  {t("Confirm")}
                </Button>
              }
            />
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>

      <Dialog open={renameDialogOpen} onOpenChange={handleRenameOpenChange} disablePointerDismissal>
        <DialogContent className="sm:max-w-md">
          <DialogHeader>
            <DialogTitle>{t("Rename Object")}</DialogTitle>
            <DialogDescription className="break-all">{renameSourceKey}</DialogDescription>
          </DialogHeader>
          <div className="space-y-2">
            <Input
              name="object-rename-name"
              value={renameName}
              onChange={(e) => setRenameName(e.target.value)}
              aria-label={t("Object Name")}
              autoComplete="off"
              spellCheck={false}
              autoFocus
            />
            {renameValidation ? (
              <p className="text-xs text-destructive">{getRenameValidationMessage(renameValidation)}</p>
            ) : null}
          </div>
          <DialogFooter>
            <Button variant="outline" disabled={renameSubmitting} onClick={() => handleRenameOpenChange(false)}>
              {t("Cancel")}
            </Button>
            <Button disabled={Boolean(renameValidation) || renameSubmitting} onClick={handleConfirmRename}>
              {t("Rename")}
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>
    </div>
  )
}
