import { BadRequestException } from '@nestjs/common';
import * as ExcelJS from 'exceljs';
import { Readable } from 'node:stream';
import { cellValue, textValue } from './imports.helpers';
import { fallbackSheetsToImportSheets, readXlsxSheetsFallback } from './imports.workbook-fallback';
import type { CellValue } from './imports.types';

const MAX_SHEETS = 30;
const MAX_ROWS_PER_SHEET = 3000;
const MAX_COLS_PER_SHEET = 60;

export interface ImportSheetData {
  name: string;
  headers: string[];
  rows: CellValue[][];
  /** 1-based header row used to build this view; defaults to 1. */
  headerRow?: number;
  /** Physical 1-based row numbers for each entry in `rows` (after headerRow). */
  rowNumbers?: number[];
}

export function extractSheets(workbook: ExcelJS.Workbook, headerRow = 1): ImportSheetData[] {
  const sheets: ImportSheetData[] = [];
  for (const worksheet of workbook.worksheets) {
    if (sheets.length >= MAX_SHEETS) break;
    const headers: string[] = [];
    const rows: CellValue[][] = [];
    const rowNumbers: number[] = [];
    const firstRow = worksheet.getRow(headerRow);
    for (let col = 1; col <= Math.min(firstRow.cellCount, MAX_COLS_PER_SHEET); col += 1) {
      const header = textValue(cellValue(firstRow.getCell(col)));
      headers.push(header);
    }
    if (!headers.some(Boolean)) continue;
    worksheet.eachRow({ includeEmpty: false }, (row, rowNumber) => {
      if (rowNumber <= headerRow || rows.length >= MAX_ROWS_PER_SHEET) return;
      const values: CellValue[] = [];
      for (let col = 1; col <= headers.length; col += 1) {
        values.push(cellValue(row.getCell(col)));
      }
      if (values.every((v) => v === null || textValue(v) === '')) return;
      rows.push(values);
      rowNumbers.push(rowNumber);
    });
    if (rows.length > 0) {
      sheets.push({ name: worksheet.name, headers, rows, headerRow, rowNumbers });
    }
  }
  return sheets;
}

export type WorkbookKind = 'csv' | 'xlsx';

export function detectWorkbookKind(originalName: string, mimeType: string): WorkbookKind | null {
  const isCsv =
    /\.csv$/i.test(originalName) ||
    /csv/i.test(mimeType) ||
    /text\/(csv|plain)/i.test(mimeType);
  const isXlsx =
    /\.xlsx$/i.test(originalName) ||
    /spreadsheetml/i.test(mimeType) ||
    /excel/i.test(mimeType);
  if (isCsv) return 'csv';
  if (isXlsx) return 'xlsx';
  return null;
}

/** 校验文件类型并解析为工作表数据；解析失败抛出可读错误。 */
export async function parseSheets(
  buffer: Buffer,
  originalName: string,
  mimeType: string,
  headerRow = 1,
): Promise<ImportSheetData[]> {
  const kind = detectWorkbookKind(originalName, mimeType);
  if (!kind) {
    throw new BadRequestException('仅支持 .xlsx / .csv 文件');
  }
  if (/\.xls$/i.test(originalName) && !/\.xlsx$/i.test(originalName)) {
    throw new BadRequestException('暂不支持 .xls，请另存为 .xlsx 或 .csv 后重试');
  }
  try {
    const workbook = new ExcelJS.Workbook();
    if (kind === 'csv') {
      await workbook.csv.read(Readable.from(Buffer.from(buffer)));
    } else {
      await workbook.xlsx.load(buffer as unknown as ArrayBuffer);
    }
    const sheets = extractSheets(workbook, headerRow);
    if (sheets.length === 0) {
      throw new BadRequestException('文件中没有可用的工作表数据');
    }
    return sheets;
  } catch (error) {
    if (error instanceof BadRequestException) throw error;
    if (kind === 'xlsx') {
      try {
        const fallbackSheets = fallbackSheetsToImportSheets(
          await readXlsxSheetsFallback(buffer),
          headerRow,
        );
        if (fallbackSheets.length > 0) return fallbackSheets;
      } catch {
        // fall through to the readable error below
      }
    }
    throw new BadRequestException('Excel 文件解析失败，请检查文件格式');
  }
}
