import crypto from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import process from "node:process";
import { pathToFileURL } from "node:url";


let FileBlob;
let SpreadsheetFile;
let Workbook;


const STANDARD_HEADERS = [
  "ARRIVAL",
  "DEPARTURE",
  "NIGHTS",
  "ADULTS",
  "CHILDREN",
  "BLOCK_CODE",
  "NO_OF_ROOMS",
  "COMPANY_NAME",
  "CONFIRMATION_NO",
  "DISP_ROOM_NO",
  "RATE_AMOUNT",
  "FULL_NAME",
  "RES_COMMENT",
  "TRACE_TEXT",
  "PRODUCTS",
  "RATE_CODE",
  "ROOM_CATEGORY_LABEL",
  "REAL PRICE",
  "TOTAL PRICE",
];
const KB_HEADER = "KB（100/晚/间）";
const KB_SHEET = "DY-AI-Easy-KB";
const DATE_FIELDS = new Set(["ARRIVAL", "DEPARTURE"]);
const INTEGER_FIELDS = new Set(["NIGHTS", "ADULTS", "CHILDREN", "NO_OF_ROOMS"]);
const DECIMAL_FIELDS = new Set(["RATE_AMOUNT", "REAL PRICE", "TOTAL PRICE", KB_HEADER]);
const TEXT_FIELDS = new Set([
  "BLOCK_CODE",
  "COMPANY_NAME",
  "CONFIRMATION_NO",
  "DISP_ROOM_NO",
  "FULL_NAME",
  "RES_COMMENT",
  "TRACE_TEXT",
  "PRODUCTS",
  "RATE_CODE",
  "ROOM_CATEGORY_LABEL",
]);
const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
const DECIMAL_PATTERN = /^(0|[1-9]\d*)(?:\.(\d{1,2}))?$/;
const INVALID_SHEET_CHARS = /[:\\/?*\[\]]/;
const HEADER_FILL = "#FFFFFF";
const BODY_FONT = "#222222";
const BORDER_COLOR = "#BFBFBF";
const MAX_PREVIEW_ROWS = 25;


function fail(message) {
  throw new Error(message);
}


async function loadArtifactTool() {
  const configured = String(process.env.MONTHLY_REPORT_ARTIFACT_TOOL_MODULE ?? "").trim();
  const module = configured
    ? await import(pathToFileURL(path.resolve(configured)).href)
    : await import("@oai/artifact-tool");
  ({ FileBlob, SpreadsheetFile, Workbook } = module);
  if (!FileBlob || !SpreadsheetFile || !Workbook) fail("artifact-tool module is invalid");
}


function safeText(value) {
  const text = String(value ?? "");
  return /^[=+\-@]/.test(text) ? `'${text}` : text;
}


function excelDate(value) {
  if (!DATE_PATTERN.test(String(value ?? ""))) fail("invalid date value");
  const [year, month, day] = value.split("-").map(Number);
  const parsed = new Date(Date.UTC(year, month - 1, day));
  if (
    parsed.getUTCFullYear() !== year
    || parsed.getUTCMonth() !== month - 1
    || parsed.getUTCDate() !== day
  ) {
    fail("invalid date value");
  }
  return parsed;
}


function dateKey(value) {
  if (value instanceof Date && !Number.isNaN(value.getTime())) {
    return value.toISOString().slice(0, 10);
  }
  if (typeof value === "number" && Number.isFinite(value)) {
    const epoch = Date.UTC(1899, 11, 30);
    return new Date(epoch + Math.floor(value) * 86400000).toISOString().slice(0, 10);
  }
  if (typeof value === "string" && DATE_PATTERN.test(value.slice(0, 10))) {
    return value.slice(0, 10);
  }
  return "";
}


function decimalNumber(value) {
  const text = String(value ?? "");
  const match = DECIMAL_PATTERN.exec(text);
  if (!match) fail("invalid decimal value");
  const number = Number(text);
  if (
    !Number.isFinite(number)
    || number < 0
    || !Number.isSafeInteger(Math.round(number * 100))
  ) {
    fail("unsafe decimal value");
  }
  return number;
}


function columnName(index) {
  let value = index;
  let result = "";
  while (value > 0) {
    value -= 1;
    result = String.fromCharCode(65 + (value % 26)) + result;
    value = Math.floor(value / 26);
  }
  return result;
}


function validatePayload(payload) {
  if (!payload || payload.schema_version !== "1.0") fail("invalid payload schema");
  if (
    !Number.isInteger(payload.report_year)
    || !Number.isInteger(payload.report_month)
    || payload.report_month < 1
    || payload.report_month > 12
    || !DATE_PATTERN.test(String(payload.as_of_date ?? ""))
    || typeof payload.filename !== "string"
    || !payload.filename.endsWith(".xlsx")
    || !Array.isArray(payload.channels)
    || payload.channels.length < 5
  ) {
    fail("invalid monthly report contract");
  }
  const names = new Set();
  payload.channels.forEach((channel, channelIndex) => {
    const expectedHeaders = channel.worksheet === KB_SHEET
      ? [...STANDARD_HEADERS, KB_HEADER]
      : STANDARD_HEADERS;
    if (
      typeof channel.worksheet !== "string"
      || channel.worksheet.length < 1
      || channel.worksheet.length > 31
      || channel.worksheet.trim() !== channel.worksheet
      || INVALID_SHEET_CHARS.test(channel.worksheet)
      || names.has(channel.worksheet)
      || channel.worksheet_order !== channelIndex + 1
      || !Array.isArray(channel.headers)
      || JSON.stringify(channel.headers) !== JSON.stringify(expectedHeaders)
      || !Array.isArray(channel.rows)
    ) {
      fail("invalid channel worksheet contract");
    }
    names.add(channel.worksheet);
    for (const row of channel.rows) {
      if (!row || Object.keys(row).length !== expectedHeaders.length) {
        fail("invalid monthly row contract");
      }
      for (const header of expectedHeaders) {
        if (!Object.hasOwn(row, header)) fail("monthly row field is missing");
        const value = row[header];
        if (DATE_FIELDS.has(header)) {
          excelDate(value);
        } else if (INTEGER_FIELDS.has(header)) {
          if (!Number.isInteger(value) || value < 0) fail("invalid integer value");
        } else if (DECIMAL_FIELDS.has(header)) {
          decimalNumber(value);
        } else if (TEXT_FIELDS.has(header)) {
          if (typeof value !== "string") fail("invalid text value");
        } else {
          fail("unknown monthly row field");
        }
      }
    }
  });
}


function rowValues(channel, row) {
  return channel.headers.map((header) => {
    const value = row[header];
    if (DATE_FIELDS.has(header)) return excelDate(value);
    if (INTEGER_FIELDS.has(header)) return value;
    if (DECIMAL_FIELDS.has(header)) return decimalNumber(value);
    return safeText(value);
  });
}


function setColumnWidths(sheet, headers) {
  const widths = {
    ARRIVAL: 13,
    DEPARTURE: 13,
    NIGHTS: 9,
    ADULTS: 9,
    CHILDREN: 9,
    BLOCK_CODE: 18,
    NO_OF_ROOMS: 13,
    COMPANY_NAME: 28,
    CONFIRMATION_NO: 20,
    DISP_ROOM_NO: 18,
    RATE_AMOUNT: 14,
    FULL_NAME: 22,
    RES_COMMENT: 28,
    TRACE_TEXT: 26,
    PRODUCTS: 24,
    RATE_CODE: 17,
    ROOM_CATEGORY_LABEL: 22,
    "REAL PRICE": 14,
    "TOTAL PRICE": 16,
    [KB_HEADER]: 18,
  };
  headers.forEach((header, index) => {
    sheet.getRange(`${columnName(index + 1)}1`).format.columnWidth = widths[header] ?? 13;
  });
}


function writeSheet(workbook, channel) {
  const sheet = workbook.worksheets.add(channel.worksheet);
  const matrix = [channel.headers, ...channel.rows.map((row) => rowValues(channel, row))];
  const lastColumn = columnName(channel.headers.length);
  const lastRow = matrix.length;
  const used = sheet.getRange(`A1:${lastColumn}${lastRow}`);
  used.values = matrix;
  if (channel.rows.length > 0) {
    sheet.getRange(`S2:S${lastRow}`).formulas = channel.rows.map((_, index) => [
      `=R${index + 2}*C${index + 2}*G${index + 2}`,
    ]);
  }
  used.format.font = { name: "Arial", size: 10, color: BODY_FONT };
  used.format.verticalAlignment = "center";
  used.format.borders = {
    top: { style: "thin", color: BORDER_COLOR },
    bottom: { style: "thin", color: BORDER_COLOR },
    left: { style: "thin", color: BORDER_COLOR },
    right: { style: "thin", color: BORDER_COLOR },
  };
  setColumnWidths(sheet, channel.headers);

  const header = sheet.getRange(`A1:${lastColumn}1`);
  header.format.fill = HEADER_FILL;
  header.format.font = { name: "Arial", size: 10, bold: true, color: "#000000" };
  header.format.horizontalAlignment = "center";
  header.format.rowHeight = 24;
  header.format.wrapText = true;
  sheet.freezePanes.freezeRows(1);

  if (channel.rows.length > 0) {
    const data = sheet.getRange(`A2:${lastColumn}${lastRow}`);
    data.format.rowHeight = 22;
    sheet.getRange(`A2:B${lastRow}`).setNumberFormat("dd-mmm-yy");
    sheet.getRange(`C2:E${lastRow}`).setNumberFormat("0");
    sheet.getRange(`G2:G${lastRow}`).setNumberFormat("0");
    sheet.getRange(`K2:K${lastRow}`).setNumberFormat("0.##");
    sheet.getRange(`R2:S${lastRow}`).setNumberFormat("0.##");
    if (channel.worksheet === KB_SHEET) {
      sheet.getRange(`T2:T${lastRow}`).setNumberFormat("0.##");
    }
    sheet.getRange(`A2:K${lastRow}`).format.horizontalAlignment = "center";
    sheet.getRange(`L2:${lastColumn}${lastRow}`).format.horizontalAlignment = "left";
    sheet.getRange(`F2:${lastColumn}${lastRow}`).format.wrapText = true;
  }
  sheet.showGridLines = true;
}


function expectedCell(header, value) {
  if (DATE_FIELDS.has(header)) return { kind: "date", value: String(value) };
  if (INTEGER_FIELDS.has(header)) return { kind: "number", value };
  if (DECIMAL_FIELDS.has(header)) return { kind: "number", value: decimalNumber(value) };
  // A leading apostrophe is the Excel formula-escape marker. The cell API
  // exposes the displayed text after consuming that marker.
  return { kind: "text", value: String(value ?? "") };
}


async function validateWorkbook(workbook, payload, stage) {
  const names = workbook.worksheets.items.map((sheet) => sheet.name);
  const expectedNames = payload.channels.map((channel) => channel.worksheet);
  if (JSON.stringify(names) !== JSON.stringify(expectedNames)) {
    fail(`${stage} worksheet names do not match`);
  }

  let formulaCount = 0;
  for (const channel of payload.channels) {
    const sheet = workbook.worksheets.getItem(channel.worksheet);
    const expectedRows = channel.rows.length + 1;
    const expectedColumns = channel.headers.length;
    const lastColumn = columnName(expectedColumns);
    const used = sheet.getUsedRange();
    const values = used?.values ?? [];
    if (
      values.length !== expectedRows
      || (values[0]?.length ?? 0) !== expectedColumns
      || !channel.headers.every((header, index) => values[0][index] === header)
    ) {
      fail(`${stage} worksheet dimensions or headers do not match`);
    }
    channel.rows.forEach((row, rowIndex) => {
      const actual = values[rowIndex + 1] ?? [];
      channel.headers.forEach((header, columnIndex) => {
        const expected = expectedCell(header, row[header]);
        const actualValue = actual[columnIndex];
        if (
          (expected.kind === "date" && dateKey(actualValue) !== expected.value)
          || (expected.kind === "number" && Number(actualValue) !== expected.value)
          || (expected.kind === "text" && String(actualValue ?? "") !== expected.value)
        ) {
          fail(
            `${stage} worksheet values do not match at ${channel.worksheet}!`
            + `${columnName(columnIndex + 1)}${rowIndex + 2}`,
          );
        }
      });
    });
    const formulas = used?.formulas ?? [];
    formulas.forEach((formulaRow, rowIndex) => {
      formulaRow.forEach((formulaValue, columnIndex) => {
        const formula = String(formulaValue ?? "").trim();
        if (!formula) return;
        formulaCount += 1;
        const expectedFormula = `=R${rowIndex + 1}*C${rowIndex + 1}*G${rowIndex + 1}`;
        if (
          rowIndex < 1
          || columnIndex !== channel.headers.indexOf("TOTAL PRICE")
          || formula !== expectedFormula
        ) {
          fail(`${stage} workbook contains an unauthorized formula`);
        }
      });
    });
    const inspected = await workbook.inspect({
      kind: "formula",
      sheetId: channel.worksheet,
      range: `A1:${lastColumn}${expectedRows}`,
      maxChars: 6000,
      options: { maxResults: Math.max(channel.rows.length + 10, 100) },
    });
    const inspectionText = String(inspected.ndjson ?? "");
    if (/#REF!|#DIV\/0!|#VALUE!|#NAME\?|#N\/A/.test(inspectionText)) {
      fail(`${stage} workbook contains a formula error`);
    }
  }
  const expectedFormulaCount = payload.channels.reduce(
    (total, channel) => total + channel.rows.length,
    0,
  );
  if (formulaCount !== expectedFormulaCount) {
    fail(`${stage} workbook TOTAL PRICE formulas are incomplete`);
  }
  return formulaCount;
}


async function main() {
  const [inputPath, outputPath, previewDir, summaryPath] = process.argv.slice(2);
  if (!inputPath || !outputPath || !previewDir || !summaryPath) {
    fail("usage: build_workbook.mjs INPUT_JSON OUTPUT_XLSX PREVIEW_DIR SUMMARY_JSON");
  }
  await loadArtifactTool();
  const payload = JSON.parse(await fs.readFile(inputPath, "utf8"));
  validatePayload(payload);
  await fs.mkdir(path.dirname(outputPath), { recursive: true });
  await fs.mkdir(previewDir, { recursive: true });
  await fs.mkdir(path.dirname(summaryPath), { recursive: true });

  const workbook = Workbook.create();
  payload.channels.forEach((channel) => writeSheet(workbook, channel));
  await validateWorkbook(workbook, payload, "pre-export");
  const xlsx = await SpreadsheetFile.exportXlsx(workbook);
  await xlsx.save(outputPath);
  await fs.chmod(outputPath, 0o600);

  const reopened = await SpreadsheetFile.importXlsx(await FileBlob.load(outputPath));
  const formulaCount = await validateWorkbook(reopened, payload, "post-export");
  const previews = [];
  for (let index = 0; index < payload.channels.length; index += 1) {
    const channel = payload.channels[index];
    const lastColumn = columnName(channel.headers.length);
    const previewRows = Math.min(channel.rows.length + 1, MAX_PREVIEW_ROWS);
    const preview = await reopened.render({
      sheetName: channel.worksheet,
      range: `A1:${lastColumn}${previewRows}`,
      autoCrop: "all",
      scale: 1.2,
      format: "png",
    });
    const previewPath = path.join(
      previewDir,
      `sheet-${String(index + 1).padStart(2, "0")}.png`,
    );
    await fs.writeFile(previewPath, new Uint8Array(await preview.arrayBuffer()), {
      mode: 0o600,
    });
    previews.push(previewPath);
  }

  const stat = await fs.stat(outputPath);
  const semanticSha256 = crypto
    .createHash("sha256")
    .update(JSON.stringify({ channels: payload.channels }), "utf8")
    .digest("hex");
  const summary = {
    status: "success",
    schema_version: payload.schema_version,
    filename: payload.filename,
    report_year: payload.report_year,
    report_month: payload.report_month,
    as_of_date: payload.as_of_date,
    sheet_names: payload.channels.map((channel) => channel.worksheet),
    row_counts: payload.channels.map((channel) => channel.rows.length),
    formula_count: formulaCount,
    semantic_sha256: semanticSha256,
    preview_count: previews.length,
    preview_rows: payload.channels.map((channel) => Math.min(channel.rows.length + 1, MAX_PREVIEW_ROWS)),
    byte_size: stat.size,
  };
  await fs.writeFile(summaryPath, `${JSON.stringify(summary, null, 2)}\n`, {
    encoding: "utf8",
    mode: 0o600,
  });
  await fs.rm(`${outputPath}.inspect.ndjson`, { force: true });
  process.stdout.write(`${JSON.stringify(summary)}\n`);
}


try {
  await main();
} catch (_error) {
  const outputPath = process.argv[3];
  if (outputPath) {
    await fs.rm(`${outputPath}.inspect.ndjson`, { force: true }).catch(() => undefined);
  }
  if (String(process.env.MONTHLY_REPORT_DEBUG ?? "") === "1") {
    process.stderr.write(`${String(_error?.stack ?? _error)}\n`);
  }
  process.stderr.write(
    `${JSON.stringify({
      status: "failed",
      code: "MONTHLY_REPORT_OUTPUT_VALIDATION_FAILED",
    })}\n`,
  );
  process.exitCode = 4;
}
