import { useRef, useState } from 'react';
import {
  ActionIcon,
  Alert,
  Badge,
  Button,
  Card,
  Divider,
  FileButton,
  Group,
  Loader,
  Modal,
  Progress,
  SimpleGrid,
  Stack,
  Text,
  Tooltip,
} from '@mantine/core';
import {
  IconAlertTriangle,
  IconPaperclip,
  IconRefresh,
  IconTrash,
  IconUpload,
} from '@tabler/icons-react';
import { useTranslation } from 'react-i18next';
import { ModalFooter } from '@ema-platform/ui';
import { useLocalized } from '@ema-platform/api';
import {
  replacePersonalDocumentFile,
  uploadPersonalDocumentFile,
  useDeletePersonalDocumentFileMutation,
  useGetMyPersonalDocumentsQuery,
  type AttachmentFile,
  type PersonalDocumentError,
  type PersonalDocumentSlot,
  type PersonalDocumentUploadResult,
} from '@ema-platform/api';
import { PORTAL_PERMISSIONS, RequirePermission } from '@ema-platform/auth';

/**
 * A refused delete comes back through RTK, which nests the server's payload
 * under `data`. Uploads report theirs directly — see the XHR helper.
 */
function errorBody(err: unknown): PersonalDocumentError {
  const payload = (err as { data?: { message?: unknown } })?.data?.message;
  return typeof payload === 'object' && payload !== null
    ? (payload as PersonalDocumentError)
    : { message: typeof payload === 'string' ? payload : undefined };
}

/**
 * The applicant's own document vault.
 *
 * Slots are configuration, not code: the backoffice decides which documents
 * everyone keeps and how many files each holds, so labels, accepted types and
 * limits all arrive with the data. The upload button knows it is full for the
 * same reason the server refuses a third file.
 */
export function PersonalDocumentSlots({
  onPreview,
}: {
  onPreview: (preview: { url: string; title: string; mimeType?: string | null }) => void;
}) {
  const { t } = useTranslation();
  const localized = useLocalized();

  const { data, isLoading, refetch } = useGetMyPersonalDocumentsQuery();
  const [deleteFile] = useDeletePersonalDocumentFileMutation();

  const [busy, setBusy] = useState<string | null>(null);
  // Percent for the upload in flight. A video is minutes of waiting, so the
  // bar is the difference between waiting and reloading the page.
  const [progress, setProgress] = useState<number | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [deleteTarget, setDeleteTarget] = useState<AttachmentFile | null>(null);
  // Mantine's FileButton clears its input through a ref object, and there is
  // one input per slot and per file, so the objects are kept by key.
  const resetRefs = useRef<Record<string, { current: (() => void) | null }>>({});
  function resetRef(key: string) {
    resetRefs.current[key] ??= { current: null };
    return resetRefs.current[key] as { current: () => void };
  }
  function clearInput(key: string) {
    resetRefs.current[key]?.current?.();
  }

  /**
   * Checked here as well as on the server so the common mistakes — a PDF where
   * a photograph belongs, a 12 MB scan — never cost a round trip.
   */
  function rejectFile(slot: PersonalDocumentSlot, file: File): string | null {
    if (slot.allowedMimeTypes.length && !slot.allowedMimeTypes.includes(file.type)) {
      return t('documents.personal.errors.unsupported_document_type', {
        allowed: slot.allowedMimeTypes.join(', '),
      });
    }
    if (file.size > slot.maxSizeMb * 1024 * 1024) {
      return t('documents.personal.errors.document_too_large', {
        maxBytes: slot.maxSizeMb * 1024 * 1024,
      });
    }
    return null;
  }

  function describe(body: PersonalDocumentError): string {
    return t(`documents.personal.errors.${body.message ?? 'unknown'}`, {
      ...body,
      defaultValue: t('documents.personal.errors.unknown'),
    });
  }

  /** Deletes, which still go through RTK and refetch themselves. */
  async function run(busyKey: string, action: () => Promise<unknown>) {
    setBusy(busyKey);
    setError(null);
    try {
      await action();
    } catch (err) {
      setError(describe(errorBody(err)));
    } finally {
      setBusy(null);
      clearInput(busyKey);
    }
  }

  /**
   * Uploads, which report progress and so bypass RTK — the vault is refetched
   * by hand once the file has landed.
   */
  async function send(
    busyKey: string,
    action: (onProgress: (percent: number) => void) => Promise<PersonalDocumentUploadResult>,
  ) {
    setBusy(busyKey);
    setProgress(0);
    setError(null);
    const result = await action(setProgress);
    if (result.ok) await refetch();
    else setError(describe(result.error));
    setBusy(null);
    setProgress(null);
    clearInput(busyKey);
  }

  function handleUpload(slot: PersonalDocumentSlot, file: File | null) {
    if (!file) return;
    const rejected = rejectFile(slot, file);
    if (rejected) {
      setError(rejected);
      clearInput(slot.key);
      return;
    }
    return send(slot.key, (onProgress) =>
      uploadPersonalDocumentFile({ documentKey: slot.key, file, onProgress }),
    );
  }

  function handleReplace(slot: PersonalDocumentSlot, fileId: string, file: File | null) {
    if (!file) return;
    const rejected = rejectFile(slot, file);
    if (rejected) {
      setError(rejected);
      clearInput(fileId);
      return;
    }
    return send(fileId, (onProgress) =>
      replacePersonalDocumentFile({ fileId, file, onProgress }),
    );
  }

  async function confirmDelete() {
    if (!deleteTarget) return;
    await run(deleteTarget.id, () => deleteFile(deleteTarget.id).unwrap());
    setDeleteTarget(null);
  }

  /** The bar belongs to the card whose slot, or whose file, is uploading. */
  function isThisSlot(slot: PersonalDocumentSlot, busyKey: string) {
    return busyKey === slot.key || slot.files.some((f) => f.id === busyKey);
  }

  if (isLoading) return <Loader size="sm" type="oval" />;

  const slots = data?.slots ?? [];
  if (slots.length === 0) {
    return (
      <Text fz="sm" c="dimmed">
        {t('documents.personal.empty')}
      </Text>
    );
  }

  return (
    <Stack gap="md">
      <Text fz="sm" c="dimmed">
        {t('documents.personal.description')}
      </Text>

      {error && (
        <Alert color="red" icon={<IconAlertTriangle size={16} />} onClose={() => setError(null)} withCloseButton>
          {error}
        </Alert>
      )}

      <SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md">
        {slots.map((slot) => {
          const full = slot.maxFiles !== null && slot.files.length >= slot.maxFiles;
          return (
            <Card
              key={slot.key}
              withBorder
              radius="md"
              padding="md"
              style={{
                borderStyle: slot.files.length ? 'solid' : 'dashed',
                borderColor: slot.files.length ? 'var(--mantine-color-teal-4)' : undefined,
              }}
            >
              <Group justify="space-between" align="flex-start" wrap="nowrap">
                <div>
                  <Text fz="sm" fw={600}>
                    {localized(slot.name)}
                  </Text>
                  {slot.description && (
                    <Text fz="xs" c="dimmed">
                      {localized(slot.description)}
                    </Text>
                  )}
                </div>
                <Badge
                  size="sm"
                  variant="light"
                  color={slot.files.length ? 'teal' : 'gray'}
                  style={{ flexShrink: 0 }}
                >
                  {slot.maxFiles === null
                    ? t('documents.personal.fileCountUnlimited', { count: slot.files.length })
                    : t('documents.personal.fileCount', {
                        count: slot.files.length,
                        max: slot.maxFiles,
                      })}
                </Badge>
              </Group>

              <Divider my="sm" />

              {slot.files.length === 0 ? (
                <Text fz="xs" c="dimmed">
                  {t('documents.files.none')}
                </Text>
              ) : (
                <Stack gap={6}>
                  {slot.files.map((file) => (
                    <Group key={file.id} gap={6} wrap="nowrap">
                      <IconPaperclip size={14} />
                      <Text
                        fz="xs"
                        style={{ flex: 1, cursor: file.url ? 'pointer' : undefined }}
                        c={file.url ? 'blue' : undefined}
                        truncate
                        onClick={() =>
                          file.url &&
                          onPreview({
                            url: file.url,
                            title: file.originalName,
                            mimeType: file.mimeType,
                          })
                        }
                      >
                        {file.originalName}
                      </Text>
                      <RequirePermission
                        anyOf={[PORTAL_PERMISSIONS.UPLOAD_DOCUMENTS]}
                        hideOnly
                      >
                        <Group gap={2} wrap="nowrap">
                          <FileButton
                            resetRef={resetRef(file.id)}
                            onChange={(picked) => handleReplace(slot, file.id, picked)}
                            accept={slot.allowedMimeTypes.join(',')}
                          >
                            {(props) => (
                              <Tooltip label={t('licensing.documents.replace')}>
                                <ActionIcon
                                  variant="subtle"
                                  size="sm"
                                  loading={busy === file.id}
                                  {...props}
                                >
                                  <IconRefresh size={13} />
                                </ActionIcon>
                              </Tooltip>
                            )}
                          </FileButton>
                          <Tooltip label={t('documents.personal.delete')}>
                            <ActionIcon
                              variant="subtle"
                              color="red"
                              size="sm"
                              onClick={() => setDeleteTarget(file)}
                            >
                              <IconTrash size={13} />
                            </ActionIcon>
                          </Tooltip>
                        </Group>
                      </RequirePermission>
                    </Group>
                  ))}
                </Stack>
              )}

              {busy !== null && progress !== null && isThisSlot(slot, busy) && (
                <Stack gap={2} mt="sm">
                  <Progress value={progress} size="sm" radius="xl" animated />
                  <Text fz="xs" c="dimmed" ta="right">
                    {t('documents.personal.uploading', { percent: progress })}
                  </Text>
                </Stack>
              )}

              <RequirePermission anyOf={[PORTAL_PERMISSIONS.UPLOAD_DOCUMENTS]} hideOnly>
                <Tooltip label={t('documents.personal.slotFull')} disabled={!full}>
                  <div>
                    <FileButton
                      resetRef={resetRef(slot.key)}
                      onChange={(picked) => handleUpload(slot, picked)}
                      accept={slot.allowedMimeTypes.join(',')}
                    >
                      {(props) => (
                        <Button
                          mt="sm"
                          size="xs"
                          variant="light"
                          fullWidth
                          leftSection={<IconUpload size={13} />}
                          loading={busy === slot.key}
                          disabled={full}
                          {...props}
                        >
                          {t('licensing.documents.upload')}
                        </Button>
                      )}
                    </FileButton>
                  </div>
                </Tooltip>
              </RequirePermission>
            </Card>
          );
        })}
      </SimpleGrid>

      <Modal
        opened={deleteTarget !== null}
        onClose={() => setDeleteTarget(null)}
        title={t('documents.personal.confirmDelete.title')}
        size="sm"
        centered
      >
        <Stack gap="md">
          <Text fz="sm">
            {t('documents.personal.confirmDelete.body', {
              name: deleteTarget?.originalName ?? '',
            })}
          </Text>
          <ModalFooter>
            <Button variant="default" onClick={() => setDeleteTarget(null)}>
              {t('common.cancel')}
            </Button>
            <Button color="red" loading={busy === deleteTarget?.id} onClick={confirmDelete}>
              {t('documents.personal.confirmDelete.confirm')}
            </Button>
          </ModalFooter>
        </Stack>
      </Modal>
    </Stack>
  );
}
