/**
 * Xuất và đọc file Excel.
 *
 * Đội kinh doanh đang sống bằng Excel, nên hệ thống nhận Excel vào và trả Excel ra
 * thay vì bắt bỏ thói quen cũ ngay lập tức. Toàn bộ định dạng gom về một chỗ để
 * mọi báo cáo xuất ra trông như nhau.
 */
// Không đánh dấu 'server-only' — xem chú thích cùng chủ đề trong `import.ts`.
import ExcelJS from "exceljs";

export type ColumnType = "text" | "number" | "money" | "percent" | "date";

export type SheetColumn<T> = {
  header: string;
  /** Lấy giá trị thô từ một dòng dữ liệu. Trả về giá trị số, đừng tự định dạng. */
  value: (row: T) => string | number | null;
  type?: ColumnType;
  width?: number;
};

export type SheetSpec<T> = {
  name: string;
  columns: SheetColumn<T>[];
  rows: T[];
  /** Dòng tiêu đề phụ đặt trên bảng, ví dụ khoảng thời gian của báo cáo. */
  subtitle?: string;
  /** Thêm dòng tổng cộng ở cuối cho các cột số. */
  totals?: boolean;
};

/**
 * Một file Excel gồm nhiều sheet với kiểu dữ liệu dòng khác nhau. TypeScript không
 * có kiểu tồn tại (existential type) để mô tả danh sách như vậy, nên kiểu dòng được
 * xoá ở mức mảng. Dùng `defineSheet()` để mỗi sheet vẫn được kiểm tra kiểu đầy đủ
 * tại nơi khai báo — đó mới là chỗ dễ viết sai.
 */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AnySheetSpec = SheetSpec<any>;

export function defineSheet<T>(spec: SheetSpec<T>): AnySheetSpec {
  return spec as AnySheetSpec;
}

const NUMBER_FORMAT: Record<ColumnType, string | undefined> = {
  text: undefined,
  number: "#,##0",
  money: "#,##0 \\₫",
  percent: "0.0%",
  date: "dd/mm/yyyy",
};

const HEADER_FILL = "FFF05A2A";
const BORDER_COLOR = "FFE2E8F0";

/**
 * Dựng một file Excel gồm nhiều sheet và trả về dạng buffer để gửi cho trình duyệt.
 */
export async function buildWorkbook(
  sheets: AnySheetSpec[],
  meta: { title: string; author?: string },
): Promise<Buffer> {
  const workbook = new ExcelJS.Workbook();
  workbook.creator = meta.author ?? "CRM Quản trị Bán hàng";
  workbook.created = new Date();
  workbook.title = meta.title;

  for (const spec of sheets) {
    // Tên sheet trong Excel tối đa 31 ký tự và không nhận một số ký tự đặc biệt.
    const sheet = workbook.addWorksheet(spec.name.replace(/[\\/*?:[\]]/g, " ").slice(0, 31));

    let cursor = 1;

    sheet.mergeCells(cursor, 1, cursor, Math.max(1, spec.columns.length));
    const titleCell = sheet.getCell(cursor, 1);
    titleCell.value = meta.title;
    titleCell.font = { bold: true, size: 13, color: { argb: "FF1B2739" } };
    cursor += 1;

    if (spec.subtitle) {
      sheet.mergeCells(cursor, 1, cursor, Math.max(1, spec.columns.length));
      const subtitleCell = sheet.getCell(cursor, 1);
      subtitleCell.value = spec.subtitle;
      subtitleCell.font = { size: 10, color: { argb: "FF6A7A92" } };
      cursor += 1;
    }

    cursor += 1; // một dòng trống ngăn cách tiêu đề với bảng
    const headerRowIndex = cursor;

    const headerRow = sheet.getRow(headerRowIndex);
    spec.columns.forEach((column, index) => {
      const cell = headerRow.getCell(index + 1);
      cell.value = column.header;
      cell.font = { bold: true, size: 10, color: { argb: "FFFFFFFF" } };
      cell.fill = { type: "pattern", pattern: "solid", fgColor: { argb: HEADER_FILL } };
      cell.alignment = { vertical: "middle", horizontal: "left", wrapText: true };
    });
    headerRow.height = 22;
    cursor += 1;

    for (const row of spec.rows) {
      const excelRow = sheet.getRow(cursor);
      spec.columns.forEach((column, index) => {
        const cell = excelRow.getCell(index + 1);
        cell.value = column.value(row);
        const format = NUMBER_FORMAT[column.type ?? "text"];
        if (format) cell.numFmt = format;
        cell.border = {
          bottom: { style: "hair", color: { argb: BORDER_COLOR } },
        };
      });
      cursor += 1;
    }

    if (spec.totals && spec.rows.length > 0) {
      const totalRow = sheet.getRow(cursor);
      spec.columns.forEach((column, index) => {
        const cell = totalRow.getCell(index + 1);
        if (index === 0) {
          cell.value = "Tổng cộng";
        } else if (column.type === "number" || column.type === "money") {
          const letter = sheet.getColumn(index + 1).letter;
          cell.value = {
            formula: `SUM(${letter}${headerRowIndex + 1}:${letter}${cursor - 1})`,
          };
          const totalFormat = NUMBER_FORMAT[column.type];
          if (totalFormat) cell.numFmt = totalFormat;
        }
        cell.font = { bold: true };
        cell.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "FFFDEDE7" } };
      });
    }

    spec.columns.forEach((column, index) => {
      sheet.getColumn(index + 1).width = column.width ?? guessWidth(column);
    });

    // Cố định dòng tiêu đề và bật bộ lọc để người dùng lọc ngay trong Excel.
    sheet.views = [{ state: "frozen", ySplit: headerRowIndex }];
    if (spec.rows.length > 0) {
      sheet.autoFilter = {
        from: { row: headerRowIndex, column: 1 },
        to: { row: headerRowIndex + spec.rows.length, column: spec.columns.length },
      };
    }
  }

  const buffer = await workbook.xlsx.writeBuffer();
  return Buffer.from(buffer);
}

function guessWidth(column: SheetColumn<unknown>): number {
  if (column.type === "money") return 18;
  if (column.type === "date") return 13;
  if (column.type === "number" || column.type === "percent") return 12;
  return Math.min(42, Math.max(14, column.header.length + 6));
}

/** Tên file có dấu tiếng Việt gây lỗi ở một số trình duyệt — bỏ dấu trước khi gửi. */
export function safeFileName(name: string): string {
  return name
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "")
    .replace(/đ/g, "d")
    .replace(/Đ/g, "D")
    .replace(/[^a-zA-Z0-9._-]+/g, "-")
    .replace(/-+/g, "-")
    .replace(/^-|-$/g, "");
}

/* -------------------------------------------------------------------------- */
/* Đọc file Excel khi nhập dữ liệu                                            */
/* -------------------------------------------------------------------------- */

export type ParsedSheet = { headers: string[]; rows: Record<string, string>[] };

/**
 * Đọc sheet đầu tiên của file Excel thành danh sách bản ghi, khoá là tiêu đề cột.
 *
 * Mọi giá trị trả về dạng chuỗi đã cắt khoảng trắng: file thật do khách gửi luôn
 * lẫn số lưu dưới dạng text, ngày ở nhiều định dạng, ô có công thức. Việc ép kiểu
 * để phía nhập liệu quyết định, kèm thông báo lỗi theo từng dòng.
 */
export async function parseSheet(buffer: ArrayBuffer): Promise<ParsedSheet> {
  const workbook = new ExcelJS.Workbook();
  await workbook.xlsx.load(buffer);

  const sheet = workbook.worksheets[0];
  if (!sheet) throw new Error("File Excel không có sheet nào.");

  const headerRow = sheet.getRow(1);
  const headers: string[] = [];
  headerRow.eachCell((cell, colNumber) => {
    headers[colNumber - 1] = String(cell.value ?? "").trim();
  });

  if (headers.filter(Boolean).length === 0) {
    throw new Error("Dòng đầu tiên của file phải là tên các cột.");
  }

  const rows: Record<string, string>[] = [];
  sheet.eachRow((row, rowNumber) => {
    if (rowNumber === 1) return;
    const record: Record<string, string> = {};
    let hasValue = false;

    headers.forEach((header, index) => {
      if (!header) return;
      const raw = row.getCell(index + 1).value;
      const text = cellToText(raw);
      if (text) hasValue = true;
      record[header] = text;
    });

    if (hasValue) rows.push(record);
  });

  return { headers: headers.filter(Boolean), rows };
}

function cellToText(value: ExcelJS.CellValue): string {
  if (value == null) return "";
  if (value instanceof Date) return value.toISOString().slice(0, 10);
  if (typeof value === "object") {
    if ("text" in value && typeof value.text === "string") return value.text.trim();
    if ("result" in value) return String(value.result ?? "").trim();
    if ("richText" in value && Array.isArray(value.richText)) {
      return value.richText.map((part) => part.text).join("").trim();
    }
    return "";
  }
  return String(value).trim();
}
