import { useCallback, 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,
  Title,
} 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,
  localized,
  useClaimApplicationMutation,
  useGetAllApplicationsQuery,
  useGetAssignedToMeQuery,
  useGetLicenseTypesQuery,
  useGetQueueCountsQuery,
  useGetQueueQuery,
  useLazyExportApplicationsQuery,
  type LicenseApplication,
  type LicenseStatus,
  type QueueFilter,
} from "@ema-platform/api";
import {
  AdvancedTable,
  EmptyState,
  ErrorState,
  AmharicDatePicker,
  type AdvancedColumn,
} from "@ema-platform/ui";
import { LICENSE_PERMISSIONS, RequirePermission } from "@ema-platform/auth";
import {
  DEFAULT_VIEW,
  SAVED_VIEWS,
  filterFromSearchParams,
  readLastView,
  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 { licenseQueueActionsColumn } from "./actions";

const PAGE_SIZE = 10;
const SEARCH_DEBOUNCE_MS = 300;

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

/**
 * 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);

  const [view, setView] = useState<SavedViewId>(
    () => (searchParams.get("view") as SavedViewId) || readLastView(),
  );
  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);

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

  const { data: licenseTypes } = useGetLicenseTypesQuery();
  const { data: counts } = useGetQueueCountsQuery();

  // 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]);

  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 [claim, { isLoading: claiming }] = useClaimApplicationMutation();
  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);
  };

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

  async function handleClaim(id: string) {
    try {
      await claim(id).unwrap();
      notifications.show({
        color: "teal",
        title: t("queue.claimed", "Claimed"),
        message: t(
          "queue.claimedBody",
          "The application is now assigned to you.",
        ),
      });
      changeView("mine");
    } catch (err) {
      // A 409 means another officer got there first — refresh so the queue
      // stops showing work that is no longer available.
      notifications.show({
        color: "red",
        title: t("queue.claimFailed", "Could not claim"),
        message: extractErrorMessage(
          err,
          t("queue.claimRace", "Another officer already claimed it."),
        ),
      });
      active.refetch();
    }
  }

  async function handleBulkClaim() {
    const results = await Promise.allSettled(
      selected.map((id) => claim(id).unwrap()),
    );
    const claimed = results.filter((r) => r.status === "fulfilled").length;
    const lost = results.length - claimed;
    notifications.show({
      color: lost ? "yellow" : "teal",
      title: t("queue.bulkClaimed", {
        count: claimed,
        defaultValue: "{{count}} claimed",
      }),
      // Partial success is the normal case in a shared queue, so it is
      // reported rather than swallowed or treated as total failure.
      message: lost
        ? t("queue.bulkClaimPartial", {
            count: lost,
            defaultValue: "{{count}} were already taken by another officer.",
          })
        : "",
    });
    setSelected([]);
    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: () => {
      // Only unclaimed rows can be claimed; pressing c elsewhere is a no-op
      // rather than an error the officer has to read.
      if (cursorRow && cursorRow.assignedOfficerId === null)
        handleClaim(cursorRow.id);
    },
    onEscape: () => setSelected([]),
    onHelp: () => setHelpOpen(true),
  });

  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.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, {
        typeCode,
        items,
        selected,
        setSelected,
        allSelected,
        sortableHeader,
      }),
      licenseQueueActionsColumn(t, {
        claiming,
        onClaim: handleClaim,
        onOpen: (id) => navigate(`/licence-review/${id}`),
      }),
    ],
    [
      t,
      i18n.language,
      urlFilter.sortBy,
      sortIcon,
      selected,
      allSelected,
      items,
      claiming,
      typeCode,
    ],
  );

  return (
    <Container size="xl" py="md" pb={selected.length ? 80 : "md"}>
      <Group justify="space-between" mb="md">
        <div>
          <Title order={3}>{t("queue.title", "Licence applications")}</Title>
          {typeCode && (
            <Text size="sm" c="dimmed">
              {t(`nav.type${typeCode}`, { defaultValue: typeCode })}
            </Text>
          )}
        </div>
        <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>
      </Group>

      {/* Saved views, counted. */}
      <Tabs
        value={view}
        onChange={(v) => changeView((v as SavedViewId) ?? DEFAULT_VIEW)}
        mb="sm"
      >
        <Tabs.List>
          {SAVED_VIEWS.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={ALL_STATUSES.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", "Licence 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) => setFacet({ licenseTypeId: v ?? undefined })}
              clearable
              w={220}
            />
          )}
          <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={t("queue.title", "Licence applications")}
              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>
              <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>
      )}
    </Container>
  );
}

export default LicenseQueuePage;
