import http from "node:http";
import { readFile, readdir, stat } from "node:fs/promises";
import { existsSync, readFileSync } from "node:fs";
import { basename, extname, join, resolve } from "node:path";
import { createRequire } from "node:module";
import { fileURLToPath } from "node:url";
import { parseCostWorkbook, parsePricingWorkbook, parsePurchaseWorkbook, parseStockWorkbook } from "./src/excel.mjs";
import { createStore } from "./src/store.mjs";

const root = resolve(fileURLToPath(new URL(".", import.meta.url)));
const publicRoot = join(root, "public");
const require = createRequire(import.meta.url);
const Busboy = require("./node_modules/busboy");
const defaultPort = Number(process.env.PORT || process.argv.find((item) => item.startsWith("--port="))?.split("=")[1] || 5328);
const maxBodyBytes = 800 * 1024 * 1024;
const buildInfo = loadBuildInfo();
const localDataDir = resolve(process.env.HANMI_LOCAL_QUOTE_HOME || join(root, "data"));
const store = createStore(join(localDataDir, "hanmi-pricing-tool.sqlite"));

function loadBuildInfo() {
  try {
    return JSON.parse(readFileSync(join(root, "build-info.json"), "utf8"));
  } catch {
    return {
      name: "hanmi-local-pricing-tool",
      buildId: "dev",
      builtAt: "",
      sourceHash: ""
    };
  }
}

function send(res, statusCode, body, headers = {}) {
  const buffer = Buffer.isBuffer(body) ? body : Buffer.from(String(body ?? ""), "utf8");
  res.writeHead(statusCode, {
    "Content-Length": buffer.length,
    "X-Content-Type-Options": "nosniff",
    ...headers
  });
  res.end(buffer);
}

function sendJson(res, statusCode, body) {
  send(res, statusCode, JSON.stringify(body), {
    "Content-Type": "application/json; charset=utf-8",
    "Cache-Control": "no-store"
  });
}

function sendError(res, error) {
  sendJson(res, error.statusCode || 500, {
    error: error.message || "服务异常",
    details: error.details || null
  });
}

function contentType(filePath) {
  return {
    ".html": "text/html; charset=utf-8",
    ".js": "application/javascript; charset=utf-8",
    ".css": "text/css; charset=utf-8",
    ".svg": "image/svg+xml"
  }[extname(filePath).toLowerCase()] || "application/octet-stream";
}

function requestUrl(req) {
  return new URL(req.url || "/", `http://${req.headers.host || "127.0.0.1"}`);
}

function queryObject(url) {
  return Object.fromEntries(url.searchParams.entries());
}

function exportFileName(name) {
  return encodeURIComponent(name);
}

function headerJson(value) {
  return Buffer.from(JSON.stringify(value || {}), "utf8").toString("base64url");
}

function fileDateLabel(mtimeMs) {
  const date = new Date(Number(mtimeMs || 0));
  if (!Number.isFinite(date.getTime())) return "";
  const pad = (value) => String(value).padStart(2, "0");
  return `${pad(date.getMonth() + 1)}${pad(date.getDate())}`;
}

function datedDisplayName(fileName, dateLabel) {
  if (!fileName || !dateLabel) return fileName || "";
  return fileName.replace(/(\.[^.]+)$/i, `-${dateLabel}$1`);
}

function todayIso() {
  const date = new Date();
  const pad = (value) => String(value).padStart(2, "0");
  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
}

function addDays(isoDate, days) {
  const date = new Date(`${isoDate}T00:00:00`);
  date.setDate(date.getDate() + days);
  const pad = (value) => String(value).padStart(2, "0");
  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
}

function dateParts(isoDate) {
  const match = String(isoDate || "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
  if (!match) throw Object.assign(new Error("日期格式应为 YYYY-MM-DD"), { statusCode: 400 });
  return { year: match[1], month: Number(match[2]), day: Number(match[3]) };
}

function dateLabels(isoDate) {
  const { month, day } = dateParts(isoDate);
  const mm = String(month).padStart(2, "0");
  const dd = String(day).padStart(2, "0");
  return [...new Set([
    `${month}.${day}`,
    `${month}-${day}`,
    `${month}+${day}`,
    `${mm}.${dd}`,
    `${mm}-${dd}`,
    `${mm}+${dd}`
  ])];
}

function isoFromParts(year, month, day) {
  const fullYear = Number(year);
  const monthNo = Number(month);
  const dayNo = Number(day);
  const lastDay = new Date(Date.UTC(fullYear, monthNo, 0)).getUTCDate();
  if (!Number.isInteger(fullYear) || fullYear < 2000 || fullYear > 2099) return "";
  if (!Number.isInteger(monthNo) || monthNo < 1 || monthNo > 12) return "";
  if (!Number.isInteger(dayNo) || dayNo < 1 || dayNo > lastDay) return "";
  return `${fullYear}-${String(monthNo).padStart(2, "0")}-${String(dayNo).padStart(2, "0")}`;
}

function dateFromFileName(name, fallbackYear) {
  const text = String(name || "").normalize("NFKC").replace(/\.(xlsx|xlsm)$/i, "");
  let match = text.match(/(?:^|[^\d])((?:19|20)\d{2})[.\-_/年](\d{1,2})[.\-_/月](\d{1,2})日?(?=$|[^\d])/);
  if (match) return isoFromParts(match[1], match[2], match[3]);
  match = text.match(/(?:^|[^\d])(\d{1,2})[.\-_/+月](\d{1,2})日?(?=$|[^\d])/);
  if (match) return isoFromParts(fallbackYear, match[1], match[2]);
  return "";
}

function monthFolderName(isoDate) {
  const { year, month } = dateParts(isoDate);
  return `${year}-${String(month).padStart(2, "0")}`;
}

function isMonthFolderName(name) {
  return /^(?:19|20)\d{2}-(?:0[1-9]|1[0-2])$/.test(String(name || ""));
}

function isExcelFile(name) {
  return /\.(xlsx|xlsm)$/i.test(name) && !/^~\$/.test(name);
}

function strictAutoFileInfo(name, fallbackYear = new Date().getFullYear()) {
  if (!isExcelFile(name)) return null;
  const text = String(name || "").normalize("NFKC").replace(/\.(xlsx|xlsm)$/i, "").trim();
  const match = text.match(/^(?:(?<year>(?:19|20)\d{2})[.\-_/年])?(?<month>\d{1,2})[.\-_/月](?<day>\d{1,2})日?\s*(?:[+_\- ]\s*)?(?<label>香港现货1|香港现货|成本|进货明细)\s*$/);
  if (!match?.groups) return null;
  const quoteDate = isoFromParts(match.groups.year || fallbackYear, match.groups.month, match.groups.day);
  if (!quoteDate) return null;
  return { quoteDate, label: match.groups.label };
}

function strictAutoFileInfoForDir(fileName, dir) {
  const fallbackYear = isMonthFolderName(basename(dir)) ? Number(basename(dir).slice(0, 4)) : new Date().getFullYear();
  return strictAutoFileInfo(fileName, fallbackYear);
}

function strictAutoFileMatches(fileName, dir, label, quoteDate = "") {
  const info = strictAutoFileInfoForDir(fileName, dir);
  return Boolean(info && info.label === label && (!quoteDate || info.quoteDate === quoteDate));
}

function strictFileRequirement(label) {
  if (label.includes("香港现货1")) return "文件名必须为「日期 香港现货1.xlsx」或「日期+香港现货1.xlsx」，汉字部分必须精确等于“香港现货1”";
  if (label.includes("香港现货表") || label.includes("香港现货")) return "文件名必须为「日期 香港现货.xlsx」或「日期+香港现货.xlsx」，汉字部分必须精确等于“香港现货”";
  if (label.includes("成本")) return "文件名必须为「日期 成本.xlsx」或「日期+成本.xlsx」，汉字部分必须精确等于“成本”";
  if (label.includes("进货明细")) return "文件名必须为「日期 进货明细.xlsx」或「日期+进货明细.xlsx」，汉字部分必须精确等于“进货明细”";
  return "";
}

function invalidStrictFileNames(files, dir) {
  return files
    .filter((file) => !strictAutoFileInfoForDir(file.fileName, dir))
    .slice(0, 20)
    .map((file) => file.fileName);
}

function fileCandidateDiagnostics(files, dir, label = "", quoteDate = "") {
  return files.slice(0, 30).map((file) => {
    const sourceDir = file.sourceDir || dir;
    const info = strictAutoFileInfoForDir(file.fileName, sourceDir);
    const reasons = [];
    if (!info) {
      reasons.push("文件名不符合严格格式");
    } else {
      if (label && info.label !== label) reasons.push(`类型是「${info.label}」，不是「${label}」`);
      if (quoteDate && info.quoteDate !== quoteDate) reasons.push(`日期是 ${info.quoteDate}，不是所选 ${quoteDate}`);
    }
    return {
      fileName: file.fileName,
      sourceDir,
      fullPath: file.fullPath || "",
      parsedDate: info?.quoteDate || "",
      parsedType: info?.label || "",
      reason: reasons.join("；") || "符合格式但未命中当前条件"
    };
  });
}

function isAnyHongKongStockFileName(name, quoteDate = "", dir = "") {
  const info = dir ? strictAutoFileInfoForDir(name, dir) : strictAutoFileInfo(name);
  return Boolean(info && (info.label === "香港现货" || info.label === "香港现货1") && (!quoteDate || info.quoteDate === quoteDate));
}

function isRegularStockFileName(name, quoteDate = "", dir = "") {
  const info = dir ? strictAutoFileInfoForDir(name, dir) : strictAutoFileInfo(name);
  return Boolean(info && info.label === "香港现货" && (!quoteDate || info.quoteDate === quoteDate));
}

function isStockOneForDate(name, quoteDate = "", dir = "") {
  const info = dir ? strictAutoFileInfoForDir(name, dir) : strictAutoFileInfo(name);
  return Boolean(info && info.label === "香港现货1" && (!quoteDate || info.quoteDate === quoteDate));
}

async function listExcelFiles(dir) {
  if (!existsSync(dir)) return [];
  const entries = await readdir(dir, { withFileTypes: true });
  const files = [];
  for (const entry of entries) {
    if (!entry.isFile() || !isExcelFile(entry.name)) continue;
    const fullPath = join(dir, entry.name);
    const info = await stat(fullPath);
    files.push({ fileName: entry.name, fullPath, mtimeMs: info.mtimeMs, size: info.size });
  }
  return files.sort((a, b) => b.mtimeMs - a.mtimeMs);
}

async function listExcelFilesFromDirs(dirs) {
  const files = [];
  const seen = new Set();
  for (const dir of dirs) {
    if (!dir || seen.has(dir)) continue;
    seen.add(dir);
    const dirFiles = await listExcelFiles(dir);
    files.push(...dirFiles.map((file) => ({ ...file, sourceDir: dir })));
  }
  return files.sort((a, b) => b.mtimeMs - a.mtimeMs);
}

async function findStoredFile(dir, fileName, label, validator = null) {
  const files = await listExcelFiles(dir);
  const match = files.find((file) => file.fileName === fileName);
  if (match && (!validator || validator(match.fileName, dir))) return match;
  if (match && validator && !validator(match.fileName, dir)) {
    throw Object.assign(new Error(`历史文件组回退失败：${label}「${fileName || "-"}」文件名不符合当前严格格式`), {
      statusCode: 400,
      details: {
        fileLabel: label,
        fileName: fileName || "",
        stage: "历史文件组回退",
        searchDir: dir,
        requiredPattern: strictFileRequirement(label),
        candidates: files.slice(0, 20).map((file) => file.fileName)
      }
    });
  }
  throw Object.assign(new Error(`历史文件组回退失败：${label}「${fileName || "-"}」未找到`), {
    statusCode: 400,
    details: {
      fileLabel: label,
      fileName: fileName || "",
      stage: "历史文件组回退",
      searchDir: dir,
      requiredPattern: strictFileRequirement(label),
      candidates: files.slice(0, 20).map((file) => file.fileName)
    }
  });
}

function includesAnyDateLabel(name, isoDate) {
  const text = String(name || "").normalize("NFKC");
  return dateLabels(isoDate).some((label) => text.includes(label));
}

function pickRequiredFile(files, predicate, label, dir, expected = {}) {
  const matches = files.filter(predicate);
  if (matches.length) return matches[0];
  const expectedType = expected.type || "";
  const expectedDate = expected.quoteDate || "";
  const rejected = [];
  const grouped = new Map();
  for (const file of files) {
    const sourceDir = file.sourceDir || dir;
    if (!grouped.has(sourceDir)) grouped.set(sourceDir, []);
    grouped.get(sourceDir).push(file);
  }
  for (const [sourceDir, dirFiles] of grouped) {
    rejected.push(...invalidStrictFileNames(dirFiles, sourceDir).map((fileName) => `${basename(sourceDir)}\\${fileName}`));
  }
  throw Object.assign(new Error(`自动找文件失败：${label}未找到`), {
    statusCode: 400,
    details: {
      fileLabel: label,
      fileName: "",
      stage: "自动查找文件",
      searchDir: dir,
      requestedDate: expectedDate,
      expectedType,
      requiredPattern: strictFileRequirement(label),
      candidates: files.slice(0, 20).map((file) => file.fileName),
      candidateDiagnostics: fileCandidateDiagnostics(files, dir, expectedType, expectedDate),
      rejectedByStrictName: rejected.slice(0, 20)
    }
  });
}

function pickLatestDatedFile(files, predicate, label, dir) {
  const matches = files.filter(predicate).map((file) => ({
    ...file,
    parsedDate: dateFromFileName(
      file.fileName,
      isMonthFolderName(basename(dir)) ? Number(basename(dir).slice(0, 4)) : new Date().getFullYear()
    )
  }));
  if (matches.length) {
    return matches.sort((a, b) => {
      const dateCompare = String(b.parsedDate || "").localeCompare(String(a.parsedDate || ""));
      if (dateCompare) return dateCompare;
      return b.mtimeMs - a.mtimeMs;
    })[0];
  }
  throw Object.assign(new Error(`自动找文件失败：${label}未找到`), {
    statusCode: 400,
    details: {
      fileLabel: label,
      fileName: "",
      stage: "自动查找文件",
      searchDir: dir,
      requiredPattern: strictFileRequirement(label),
      candidates: files.slice(0, 20).map((file) => file.fileName),
      rejectedByStrictName: invalidStrictFileNames(files, dir)
    }
  });
}

function costCandidateFiles(files, dir) {
  const fallbackYear = isMonthFolderName(basename(dir)) ? Number(basename(dir).slice(0, 4)) : new Date().getFullYear();
  return files
    .map((file) => {
      const strictInfo = strictAutoFileInfo(file.fileName, fallbackYear);
      const parsedDate = strictInfo?.quoteDate || dateFromFileName(file.fileName, fallbackYear);
      const normalizedName = String(file.fileName || "").normalize("NFKC");
      const relaxedCost = Boolean(parsedDate && normalizedName.includes("成本"));
      const strictCost = strictInfo?.label === "成本";
      if (!strictCost && !relaxedCost) return null;
      return {
        ...file,
        parsedDate,
        isStrictCost: strictCost,
        matchMode: strictCost ? "strict" : "relaxed"
      };
    })
    .filter(Boolean)
    .sort((a, b) => {
      const dateCompare = String(b.parsedDate || "").localeCompare(String(a.parsedDate || ""));
      if (dateCompare) return dateCompare;
      const modifiedCompare = b.mtimeMs - a.mtimeMs;
      if (modifiedCompare) return modifiedCompare;
      return Number(b.isStrictCost) - Number(a.isStrictCost);
    });
}

function pickLatestCostCandidates(files, label, dir) {
  const candidates = costCandidateFiles(files, dir);
  if (candidates.length) return candidates;
  throw Object.assign(new Error(`自动找文件失败：${label}未找到`), {
    statusCode: 400,
    details: {
      fileLabel: label,
      fileName: "",
      stage: "自动查找文件",
      searchDir: dir,
      requiredPattern: `${strictFileRequirement(label)}；如已手工解密另存，可使用「日期 成本 解密.xlsx」等含日期和“成本”的文件名`,
      candidates: files.slice(0, 20).map((file) => file.fileName),
      rejectedByStrictName: invalidStrictFileNames(files, dir)
    }
  });
}

function pickLatestModifiedFile(files, predicate, label, dir) {
  const matches = files.filter(predicate).sort((a, b) => {
    const modifiedCompare = b.mtimeMs - a.mtimeMs;
    if (modifiedCompare) return modifiedCompare;
    return b.fileName.localeCompare(a.fileName);
  });
  if (matches.length) return matches[0];
  throw Object.assign(new Error(`自动找文件失败：${label}未找到`), {
    statusCode: 400,
    details: {
      fileLabel: label,
      fileName: "",
      stage: "自动查找文件",
      searchDir: dir,
      candidates: files.slice(0, 20).map((file) => file.fileName)
    }
  });
}

async function dirsWithMonthChildren(rootDir) {
  const dirs = [];
  if (existsSync(rootDir)) {
    const entries = await readdir(rootDir, { withFileTypes: true });
    for (const entry of entries) {
      if (entry.isDirectory() && isMonthFolderName(entry.name)) dirs.push(join(rootDir, entry.name));
    }
  }
  dirs.sort((a, b) => basename(b).localeCompare(basename(a)));
  dirs.push(rootDir);
  return [...new Set(dirs)];
}

async function stockDirsForLatestSearch() {
  return dirsWithMonthChildren(store.stockSourceRoot());
}

function dataSourceDirForDate(resolvedQuoteDate) {
  const monthDir = join(store.sourceRoot(), monthFolderName(resolvedQuoteDate));
  return existsSync(monthDir) ? monthDir : store.sourceRoot();
}

async function latestDataSourceDir() {
  const rootDir = store.sourceRoot();
  const dirs = await dirsWithMonthChildren(rootDir);
  const latestMonthDir = dirs.find((dir) => isMonthFolderName(basename(dir)));
  return latestMonthDir || rootDir;
}

function stockSourceDirsForDate(resolvedQuoteDate, forcedStockDir = "") {
  if (forcedStockDir) return [forcedStockDir];
  const rootDir = store.stockSourceRoot();
  const monthDir = join(rootDir, monthFolderName(resolvedQuoteDate));
  return [...new Set([existsSync(monthDir) ? monthDir : "", rootDir])].filter(Boolean);
}

async function discoverLatestQuoteDate() {
  const candidates = [];
  const searchDirs = await stockDirsForLatestSearch();
  const latestMonthDir = searchDirs.find((dir) => isMonthFolderName(basename(dir)));
  const dirsToSearch = latestMonthDir ? [...new Set([latestMonthDir, store.stockSourceRoot()])] : searchDirs;
  const rejectedByStrictName = [];
  for (const dir of dirsToSearch) {
    const fallbackYear = isMonthFolderName(basename(dir)) ? Number(basename(dir).slice(0, 4)) : new Date().getFullYear();
    const files = await listExcelFiles(dir);
    rejectedByStrictName.push(...invalidStrictFileNames(files, dir).map((fileName) => `${basename(dir)}\\${fileName}`));
    for (const file of files) {
      const info = strictAutoFileInfo(file.fileName, fallbackYear);
      if (!info || (info.label !== "香港现货" && info.label !== "香港现货1")) continue;
      const quoteDate = info.quoteDate;
      if (quoteDate) candidates.push({ quoteDate, sourceDir: dir, stockFile: file });
    }
  }
  const dates = [...new Set(candidates.map((item) => item.quoteDate))].sort((a, b) => b.localeCompare(a));
  if (dates.length) {
    const quoteDate = dates[0];
    const dirs = [...new Set(candidates.filter((item) => item.quoteDate === quoteDate).map((item) => item.sourceDir))];
    const errors = [];
    for (const stockSourceDir of dirs) {
      try {
        return await discoverAutoFilesForDate(quoteDate, stockSourceDir);
      } catch (error) {
        errors.push({ stockSourceDir, message: error.message || "文件组不完整", details: error.details || null });
      }
    }
    throw Object.assign(new Error(`自动找文件失败：最新日期 ${quoteDate} 文件组不完整，已停止回退旧月份`), {
      statusCode: 400,
      details: {
        fileLabel: "自动文件组",
        fileName: "",
        stage: "自动查找最新完整文件组",
        searchDir: dirsToSearch.join(" | "),
        latestMonthDir: latestMonthDir || "",
        latestDate: quoteDate,
        candidateDates: dates.slice(0, 10),
        errors
      }
    });
  }
  throw Object.assign(new Error("自动找文件失败：最新月份未找到可识别日期的香港现货"), {
    statusCode: 400,
    details: {
      fileLabel: "当天香港现货",
      fileName: "",
      stage: "自动查找最新日期",
      searchDir: dirsToSearch.join(" | "),
      latestMonthDir: latestMonthDir || "",
      requiredPattern: strictFileRequirement("香港现货表"),
      rejectedByStrictName: rejectedByStrictName.slice(0, 20)
    }
  });
}

async function discoverAutoFilesForDate(resolvedQuoteDate, forcedStockDir = "") {
  const sourceDir = dataSourceDirForDate(resolvedQuoteDate);
  const costSourceDir = await latestDataSourceDir();
  const regularStockSourceDirs = stockSourceDirsForDate(resolvedQuoteDate, forcedStockDir);
  const regularStockSourceDirText = regularStockSourceDirs.join(" | ");
  const regularStockFiles = await listExcelFilesFromDirs(regularStockSourceDirs);
  const sourceFiles = await listExcelFiles(sourceDir);
  const costFiles = costSourceDir === sourceDir ? sourceFiles : await listExcelFiles(costSourceDir);
  const pricingFiles = await listExcelFiles(store.pricingDir());
  const stockFile = pickRequiredFile(sourceFiles, (file) => {
    return isStockOneForDate(file.fileName, resolvedQuoteDate, sourceDir);
  }, "当天香港现货1表", sourceDir, { type: "香港现货1", quoteDate: resolvedQuoteDate });
  const regularStockFile = pickRequiredFile(regularStockFiles, (file) => {
    return isRegularStockFileName(file.fileName, resolvedQuoteDate, file.sourceDir || regularStockSourceDirs[0]);
  }, "当天香港现货表", regularStockSourceDirText, { type: "香港现货", quoteDate: resolvedQuoteDate });
  const costCandidates = pickLatestCostCandidates(costFiles, "成本表", costSourceDir);
  const costFile = costCandidates[0];
  const purchaseFile = pickRequiredFile(sourceFiles, (file) => {
    return strictAutoFileMatches(file.fileName, sourceDir, "进货明细", resolvedQuoteDate);
  }, "进货明细", sourceDir, { type: "进货明细", quoteDate: resolvedQuoteDate });
  const pricingFile = pickLatestModifiedFile(pricingFiles, (file) => {
    const name = file.fileName.normalize("NFKC");
    return /定[价级]表模[版板]/.test(name);
  }, "定价表模版", store.pricingDir());
  return {
    stockSourceDir: sourceDir,
    regularStockSourceDir: regularStockFile.sourceDir || regularStockSourceDirs[0],
    regularStockSourceDirs,
    costSourceDir,
    sourceDir,
    pricingDir: store.pricingDir(),
    quoteDate: resolvedQuoteDate,
    stockFile,
    regularStockFile,
    costFile,
    costCandidates: costCandidates.slice(0, 12).map((file) => ({
      fileName: file.fileName,
      fullPath: file.fullPath,
      parsedDate: file.parsedDate || "",
      matchMode: file.matchMode || "",
      mtimeMs: file.mtimeMs,
      size: file.size
    })),
    purchaseFile,
    pricingFile
  };
}

async function discoverAutoFiles(quoteDate = "") {
  async function discoverFromPreviousBatch(reason = "") {
    const latest = store.latestAutoFiles();
    if (!latest?.autoFiles) {
      throw Object.assign(new Error("自动找文件失败：未找到完整文件组，也没有可回退的历史批次文件组"), {
        statusCode: 400,
        details: {
          fileLabel: "历史文件组",
          fileName: "",
          stage: "历史文件组回退",
          reason
        }
      });
    }
    const autoFiles = latest.autoFiles;
    const sourceDir = autoFiles.sourceDir || join(store.sourceRoot(), monthFolderName(autoFiles.quoteDate));
    const stockSourceDir = sourceDir;
    const regularStockSourceDir = autoFiles.regularStockSourceDir || autoFiles.stockSourceDir || store.stockSourceRoot();
    const costSourceDir = autoFiles.costSourceDir || sourceDir;
    const pricingDir = autoFiles.pricingDir || store.pricingDir();
    const stockFile = await findStoredFile(stockSourceDir, autoFiles.stockFileName, "历史香港现货1表", (fileName, dir) => strictAutoFileMatches(fileName, dir, "香港现货1"));
    const regularStockFile = await findStoredFile(regularStockSourceDir, autoFiles.regularStockFileName, "历史香港现货表", (fileName, dir) => strictAutoFileMatches(fileName, dir, "香港现货"));
    const costFile = await findStoredFile(costSourceDir, autoFiles.costFileName, "历史成本表", (fileName, dir) => {
      return Boolean(costCandidateFiles([{ fileName, fullPath: "", mtimeMs: 0, size: 0 }], dir).length);
    });
    const purchaseFile = await findStoredFile(sourceDir, autoFiles.purchaseFileName, "历史进货明细", (fileName, dir) => strictAutoFileMatches(fileName, dir, "进货明细"));
    const pricingFile = await findStoredFile(pricingDir, autoFiles.pricingFileName, "历史定价表模版");
    return {
      sourceDir,
      stockSourceDir,
      regularStockSourceDir,
      costSourceDir,
      pricingDir,
      quoteDate: autoFiles.quoteDate,
      requestedDate: quoteDate && quoteDate !== "latest" ? quoteDate : "",
      fallbackFromRequestedDate: Boolean(quoteDate && quoteDate !== "latest"),
      fallbackReason: reason || "已回退到上一成功批次实际读取文件组",
      fallbackFromHistoryBatch: true,
      historyBatchId: latest.batchId,
      autoLatest: true,
      stockFile,
      regularStockFile,
      costFile,
      purchaseFile,
      pricingFile
    };
  }
  if (!quoteDate || quoteDate === "latest") {
    try {
      return { ...(await discoverLatestQuoteDate()), autoLatest: true };
    } catch (error) {
      try {
        return await discoverFromPreviousBatch(error.message || "自动最新文件组未找到");
      } catch {
        throw error;
      }
    }
  }
  try {
    return { ...(await discoverAutoFilesForDate(quoteDate)), autoLatest: false };
  } catch (error) {
    error.details = {
      ...(error.details || {}),
      requestedDate: quoteDate,
      strictRequestedDate: true
    };
    throw error;
  }
}

async function readDiscoveredFile(file) {
  return {
    fieldName: "",
    fileName: basename(file.fullPath),
    fullPath: file.fullPath,
    buffer: await readFile(file.fullPath),
    bytes: file.size || 0
  };
}

function discoveredFileSummary(discovered = {}) {
  discovered ||= {};
  const parts = [
    discovered.quoteDate ? `读取日期 ${discovered.quoteDate}` : "",
    discovered.stockSourceDir ? `现货1目录 ${discovered.stockSourceDir}` : "",
    discovered.regularStockSourceDir ? `现货目录 ${discovered.regularStockSourceDir}` : "",
    discovered.costSourceDir ? `成本目录 ${discovered.costSourceDir}` : "",
    discovered.sourceDir ? `业务数据目录 ${discovered.sourceDir}` : "",
    discovered.pricingDir ? `定价目录 ${discovered.pricingDir}` : "",
    discovered.fallbackFromRequestedDate ? `所选日期 ${discovered.requestedDate || "-"} 已回退` : "",
    discovered.stockFile?.fileName ? `现货1 ${discovered.stockFile.fileName}` : "",
    discovered.regularStockFile?.fileName ? `现货 ${discovered.regularStockFile.fileName}` : "",
    discovered.costFile?.fileName ? `成本 ${discovered.costFile.fileName}` : "",
    discovered.purchaseFile?.fileName ? `进货明细 ${discovered.purchaseFile.fileName}` : "",
    discovered.pricingFile?.fileName ? `定价表 ${discovered.pricingFile.fileName}` : ""
  ].filter(Boolean);
  return parts.join("；");
}

function repairUploadFileName(value) {
  const text = String(value || "");
  if (!text || /[\u4e00-\u9fff]/.test(text)) return text;
  const repaired = Buffer.from(text, "latin1").toString("utf8");
  return /[\u4e00-\u9fff]/.test(repaired) && !repaired.includes("\uFFFD") ? repaired : text;
}

async function readJsonBody(req) {
  const chunks = [];
  let size = 0;
  for await (const chunk of req) {
    size += chunk.length;
    if (size > 2 * 1024 * 1024) {
      throw Object.assign(new Error("请求内容过大"), { statusCode: 413 });
    }
    chunks.push(chunk);
  }
  const text = Buffer.concat(chunks).toString("utf8").trim();
  if (!text) return {};
  try {
    return JSON.parse(text);
  } catch {
    throw Object.assign(new Error("JSON 请求格式错误"), { statusCode: 400 });
  }
}

function parseMultipart(req) {
  return new Promise((resolvePromise, reject) => {
    const files = {};
    const fields = {};
    let totalBytes = 0;
    let settled = false;

    function fail(error) {
      if (settled) return;
      settled = true;
      reject(error);
    }

    const busboy = Busboy({
      headers: req.headers,
      limits: {
        files: 3,
        fields: 10,
        fileSize: maxBodyBytes
      }
    });

    busboy.on("field", (name, value) => {
      fields[name] = value;
    });
    busboy.on("file", (name, stream, info) => {
      const chunks = [];
      let fileBytes = 0;
      stream.on("data", (chunk) => {
        fileBytes += chunk.length;
        totalBytes += chunk.length;
        if (totalBytes > maxBodyBytes) {
          fail(Object.assign(new Error("导入文件过大，单次最多 800MB"), { statusCode: 413 }));
          stream.resume();
          return;
        }
        chunks.push(chunk);
      });
      stream.on("limit", () => {
        fail(Object.assign(new Error(`${info.filename || name} 超过 800MB`), { statusCode: 413 }));
      });
      stream.on("end", () => {
        files[name] = {
          fieldName: name,
          fileName: repairUploadFileName(info.filename || ""),
          mimeType: info.mimeType || "",
          buffer: Buffer.concat(chunks),
          bytes: fileBytes
        };
      });
    });
    busboy.on("error", fail);
    busboy.on("finish", () => {
      if (settled) return;
      settled = true;
      resolvePromise({ files, fields });
    });
    req.pipe(busboy);
  });
}

function requireUploadFile(files, key, label) {
  const file = files[key];
  if (!file || !file.buffer?.length) {
    throw Object.assign(new Error(`请上传${label}`), { statusCode: 400 });
  }
  return file;
}

function stageError(error, { fileLabel, fileName, stage }) {
  const alreadyContextual = error?.details?.fileLabel || String(error?.message || "").includes(fileLabel);
  const wrapped = alreadyContextual ? error : new Error(`${fileLabel}${fileName ? `「${fileName}」` : ""}：${error.message || "处理失败"}`);
  wrapped.statusCode = error.statusCode || 400;
  wrapped.details = {
    fileLabel,
    fileName,
    stage,
    ...(error.details || {})
  };
  return wrapped;
}

async function parseStage({ file, fileLabel, stage, parser, password }) {
  try {
    return await parser(file.buffer, file.fileName, password);
  } catch (error) {
    throw stageError(error, { fileLabel, fileName: file.fileName, stage });
  }
}

function costCandidateForDetails(file) {
  return {
    fileName: file.fileName || "",
    fullPath: file.fullPath || "",
    parsedDate: file.parsedDate || "",
    matchMode: file.matchMode || "",
    mtimeMs: file.mtimeMs || 0,
    size: file.size || 0
  };
}

function uniqueCostCandidates(discovered = {}) {
  const candidates = [];
  const seen = new Set();
  for (const file of [discovered.costFile, ...(discovered.costCandidates || [])]) {
    if (!file?.fullPath || seen.has(file.fullPath)) continue;
    seen.add(file.fullPath);
    candidates.push(file);
  }
  return candidates;
}

async function parseCostStageWithFallback(discovered, costPassword) {
  const candidates = uniqueCostCandidates(discovered);
  const failures = [];
  for (const candidate of candidates) {
    const file = await readDiscoveredFile(candidate);
    try {
      const cost = await parseStage({
        file,
        fileLabel: "成本表",
        stage: "解密并读取成本表",
        parser: parseCostWorkbook,
        password: costPassword
      });
      return { costFile: file, cost, costCandidateFailures: failures };
    } catch (error) {
      failures.push({
        fileName: file.fileName,
        fullPath: file.fullPath,
        message: error.message || "读取失败",
        stage: error.details?.stage || "解密并读取成本表",
        sourceError: error.details?.sourceError || error.details?.details?.sourceError || ""
      });
    }
  }
  const error = new Error(`成本表全部候选读取失败：${failures.map((item) => item.fileName).join("、") || "未找到候选"}`);
  error.statusCode = 400;
  error.details = {
    fileLabel: "成本表",
    fileName: failures[0]?.fileName || "",
    stage: "解密并读取成本表",
    searchDir: discovered.costSourceDir || "",
    costCandidates: candidates.map(costCandidateForDetails),
    costCandidateFailures: failures
  };
  throw error;
}

async function runAutoProcess(quoteDate) {
  const discovered = await discoverAutoFiles(quoteDate);
  try {
    const pricingPassword = store.pricingPassword() || "1103";
    const costPassword = store.costPassword() || pricingPassword;
    const purchasePassword = store.purchasePassword() || costPassword || pricingPassword;
    const stockFile = await readDiscoveredFile(discovered.stockFile);
    const regularStockFile = await readDiscoveredFile(discovered.regularStockFile);
    const pricingFile = await readDiscoveredFile(discovered.pricingFile);
    const purchaseFile = await readDiscoveredFile(discovered.purchaseFile);

    const stock = await parseStage({ file: stockFile, fileLabel: "现货表", stage: "识别现货表红字行并读取全量定价", parser: parseStockWorkbook });
    const regularStock = await parseStage({
      file: regularStockFile,
      fileLabel: "普通现货表",
      stage: "读取普通现货表用于同步进货明细定价",
      parser: (buffer, fileName) => parseStockWorkbook(buffer, fileName, { allowNoRedRows: true })
    });
    const pricing = await parseStage({ file: pricingFile, fileLabel: "定价表", stage: "解密并读取定价表", parser: parsePricingWorkbook, password: pricingPassword });
    const { costFile, cost, costCandidateFailures } = await parseCostStageWithFallback(discovered, costPassword);
    const purchase = await parseStage({ file: purchaseFile, fileLabel: "进货明细", stage: "解密并读取进货明细", parser: parsePurchaseWorkbook, password: purchasePassword });
    const pricingFileDateLabel = fileDateLabel(discovered.pricingFile?.mtimeMs);
    const pricingFileDisplayName = datedDisplayName(pricingFile.fileName, pricingFileDateLabel);
    const batch = store.processBatch({
      stockFileName: stockFile.fileName,
      pricingFileName: pricingFile.fileName,
      costFileName: costFile.fileName,
      purchaseFileName: purchaseFile.fileName,
      stock,
      purchaseStock: regularStock,
      pricing,
      cost,
      purchase,
      autoFiles: {
        quoteDate: discovered.quoteDate,
        requestedDate: discovered.requestedDate || "",
        fallbackFromRequestedDate: Boolean(discovered.fallbackFromRequestedDate),
        fallbackReason: discovered.fallbackReason || "",
        fallbackFromHistoryBatch: Boolean(discovered.fallbackFromHistoryBatch),
        historyBatchId: discovered.historyBatchId || "",
        stockSourceDir: discovered.stockSourceDir || "",
        regularStockSourceDir: discovered.regularStockSourceDir || "",
        regularStockSourceDirs: discovered.regularStockSourceDirs || [],
        costSourceDir: discovered.costSourceDir || "",
        sourceDir: discovered.sourceDir,
        pricingDir: discovered.pricingDir,
        stockFileName: stockFile.fileName,
        regularStockFileName: regularStockFile.fileName,
        costFileName: costFile.fileName,
        costCandidates: discovered.costCandidates || [],
        costCandidateFailures,
        purchaseFileName: purchaseFile.fileName,
        pricingFileName: pricingFile.fileName,
        pricingFileDateLabel,
        pricingFileDisplayName
      }
    });
    return { discovered: { ...discovered, costFile: { ...discovered.costFile, fileName: costFile.fileName, fullPath: costFile.fullPath } }, batch };
  } catch (error) {
    error.details = {
      ...(error.details || {}),
      discovered
    };
    throw error;
  }
}

async function handleProcess(req, res) {
  let upload = null;
  try {
    upload = await parseMultipart(req);
    const stockFile = requireUploadFile(upload.files, "stock", "现货表");
    const pricingFile = requireUploadFile(upload.files, "pricing", "定价表");
    const costFile = requireUploadFile(upload.files, "cost", "成本表");
    const pricingPassword = String(upload.fields.pricingPassword ?? upload.fields.password ?? store.pricingPassword() ?? "1103");
    const costPassword = String(upload.fields.costPassword ?? store.costPassword() ?? pricingPassword);

    const stock = await parseStage({ file: stockFile, fileLabel: "现货表", stage: "识别现货表红字行", parser: parseStockWorkbook });
    const pricing = await parseStage({ file: pricingFile, fileLabel: "定价表", stage: "解密并读取定价表", parser: parsePricingWorkbook, password: pricingPassword });
    const cost = await parseStage({ file: costFile, fileLabel: "成本表", stage: "解密并读取成本表", parser: parseCostWorkbook, password: costPassword });
    const result = store.processBatch({
      stockFileName: stockFile.fileName,
      pricingFileName: pricingFile.fileName,
      costFileName: costFile.fileName,
      stock,
      pricing,
      cost
    });
    return sendJson(res, 200, { ok: true, batch: result });
  } catch (error) {
    const fileName = error.details?.fileName || upload?.files?.stock?.fileName || "";
    const log = store.addOperationLog({
      action: "生成批次",
      target: "待定价",
      fileName,
      status: "failure",
      summary: error.message || "处理失败",
      details: {
        error: error.message || "处理失败",
        fileLabel: error.details?.fileLabel || "",
        fileName,
        stage: error.details?.stage || "",
        details: error.details || null
      }
    });
    return sendJson(res, error.statusCode || 500, {
      error: error.message || "处理失败",
      details: error.details || null,
      failedAt: log.createdAt,
      logId: log.id
    });
  }
}

async function handleAutoExport(req, res, url) {
  const quoteDate = url.searchParams.get("date") || "latest";
  let discovered = null;
  try {
    const result = await runAutoProcess(quoteDate);
    discovered = result.discovered;
    const body = Buffer.from(await store.exportBatch(result.batch.id));
    const autoFiles = result.batch.details?.autoFiles || {};
    return send(res, 200, body, {
      "Content-Type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
      "Content-Disposition": `attachment; filename*=UTF-8''${exportFileName(`待定价表_${result.batch.quoteDate}.xlsx`)}`,
      "Cache-Control": "no-store",
      "X-Hanmi-Batch-Id": result.batch.id,
      "X-Hanmi-Auto-Files": headerJson(autoFiles)
    });
  } catch (error) {
    const errorDiscovered = discovered || error.details?.discovered || null;
    const readFilesSummary = discoveredFileSummary(errorDiscovered);
    const fileName = error.details?.fileName || readFilesSummary || errorDiscovered?.stockFile?.fileName || "";
    const log = store.addOperationLog({
      action: "一键生成导出",
      target: "当天待定价表",
      fileName,
      status: "failure",
      summary: `${error.message || "一键生成导出失败"}${readFilesSummary ? `；读取文件：${readFilesSummary}` : ""}`,
      details: {
        quoteDate,
        discovered: errorDiscovered,
        readFilesSummary,
        error: error.message || "一键生成导出失败",
        fileLabel: error.details?.fileLabel || "",
        fileName,
        stage: error.details?.stage || "",
        details: error.details || null
      }
    });
    return sendJson(res, error.statusCode || 500, {
      error: error.message || "一键生成导出失败",
      details: error.details || null,
      discovered: errorDiscovered,
      failedAt: log.createdAt,
      logId: log.id
    });
  }
}

async function serveStatic(res, pathname) {
  const target = pathname === "/" ? join(publicRoot, "index.html") : join(publicRoot, pathname.replace(/^\/+/, ""));
  const resolved = resolve(target);
  if (!resolved.startsWith(publicRoot) || !existsSync(resolved)) {
    return send(res, 404, "Not found", { "Content-Type": "text/plain; charset=utf-8" });
  }
  const body = await readFile(resolved);
  return send(res, 200, body, { "Content-Type": contentType(resolved), "Cache-Control": "no-store" });
}

const server = http.createServer(async (req, res) => {
  try {
    if (req.method === "OPTIONS") {
      return send(res, 204, "", {
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Methods": "GET,POST,OPTIONS",
        "Access-Control-Allow-Headers": "Content-Type,X-File-Name"
      });
    }

    const url = requestUrl(req);
    if (req.method === "GET" && url.pathname === "/api/health") {
      return sendJson(res, 200, {
        ok: true,
        service: "hanmi-local-pricing-tool",
        dbPath: store.dbPath,
        appRoot: root,
        dataDir: localDataDir,
        ...buildInfo
      });
    }
    if (req.method === "GET" && url.pathname === "/api/settings") {
      return sendJson(res, 200, store.settings());
    }
    if (req.method === "POST" && url.pathname === "/api/settings") {
      const body = await readJsonBody(req);
      return sendJson(res, 200, store.updateSettings(body));
    }
    if (req.method === "GET" && url.pathname === "/api/stats") {
      return sendJson(res, 200, store.stats());
    }
    if (req.method === "GET" && url.pathname === "/api/batches") {
      return sendJson(res, 200, store.batches(queryObject(url)));
    }
    if (req.method === "GET" && url.pathname.startsWith("/api/batches/")) {
      const batchId = decodeURIComponent(url.pathname.split("/").pop() || "");
      const result = store.batch(batchId, { includeRows: url.searchParams.get("includeRows") === "1" });
      if (!result) return sendJson(res, 404, { error: "未找到批次" });
      return sendJson(res, 200, result);
    }
    if (req.method === "GET" && url.pathname === "/api/items") {
      return sendJson(res, 200, store.queryItems(queryObject(url)));
    }
    if (req.method === "GET" && url.pathname === "/api/logs") {
      return sendJson(res, 200, store.operationLogs(queryObject(url)));
    }
    if (req.method === "POST" && url.pathname === "/api/process") {
      return await handleProcess(req, res);
    }
    if (req.method === "GET" && url.pathname === "/api/auto-files") {
      const quoteDate = url.searchParams.get("date") || "latest";
      return sendJson(res, 200, await discoverAutoFiles(quoteDate));
    }
    if (req.method === "GET" && url.pathname === "/api/export/auto") {
      return await handleAutoExport(req, res, url);
    }
    if (req.method === "POST" && /^\/api\/batches\/[^/]+\/confirm$/.test(url.pathname)) {
      const batchId = decodeURIComponent(url.pathname.split("/")[3] || "");
      const body = await readJsonBody(req);
      return sendJson(res, 200, store.confirmBatch(batchId, body.itemIds || []));
    }
    if (req.method === "GET" && /^\/api\/export\/batches\/[^/]+$/.test(url.pathname)) {
      const batchId = decodeURIComponent(url.pathname.split("/").pop() || "");
      const target = store.batch(batchId);
      if (!target) return sendJson(res, 404, { error: "未找到批次" });
      try {
        const body = Buffer.from(await store.exportBatch(batchId));
        return send(res, 200, body, {
          "Content-Type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
          "Content-Disposition": `attachment; filename*=UTF-8''${exportFileName(`待定价表_${target.quoteDate}.xlsx`)}`,
          "Cache-Control": "no-store"
        });
      } catch (error) {
        const log = store.addOperationLog({
          action: "导出",
          target: "当天待定价表",
          fileName: target.stockFileName,
          status: "failure",
          summary: error.message || "导出失败",
          details: { batchId, error: error.message || "导出失败" }
        });
        return sendJson(res, error.statusCode || 500, {
          error: error.message || "导出失败",
          failedAt: log.createdAt,
          logId: log.id
        });
      }
    }

    if (req.method === "GET") return await serveStatic(res, url.pathname);
    return sendJson(res, 405, { error: "method not allowed" });
  } catch (error) {
    return sendError(res, error);
  }
});

server.listen(defaultPort, "127.0.0.1", () => {
  console.log(JSON.stringify({ ok: true, port: defaultPort, dbPath: store.dbPath }));
});
