import ExcelJS from "exceljs";

export type ExportColumn<T = Record<string, unknown>> = {
  /** Key to read from each row object. */
  key: keyof T & string;
  /** Header label shown in the sheet. */
  label: string;
  /** Optional column width (characters). */
  width?: number;
};

export type ExportRowsOptions<T = Record<string, unknown>> = {
  sheetName: string;
  columns: ExportColumn<T>[];
  rows: T[];
  /** File name without extension; ".xlsx" is appended automatically. */
  filename: string;
};

/** One worksheet's worth of data for a multi-sheet workbook. */
export type ExportSheet = {
  sheetName: string;
  columns: ExportColumn<Record<string, unknown>>[];
  rows: Record<string, unknown>[];
};

/** Write a single set of columns+rows onto an existing ExcelJS worksheet. */
function writeSheet(
  sheet: ExcelJS.Worksheet,
  columns: ExportColumn<Record<string, unknown>>[],
  rows: Record<string, unknown>[],
): void {
  sheet.columns = columns.map((c) => ({
    header: c.label,
    key: c.key,
    width: c.width ?? Math.max(12, c.label.length + 2),
  }));

  // Bold header row.
  sheet.getRow(1).font = { bold: true };

  for (const row of rows) {
    const record: Record<string, string | number | boolean> = {};
    for (const col of columns) {
      const value = row[col.key];
      record[col.key] =
        value === null || value === undefined
          ? ""
          : typeof value === "number" || typeof value === "boolean"
            ? value
            : String(value);
    }
    sheet.addRow(record);
  }
}

/** Trigger a browser download of an ExcelJS workbook. */
async function downloadWorkbook(
  workbook: ExcelJS.Workbook,
  filename: string,
): Promise<void> {
  const buffer = await workbook.xlsx.writeBuffer();
  const blob = new Blob([buffer], {
    type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
  });
  const url = URL.createObjectURL(blob);
  const anchor = document.createElement("a");
  anchor.href = url;
  anchor.download = filename.endsWith(".xlsx") ? filename : `${filename}.xlsx`;
  document.body.appendChild(anchor);
  anchor.click();
  anchor.remove();
  URL.revokeObjectURL(url);
}

/**
 * Build an .xlsx workbook from one or more sheets and trigger a browser
 * download. Runs entirely client-side (ExcelJS) — no server round-trip. Cell
 * values are coerced to primitives; null/undefined become empty cells.
 */
export async function exportSheetsToXlsx({
  filename,
  sheets,
}: {
  filename: string;
  sheets: ExportSheet[];
}): Promise<void> {
  await downloadWorkbook(buildWorkbook(sheets), filename);
}

/** Builds the workbook, shared by the download and approval-request paths. */
function buildWorkbook(sheets: ExportSheet[]): ExcelJS.Workbook {
  const workbook = new ExcelJS.Workbook();
  const safeSheets = sheets.length
    ? sheets
    : [{ sheetName: "Sheet1", columns: [], rows: [] }];
  safeSheets.forEach((s, i) => {
    const name = s.sheetName.slice(0, 31) || `Sheet${i + 1}`;
    const sheet = workbook.addWorksheet(name);
    writeSheet(sheet, s.columns, s.rows);
  });
  return workbook;
}

/**
 * The same workbook, base64-encoded instead of downloaded.
 *
 * Used by the export-approval path so the reviewer approves the exact bytes the
 * requester would have received, rather than a regeneration that might differ.
 *
 * Encoded in chunks: a spread or `String.fromCharCode(...bytes)` over a few
 * hundred thousand bytes overflows the call stack.
 */
export async function buildSheetsXlsxBase64(
  sheets: ExportSheet[],
): Promise<string> {
  const buffer = await buildWorkbook(sheets).xlsx.writeBuffer();
  const bytes = new Uint8Array(buffer as ArrayBuffer);
  let binary = "";
  const CHUNK = 0x8000;
  for (let i = 0; i < bytes.length; i += CHUNK) {
    binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
  }
  return btoa(binary);
}

/**
 * Build a single-sheet .xlsx workbook from a flat list of rows and download it.
 * Thin wrapper over {@link exportSheetsToXlsx} kept for existing callers.
 */
export async function exportRowsToXlsx<T extends Record<string, unknown>>({
  sheetName,
  columns,
  rows,
  filename,
}: ExportRowsOptions<T>): Promise<void> {
  await exportSheetsToXlsx({
    filename,
    sheets: [
      {
        sheetName,
        columns: columns as ExportColumn<Record<string, unknown>>[],
        rows,
      },
    ],
  });
}
