import { useMemo, useState } from 'react';
import {
  ActionIcon,
  Alert,
  Badge,
  Button,
  Group,
  Modal,
  Paper,
  Select,
  Stack,
  Text,
  TextInput,
  Title,
  Tooltip,
} from '@mantine/core';
import { useDebouncedValue } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash, IconX } from '@tabler/icons-react';
import { useTranslation } from 'react-i18next';
import {
  AdvancedTable,
  ModalFooter,
  useServerTable,
  type AdvancedColumn,
} from '@ema-platform/ui';
import {
  useCreateDocumentRequirementMutation,
  useDeleteDocumentRequirementMutation,
  useGetLicenseTypesQuery,
  useGetPersonalDocumentsQuery,
  useLocalized,
  useUpdateDocumentRequirementMutation,
  type DocumentRequirement,
  type PersonalDocumentGroup as PersonalDocumentGroupDto,
} from '@ema-platform/api';
import { useRequirementActions } from '../hooks/useRequirementActions';
import {
  DocumentRequirementEditorDrawer,
  type PersonalScope,
} from './DocumentRequirementEditorDrawer';

type DraftRequirement = Omit<DocumentRequirement, 'id' | 'licenseTypeId' | 'isActive'>;

/** Filter value for "the documents every licence asks for". */
const GLOBAL_ONLY = 'GLOBAL';

const SEARCH_DEBOUNCE_MS = 300;

/**
 * Badge colours for licence types.
 *
 * Red is left out: it reads as a problem, and a licence type is not one.
 * Everything else the theme offers is in, because the point of the colour is
 * telling two licence types apart at a glance.
 */
const SCOPE_COLORS = [
  'blue',
  'grape',
  'teal',
  'orange',
  'violet',
  'cyan',
  'pink',
  'lime',
  'indigo',
  'green',
  'yellow',
  'gray',
];

/** Doubles the palette: the same hue, a visibly different badge. */
const SCOPE_VARIANTS = ['light', 'outline'] as const;

/**
 * A colour per licence type, assigned by position in the catalogue.
 *
 * Hashing the id looked tidier and was wrong: eight buckets over sixteen
 * licence types collide by the pigeonhole principle, so Vessel Registration
 * and Freight Forwarder came out the same colour and the badge stopped
 * carrying information. Walking the sorted catalogue instead gives every type
 * a distinct colour until the palette runs out, and only then repeats a hue in
 * the other variant — 24 distinct badges before any two can look alike.
 *
 * Sorted by `sortOrder` so the assignment is the same for every officer and
 * survives a refresh; a type added later takes the next free style rather than
 * reshuffling the ones already learned.
 */
function buildScopeStyles(
  types: { id: string; sortOrder: number }[],
): Map<string, { color: string; variant: (typeof SCOPE_VARIANTS)[number] }> {
  const styles = new Map<
    string,
    { color: string; variant: (typeof SCOPE_VARIANTS)[number] }
  >();
  types
    .slice()
    .sort((a, b) => a.sortOrder - b.sortOrder)
    .forEach((type, index) => {
      styles.set(type.id, {
        color: SCOPE_COLORS[index % SCOPE_COLORS.length],
        variant:
          SCOPE_VARIANTS[
            Math.floor(index / SCOPE_COLORS.length) % SCOPE_VARIANTS.length
          ],
      });
    });
  return styles;
}

/** `application/vnd.openxmlformats-…-document` is nobody's idea of a column. */
const MIME_LABELS: Record<string, string> = {
  'application/pdf': 'PDF',
  'application/msword': 'DOC',
  'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'DOCX',
  'application/vnd.ms-excel': 'XLS',
  'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'XLSX',
};

function shortMime(mime: string): string {
  return MIME_LABELS[mime] ?? mime.split('/')[1]?.toUpperCase() ?? mime;
}

/**
 * A document as the table renders it: what the server sent, plus the row id
 * `AdvancedTable` keys on and the scope read off its rows.
 *
 * The grouping itself belongs to the server — a page of rows would split a
 * document configured for three licence types across two pages and misreport
 * the scope of both halves.
 */
interface PersonalDocumentGroup extends PersonalDocumentGroupDto {
  /** The key doubles as the row id; one group is one document. */
  id: string;
  /** Empty when the document applies to every licence. */
  scope: PersonalScope;
}

/**
 * Documents an applicant keeps in their own vault.
 *
 * Same `document_requirements` table as a licence type's upload slots, flagged
 * `isPersonal`: these are not asked for on an application form but held once,
 * under My Documents in the portal. A document can apply to every licence or
 * only to the modes of operation an applicant has declared — a sea service
 * book is worth asking a seafarer for and pointless for a freight forwarder.
 */
export function PersonalDocumentsCard() {
  const { t, i18n } = useTranslation();
  const localized = useLocalized();
  const run = useRequirementActions();

  const { data: licenseTypes } = useGetLicenseTypesQuery();
  const [createRequirement, { isLoading: creating }] = useCreateDocumentRequirementMutation();
  const [updateRequirement, { isLoading: updating }] = useUpdateDocumentRequirementMutation();
  const [deleteRequirement] = useDeleteDocumentRequirementMutation();

  const [editing, setEditing] = useState<{ group: PersonalDocumentGroup | null } | null>(null);
  const [deleteTarget, setDeleteTarget] = useState<PersonalDocumentGroup | null>(null);
  /** null = any licence, GLOBAL_ONLY = the all-licence ones, else a type id. */
  const [licenseTypeFilter, setLicenseTypeFilter] = useState<string | null>(null);

  const { pageIndex, setPageIndex, pageSize, setPageSize } = useServerTable({
    pageSize: 10,
  });
  const [searchInput, setSearchInput] = useState('');
  // Typing must not fire a request per keystroke; same 300ms as the queue.
  const [search] = useDebouncedValue(searchInput, SEARCH_DEBOUNCE_MS);

  // Every facet goes to the server: it filters and searches in SQL, groups the
  // rows into documents, then pages the documents.
  const { data, isFetching, refetch } = useGetPersonalDocumentsQuery({
    search: search.trim() || undefined,
    licenseTypeId:
      licenseTypeFilter && licenseTypeFilter !== GLOBAL_ONLY
        ? licenseTypeFilter
        : undefined,
    globalOnly: licenseTypeFilter === GLOBAL_ONLY,
    take: pageSize,
    skip: pageIndex * pageSize,
    locale: i18n.language === 'am' ? 'am' : 'en',
  });

  const groups = useMemo<PersonalDocumentGroup[]>(
    () =>
      (data?.items ?? []).map((group) => ({
        ...group,
        id: group.key,
        // A single row with no licence type means "every licence"; the two
        // never coexist, because the editor writes one shape or the other.
        scope: group.rows
          .map((r) => r.licenseTypeId)
          .filter((id): id is string => id !== null),
      })),
    [data],
  );

  /** Filters are the server's business now; an empty page is its answer. */
  const isFiltered = search.trim() !== '' || licenseTypeFilter !== null;

  function clearFilters() {
    setSearchInput('');
    setLicenseTypeFilter(null);
    setPageIndex(0);
  }

  const scopeStyles = useMemo(
    () => buildScopeStyles(licenseTypes?.items ?? []),
    [licenseTypes],
  );

  const typeName = (id: string) => {
    const found = (licenseTypes?.items ?? []).find((lt) => lt.id === id);
    return found ? localized(found.name) || found.key : id;
  };

  const columns = useMemo<AdvancedColumn<PersonalDocumentGroup>[]>(
    () => [
      {
        header: t('certReq.personal.columns.document', 'Document'),
        label: t('certReq.personal.columns.document', 'Document'),
        cell: ({ row }) => (
          <div>
            <Text fz="sm" fw={600}>
              {localized(row.original.rows[0].name) || row.original.key}
            </Text>
            <Text fz="xs" c="dimmed">
              {row.original.key}
            </Text>
          </div>
        ),
      },
      {
        header: t('certReq.doc.scope', 'Applies to'),
        label: t('certReq.doc.scope', 'Applies to'),
        cell: ({ row }) =>
          row.original.scope.length === 0 ? (
            // Filled, where a licence type is outlined: "every licence" is a
            // different kind of answer, not one more item in the same list.
            <Badge size="sm" variant="filled" color="gray">
              {t('certReq.doc.scopeAll', 'All licences')}
            </Badge>
          ) : (
            <Group gap={4}>
              {row.original.scope.map((id) => {
                // A type the catalogue no longer lists still needs a badge.
                const style = scopeStyles.get(id) ?? { color: 'gray', variant: 'light' };
                return (
                  <Badge key={id} size="sm" variant={style.variant} color={style.color}>
                    {typeName(id)}
                  </Badge>
                );
              })}
            </Group>
          ),
      },
      {
        header: t('certReq.doc.maxFiles', 'Files accepted'),
        label: t('certReq.doc.maxFiles', 'Files accepted'),
        align: 'center',
        cell: ({ row }) =>
          row.original.rows[0].maxFiles === null
            ? t('certReq.doc.maxFilesUnlimited', 'No limit')
            : row.original.rows[0].maxFiles,
      },
      {
        header: t('certReq.doc.allowedTypes', 'Allowed file types'),
        label: t('certReq.doc.allowedTypes', 'Allowed file types'),
        cell: ({ row }) => {
          const types = row.original.rows[0].allowedMimeTypes ?? [];
          return (
            // Twenty-odd mime types would own the row; the full list is one
            // hover away instead.
            <Tooltip label={types.join(', ')} multiline w={280} disabled={types.length <= 3}>
              <Text fz="xs">
                {types.slice(0, 3).map(shortMime).join(', ')}
                {types.length > 3
                  ? t('certReq.personal.moreTypes', ' +{{count}} more', {
                      count: types.length - 3,
                    })
                  : ''}
              </Text>
            </Tooltip>
          );
        },
      },
      {
        header: t('certReq.doc.maxSize', 'Max file size (MB)'),
        label: t('certReq.doc.maxSize', 'Max file size (MB)'),
        align: 'center',
        cell: ({ row }) => `${row.original.rows[0].maxSizeMb} MB`,
      },
      {
        header: '',
        label: t('certReq.personal.columns.actions', 'Actions'),
        align: 'right',
        cell: ({ row }) => (
          <Group gap={4} justify="flex-end" wrap="nowrap">
            <ActionIcon
              variant="subtle"
              color="blue"
              onClick={() => setEditing({ group: row.original })}
            >
              <IconEdit size={14} />
            </ActionIcon>
            <ActionIcon
              variant="subtle"
              color="red"
              onClick={() => setDeleteTarget(row.original)}
            >
              <IconTrash size={14} />
            </ActionIcon>
          </Group>
        ),
      },
    ],
    // `typeName` and `scopeStyles` both close over the licence-type list.
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [t, localized, licenseTypes, scopeStyles],
  );

  /**
   * Saves the group as the set of rows it now means.
   *
   * The scope is edited as a whole, so the diff is the honest way to apply it:
   * rows for licence types that were added get created, rows for types that
   * were dropped get deleted, and everything still in scope is updated. A
   * document moved to "all licences" collapses to a single row with none.
   */
  async function handleSave(draft: DraftRequirement, scope: PersonalScope) {
    const existing = editing?.group?.rows ?? [];
    // `null` is a licence type here too — the one meaning "every licence".
    const wanted: (string | null)[] = scope.length ? scope : [null];

    const ok = await run(async () => {
      const stale = existing.filter((row) => !wanted.includes(row.licenseTypeId));
      const kept = existing.filter((row) => wanted.includes(row.licenseTypeId));
      const added = wanted.filter(
        (id) => !existing.some((row) => row.licenseTypeId === id),
      );

      await Promise.all([
        ...kept.map((row) => updateRequirement({ id: row.id, ...draft }).unwrap()),
        ...added.map((licenseTypeId) =>
          createRequirement({ ...draft, licenseTypeId }).unwrap(),
        ),
        ...stale.map((row) => deleteRequirement(row.id).unwrap()),
      ]);
    }, editing?.group
      ? t('certReq.doc.updated', 'Document requirement updated')
      : t('certReq.doc.created', 'Document requirement added'));

    if (ok) setEditing(null);
  }

  async function confirmDelete() {
    if (!deleteTarget) return;
    const ok = await run(
      () => Promise.all(deleteTarget.rows.map((row) => deleteRequirement(row.id).unwrap())),
      t('certReq.doc.deleted', 'Document requirement removed'),
    );
    if (ok) setDeleteTarget(null);
  }

  return (
    <Stack gap="sm">
      <Group justify="space-between" align="flex-start" wrap="nowrap">
        <div>
          <Title order={5}>{t('certReq.personal.title', 'Personal documents')}</Title>
          <Text fz="sm" c="dimmed">
            {t(
              'certReq.personal.subtitle',
              'Identity and education documents an applicant keeps once, in My Documents, instead of uploading with every application. Scope one to licence types and only applicants who declared those modes of operation are asked for it.',
            )}
          </Text>
        </div>
        <Button
          size="xs"
          variant="light"
          leftSection={<IconPlus size={13} />}
          onClick={() => setEditing({ group: null })}
          style={{ flexShrink: 0 }}
        >
          {t('certReq.personal.add', 'Add personal document')}
        </Button>
      </Group>

      {/* Facets, in the shape the licence-review queue uses. No date range:
          a configuration row has no submission date to filter on. */}
      <Paper withBorder p="sm">
        <Group gap="sm" align="flex-end" wrap="wrap">
          <TextInput
            label={t('certReq.personal.search', 'Search')}
            placeholder={t('certReq.personal.searchPlaceholder', 'Search by name or key')}
            leftSection={<IconSearch size={14} />}
            value={searchInput}
            onChange={(e) => {
              const { value } = e.currentTarget;
              setSearchInput(value);
              setPageIndex(0);
            }}
            w={240}
          />
          <Select
            label={t('certReq.doc.scope', 'Applies to')}
            placeholder={t('certReq.personal.filterAny', 'Any licence type')}
            data={[
              {
                value: GLOBAL_ONLY,
                label: t('certReq.personal.filterGlobal', 'All-licence documents only'),
              },
              ...(licenseTypes?.items ?? [])
                .slice()
                .sort((a, b) => a.sortOrder - b.sortOrder)
                .map((lt) => ({ value: lt.id, label: localized(lt.name) || lt.key })),
            ]}
            value={licenseTypeFilter}
            onChange={(value) => {
              setLicenseTypeFilter(value);
              // A narrower list can be shorter than the page you were on.
              setPageIndex(0);
            }}
            searchable
            clearable
            w={240}
          />
          {isFiltered && (
            <Button
              variant="subtle"
              leftSection={<IconX size={14} />}
              onClick={clearFilters}
            >
              {t('certReq.personal.clearFilters', 'Clear')}
            </Button>
          )}
        </Group>
      </Paper>

      <AdvancedTable
        tableName={t('certReq.personal.title', 'Personal documents')}
        columns={columns}
        data={groups}
        itemCount={data?.total ?? 0}
        pageIndex={pageIndex}
        onPageChange={setPageIndex}
        pageSize={pageSize}
        onPageSizeChange={setPageSize}
        refresh={refetch}
        isLoading={isFetching}
        emptyText={
          isFiltered
            ? t('certReq.personal.noMatch', 'No personal document matches those filters.')
            : t('certReq.personal.empty', 'No personal documents configured yet.')
        }
      />

      <DocumentRequirementEditorDrawer
        opened={editing !== null}
        onClose={() => setEditing(null)}
        requirement={editing?.group?.rows[0] ?? null}
        defaultApplicationKind="NEW"
        onSave={handleSave}
        palette={undefined}
        conditionTargets={[]}
        saving={creating || updating}
        personal
        licenseTypes={licenseTypes?.items ?? []}
        scope={editing?.group?.scope ?? []}
      />

      <Modal
        opened={deleteTarget !== null}
        onClose={() => setDeleteTarget(null)}
        title={t('certReq.doc.delete', 'Delete document requirement')}
        size="sm"
      >
        <Stack gap="md">
          <Alert color="yellow" variant="light">
            {t(
              'certReq.personal.deleteWarning',
              'The slot disappears from every applicant’s My Documents. Files already uploaded are kept, but nobody can reach them.',
            )}
          </Alert>
          <Text fz="sm">
            {t('certReq.doc.deleteConfirm', 'Remove "{{name}}"?', {
              name: deleteTarget
                ? localized(deleteTarget.rows[0].name) || deleteTarget.key
                : '',
            })}
          </Text>
          <ModalFooter>
            <Button variant="default" onClick={() => setDeleteTarget(null)}>
              {t('certReq.cancel', 'Cancel')}
            </Button>
            <Button color="red" onClick={confirmDelete}>
              {t('certReq.delete', 'Delete')}
            </Button>
          </ModalFooter>
        </Stack>
      </Modal>
    </Stack>
  );
}
