import { ActionIcon, Button, Group, Menu, Text } from "@mantine/core";
import { IconChevronDown, IconMailForward } from "@tabler/icons-react";
import { submitExportRequest } from "@/lib/features/export-requests/query";
import { notifications } from "@mantine/notifications";
import { IconFileSpreadsheet, IconX } from "@tabler/icons-react";
import { useEffect, useState } from "react";
import {
  buildSheetsXlsxBase64,
  exportSheetsToXlsx,
  type ExportColumn,
  type ExportSheet,
} from "./exportXlsx";
import {
  ExportLimitError,
  ExportTimeoutError,
  MAX_EXPORT_ROWS,
} from "./exportLimits";
import { useRoleAccess } from "@/hooks/useRoleAccess";
import {
  getMyExportPermissions,
  type ExportMode,
} from "@/lib/features/access-list/query";

const EXPORT_SECTION_KEY = {
  campaigns: "canExportCampaigns",
  promoCodes: "canExportPromoCodes",
  memberReferrals: "canExportMemberReferrals",
  reports: "canExportReports",
  bookings: "canExportCampaigns",
  promoCodeMembers: "canExportCampaigns",
} as const;

/**
 * Section name as stored, for looking up the mode.
 *
 * `bookings` and `promoCodeMembers` fall back to the campaigns *boolean* above —
 * neither has a boolean of its own — but each has its own mode, so each can be
 * controlled separately. The boolean is only consulted for rows written before the
 * modes existed, and falling back to campaigns there reproduces exactly what
 * governed these exports at the time those rows were written.
 */
const EXPORT_SECTION_MODE_KEY = {
  campaigns: "campaigns",
  promoCodes: "promo_codes",
  memberReferrals: "member_referrals",
  reports: "reports",
  bookings: "bookings",
  promoCodeMembers: "promo_code_members",
} as const;

export type ExportSection = keyof typeof EXPORT_SECTION_KEY;

export type ExportData<T extends Record<string, unknown>> = {
  columns: ExportColumn<T>[];
  rows: T[];
  /** Optional sheet name; defaults to the button label. */
  sheetName?: string;
  /** Extra worksheets appended after the main sheet (e.g. an Analytics sheet). */
  extraSheets?: ExportSheet[];
};

interface ExportButtonProps<T extends Record<string, unknown>> {
  /** Button text, e.g. "Export Promo Codes". */
  label: string;
  /** File name without extension. */
  filename: string;
  /**
   * Fetches the FULL dataset (all pages, not just what's visible) and returns
   * the columns + rows to write. Runs in the background behind a loading popup.
   * Receives an AbortSignal — long-running fetch loops should check
   * `signal.aborted` and stop early so the user can cancel the export.
   */
  getData: (signal: AbortSignal) => Promise<ExportData<T>>;
  /**
   * Which Access List export toggle gates this button, on top of the
   */
  section: ExportSection;
  /**
   * Human summary of the active filters, shown to whoever reviews the request.
   *
   * Optional: without it an approver only sees the report name and row count,
   * which is usually enough but not always — a page that can be narrowed
   * heavily should pass it so the ask can be judged.
   */
  filtersSummary?: string;
  /**
   * Feature slug whose `update` privilege authorises this export.
   *
   * Defaults to `campaign-reports`, which is what every caller predating this
   * prop relied on. A module with its own feature slug passes it here so its
   * export permission is governed by its own role rather than an unrelated one.
   */
  roleSlug?: string;
}

/** True when an error represents the user cancelling the export. */
function isAbort(e: unknown): boolean {
  return e instanceof DOMException && e.name === "AbortError";
}

/**
 * Top-right export action. On click it shows a persistent loading notification
 * (with an ✕ to cancel), fetches the entire dataset in the background, builds
 * an .xlsx and downloads it, then swaps the popup to success / cancelled /
 * error. The button stays disabled while a job is running so it can't be
 * double-fired.
 */
export function ExportButton<T extends Record<string, unknown>>({
  label,
  filename,
  getData,
  section,
  filtersSummary,
  roleSlug = "campaign-reports",
}: ExportButtonProps<T>) {
  const [busy, setBusy] = useState(false);
  // Exporting requires <roleSlug>:update (super admins pass automatically).
  const { checkClientAccess } = useRoleAccess();
  const hasRoleAccess = checkClientAccess("update", roleSlug);

  /*
   * Per-section export mode from the Access List.
   *
   * Starts at `download` so the button behaves normally for the unrestricted
   * majority while the request is in flight; a restricted user's mode arrives a
   * moment later and narrows it.
   */
  const [mode, setMode] = useState<ExportMode>("download");
  useEffect(() => {
    let cancelled = false;
    void (async () => {
      const res = await getMyExportPermissions();
      if (cancelled || !res?.data) return;
      const modes = res.data.exportModes;
      const fromMode = modes?.[EXPORT_SECTION_MODE_KEY[section]];
      if (fromMode) {
        setMode(fromMode);
        return;
      }
      // Core predates the modes: fall back to the boolean, where false meant
      // "no direct download" — which is `request` under the new model.
      setMode(res.data[EXPORT_SECTION_KEY[section]] ? "download" : "request");
    })();
    return () => {
      cancelled = true;
    };
  }, [section]);

  /*
   * Two capabilities, not one.
   *
   * `hasRoleAccess` (campaign-reports:update) decides whether the user may obtain
   * this data at all. The per-section access-list flag then decides *how*:
   *
   *   role + flag  -> download directly (and may still request if they prefer)
   *   role, no flag -> request only; a super admin approves and it is emailed
   *   no role       -> no button
   *
   * This is a deliberate change to what the flag means. It used to remove the
   * export button outright; now it downgrades the user to the approval route, so
   * revoking direct download no longer blocks the work — it just puts a super
   * admin in the loop.
   */
  const canDownload = hasRoleAccess && mode === "download";
  // `none` withholds the request route as well — that is the whole point of it.
  const canRequest = hasRoleAccess && mode !== "none";
  const requestOnly = canRequest && !canDownload;

  const handleClick = async (mode: "download" | "request" = "download") => {
    if (busy) return;
    setBusy(true);
    const controller = new AbortController();
    const notificationId = `export-${filename}`;
    notifications.show({
      id: notificationId,
      loading: true,
      title: mode === "request" ? "Preparing request" : "Exporting",
      message: (
        <Group justify="space-between" wrap="nowrap" gap="sm">
          <Text size="sm">Preparing your file… you can keep working.</Text>
          <ActionIcon
            variant="subtle"
            color="gray"
            size="sm"
            aria-label="Cancel export"
            onClick={() => controller.abort()}
          >
            <IconX size={16} />
          </ActionIcon>
        </Group>
      ),
      autoClose: false,
      withCloseButton: false,
    });
    try {
      const { columns, rows, sheetName, extraSheets } = await getData(
        controller.signal,
      );
      // The data fetch may have completed even though the user clicked cancel
      // mid-flight — don't build/download a file they asked to stop.
      if (controller.signal.aborted) {
        throw new DOMException("Export cancelled", "AbortError");
      }
      if (rows.length > MAX_EXPORT_ROWS) {
        throw new ExportLimitError(rows.length);
      }
      /*
       * Nothing to export is reported, not exported.
       *
       * Without this the workbook was still built and downloaded with only its
       * header row — a file that looks like a real export, opens like one, and says
       * nothing about being empty. On the approval path it was worse: a super admin
       * would be asked to approve an empty spreadsheet. Returning early leaves the
       * filters visibly unchanged, which is the actual thing to fix.
       */
      if (rows.length === 0) {
        notifications.update({
          id: notificationId,
          color: "yellow",
          loading: false,
          title: "Nothing to export",
          message:
            "No rows match the current filters, so no file was created. Widen the filters and try again.",
          autoClose: 5000,
          withCloseButton: true,
        });
        return;
      }
      const sheets = [
        {
          sheetName: sheetName ?? label,
          columns: columns as ExportColumn<Record<string, unknown>>[],
          rows,
        },
        ...(extraSheets ?? []),
      ];
      /*
       * Both paths build the identical workbook; only the destination differs.
       * The approval path uploads it so the reviewer approves the exact bytes
       * that will be emailed, rather than a later regeneration.
       */
      if (mode === "request") {
        const fileBase64 = await buildSheetsXlsxBase64(sheets);
        const res = await submitExportRequest({
          section,
          label,
          filename,
          filtersSummary,
          rowCount: rows.length,
          fileBase64,
        });
        if (!res.success) {
          throw new Error(res.message || "Could not submit the request");
        }
      } else {
        await exportSheetsToXlsx({ filename, sheets });
      }
      const rowLabel = `${rows.length} row${rows.length === 1 ? "" : "s"}`;
      notifications.update({
        id: notificationId,
        color: "green",
        loading: false,
        // Wording follows the path taken — a submitted request must not claim the
        // file was downloaded, or the user will go looking for it.
        title: mode === "request" ? "Sent for approval" : "Export ready",
        message:
          mode === "request"
            ? `${rowLabel} submitted. You'll be emailed once a super admin approves it.`
            : `Downloaded ${rowLabel}.`,
        autoClose: mode === "request" ? 6000 : 4000,
        withCloseButton: true,
      });
    } catch (e) {
      if (isAbort(e) || controller.signal.aborted) {
        notifications.update({
          id: notificationId,
          color: "gray",
          loading: false,
          title: "Export cancelled",
          message:
            mode === "request"
              ? "No request was submitted."
              : "No file was downloaded.",
          autoClose: 3000,
          withCloseButton: true,
        });
      } else if (e instanceof ExportLimitError) {
        notifications.update({
          id: notificationId,
          color: "yellow",
          loading: false,
          title: "Export too large",
          message: `${e.message} Please change the filters so the export has ${MAX_EXPORT_ROWS.toLocaleString()} rows or fewer, then try again.`,
          autoClose: 9000,
          withCloseButton: true,
        });
      } else if (e instanceof ExportTimeoutError) {
        notifications.update({
          id: notificationId,
          color: "red",
          loading: false,
          title: "Export timed out",
          message: e.message,
          autoClose: 6000,
          withCloseButton: true,
        });
      } else {
        notifications.update({
          id: notificationId,
          color: "red",
          loading: false,
          title: "Export failed",
          message:
            e instanceof Error ? e.message : "Could not generate the export.",
          autoClose: 6000,
          withCloseButton: true,
        });
      }
    } finally {
      setBusy(false);
    }
  };

  if (!canDownload && !canRequest) return null;

  /*
   * Request-only users get a single, unambiguous button.
   *
   * Showing a disabled "Export" beside it would read as a fault rather than a
   * policy, and a split control whose primary action is forbidden invites clicks
   * that can only fail.
   */
  if (requestOnly) {
    return (
      <Button
        variant="light"
        color="teal"
        leftSection={<IconMailForward size={16} />}
        onClick={() => void handleClick("request")}
        loading={busy}
        loaderProps={{ size: "sm" }}
        title="A super admin reviews the request; once approved the file is emailed to you."
      >
        Request {label.replace(/^Export\s+/i, "") || "export"}
      </Button>
    );
  }


  return (
    <Group gap={0} wrap="nowrap">
      {/* Direct download stays the primary action — the approval route is the
          alternative, not a replacement. */}
      <Button
        variant="light"
        color="teal"
        leftSection={<IconFileSpreadsheet size={16} />}
        onClick={() => void handleClick("download")}
        loading={busy}
        loaderProps={{ size: "sm" }}
        style={{ borderTopRightRadius: 0, borderBottomRightRadius: 0 }}
      >
        {label}
      </Button>
      <Menu position="bottom-end" withArrow shadow="md">
        <Menu.Target>
          <Button
            variant="light"
            color="teal"
            px={8}
            disabled={busy}
            aria-label="More export options"
            style={{
              borderTopLeftRadius: 0,
              borderBottomLeftRadius: 0,
              borderLeft:
                "1px solid color-mix(in srgb, var(--mantine-color-teal-6) 25%, transparent)",
            }}
          >
            <IconChevronDown size={15} />
          </Button>
        </Menu.Target>
        <Menu.Dropdown>
          <Menu.Item
            leftSection={<IconFileSpreadsheet size={15} />}
            onClick={() => void handleClick("download")}
          >
            Download now
          </Menu.Item>
          <Menu.Item
            leftSection={<IconMailForward size={15} />}
            onClick={() => void handleClick("request")}
          >
            Request approval
          </Menu.Item>
          <Menu.Label>
            A super admin reviews the request; once approved the file is emailed
            to you.
          </Menu.Label>
        </Menu.Dropdown>
      </Menu>
    </Group>
  );
}
