import { useCallback, useEffect, useMemo, useState } from "react";
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import {
  Badge,
  Button,
  Card,
  Container,
  Group,
  MultiSelect,
  Paper,
  SegmentedControl,
  Select,
  Skeleton,
  Stack,
  Kbd,
  Modal,
  Tabs,
  Text,
  TextInput,
} from "@mantine/core";
import { useDebouncedValue } from "@mantine/hooks";
import {
  IconAlertCircle,
  IconDownload,
  IconSearch,
  IconSortAscending,
  IconSortDescending,
  IconX,
} from "@tabler/icons-react";
import { notifications } from "@mantine/notifications";
import { useTranslation } from "react-i18next";
import {
  STATUS_LABELS,
  extractErrorMessage,
  familyLabels,
  localized,
  resolveFamilyKind,
  useAssignReviewerMutation,
  useGetAllApplicationsQuery,
  useGetAssignedToMeQuery,
  useGetLicenseTypesQuery,
  useGetQueueCountsQuery,
  useGetQueueQuery,
  useLazyExportApplicationsQuery,
  type ApplicationKind,
  type LicenseApplication,
  type LicenseStatus,
  type LicenseType,
  type QueueFilter,
} from "@ema-platform/api";
import {
  AdvancedTable,
  EmptyState,
  ErrorState,
  AmharicDatePicker,
  type AdvancedColumn,
} from "@ema-platform/ui";
import {
  DEFAULT_VIEW,
  SAVED_VIEWS,
  filterFromSearchParams,
  readLastView,
  hasUnclaimedPool,
  savedViewsForFamily,
  searchParamsFromFilter,
  writeLastView,
  type SavedViewId,
} from "../../queue-views";
import { exportApplicationsCsv } from "../../export";
import { setDensity } from "../../../../store/preferences.slice";
import { useAppDispatch, useAppSelector } from "../../../../store/hooks";
import { KEYBOARD_SHORTCUTS, useQueueKeyboard } from "../../useQueueKeyboard";
import { licenseQueueColumns } from "./columns";
import { AssignDialog } from "../../components/AssignDialog";
import { licenseQueueActionsColumn } from "./actions";
import { PageHeader } from '@ema-platform/ui';

const PAGE_SIZE = 10;
const SEARCH_DEBOUNCE_MS = 300;

/**
 * Statuses only the STANDARD course can reach. A registration goes straight
 * review → approval, so offering these in its facet would be offering filters
 * that can only ever match nothing.
 */
const STANDARD_ONLY_STATUSES: LicenseStatus[] = [
  "UNDER_EVALUATION",
  "INSPECTION_PENDING",
  "INSPECTION_COMPLETED",
];

/** Statuses only an examined cert (CoC, or a CoP with requiresExamination) reaches. */
const EXAM_ONLY_STATUSES: LicenseStatus[] = [
  "ELIGIBILITY_PAYMENT_PENDING",
  "ELIGIBILITY_PAID",
  "EXAM_PAYMENT_PENDING",
  "EXAM_PAID",
  "EXAM_SCHEDULED",
  "EXAM_PASSED",
  "EXAM_FAILED",
];

const ALL_STATUSES: LicenseStatus[] = [
  "SUBMITTED",
  "UNDER_REVIEW",
  "UNDER_EVALUATION",
  "RESUBMIT_REQUIRED",
  "INSPECTION_PENDING",
  "INSPECTION_COMPLETED",
  "ON_HOLD",
  "APPROVED",
  ...EXAM_ONLY_STATUSES,
  "PAYMENT_PENDING",
  "PAID",
  "PAYMENT_CONFIRMED",
  "SCHEDULED",
  "CERTIFICATE_ISSUED",
  "COMPLETED",
  "REJECTED",
];

/** Statuses an application of this type can actually occupy. */
function statusesFor(type: LicenseType | undefined): LicenseStatus[] {
  if (!type) return ALL_STATUSES;
  return ALL_STATUSES.filter((status) => {
    if (
      type.workflowProfile === "REGISTRATION" &&
      STANDARD_ONLY_STATUSES.includes(status)
    ) {
      return false;
    }
    if (status === "INSPECTION_PENDING" || status === "INSPECTION_COMPLETED") {
      return type.inspectionRequired;
    }
    if (EXAM_ONLY_STATUSES.includes(status)) return Boolean(type.requiresExamination);
    if (status === "CERTIFICATE_ISSUED") return type.issuesCertificate;
    return true;
  });
}

/**
 * The officer work pool.
 *
 * Saved views across the top, facets serialised into the URL so a filtered
 * queue can be shared, and server-side pagination — the previous version
 * rendered `data.items` unpaged, which was fine at demo volumes and would have
 * stopped being fine somewhere in the hundreds.
 */
export function LicenseQueuePage() {
  const { t, i18n } = useTranslation();
  const navigate = useNavigate();
  const { typeCode } = useParams();
  const [searchParams, setSearchParams] = useSearchParams();
  const dispatch = useAppDispatch();
  const density = useAppSelector((state) => state.preferences.density);

  // Type-pinned queues resolve a family straight from the URL, no query
  // needed — `resolveFamilyKind` falls back to LOGISTICS_LICENSE for unknown
  // keys and undefined for the mixed All/Mine grids, which is the safe
  // default (nothing hidden) in both cases.
  const isLogistics = typeCode
    ? resolveFamilyKind(typeCode) === "LOGISTICS_LICENSE"
    : undefined;
  // Claiming is a workflow property, not a label one — Vessel Registration is
  // a DOCUMENT family but is still triaged off a shared unclaimed pool, so it
  // keeps the Unassigned tab and the claim actions.
  const claimable = hasUnclaimedPool(typeCode);
  const visibleViews = savedViewsForFamily(claimable);

  const [view, setView] = useState<SavedViewId>(
    () =>
      (searchParams.get("view") as SavedViewId) ||
      (claimable ? readLastView() : "all"),
  );
  const [page, setPage] = useState(() => Number(searchParams.get("page")) || 1);
  const [pageSize, setPageSize] = useState(PAGE_SIZE);
  const [selected, setSelected] = useState<string[]>([]);
  const [searchInput, setSearchInput] = useState(searchParams.get("q") ?? "");
  const [cursor, setCursor] = useState(0);
  const [helpOpen, setHelpOpen] = useState(false);
  const [debouncedSearch] = useDebouncedValue(searchInput, SEARCH_DEBOUNCE_MS);

  // Queues with no unclaimed pool (see `hasUnclaimedPool`) have no unassigned
  // tab, so a stale "unassigned" view — e.g. restored from
  // `readLastView()` — must fall back to "all" rather than land on a tab that
  // no longer exists. Auto-created BTC requests specifically start at
  // PAYMENT_PENDING, outside "mine" too, so "all" is the one view guaranteed
  // to show them.
  useEffect(() => {
    if (!claimable && !searchParams.has("view") && view === "unassigned") {
      setView("all");
    }
  }, [claimable, searchParams, view]);

  const urlFilter = useMemo(
    () => filterFromSearchParams(searchParams),
    [searchParams],
  );
  const activeView = SAVED_VIEWS.find((v) => v.id === view) ?? SAVED_VIEWS[0];

  const { data: licenseTypes } = useGetLicenseTypesQuery();
  // A `/licence-review/type/:typeCode` deep link pins the type facet.
  const pinnedTypeId = useMemo(() => {
    if (!typeCode) return undefined;
    return licenseTypes?.items?.find((type) => type.key === typeCode)?.id;
  }, [typeCode, licenseTypes]);

  // Counts endpoint keys off `key`, the facet off `id` — resolve whichever the
  // route or the dropdown set, so the tab badges always count the same rows
  // the grid is showing rather than system-wide totals.
  const countsKey = useMemo(
    () =>
      typeCode ??
      licenseTypes?.items?.find((type) => type.id === urlFilter.licenseTypeId)
        ?.key,
    [typeCode, urlFilter.licenseTypeId, licenseTypes],
  );
  const { data: counts } = useGetQueueCountsQuery(countsKey);

  const selectedType = useMemo(() => {
    const typeId = pinnedTypeId ?? urlFilter.licenseTypeId;
    if (!typeId) return undefined;
    return licenseTypes?.items?.find((type) => type.id === typeId);
  }, [pinnedTypeId, urlFilter.licenseTypeId, licenseTypes]);

  // The facet offers what the chosen type can actually reach. With no type
  // chosen the queue spans every course, so the full list is correct.
  const statusOptions = useMemo(
    () => statusesFor(selectedType),
    [selectedType],
  );

  const filter: QueueFilter = useMemo(
    () => ({
      ...activeView.filter,
      ...urlFilter,
      search: debouncedSearch || undefined,
      licenseTypeId: pinnedTypeId ?? urlFilter.licenseTypeId,
      // No explicit sort in the URL or view → newest submissions first, so
      // the queue opens showing what most needs attention rather than
      // whatever order the backend happens to return. "Mine" sorts by
      // claimedAt instead — officers care when they picked it up, not when
      // it was originally submitted.
      sortBy:
        urlFilter.sortBy ??
        (activeView.id === "mine" ? "submittedAt" : "submittedAt"),
      sortDir: urlFilter.sortDir ?? "DESC",
      take: pageSize,
      skip: (page - 1) * pageSize,
    }),
    [activeView, urlFilter, debouncedSearch, pinnedTypeId, page, pageSize],
  );

  // One query per source; the two inactive ones are skipped, so switching
  // views costs a single request rather than keeping three in flight.
  const queueQuery = useGetQueueQuery(filter, {
    skip: activeView.source !== "queue",
  });
  const mineQuery = useGetAssignedToMeQuery(filter, {
    skip: activeView.source !== "mine",
  });
  const allQuery = useGetAllApplicationsQuery(filter, {
    skip: activeView.source !== "all",
  });
  const active =
    activeView.source === "queue"
      ? queueQuery
      : activeView.source === "mine"
        ? mineQuery
        : allQuery;

  const [assignReviewer, { isLoading: assigning }] = useAssignReviewerMutation();
  const [assignTarget, setAssignTarget] = useState<LicenseApplication | null>(null);
  const [runExport, { isFetching: exporting }] =
    useLazyExportApplicationsQuery();

  /**
   * Exports every row the filter matches, not just the page on screen.
   * The server caps the result set and reports when it did, so a truncated
   * export says so instead of quietly being wrong.
   */
  async function handleExport() {
    try {
      const result = await runExport({
        ...filter,
        take: undefined,
        skip: undefined,
      }).unwrap();
      exportApplicationsCsv(result.items, i18n.language);
      if (result.truncated) {
        notifications.show({
          color: "yellow",
          title: t("queue.exportTruncated", "Export truncated"),
          message: t("queue.exportTruncatedBody", {
            exported: result.items.length,
            total: result.total,
            defaultValue:
              "Exported the first {{exported}} of {{total}} rows. Narrow the filter for the rest.",
          }),
        });
      }
    } catch (err) {
      notifications.show({
        color: "red",
        title: t("queue.exportFailed", "Export failed"),
        message: extractErrorMessage(err),
      });
    }
  }

  const items = active.data?.items ?? [];
  const total = active.data?.total ?? 0;

  const updateUrl = useCallback(
    (next: Partial<QueueFilter>, nextView: SavedViewId, nextPage: number) => {
      setSearchParams(
        searchParamsFromFilter({ ...urlFilter, ...next }, nextView, nextPage),
        { replace: true },
      );
    },
    [urlFilter, setSearchParams],
  );

  const changeView = (next: SavedViewId) => {
    setView(next);
    writeLastView(next);
    setPage(1);
    setSelected([]);
    updateUrl({}, next, 1);
  };

  const setFacet = (next: Partial<QueueFilter>) => {
    setPage(1);
    updateUrl(next, view, 1);
  };

  /**
   * Switching type drops any selected status the new type cannot reach —
   * otherwise the facet keeps an invisible filter that matches nothing and the
   * grid looks empty for no reason the officer can see.
   */
  const changeType = (typeId: string | undefined) => {
    const allowed = statusesFor(
      licenseTypes?.items?.find((type) => type.id === typeId),
    );
    setFacet({
      licenseTypeId: typeId,
      status: urlFilter.status?.filter((s) => allowed.includes(s)),
    });
  };

  const toggleSort = (field: NonNullable<QueueFilter["sortBy"]>) => {
    const dir =
      urlFilter.sortBy === field && urlFilter.sortDir !== "DESC"
        ? "DESC"
        : "ASC";
    setFacet({ sortBy: field, sortDir: dir });
  };

  /**
   * The team leader dispatching one application.
   *
   * Replaces the old self-service claim: nothing is picked up any more, so the
   * queue's primary action is handing a file to an employee. The dialog holds
   * the choice; this only sends it.
   */
  async function handleAssign(officerId: string, remark?: string) {
    if (!assignTarget) return;
    try {
      await assignReviewer({ id: assignTarget.id, officerId, remark }).unwrap();
      notifications.show({
        color: "teal",
        title: t("queue.assigned", "Assigned"),
        message: t(
          "queue.assignedBody",
          "The employee has been notified and the review has started.",
        ),
      });
      setAssignTarget(null);
      active.refetch();
    } catch (err) {
      notifications.show({
        color: "red",
        title: t("queue.assignFailed", "Could not assign"),
        message: extractErrorMessage(
          err,
          t("queue.assignError", "The application could not be assigned."),
        ),
      });
      active.refetch();
    }
  }

  const cursorRow = items[cursor];
  useQueueKeyboard({
    enabled: !helpOpen,
    onNext: () =>
      setCursor((c) => Math.min(c + 1, Math.max(items.length - 1, 0))),
    onPrevious: () => setCursor((c) => Math.max(c - 1, 0)),
    onOpen: () => cursorRow && navigate(`/licence-review/${cursorRow.id}`),
    onClaim: () => {
      // "c" now opens the assign dialog on an undispatched row. Kept on the
      // same key: it is still "do the queue's primary action to this row",
      // and rebinding a shortcut officers have in their fingers costs more
      // than the name mismatch.
      if (isLogistics !== false && cursorRow && cursorRow.assignedOfficerId === null)
        setAssignTarget(cursorRow);
      // Only unclaimed rows on a claimable queue can be claimed; pressing c
      // elsewhere is a no-op rather than an error the officer has to read.
      if (claimable && cursorRow && cursorRow.assignedOfficerId === null)
        handleClaim(cursorRow.id);
    },
    onEscape: () => setSelected([]),
    onHelp: () => setHelpOpen(true),
  });

  // Deep-linked by type (`/licence-review/type/:typeCode`), so the queue
  // title/labels read "Certificate applications" for a CoC queue and
  // "Document applications" for a Seaman Book queue rather than always
  // "Licence applications" — the All/Mine views have no single type and stay
  // on the licence-flavoured default, matching today's behaviour.
  const queueLabels = familyLabels(resolveFamilyKind(typeCode));
  const queueTitle = typeCode
    ? t("queue.titleByFamily", {
        family: queueLabels.typeLabel,
        defaultValue: `${queueLabels.typeLabel} applications`,
      })
    : t("queue.title", "Licence applications");

  const allSelected = items.length > 0 && selected.length === items.length;
  const sortIcon =
    urlFilter.sortDir === "DESC" ? (
      <IconSortDescending size={13} />
    ) : (
      <IconSortAscending size={13} />
    );

  const hasFacets = Boolean(
    urlFilter.status?.length ||
    urlFilter.licenseTypeId ||
    urlFilter.kind ||
    urlFilter.assignee ||
    urlFilter.submittedFrom ||
    debouncedSearch,
  );

  const sortableHeader = (
    label: string,
    field: NonNullable<QueueFilter["sortBy"]>,
  ) => (
    <Group
      gap={4}
      wrap="nowrap"
      style={{ cursor: "pointer" }}
      onClick={() => toggleSort(field)}
    >
      <span>{label}</span>
      {urlFilter.sortBy === field && sortIcon}
    </Group>
  );

  const columns: AdvancedColumn<LicenseApplication>[] = useMemo(
    () => [
      ...licenseQueueColumns(t, i18n.language, {
        items,
        selected,
        setSelected,
        allSelected,
        sortableHeader,
        isLogistics,
      }),
      licenseQueueActionsColumn(t, {
        assigning,
        onAssign: setAssignTarget,
        onOpen: (id) => navigate(`/licence-review/${id}`),
        // Non-logistics applications aren't dispatched off a shared queue (see
        // `savedViewsForFamily`) — every row opens straight to Review.
        assignable: isLogistics !== false,
        // Applications with no unclaimed pool (see `hasUnclaimedPool`) are
        // never claimed — every row opens straight to Review.
        claimable,
      }),
    ],
    [
      t,
      i18n.language,
      urlFilter.sortBy,
      sortIcon,
      selected,
      allSelected,
      items,
      assigning,
      isLogistics,
      claimable,
    ],
  );

  return (
    <Container size="xl" py="md" pb={selected.length ? 80 : "md"}>
      <PageHeader
        title={queueTitle}
        subtitle={typeCode ? t(`nav.type${typeCode}`, { defaultValue: typeCode }) : undefined}
        action={
          <Group gap="xs">
          <SegmentedControl
            size="xs"
            value={density}
            onChange={(v) =>
              dispatch(setDensity(v as "comfortable" | "compact"))
            }
            data={[
              {
                label: t("queue.comfortable", "Comfortable"),
                value: "comfortable",
              },
              { label: t("queue.compact", "Compact"), value: "compact" },
            ]}
          />
          <Button
            variant="default"
            leftSection={<IconDownload size={16} />}
            onClick={handleExport}
            loading={exporting}
            disabled={total === 0}
          >
            {t("queue.export", "Export CSV")}
          </Button>
          </Group>
        }
      />

      {/* Saved views, counted. */}
      <Tabs
        value={view}
        onChange={(v) => changeView((v as SavedViewId) ?? DEFAULT_VIEW)}
        mb="sm"
      >
        <Tabs.List>
          {visibleViews.map((savedView) => (
            <Tabs.Tab
              key={savedView.id}
              value={savedView.id}
              rightSection={
                counts?.[savedView.countKey] ? (
                  <Badge size="xs" variant="light">
                    {counts[savedView.countKey]}
                  </Badge>
                ) : undefined
              }
            >
              {t(savedView.labelKey)}
            </Tabs.Tab>
          ))}
        </Tabs.List>
      </Tabs>

      {/* Facets — every one of these is reflected in the URL. */}
      <Paper withBorder p="sm" mb="sm">
        <Group gap="sm" align="flex-end" wrap="wrap">
          <TextInput
            label={t("queue.search", "Search")}
            placeholder={t("queue.searchPlaceholder", "Company, TIN or number")}
            leftSection={<IconSearch size={14} />}
            value={searchInput}
            onChange={(e) => setSearchInput(e.currentTarget.value)}
            w={240}
          />
          <MultiSelect
            label={t("queue.status", "Status")}
            placeholder={t("queue.anyStatus", "Any")}
            data={statusOptions.map((s) => ({
              value: s,
              label: t(`queue.statusValues.${s}`, STATUS_LABELS[s]),
            }))}
            value={urlFilter.status ?? []}
            onChange={(v) => setFacet({ status: v as LicenseStatus[] })}
            clearable
            w={240}
          />
          {!typeCode && (
            <Select
              label={t("queue.type", "Type")}
              placeholder={t("queue.anyType", "Any")}
              data={(licenseTypes?.items ?? []).map((type) => ({
                value: type.id,
                label: localized(type.name, i18n.language) || type.key,
              }))}
              value={urlFilter.licenseTypeId ?? null}
              onChange={(v) => changeType(v ?? undefined)}
              clearable
              w={220}
            />
          )}
          <Select
            label={t("queue.kind", "Application kind")}
            placeholder={t("queue.anyType", "Any")}
            data={[
              { value: "NEW", label: t("queue.kindValues.NEW", "New") },
              { value: "RENEWAL", label: t("queue.kindValues.RENEWAL", "Renewal") },
              { value: "REISSUE", label: t("queue.kindValues.REISSUE", "Replacement") },
            ]}
            value={urlFilter.kind ?? null}
            onChange={(v) => setFacet({ kind: (v as ApplicationKind) ?? undefined })}
            clearable
            w={180}
          />
          <AmharicDatePicker
            label={t("queue.submittedFrom", "Submitted from")}
            value={urlFilter.submittedFrom ?? ""}
            onChange={(v) => setFacet({ submittedFrom: v || undefined })}
            dateFormat="date"
            w={170}
          />
          <AmharicDatePicker
            label={t("queue.submittedTo", "Submitted to")}
            value={urlFilter.submittedTo ?? ""}
            onChange={(v) => setFacet({ submittedTo: v || undefined })}
            dateFormat="date"
            w={170}
          />
          {hasFacets && (
            <Button
              variant="subtle"
              leftSection={<IconX size={14} />}
              onClick={() => {
                setSearchInput("");
                setSearchParams(new URLSearchParams(), { replace: true });
              }}
            >
              {t("queue.clearFilters", "Clear")}
            </Button>
          )}
        </Group>
      </Paper>

      <Card withBorder padding={0}>
        {active.isLoading ? (
          // Skeleton rows match the real table, so the layout does not jump
          // when data lands.
          <Stack gap={0} p="md">
            {Array.from({ length: 6 }).map((_, i) => (
              <Skeleton key={i} height={44} mb="xs" radius="sm" />
            ))}
          </Stack>
        ) : active.isError ? (
          <ErrorState
            title={t("queue.errorTitle", "Could not load the queue")}
            description={extractErrorMessage(active.error)}
            onRetry={() => active.refetch()}
            icon={IconAlertCircle}
          />
        ) : items.length === 0 ? (
          <EmptyState
            title={
              hasFacets
                ? t(
                    "queue.emptyFiltered",
                    "No applications match these filters",
                  )
                : t("queue.empty", "Nothing waiting here")
            }
            description={
              hasFacets
                ? t(
                    "queue.emptyFilteredBody",
                    "Try widening or clearing the filters.",
                  )
                : t(
                    "queue.emptyBody",
                    "New applications will appear here as they are submitted.",
                  )
            }
            action={
              hasFacets
                ? {
                    label: t("queue.clearFilters", "Clear"),
                    onClick: () =>
                      setSearchParams(new URLSearchParams(), { replace: true }),
                  }
                : undefined
            }
          />
        ) : (
          <>
            <Group justify="flex-end" p="sm" pb={0}>
              <Text size="sm" c="dimmed">
                {t("queue.showing", {
                  from: (page - 1) * pageSize + 1,
                  to: Math.min(page * pageSize, total),
                  total,
                  defaultValue: "Showing {{from}}–{{to}} of {{total}}",
                })}
              </Text>
            </Group>
            <AdvancedTable
              columns={columns}
              data={items}
              tableName={queueTitle}
              itemCount={total}
              pageIndex={page - 1}
              onPageChange={(pageIndex) => {
                const next = pageIndex + 1;
                setPage(next);
                updateUrl({}, view, next);
              }}
              pageSize={pageSize}
              onPageSizeChange={(next) => {
                setPageSize(next);
                setPage(1);
                updateUrl({}, view, 1);
              }}
              refresh={() => active.refetch()}
              isLoading={active.isFetching}
              verticalSpacing={density === "compact" ? 4 : "sm"}
              rowStyle={(_row, index) =>
                // Keyboard cursor. A left border rather than a background keeps
                // it distinguishable from row selection and from hover.
                index === cursor
                  ? { boxShadow: "inset 3px 0 0 var(--mantine-color-blue-6)" }
                  : undefined
              }
            />
          </>
        )}
      </Card>

      <Modal
        opened={helpOpen}
        onClose={() => setHelpOpen(false)}
        title={t("shortcuts.title", "Keyboard shortcuts")}
        size="sm"
      >
        <Stack gap="xs">
          {KEYBOARD_SHORTCUTS.map((shortcut) => (
            <Group key={shortcut.keys} justify="space-between">
              <Text size="sm">{t(shortcut.labelKey)}</Text>
              <Kbd>{shortcut.keys}</Kbd>
            </Group>
          ))}
        </Stack>
      </Modal>

      {/* Bulk bar. Floating, with the count stated so the scope of the action
          is never ambiguous. */}
      {selected.length > 0 && (
        <Paper
          withBorder
          shadow="md"
          p="sm"
          style={{ position: "sticky", bottom: 16, zIndex: 50 }}
        >
          <Group justify="space-between">
            <Text size="sm" fw={500}>
              {t("queue.selectedCount", {
                count: selected.length,
                defaultValue: "{{count}} selected",
              })}
            </Text>
            <Group gap="xs">
              <Button variant="subtle" onClick={() => setSelected([])}>
                {t("common.cancel", "Cancel")}
              </Button>
              <Button
                variant="default"
                leftSection={<IconDownload size={16} />}
                onClick={() =>
                  exportApplicationsCsv(
                    items.filter((a) => selected.includes(a.id)),
                    i18n.language,
                  )
                }
              >
                {t("queue.export", "Export CSV")}
              </Button>
              {claimable && (
                <RequirePermission
                  anyOf={[LICENSE_PERMISSIONS.CLAIM_APPLICATION]}
                  hideOnly
                >
                  <Button loading={claiming} onClick={handleBulkClaim}>
                    {t("queue.bulkClaim", {
                      count: selected.length,
                      defaultValue: "Claim {{count}}",
                    })}
                  </Button>
                </RequirePermission>
              )}
            </Group>
          </Group>
        </Paper>
      )}
      <AssignDialog
        opened={assignTarget !== null}
        onClose={() => setAssignTarget(null)}
        kind="review"
        applicationNumber={assignTarget?.applicationNumber}
        loading={assigning}
        onConfirm={handleAssign}
      />
    </Container>
  );
}

export default LicenseQueuePage;
