const http = require("http");
const fs = require("fs").promises;
const path = require("path");
const crypto = require("crypto");

const rootDir = __dirname;
const port = Number(process.env.PORT || 8787);
const host = process.env.HOST || "0.0.0.0";
const sessions = new Map();
const customerTiers = ["A", "A+"];

function getInitialPassword(username) {
  const key = `KEYISSUE_${username.toUpperCase()}_PASSWORD`;
  if (process.env[key]) return process.env[key];
  const digest = crypto.createHash("sha256").update(`${username}:${rootDir}`).digest("hex").slice(0, 12);
  return `keyissue-${digest}`;
}

function hashPassword(password, salt = crypto.randomBytes(16).toString("hex")) {
  const hash = crypto.pbkdf2Sync(String(password), salt, 120000, 32, "sha256").toString("hex");
  return `pbkdf2:sha256:120000:${salt}:${hash}`;
}

function verifyPasswordHash(password, encodedHash) {
  const parts = String(encodedHash || "").split(":");
  if (parts.length !== 5 || parts[0] !== "pbkdf2" || parts[1] !== "sha256") return false;
  const iterations = Number(parts[2]);
  const salt = parts[3];
  const expected = parts[4];
  if (!iterations || !salt || !expected) return false;
  const actual = crypto.pbkdf2Sync(String(password), salt, iterations, 32, "sha256").toString("hex");
  const expectedBuffer = Buffer.from(expected, "hex");
  const actualBuffer = Buffer.from(actual, "hex");
  return expectedBuffer.length === actualBuffer.length && crypto.timingSafeEqual(expectedBuffer, actualBuffer);
}

function setUserPassword(user, password) {
  user.passwordHash = hashPassword(password);
  delete user.password;
}

function verifyUserPassword(user, password) {
  if (!user) return false;
  if (user.passwordHash) return verifyPasswordHash(password, user.passwordHash);
  return user.password === password;
}

const pageCatalog = [
  { key: "dashboard", name: "动态驾驶舱" },
  { key: "customers", name: "A/A+客户库" },
  { key: "inspections", name: "每日线上巡查" },
  { key: "issues", name: "问题反馈池" },
  { key: "audit", name: "监察核查" },
  { key: "analysis", name: "问题分析" },
  { key: "rules", name: "预警规则" },
  { key: "admin", name: "系统后台" }
];

const permissionCatalog = [
  { key: "issue:view", name: "查看问题详情" },
  { key: "issue:create", name: "提交问题反馈" },
  { key: "issue:assign", name: "指派责任人" },
  { key: "issue:handle", name: "提交处置" },
  { key: "inspection:view", name: "查看每日巡查" },
  { key: "inspection:create", name: "提交每日巡查" },
  { key: "audit:review", name: "监察核查" },
  { key: "customer:submit", name: "提交客户入池申请" },
  { key: "customer:approve", name: "审核客户入池" },
  { key: "rules:edit", name: "保存预警规则" },
  { key: "admin:manage", name: "系统后台管理" }
];

let roles = {
  admin: {
    roleName: "系统管理员",
    summary: "全量功能、规则配置、后台权限管理",
    pages: ["dashboard", "customers", "inspections", "issues", "audit", "analysis", "rules", "admin"],
    permissions: ["*", "admin:manage"]
  },
  executive: {
    roleName: "管理层",
    summary: "全局查看、督办、指派责任人",
    pages: ["dashboard", "customers", "inspections", "issues", "audit", "analysis"],
    permissions: ["issue:view", "issue:assign", "inspection:view", "customer:submit"]
  },
  delivery: {
    roleName: "交付助理",
    summary: "提交每日巡查、问题反馈、查看客户与问题",
    pages: ["dashboard", "customers", "inspections", "issues", "analysis"],
    permissions: ["issue:view", "issue:create", "inspection:view", "inspection:create", "customer:submit"]
  },
  deliveryManager: {
    roleName: "交付经理",
    summary: "交付线全量查看、问题督办与客户入池提交",
    pages: ["dashboard", "customers", "inspections", "issues", "audit", "analysis"],
    permissions: ["issue:view", "issue:create", "issue:assign", "issue:handle", "inspection:view", "inspection:create", "customer:submit"]
  },
  customerOwner: {
    roleName: "客管人员",
    summary: "仅查看本人负责客户、提交反馈与客户入池申请",
    pages: ["dashboard", "customers", "inspections", "issues", "analysis"],
    permissions: ["issue:view", "issue:create", "issue:handle", "inspection:view", "inspection:create", "customer:submit"]
  },
  customerManager: {
    roleName: "客管经理",
    summary: "查看本部门客管人员名下客户、督办反馈与客户入池提交",
    pages: ["dashboard", "customers", "inspections", "issues", "analysis"],
    permissions: ["issue:view", "issue:create", "issue:assign", "issue:handle", "inspection:view", "inspection:create", "customer:submit"]
  },
  manager: {
    roleName: "项目经理",
    summary: "问题处置、提交巡查、提交核查、补充根因",
    pages: ["dashboard", "customers", "inspections", "issues", "analysis"],
    permissions: ["issue:view", "issue:handle", "inspection:view", "inspection:create", "customer:submit"]
  },
  designer: {
    roleName: "设计师",
    summary: "查看客户与问题、提交设计相关反馈、补充处置记录",
    pages: ["dashboard", "customers", "inspections", "issues", "analysis"],
    permissions: ["issue:view", "issue:create", "issue:handle", "inspection:view", "inspection:create", "customer:submit"]
  },
  designManager: {
    roleName: "设计经理",
    summary: "设计问题统筹、责任指派、处置跟进与客户入池提交",
    pages: ["dashboard", "customers", "inspections", "issues", "analysis"],
    permissions: ["issue:view", "issue:create", "issue:assign", "issue:handle", "inspection:view", "inspection:create", "customer:submit"]
  },
  auditor: {
    roleName: "监察部",
    summary: "查看巡查、监察核查、通过或驳回整改",
    pages: ["dashboard", "customers", "inspections", "issues", "audit", "analysis"],
    permissions: ["issue:view", "audit:review", "inspection:view"]
  },
  supplyChain: {
    roleName: "供应链",
    summary: "供应链相关岗位全量查看客户与问题反馈",
    pages: ["dashboard", "customers", "inspections", "issues", "analysis"],
    permissions: ["issue:view", "issue:create", "inspection:view", "customer:submit"]
  }
};

let users = [
  { id: "U001", username: "admin", password: getInitialPassword("admin"), displayName: "肖总", role: "admin", status: "启用" },
  { id: "U002", username: "executive", password: getInitialPassword("executive"), displayName: "管理层", role: "executive", status: "启用" },
  { id: "U003", username: "delivery", password: getInitialPassword("delivery"), displayName: "交付助理", role: "delivery", status: "启用" },
  { id: "U004", username: "manager", password: getInitialPassword("manager"), displayName: "项目经理", role: "manager", status: "启用" },
  { id: "U005", username: "auditor", password: getInitialPassword("auditor"), displayName: "监察部", role: "auditor", status: "启用" },
  { id: "U006", username: "designer", password: getInitialPassword("designer"), displayName: "设计师", role: "designer", status: "启用" },
  { id: "U007", username: "design_manager", password: getInitialPassword("design_manager"), displayName: "设计经理", role: "designManager", status: "启用" }
];

let dictionaries = {
  departments: ["光宸", "澜光", "蘭堂", "焕境"],
  customerSources: ["转介绍", "公司特殊关系", "标杆项目", "风险关注"],
  issueSources: ["线下巡检", "客户群反馈", "每日线上巡查", "客户主动反馈", "管理层转办", "监察部发现"],
  issueTypes: ["施工质量", "设计变更", "沟通响应", "材料交付", "客户满意度"],
  progressStatuses: ["正常", "延期"]
};

const requiredRoleDefaults = {
  executive: {
    roleName: "管理层",
    summary: "全局查看、督办、指派责任人",
    pages: ["dashboard", "customers", "inspections", "issues", "audit", "analysis"],
    permissions: ["issue:view", "issue:assign", "inspection:view", "customer:submit"]
  },
  deliveryManager: {
    roleName: "交付经理",
    summary: "交付线全量查看、问题督办与客户入池提交",
    pages: ["dashboard", "customers", "inspections", "issues", "audit", "analysis"],
    permissions: ["issue:view", "issue:create", "issue:assign", "issue:handle", "inspection:view", "inspection:create", "customer:submit"]
  },
  customerOwner: {
    roleName: "客管人员",
    summary: "仅查看本人负责客户、提交反馈与客户入池申请",
    pages: ["dashboard", "customers", "inspections", "issues", "analysis"],
    permissions: ["issue:view", "issue:create", "issue:handle", "inspection:view", "inspection:create", "customer:submit"]
  },
  customerManager: {
    roleName: "客管经理",
    summary: "查看本部门客管人员名下客户、督办反馈与客户入池提交",
    pages: ["dashboard", "customers", "inspections", "issues", "analysis"],
    permissions: ["issue:view", "issue:create", "issue:assign", "issue:handle", "inspection:view", "inspection:create", "customer:submit"]
  },
  designer: {
    roleName: "设计师",
    summary: "查看客户与问题、提交设计相关反馈、补充处置记录",
    pages: ["dashboard", "customers", "inspections", "issues", "analysis"],
    permissions: ["issue:view", "issue:create", "issue:handle", "inspection:view", "inspection:create", "customer:submit"]
  },
  designManager: {
    roleName: "设计经理",
    summary: "设计问题统筹、责任指派、处置跟进与客户入池提交",
    pages: ["dashboard", "customers", "inspections", "issues", "analysis"],
    permissions: ["issue:view", "issue:create", "issue:assign", "issue:handle", "inspection:view", "inspection:create", "customer:submit"]
  },
  auditor: {
    roleName: "监察部",
    summary: "查看客户、巡查、监察核查、通过或驳回整改",
    pages: ["dashboard", "customers", "inspections", "issues", "audit", "analysis"],
    permissions: ["issue:view", "audit:review", "inspection:view"]
  },
  supplyChain: {
    roleName: "供应链",
    summary: "供应链相关岗位全量查看客户与问题反馈",
    pages: ["dashboard", "customers", "inspections", "issues", "analysis"],
    permissions: ["issue:view", "issue:create", "inspection:view", "customer:submit"]
  }
};

const requiredUserDefaults = [
  { username: "designer", displayName: "设计师", role: "designer" },
  { username: "design_manager", displayName: "设计经理", role: "designManager" }
];

let rosterMembers = [];

let customers = [
  { id: "C001", dataTag: "测试", name: "肖女士", tier: "A+", source: "转介绍", keyCustomerReason: "业主具备持续转介绍潜力，当前木作节点需要重点口碑维护。", customerOwner: "客管负责人-罗敏", address: "阅花溪 9 栋 B 座 1702", department: "光宸", designer: "蔡菁", manager: "刘江新", stage: "木作安装", startDate: "2026-05-18", plannedFinishDate: "2026-08-20", progressStatus: "延期", satisfaction: 2, inspectionStatus: "异常", inspectionAt: "2026-07-04 09:30", inspector: "交付助理", inspectionSummary: "客户群反馈木作收口疑问，现场材料堆放偏乱。" },
  { id: "C002", dataTag: "测试", name: "周先生", tier: "A+", source: "标杆项目", keyCustomerReason: "别墅全案标杆项目，后续样板案例展示价值高。", customerOwner: "客管负责人-周芸", address: "江山府别墅 18 号", department: "澜光", designer: "何敏", manager: "张峥", stage: "水电验收", startDate: "2026-06-02", plannedFinishDate: "2026-09-15", progressStatus: "正常", satisfaction: 3, inspectionStatus: "已巡查", inspectionAt: "2026-07-04 10:10", inspector: "设计师", inspectionSummary: "群内沟通已回复，水电验收节点正常。" },
  { id: "C003", dataTag: "测试", name: "林女士", tier: "A", source: "特殊关系", keyCustomerReason: "公司特殊关系客户，主材确认阶段需要保持高频同步。", customerOwner: "客管负责人-陈希", address: "云麓大平层 2601", department: "蘭堂", designer: "邱然", manager: "顾森", stage: "主材确认", startDate: "2026-06-10", plannedFinishDate: "2026-10-01", progressStatus: "正常", satisfaction: 4, inspectionStatus: "已巡查", inspectionAt: "2026-07-04 10:45", inspector: "交付助理", inspectionSummary: "主材确认进度正常，客户暂无新增反馈。" },
  { id: "C004", dataTag: "测试", name: "陈先生", tier: "A", source: "风险关注", keyCustomerReason: "油漆阶段延期风险已显现，需要纳入重点风险控制。", customerOwner: "客管负责人-何静", address: "天境花园 5 栋 902", department: "焕境", designer: "姚晨", manager: "蒋川", stage: "油漆阶段", startDate: "2026-05-25", plannedFinishDate: "2026-07-30", progressStatus: "延期", satisfaction: 3, inspectionStatus: "未巡查", inspectionAt: "-", inspector: "-", inspectionSummary: "今日尚未提交线上巡查。" },
  { id: "C005", dataTag: "测试", name: "王女士", tier: "A+", source: "标杆项目", keyCustomerReason: "竣工交付案例价值高，软装点位变更需重点关注。", customerOwner: "客管负责人-罗敏", address: "中海天钻 11 栋 1801", department: "光宸", designer: "苏禧", manager: "李成", stage: "竣工交付", startDate: "2026-04-28", plannedFinishDate: "2026-07-20", progressStatus: "正常", satisfaction: 3, inspectionStatus: "异常", inspectionAt: "2026-07-04 11:20", inspector: "服务部", inspectionSummary: "客户提到软装点位变更记录需要再次确认。" }
];

let customerDrafts = [
  {
    id: "CR-240701",
    dataTag: "测试",
    status: "待审核",
    submittedBy: "交付助理",
    submittedRole: "交付助理",
    submittedAt: "2026-07-04 14:20",
    reviewedBy: "",
    reviewedAt: "",
    reviewNote: "",
    customerId: "",
    customer: {
      name: "许女士",
      tier: "A+",
      source: "客户群反馈",
      keyCustomerReason: "客户在群内明确表达对交付节奏的担忧，同时具备转介绍潜力，建议纳入重点客户跟进。",
      customerOwner: "客管负责人-周芸",
      address: "麓湖云境 7 栋 1501",
      department: "澜光",
      designer: "何敏",
      manager: "张峥",
      stage: "泥木阶段",
      startDate: "2026-06-18",
      plannedFinishDate: "2026-09-28",
      progressStatus: "正常",
      satisfaction: 3
    }
  }
];

let inspections = [
  { id: "R-240701", dataTag: "测试", customerId: "C001", customerName: "肖女士", inspector: "交付助理", status: "异常", createdAt: "2026-07-04 09:30", siteQuality: "木作收口与效果图有差异", civility: "现场材料堆放偏乱", groupFeedback: "客户群追问整改到场时间", summary: "需设计师和项目经理共同到场说明整改口径。" },
  { id: "R-240702", dataTag: "测试", customerId: "C002", customerName: "周先生", inspector: "设计师", status: "已巡查", createdAt: "2026-07-04 10:10", siteQuality: "水电验收节点正常", civility: "现场整洁", groupFeedback: "客户群暂无新增疑问", summary: "按计划推进，保持每日同步。" },
  { id: "R-240703", dataTag: "测试", customerId: "C005", customerName: "王女士", inspector: "服务部", status: "异常", createdAt: "2026-07-04 11:20", siteQuality: "竣工收口基本完成", civility: "现场已清理", groupFeedback: "客户要求复核软装点位", summary: "建议补齐变更确认链路后回访客户。" }
];

let issues = [
  {
    id: "I-240701",
    dataTag: "测试",
    customerId: "C001",
    type: "施工质量",
    source: "客户主动反馈",
    status: "红色预警",
    owner: "刘江新",
    score: 91,
    summary: "客户反馈木作收口与效果图存在明显差异，希望项目经理今天到场确认整改方案。",
    rootCause: "设计交底与现场深化尺寸未完全同步",
    action: "项目经理 24 小时内到场复核",
    handlerNote: "已约设计师、项目经理今日 16:00 到场",
    postSatisfaction: "待回访",
    submitter: "交付助理",
    auditDue: "2026-07-02",
    handlingRecords: [
      { handler: "刘江新", handledAt: "2026-07-04 10:30", note: "已约设计师、项目经理到场复核木作收口，现场确认整改口径。", satisfaction: "待回访", feedback: "客户要求当天给出明确整改时间。" }
    ],
    timeline: ["客户反馈木作收口与效果图存在明显差异", "二次反馈触发红色预警", "建议设计师、项目经理、监察部联合到场确认整改口径"]
  },
  {
    id: "I-240702",
    dataTag: "测试",
    customerId: "C002",
    type: "沟通响应",
    source: "管理层转办",
    status: "红色预警",
    owner: "张峥",
    score: 86,
    summary: "客户连续两次反馈响应慢，担心后续交付过程失控。",
    rootCause: "设计师与项目经理对外答复口径不一致",
    action: "高层陪访并明确沟通机制",
    handlerNote: "已指定单一客户接口，每日 18:00 同步",
    postSatisfaction: "一般",
    submitter: "客户负责人",
    auditDue: "2026-07-03",
    handlingRecords: [
      { handler: "张峥", handledAt: "2026-07-03 18:00", note: "指定单一客户接口，每日固定同步进度。", satisfaction: "一般", feedback: "客户接受固定同步，但要求本周持续观察。" }
    ],
    timeline: ["客户连续两次反馈响应慢", "担心后续交付失控", "建议设定单一接口与固定反馈节奏"]
  },
  {
    id: "I-240703",
    dataTag: "测试",
    customerId: "C003",
    type: "材料交付",
    source: "线下巡检",
    status: "橙色预警",
    owner: "顾森",
    score: 73,
    summary: "进口岩板到货时间不确定，客户担心影响工期。",
    rootCause: "供应商排产反馈滞后",
    action: "采购补充替代方案",
    handlerNote: "待采购提供同等级替代材料",
    postSatisfaction: "待处置",
    submitter: "交付助理",
    auditDue: "2026-07-04",
    handlingRecords: [],
    timeline: ["进口岩板到货时间不确定", "客户担心影响工期", "建议 48 小时内提供替代材料与影响测算"]
  },
  {
    id: "I-240704",
    dataTag: "测试",
    customerId: "C004",
    type: "施工质量",
    source: "监察部发现",
    status: "待核查",
    owner: "蒋川",
    score: 68,
    summary: "监察部发现墙面基层处理验收不细，要求复验整改。",
    rootCause: "基层处理验收不细",
    action: "等待监察部复验",
    handlerNote: "墙面修补已完成，提交复验",
    postSatisfaction: "满意",
    submitter: "项目经理",
    auditDue: "2026-07-01",
    handlingRecords: [
      { handler: "蒋川", handledAt: "2026-07-01 16:20", note: "完成墙面修补并提交复验。", satisfaction: "满意", feedback: "客户确认修补效果可接受。" }
    ],
    timeline: ["墙面修补已完成", "项目经理提交完成申请", "监察部复验通过后标记已处理并纳入班组质量复盘"]
  },
  {
    id: "I-240705",
    dataTag: "测试",
    customerId: "C005",
    type: "设计变更",
    source: "客户主动反馈",
    status: "待核查",
    owner: "苏禧",
    score: 79,
    summary: "客户反馈软装点位与最终确认稿不一致，要求重新核对。",
    rootCause: "变更记录分散在微信沟通中",
    action: "核查变更签字与交付一致性",
    handlerNote: "已补齐变更确认链路",
    postSatisfaction: "待核查",
    submitter: "设计师",
    auditDue: "2026-07-01",
    handlingRecords: [
      { handler: "苏禧", handledAt: "2026-07-02 11:10", note: "补齐点位变更确认记录，并提交监察核查。", satisfaction: "待核查", feedback: "客户要求交付前再核对一次。" }
    ],
    timeline: ["客户反馈软装点位与最终确认稿不一致", "变更记录未固化到项目台账", "建议形成交付前核对清单"]
  },
  {
    id: "I-240706",
    dataTag: "测试",
    customerId: "C002",
    type: "客户满意度",
    source: "线下巡检",
    status: "已处理",
    owner: "何敏",
    score: 66,
    summary: "满意度低于 3 分，客户认为节点说明不够清晰。",
    rootCause: "客户期望与节点说明不一致",
    action: "已完成解释与节点确认",
    handlerNote: "监察部核查通过",
    postSatisfaction: "满意",
    submitter: "监察部",
    auditDue: "2026-06-29",
    handlingRecords: [
      { handler: "何敏", handledAt: "2026-06-29 15:00", note: "补充节点说明并重新确认计划。", satisfaction: "满意", feedback: "客户确认接受新计划。" }
    ],
    timeline: ["满意度低于 3 分触发预警", "设计师补充节点说明", "客户确认接受新计划，监察部标记已处理"]
  }
];

let rulesState = {
  updatedAt: null,
  updatedBy: null
};

const dataFile = process.env.DATA_FILE || (
  process.platform === "win32"
    ? path.join(rootDir, "data", "app-state.json")
    : "/var/lib/keyissue-prototype/app-state.json"
);
let persistQueue = Promise.resolve();

function getStateSnapshot() {
  return {
    version: 1,
    savedAt: new Date().toISOString(),
    users,
    roles,
    dictionaries,
    customers,
    customerDrafts,
    inspections,
    issues,
    rulesState
  };
}

function cloneDefault(value) {
  return JSON.parse(JSON.stringify(value));
}

function ensureRequiredRolesAndUsers() {
  let changed = false;
  Object.entries(requiredRoleDefaults).forEach(([roleKey, defaults]) => {
    if (!roles[roleKey]) {
      roles[roleKey] = cloneDefault(defaults);
      changed = true;
      return;
    }
    ["roleName", "summary", "pages", "permissions"].forEach((field) => {
      if (!roles[roleKey][field]) {
        roles[roleKey][field] = cloneDefault(defaults[field]);
        changed = true;
        return;
      }
      if (Array.isArray(defaults[field]) && Array.isArray(roles[roleKey][field])) {
        defaults[field].forEach((item) => {
          if (!roles[roleKey][field].includes(item)) {
            roles[roleKey][field].push(item);
            changed = true;
          }
        });
      }
    });
  });

  requiredUserDefaults.forEach((spec) => {
    if (users.some((user) => user.username === spec.username)) return;
    const managedUser = {
      id: nextUserId(),
      username: spec.username,
      displayName: spec.displayName,
      role: spec.role,
      status: "启用"
    };
    setUserPassword(managedUser, getInitialPassword(spec.username));
    users.push(managedUser);
    changed = true;
  });

  users.forEach((user) => {
    if (user.password && !user.passwordHash) {
      setUserPassword(user, user.password);
      changed = true;
    }
    if (user.registerSource !== "roster") return;
    const member = findRosterMemberForUser(user);
    if (!member) return;
    const nextRole = mapRosterMemberToRole(member);
    const nextDepartment = getRosterScopeDepartment(member);
    const nextPosition = member.position || "";
    if (user.role !== nextRole) {
      user.role = nextRole;
      changed = true;
    }
    if ((user.department || "") !== nextDepartment) {
      user.department = nextDepartment;
      changed = true;
    }
    if ((user.position || "") !== nextPosition) {
      user.position = nextPosition;
      changed = true;
    }
  });
  return changed;
}

function applyPersistedState(state) {
  if (!state || typeof state !== "object") return;
  if (Array.isArray(state.users)) users = state.users;
  if (state.roles && typeof state.roles === "object") roles = state.roles;
  if (state.dictionaries && typeof state.dictionaries === "object") dictionaries = state.dictionaries;
  if (Array.isArray(state.customers)) customers = state.customers;
  if (Array.isArray(state.customerDrafts)) customerDrafts = state.customerDrafts;
  if (Array.isArray(state.inspections)) inspections = state.inspections;
  if (Array.isArray(state.issues)) issues = state.issues;
  if (state.rulesState && typeof state.rulesState === "object") rulesState = state.rulesState;
}

async function loadState() {
  let loadedPersistedState = false;
  try {
    const raw = await fs.readFile(dataFile, "utf8");
    applyPersistedState(JSON.parse(raw));
    loadedPersistedState = true;
    console.log(`Loaded persisted data from ${dataFile}`);
  } catch (error) {
    if (error.code !== "ENOENT") throw error;
  }
  const changed = ensureRequiredRolesAndUsers();
  if (loadedPersistedState && changed) await persistState();
}

async function loadRoster() {
  const rosterFile = path.join(rootDir, "data", "roster-members.json");
  try {
    const raw = await fs.readFile(rosterFile, "utf8");
    const data = JSON.parse(raw);
    let activeDepartment = "";
    rosterMembers = (Array.isArray(data.members) ? data.members : []).map((member) => {
      const department = String(member.department || "").trim();
      if (department) activeDepartment = department;
      return {
        ...member,
        scopeDepartment: inferRosterScopeDepartment(member, activeDepartment)
      };
    });
    console.log(`Loaded roster whitelist: ${rosterMembers.length} members`);
  } catch (error) {
    if (error.code !== "ENOENT") throw error;
    rosterMembers = [];
    console.warn("Roster whitelist file not found; registration is disabled.");
  }
}

async function persistState() {
  persistQueue = persistQueue.catch(() => {}).then(async () => {
    await fs.mkdir(path.dirname(dataFile), { recursive: true });
    const tempFile = `${dataFile}.${process.pid}.tmp`;
    await fs.writeFile(tempFile, JSON.stringify(getStateSnapshot(), null, 2), "utf8");
    await fs.rename(tempFile, dataFile);
  });
  return persistQueue;
}

const mimeTypes = {
  ".html": "text/html; charset=utf-8",
  ".css": "text/css; charset=utf-8",
  ".js": "application/javascript; charset=utf-8",
  ".json": "application/json; charset=utf-8",
  ".md": "text/markdown; charset=utf-8",
  ".png": "image/png",
  ".jpg": "image/jpeg",
  ".jpeg": "image/jpeg",
  ".svg": "image/svg+xml; charset=utf-8",
  ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
};

function sendJson(res, status, body) {
  const payload = JSON.stringify(body);
  res.writeHead(status, {
    "Content-Type": "application/json; charset=utf-8",
    "Content-Length": Buffer.byteLength(payload),
    "Cache-Control": "no-store"
  });
  res.end(payload);
}

function sendError(res, status, code, message) {
  sendJson(res, status, { error: code, message });
}

async function readBody(req) {
  let raw = "";
  for await (const chunk of req) raw += chunk;
  if (!raw) return {};
  try {
    return JSON.parse(raw);
  } catch {
    const error = new Error("请求体不是有效的 JSON。");
    error.status = 400;
    throw error;
  }
}

function publicUser(user) {
  const role = roles[user.role];
  return {
    id: user.id,
    loginName: user.username,
    userName: user.displayName,
    roleKey: user.role,
    roleName: role.roleName,
    status: user.status,
    pages: role.pages,
    permissions: role.permissions
  };
}

function publicManagedUser(user) {
  return {
    id: user.id,
    username: user.username,
    displayName: user.displayName,
    phone: user.phone || "",
    department: user.department || "",
    position: user.position || "",
    registerSource: user.registerSource || "",
    role: user.role,
    roleName: roles[user.role]?.roleName || user.role,
    status: user.status || "启用"
  };
}

function publicCustomerDraft(draft) {
  return {
    id: draft.id,
    dataTag: draft.dataTag,
    status: draft.status,
    submittedBy: draft.submittedBy,
    submittedRole: draft.submittedRole,
    submittedAt: draft.submittedAt,
    reviewedBy: draft.reviewedBy,
    reviewedAt: draft.reviewedAt,
    reviewNote: draft.reviewNote,
    customerId: draft.customerId,
    customer: draft.customer
  };
}

function createSessionToken() {
  if (typeof crypto.randomUUID === "function") return crypto.randomUUID();
  return crypto.randomBytes(24).toString("hex");
}

function getBearerToken(req) {
  const header = req.headers.authorization || "";
  const [type, token] = header.split(" ");
  return type === "Bearer" ? token : "";
}

function getCurrentUser(req) {
  const token = getBearerToken(req);
  const userId = sessions.get(token);
  const user = users.find((item) => item.id === userId) || null;
  return user && user.status !== "停用" ? user : null;
}

function can(user, permission) {
  if (!user) return false;
  const role = roles[user.role];
  if (role.permissions.includes("*")) return true;
  if (permission.startsWith("page:")) return role.pages.includes(permission.replace("page:", ""));
  return role.permissions.includes(permission);
}

function normalizeTier(value) {
  const tier = String(value || "").trim().toUpperCase();
  return customerTiers.includes(tier) ? tier : null;
}

function normalizeProgressStatus(value) {
  const status = String(value || "").trim();
  return dictionaries.progressStatuses.includes(status) ? status : dictionaries.progressStatuses[0] || "正常";
}

function normalizeName(value) {
  return String(value || "").trim().replace(/\s+/g, "");
}

function normalizePhone(value) {
  return String(value || "").replace(/\D/g, "");
}

function normalizeDepartment(value) {
  return normalizeName(value)
    .replace(/筑境/g, "")
    .replace(/设计工作室/g, "")
    .replace(/工作室/g, "")
    .replace(/中心/g, "")
    .replace(/部门/g, "部");
}

function getRosterScopeDepartment(member) {
  return String(member?.scopeDepartment || member?.department || "").trim();
}

function inferRosterScopeDepartment(member, fallbackDepartment = "") {
  const department = String(member?.department || "").trim();
  if (department) return department;
  const position = String(member?.position || "").trim();
  if (/运营副总|总经理助理|AI工程师/.test(position)) return "总经办";
  if (/监察/.test(position)) return "监察部";
  if (/客管|客户经理/.test(position)) return String(fallbackDepartment || "客管部").trim();
  if (/设计经理|设计总监|设计师|深化设计|灯光设计|定制设计/.test(position)) return String(fallbackDepartment || "设计部").trim();
  if (/交付总监|交付经理|交付助理|产业交付/.test(position)) return String(fallbackDepartment || "交付部").trim();
  if (/供应链|采购|库管|结算|跟单|主材|辅材|定制/.test(position)) return String(fallbackDepartment || "供应链中心").trim();
  return String(fallbackDepartment || "").trim();
}

function getCustomerOwnerName(customer) {
  return String(customer?.customerOwner || "")
    .replace(/^客管负责人[-—:：]?/, "")
    .replace(/^客户负责人[-—:：]?/, "")
    .trim();
}

function findRosterMemberByName(name) {
  const target = normalizeName(name);
  if (!target) return null;
  return rosterMembers.find((member) => normalizeName(member.name) === target) || null;
}

function findRosterMemberForUser(user) {
  if (!user) return null;
  const phone = normalizePhone(user.phone || user.username);
  if (phone) {
    const byPhone = rosterMembers.find((member) => normalizePhone(member.phone) === phone);
    if (byPhone) return byPhone;
  }
  return findRosterMemberByName(user.displayName);
}

function userScopeDepartment(user) {
  return String(user?.department || getRosterScopeDepartment(findRosterMemberForUser(user))).trim();
}

function sameDepartmentGroup(left, right) {
  const a = normalizeDepartment(left);
  const b = normalizeDepartment(right);
  if (!a || !b) return false;
  return a === b || a.includes(b) || b.includes(a);
}

function personMatchesField(field, personName) {
  const fieldText = normalizeName(field);
  const name = normalizeName(personName);
  if (!fieldText || !name) return false;
  return fieldText === name || fieldText.includes(name) || name.includes(fieldText);
}

function mapRosterMemberToRole(member) {
  const text = `${getRosterScopeDepartment(member)} ${member.position || ""}`;
  if (/供应链/.test(text)) return "supplyChain";
  if (/监察|质检/.test(text)) return "auditor";
  if (/交付总监|交付经理/.test(text)) return "deliveryManager";
  if (/客管经理|用户服务经理/.test(text)) return "customerManager";
  if (/客管|客户经理|家访师|用户服务/.test(text)) return "customerOwner";
  if (/设计总监|设计经理|定制设计主管/.test(text)) return "designManager";
  if (/设计师|深化设计|灯光设计|定制设计/.test(text)) return "designer";
  if (/监察|质检/.test(text)) return "auditor";
  if (/董事长|总经理|副总|总监|管理型|AI工程师/.test(text)) return "executive";
  if (/交付助理|产业交付|销售/.test(text)) return "delivery";
  if (/项目|工种|售后|安装|采购|库管|结算|跟单|主材|辅材|主管|技师/.test(text)) return "manager";
  return "delivery";
}

function findRosterMember(name, phone) {
  const targetName = normalizeName(name);
  const targetPhone = normalizePhone(phone);
  if (!targetName || !targetPhone) return null;
  return rosterMembers.find((member) => (
    normalizeName(member.name) === targetName &&
    normalizePhone(member.phone) === targetPhone
  )) || null;
}

function publicRosterMember(member) {
  return {
    name: member.name,
    department: getRosterScopeDepartment(member),
    position: member.position || "",
    role: mapRosterMemberToRole(member),
    roleName: roles[mapRosterMemberToRole(member)]?.roleName || mapRosterMemberToRole(member)
  };
}

function normalizeCustomerKeyPart(value) {
  return String(value || "").trim().replace(/\s+/g, " ").toLowerCase();
}

function customerIdentityKey(customer) {
  return [
    customer.name,
    customer.address,
    customer.tier,
    customer.manager,
    customer.startDate,
    customer.plannedFinishDate
  ].map(normalizeCustomerKeyPart).join("|");
}

function findDuplicateCustomer(candidate) {
  const key = customerIdentityKey(candidate);
  return customers.find((customer) => customerIdentityKey(customer) === key);
}

function findDuplicateCustomerDraft(candidate) {
  const key = customerIdentityKey(candidate);
  return customerDrafts.find((draft) => {
    if (!["待审核", "已通过"].includes(draft.status)) return false;
    return draft.customer && customerIdentityKey(draft.customer) === key;
  });
}

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

function requireAuth(req, res) {
  const user = getCurrentUser(req);
  if (!user) {
    sendError(res, 401, "UNAUTHORIZED", "请先登录后再操作。");
    return null;
  }
  return user;
}

function requirePermission(req, res, permission) {
  const user = requireAuth(req, res);
  if (!user) return null;
  if (!can(user, permission)) {
    sendError(res, 403, "NO_PERMISSION", "当前登录人没有后台授权，操作已被服务器拒绝。");
    return null;
  }
  return user;
}

function nextCustomerId() {
  const max = customers.reduce((acc, customer) => Math.max(acc, Number(customer.id.slice(1)) || 0), 0);
  return `C${String(max + 1).padStart(3, "0")}`;
}

function nextCustomerDraftId() {
  const max = customerDrafts.reduce((acc, draft) => Math.max(acc, Number(draft.id.replace("CR-", "")) || 240700), 240700);
  return `CR-${max + 1}`;
}

function nextUserId() {
  const max = users.reduce((acc, user) => Math.max(acc, Number(user.id.slice(1)) || 0), 0);
  return `U${String(max + 1).padStart(3, "0")}`;
}

function normalizeStringList(value) {
  const list = Array.isArray(value) ? value : String(value || "").split(/[,，\n]/);
  return [...new Set(list.map((item) => String(item || "").trim()).filter(Boolean))];
}

function normalizeRolePages(pages) {
  const allowed = new Set(pageCatalog.map((page) => page.key));
  return normalizeStringList(pages).filter((page) => allowed.has(page));
}

function normalizeRolePermissions(permissions) {
  const allowed = new Set(permissionCatalog.map((permission) => permission.key));
  const list = normalizeStringList(permissions);
  if (list.includes("*")) return ["*"];
  return list.filter((permission) => allowed.has(permission));
}

function nextIssueId() {
  const max = issues.reduce((acc, issue) => Math.max(acc, Number(issue.id.replace("I-", "")) || 240700), 240700);
  return `I-${max + 1}`;
}

function nextInspectionId() {
  const max = inspections.reduce((acc, item) => Math.max(acc, Number(item.id.replace("R-", "")) || 240700), 240700);
  return `R-${max + 1}`;
}

function buildCustomerCandidate(body) {
  const tier = normalizeTier(body.tier);
  if (!tier) {
    const error = new Error("本系统仅保留 A / A+ 客户，请选择 A 或 A+。");
    error.status = 400;
    error.code = "INVALID_TIER";
    throw error;
  }
  const candidate = {
    name: String(body.name || "").trim(),
    tier,
    source: String(body.source || "待确认").trim(),
    keyCustomerReason: String(body.keyCustomerReason || "").trim(),
    customerOwner: String(body.customerOwner || "").trim(),
    address: String(body.address || "").trim(),
    department: String(body.department || "待确认").trim(),
    designer: String(body.designer || "").trim(),
    manager: String(body.manager || "").trim(),
    stage: String(body.stage || "待确认").trim(),
    startDate: String(body.startDate || "").trim() || "-",
    plannedFinishDate: String(body.plannedFinishDate || "").trim() || "-",
    progressStatus: normalizeProgressStatus(body.progressStatus),
    satisfaction: Number(body.satisfaction || 5)
  };
  if (!candidate.name || !candidate.address || !candidate.designer || !candidate.manager || !candidate.customerOwner || !candidate.keyCustomerReason) {
    const error = new Error("请补全客户姓名、地址、设计师、项目经理、客管人员负责人和重点客户入选说明。");
    error.status = 400;
    error.code = "INVALID_CUSTOMER";
    throw error;
  }
  return candidate;
}

function promoteCustomer(candidate, reviewer) {
  return {
    id: nextCustomerId(),
    ...candidate,
    approvedBy: reviewer.displayName,
    approvedAt: nowText(),
    inspectionStatus: "未巡查",
    inspectionAt: "-",
    inspector: "-",
    inspectionSummary: "新建客户，等待首次每日线上巡查。"
  };
}

function scoreByStatus(status) {
  if (status === "红色预警") return 90;
  if (status === "橙色预警") return 78;
  return 62;
}

function findIssue(res, issueId) {
  const issue = issues.find((item) => item.id === issueId);
  if (!issue) sendError(res, 404, "NOT_FOUND", "未找到对应的问题反馈。");
  return issue;
}

function hasFullCustomerAccess(user) {
  return ["admin", "executive", "auditor", "supplyChain", "deliveryManager", "delivery"].includes(user?.role);
}

function canAccessCustomer(user, customer) {
  if (!user || !customer) return false;
  if (hasFullCustomerAccess(user)) return true;

  if (user.role === "designer") {
    return personMatchesField(customer.designer, user.displayName);
  }

  if (user.role === "designManager") {
    const department = userScopeDepartment(user);
    const designerMember = findRosterMemberByName(customer.designer);
    const customerDesignerDepartment = getRosterScopeDepartment(designerMember) || customer.department;
    return sameDepartmentGroup(department, customerDesignerDepartment);
  }

  if (user.role === "manager") {
    return personMatchesField(customer.manager, user.displayName);
  }

  if (user.role === "customerOwner") {
    return personMatchesField(customer.customerOwner, user.displayName);
  }

  if (user.role === "customerManager") {
    const department = userScopeDepartment(user);
    const ownerMember = findRosterMemberByName(getCustomerOwnerName(customer));
    return sameDepartmentGroup(department, getRosterScopeDepartment(ownerMember));
  }

  return false;
}

function visibleCustomersFor(user) {
  return customers.filter((customer) => canAccessCustomer(user, customer));
}

function visibleCustomerIdSet(user) {
  return new Set(visibleCustomersFor(user).map((customer) => customer.id));
}

function canAccessIssue(user, issue) {
  const customer = customers.find((item) => item.id === issue?.customerId);
  return canAccessCustomer(user, customer);
}

function visibleIssuesFor(user) {
  return issues.filter((issue) => canAccessIssue(user, issue));
}

function visibleInspectionsFor(user) {
  const customerIds = visibleCustomerIdSet(user);
  return inspections.filter((inspection) => customerIds.has(inspection.customerId));
}

function requireCustomerAccess(user, res, customer) {
  if (canAccessCustomer(user, customer)) return true;
  sendError(res, 403, "CUSTOMER_SCOPE_DENIED", "当前登录人无权查看或操作该客户。");
  return false;
}

function requireIssueAccess(user, res, issue) {
  if (canAccessIssue(user, issue)) return true;
  sendError(res, 403, "CUSTOMER_SCOPE_DENIED", "当前登录人无权查看或操作该客户的问题反馈。");
  return false;
}

async function handleApi(req, res, pathname) {
  if (req.method === "GET" && pathname === "/api/auth/options") {
    sendJson(res, 200, {
      accounts: users.filter((user) => user.status !== "停用").map((user) => ({
        displayName: user.displayName,
        roleName: roles[user.role].roleName,
        summary: roles[user.role].summary
      }))
    });
    return;
  }

  if (req.method === "POST" && pathname === "/api/auth/login") {
    const body = await readBody(req);
    const user = users.find((item) => item.username === body.username && item.status !== "停用" && verifyUserPassword(item, body.password));
    if (!user) {
      sendError(res, 401, "LOGIN_FAILED", "账号或密码错误。");
      return;
    }
    const token = createSessionToken();
    sessions.set(token, user.id);
    sendJson(res, 200, { token, user: publicUser(user) });
    return;
  }

  if (req.method === "POST" && pathname === "/api/auth/register") {
    const body = await readBody(req);
    const name = normalizeName(body.name);
    const phone = normalizePhone(body.phone);
    const password = String(body.password || "").trim();
    const confirmPassword = body.confirmPassword == null ? password : String(body.confirmPassword || "").trim();
    if (!name || !phone || !password) {
      sendError(res, 400, "INVALID_REGISTER_INFO", "请填写姓名、手机号和登录密码。");
      return;
    }
    if (password.length < 6) {
      sendError(res, 400, "INVALID_PASSWORD", "密码至少需要 6 位。");
      return;
    }
    if (password !== confirmPassword) {
      sendError(res, 400, "PASSWORD_NOT_MATCH", "两次输入的密码不一致。");
      return;
    }
    const member = findRosterMember(name, phone);
    if (!member) {
      sendError(res, 403, "ROSTER_NOT_MATCHED", "姓名和手机号未在名册中匹配，暂不能注册。");
      return;
    }

    const role = mapRosterMemberToRole(member);
    let user = users.find((item) => normalizePhone(item.phone || item.username) === phone);
    if (user && normalizeName(user.displayName) !== normalizeName(member.name)) {
      sendError(res, 409, "PHONE_ALREADY_REGISTERED", "该手机号已被其他账号注册，请联系系统管理员。");
      return;
    }
    if (user) {
      if (user.status === "停用") {
        sendError(res, 403, "USER_DISABLED", "该账号已停用，请联系系统管理员。");
        return;
      }
      sendError(res, 409, "USER_ALREADY_REGISTERED", "该手机号已完成注册，请直接使用手机号和已设置密码登录，或联系系统管理员重置密码。");
      return;
    }

    user = {
      id: nextUserId(),
      username: phone,
      displayName: member.name,
      phone,
      department: getRosterScopeDepartment(member),
      position: member.position || "",
      role,
      status: "启用",
      registerSource: "roster"
    };
    setUserPassword(user, password);
    users.push(user);

    await persistState();
    const token = createSessionToken();
    sessions.set(token, user.id);
    sendJson(res, 201, {
      token,
      user: publicUser(user),
      roster: publicRosterMember(member),
      message: `注册成功，已自动匹配为${roles[role]?.roleName || role}。登录账号为手机号，请使用刚设置的密码登录。`
    });
    return;
  }

  if (req.method === "GET" && pathname === "/api/auth/me") {
    const user = requireAuth(req, res);
    if (!user) return;
    sendJson(res, 200, { user: publicUser(user) });
    return;
  }

  if (req.method === "POST" && pathname === "/api/auth/logout") {
    const token = getBearerToken(req);
    if (token) sessions.delete(token);
    sendJson(res, 200, { message: "已退出登录。" });
    return;
  }

  if (req.method === "GET" && pathname === "/api/meta") {
    const user = requireAuth(req, res);
    if (!user) return;
    sendJson(res, 200, { dictionaries });
    return;
  }

  if (req.method === "GET" && pathname === "/api/admin/config") {
    const user = requirePermission(req, res, "admin:manage");
    if (!user) return;
    sendJson(res, 200, {
      users: users.map(publicManagedUser),
      roles,
      pageCatalog,
      permissionCatalog,
      customerDrafts: customerDrafts.map(publicCustomerDraft),
      dictionaries,
      rules: rulesState
    });
    return;
  }

  if (req.method === "POST" && pathname === "/api/admin/users") {
    const user = requirePermission(req, res, "admin:manage");
    if (!user) return;
    const body = await readBody(req);
    const username = String(body.username || "").trim();
    const password = String(body.password || "").trim();
    const displayName = String(body.displayName || "").trim();
    const role = String(body.role || "").trim();
    if (!username || !password || !displayName || !roles[role]) {
      sendError(res, 400, "INVALID_USER", "请补全账号、密码、姓名，并选择有效角色。");
      return;
    }
    if (users.some((item) => item.username === username)) {
      sendError(res, 400, "DUPLICATE_USER", "该登录账号已存在。");
      return;
    }
    const managedUser = {
      id: nextUserId(),
      username,
      displayName,
      phone: normalizePhone(body.phone),
      department: String(body.department || "").trim(),
      position: String(body.position || "").trim(),
      role,
      status: body.status === "停用" ? "停用" : "启用"
    };
    setUserPassword(managedUser, password);
    users.push(managedUser);
    await persistState();
    sendJson(res, 201, { user: publicManagedUser(managedUser), message: "后台账号已创建。" });
    return;
  }

  const adminUserMatch = pathname.match(/^\/api\/admin\/users\/([^/]+)$/);
  if (req.method === "PATCH" && adminUserMatch) {
    const operator = requirePermission(req, res, "admin:manage");
    if (!operator) return;
    const target = users.find((item) => item.id === decodeURIComponent(adminUserMatch[1]));
    if (!target) {
      sendError(res, 404, "NOT_FOUND", "未找到对应账号。");
      return;
    }
    const body = await readBody(req);
    if (body.username !== undefined) {
      const username = String(body.username || "").trim();
      if (!username) {
        sendError(res, 400, "INVALID_USER", "登录账号不能为空。");
        return;
      }
      if (users.some((item) => item.id !== target.id && item.username === username)) {
        sendError(res, 400, "DUPLICATE_USER", "该登录账号已存在。");
        return;
      }
      target.username = username;
    }
    if (body.password !== undefined) {
      const nextPassword = String(body.password || "").trim();
      if (nextPassword) setUserPassword(target, nextPassword);
    }
    if (body.displayName !== undefined) target.displayName = String(body.displayName || "").trim() || target.displayName;
    if (body.phone !== undefined) target.phone = normalizePhone(body.phone);
    if (body.department !== undefined) target.department = String(body.department || "").trim();
    if (body.position !== undefined) target.position = String(body.position || "").trim();
    if (body.role !== undefined) {
      const role = String(body.role || "").trim();
      if (!roles[role]) {
        sendError(res, 400, "INVALID_ROLE", "请选择有效角色。");
        return;
      }
      target.role = role;
    }
    if (body.status !== undefined) {
      const nextStatus = body.status === "停用" ? "停用" : "启用";
      const activeAdmins = users.filter((item) => item.status !== "停用" && item.role === "admin");
      if (target.role === "admin" && nextStatus === "停用" && activeAdmins.length <= 1 && activeAdmins[0]?.id === target.id) {
        sendError(res, 400, "LAST_ADMIN", "至少保留一个启用的系统管理员。");
        return;
      }
      target.status = nextStatus;
      if (nextStatus === "停用") {
        [...sessions.entries()].forEach(([token, userId]) => {
          if (userId === target.id) sessions.delete(token);
        });
      }
    }
    await persistState();
    sendJson(res, 200, { user: publicManagedUser(target), message: "账号信息已由后台更新。" });
    return;
  }

  const adminRoleMatch = pathname.match(/^\/api\/admin\/roles\/([^/]+)$/);
  if (req.method === "PATCH" && adminRoleMatch) {
    const user = requirePermission(req, res, "admin:manage");
    if (!user) return;
    const roleKey = decodeURIComponent(adminRoleMatch[1]);
    const role = roles[roleKey];
    if (!role) {
      sendError(res, 404, "NOT_FOUND", "未找到对应角色。");
      return;
    }
    const body = await readBody(req);
    if (body.roleName !== undefined) role.roleName = String(body.roleName || "").trim() || role.roleName;
    if (body.summary !== undefined) role.summary = String(body.summary || "").trim() || role.summary;
    if (roleKey === "admin") {
      role.pages = pageCatalog.map((page) => page.key);
      role.permissions = ["*", "admin:manage"];
    } else {
      if (body.pages !== undefined) role.pages = normalizeRolePages(body.pages);
      if (body.permissions !== undefined) role.permissions = normalizeRolePermissions(body.permissions);
    }
    await persistState();
    sendJson(res, 200, { roleKey, role, message: "角色权限已由后台更新。" });
    return;
  }

  if (req.method === "PATCH" && pathname === "/api/admin/dictionaries") {
    const user = requirePermission(req, res, "admin:manage");
    if (!user) return;
    const body = await readBody(req);
    Object.keys(dictionaries).forEach((key) => {
      if (body[key] !== undefined) {
        const values = normalizeStringList(body[key]);
        if (values.length) dictionaries[key] = values;
      }
    });
    await persistState();
    sendJson(res, 200, { dictionaries, message: "基础字典已由后台更新。" });
    return;
  }

  const customerDraftMatch = pathname.match(/^\/api\/admin\/customer-drafts\/([^/]+)$/);
  if (req.method === "PATCH" && customerDraftMatch) {
    const user = requirePermission(req, res, "customer:approve");
    if (!user) return;
    const draft = customerDrafts.find((item) => item.id === decodeURIComponent(customerDraftMatch[1]));
    if (!draft) {
      sendError(res, 404, "NOT_FOUND", "未找到对应的客户入池审核单。");
      return;
    }
    if (draft.status !== "待审核") {
      sendError(res, 400, "ALREADY_REVIEWED", "该客户入池申请已经审核完成。");
      return;
    }
    const body = await readBody(req);
    const action = String(body.action || "").trim();
    draft.reviewedBy = user.displayName;
    draft.reviewedAt = nowText();
    draft.reviewNote = String(body.reviewNote || "").trim();
    if (action === "approve") {
      const duplicateCustomer = findDuplicateCustomer(draft.customer);
      if (duplicateCustomer) {
        draft.status = "已通过";
        draft.customerId = duplicateCustomer.id;
        await persistState();
        sendJson(res, 200, { customer: duplicateCustomer, draft: publicCustomerDraft(draft), message: "已存在相同客户档案，本次未重复入库。" });
        return;
      }
      const customer = promoteCustomer(draft.customer, user);
      customers.unshift(customer);
      draft.status = "已通过";
      draft.customerId = customer.id;
      await persistState();
      sendJson(res, 200, { customer, draft: publicCustomerDraft(draft), message: "客户入池申请已通过，客户已进入 A/A+ 客户库。" });
      return;
    }
    if (action === "reject") {
      draft.status = "已驳回";
      await persistState();
      sendJson(res, 200, { draft: publicCustomerDraft(draft), message: "客户入池申请已驳回，未进入客户库。" });
      return;
    }
    sendError(res, 400, "INVALID_ACTION", "请选择通过或驳回该客户入池申请。");
    return;
  }

  if (req.method === "GET" && pathname === "/api/customers") {
    const user = requireAuth(req, res);
    if (!user) return;
    sendJson(res, 200, { customers: visibleCustomersFor(user) });
    return;
  }

  if (req.method === "POST" && pathname === "/api/customers") {
    const user = requirePermission(req, res, "customer:submit");
    if (!user) return;
    const body = await readBody(req);
    let candidate;
    try {
      candidate = buildCustomerCandidate(body);
    } catch (error) {
      sendError(res, error.status || 400, error.code || "INVALID_CUSTOMER", error.message);
      return;
    }
    const duplicateCustomer = findDuplicateCustomer(candidate);
    if (duplicateCustomer) {
      const payload = { message: "已存在相同客户档案，本次未重复入库。" };
      if (canAccessCustomer(user, duplicateCustomer)) payload.customer = duplicateCustomer;
      sendJson(res, 200, payload);
      return;
    }
    const duplicateDraft = findDuplicateCustomerDraft(candidate);
    if (duplicateDraft) {
      sendJson(res, 200, { draft: publicCustomerDraft(duplicateDraft), message: "已存在相同客户入池申请，本次未重复提交。" });
      return;
    }
    if (user.role === "admin") {
      const customer = promoteCustomer(candidate, user);
      customers.unshift(customer);
      await persistState();
      sendJson(res, 201, { customer, message: "管理员已直接保存客户档案，客户已进入 A/A+ 客户库。" });
      return;
    }
    const draft = {
      id: nextCustomerDraftId(),
      status: "待审核",
      submittedBy: user.displayName,
      submittedRole: roles[user.role]?.roleName || user.role,
      submittedAt: nowText(),
      reviewedBy: "",
      reviewedAt: "",
      reviewNote: "",
      customerId: "",
      customer: candidate
    };
    customerDrafts.unshift(draft);
    await persistState();
    sendJson(res, 202, { draft: publicCustomerDraft(draft), message: "客户入池申请已提交，等待管理员审核后进入客户库。" });
    return;
  }

  if (req.method === "GET" && pathname === "/api/inspections") {
    const user = requirePermission(req, res, "inspection:view");
    if (!user) return;
    sendJson(res, 200, { inspections: visibleInspectionsFor(user) });
    return;
  }

  if (req.method === "POST" && pathname === "/api/inspections") {
    const user = requirePermission(req, res, "inspection:create");
    if (!user) return;
    const body = await readBody(req);
    const customer = customers.find((item) => item.id === body.customerId);
    if (!customer) {
      sendError(res, 400, "INVALID_CUSTOMER", "请选择有效的巡查客户。");
      return;
    }
    if (!requireCustomerAccess(user, res, customer)) return;
    const status = ["已巡查", "未巡查", "异常"].includes(body.status) ? body.status : "已巡查";
    const inspection = {
      id: nextInspectionId(),
      customerId: customer.id,
      customerName: customer.name,
      inspector: String(body.inspector || user.displayName).trim(),
      status,
      createdAt: nowText(),
      siteQuality: String(body.siteQuality || "未填写").trim(),
      civility: String(body.civility || "未填写").trim(),
      groupFeedback: String(body.groupFeedback || "未填写").trim(),
      summary: String(body.summary || "每日线上巡查已记录。").trim()
    };
    inspections.unshift(inspection);
    customer.inspectionStatus = inspection.status;
    customer.inspectionAt = inspection.createdAt;
    customer.inspector = inspection.inspector;
    customer.inspectionSummary = inspection.summary;
    await persistState();
    sendJson(res, 201, { inspection, message: "每日线上巡查已提交。" });
    return;
  }

  if (req.method === "GET" && pathname === "/api/issues") {
    const user = requirePermission(req, res, "issue:view");
    if (!user) return;
    sendJson(res, 200, { issues: visibleIssuesFor(user) });
    return;
  }

  if (req.method === "POST" && pathname === "/api/issues") {
    const user = requirePermission(req, res, "issue:create");
    if (!user) return;
    const body = await readBody(req);
    const customer = customers.find((item) => item.id === body.customerId);
    if (!customer) {
      sendError(res, 400, "INVALID_CUSTOMER", "请选择有效的关联客户。");
      return;
    }
    if (!requireCustomerAccess(user, res, customer)) return;
    const status = body.status || "蓝色关注";
    const issue = {
      id: nextIssueId(),
      customerId: customer.id,
      type: body.type || "客户反馈",
      source: body.source || dictionaries.issueSources[0] || "线下巡检",
      status,
      owner: String(body.owner || "").trim() || customer.manager,
      score: scoreByStatus(status),
      summary: String(body.summary || "客户反馈已记录").trim(),
      rootCause: String(body.rootCause || "待责任人补充根因分析").trim(),
      action: "新反馈已进入预警池，等待责任人确认处置动作",
      handlerNote: "待提交处置记录",
      postSatisfaction: "待处置",
      submitter: user.displayName,
      auditDue: body.auditDue || "",
      handlingRecords: [],
      timeline: [
        String(body.summary || "客户反馈已记录").trim(),
        String(body.rootCause || "待补充根因分析").trim(),
        "系统已生成预警并同步至问题反馈池"
      ]
    };
    issues.unshift(issue);
    await persistState();
    sendJson(res, 201, { issue, message: "问题反馈已由后台提交，并进入预警池。" });
    return;
  }

  const issueActionMatch = pathname.match(/^\/api\/issues\/([^/]+)\/([^/]+)$/);
  if (req.method === "PATCH" && issueActionMatch) {
    const [, encodedIssueId, action] = issueActionMatch;
    const issueId = decodeURIComponent(encodedIssueId);
    const issue = findIssue(res, issueId);
    if (!issue) return;
    const body = await readBody(req);

    if (action === "assign") {
      const user = requirePermission(req, res, "issue:assign");
      if (!user) return;
      if (!requireIssueAccess(user, res, issue)) return;
      issue.owner = String(body.owner || "部门负责人").trim();
      issue.action = "已由管理层指派责任人牵头处理";
      issue.timeline.push(`${user.displayName} 指派 ${issue.owner} 负责闭环`);
      await persistState();
      sendJson(res, 200, { issue, message: "后台已完成责任人指派。" });
      return;
    }

    if (action === "quick-handle") {
      const user = requirePermission(req, res, "issue:handle");
      if (!user) return;
      if (!requireIssueAccess(user, res, issue)) return;
      issue.status = "处理中";
      issue.action = "已进入处置流程，等待补充根因与整改记录";
      issue.handlerNote = `${user.displayName} 已接收并开始处置`;
      issue.handlingRecords = issue.handlingRecords || [];
      issue.handlingRecords.push({
        handler: user.displayName,
        handledAt: nowText(),
        note: "接收问题并进入处置流程。",
        satisfaction: issue.postSatisfaction || "待处置",
        feedback: "等待补充处理方案。"
      });
      issue.timeline.push(`${user.displayName} 接收问题并进入处置流程`);
      await persistState();
      sendJson(res, 200, { issue, message: "后台已将问题切换为处理中。" });
      return;
    }

    if (action === "handle") {
      const user = requirePermission(req, res, "issue:handle");
      if (!user) return;
      if (!requireIssueAccess(user, res, issue)) return;
      issue.status = "待核查";
      issue.action = "处置完成，等待监察核查";
      issue.handlerNote = String(body.handlerNote || "项目经理已提交处置记录").trim();
      issue.postSatisfaction = String(body.satisfaction || "待回访").trim();
      issue.handlingRecords = issue.handlingRecords || [];
      issue.handlingRecords.push({
        handler: user.displayName,
        handledAt: nowText(),
        note: issue.handlerNote,
        satisfaction: issue.postSatisfaction,
        feedback: String(body.customerFeedback || "待客户回访确认").trim()
      });
      issue.timeline.push(`${user.displayName} 提交处置记录，处置后满意度：${issue.postSatisfaction}`);
      await persistState();
      sendJson(res, 200, { issue, message: "处置记录已提交，后台已转入待核查。" });
      return;
    }

    if (action === "audit-pass") {
      const user = requirePermission(req, res, "audit:review");
      if (!user) return;
      if (!requireIssueAccess(user, res, issue)) return;
      issue.status = "已处理";
      issue.action = "监察核查通过，问题已处理";
      issue.handlerNote = "监察部核查通过";
      issue.timeline.push(`${user.displayName} 核查通过，问题已处理`);
      await persistState();
      sendJson(res, 200, { issue, message: "监察核查已通过，问题已处理。" });
      return;
    }

    if (action === "audit-reject") {
      const user = requirePermission(req, res, "audit:review");
      if (!user) return;
      if (!requireIssueAccess(user, res, issue)) return;
      issue.status = "红色预警";
      issue.action = "监察驳回，需重新提交整改方案";
      issue.handlerNote = "监察部驳回整改";
      issue.timeline.push(`${user.displayName} 驳回整改，问题回到红色预警`);
      await persistState();
      sendJson(res, 200, { issue, message: "已驳回整改，问题回到红色预警。" });
      return;
    }
  }

  if (req.method === "POST" && pathname === "/api/rules") {
    const user = requirePermission(req, res, "rules:edit");
    if (!user) return;
    rulesState = {
      updatedAt: new Date().toISOString(),
      updatedBy: user.displayName
    };
    await persistState();
    sendJson(res, 200, { rules: rulesState, message: "预警规则已由后台保存。" });
    return;
  }

  sendError(res, 404, "NOT_FOUND", "未找到对应的后台接口。");
}

async function serveStatic(req, res, pathname) {
  if (pathname === "/") {
    res.writeHead(302, { Location: "/prototype/index.html" });
    res.end();
    return;
  }

  let decodedPath;
  try {
    decodedPath = decodeURIComponent(pathname);
  } catch {
    res.writeHead(400);
    res.end("Bad request");
    return;
  }

  const filePath = path.resolve(rootDir, `.${decodedPath}`);
  if (!filePath.startsWith(rootDir)) {
    res.writeHead(403);
    res.end("Forbidden");
    return;
  }

  try {
    const stat = await fs.stat(filePath);
    const finalPath = stat.isDirectory() ? path.join(filePath, "index.html") : filePath;
    const data = await fs.readFile(finalPath);
    const type = mimeTypes[path.extname(finalPath).toLowerCase()] || "application/octet-stream";
    res.writeHead(200, { "Content-Type": type, "Cache-Control": "no-store" });
    res.end(data);
  } catch {
    res.writeHead(404);
    res.end("Not found");
  }
}

const server = http.createServer(async (req, res) => {
  const url = new URL(req.url, `http://${req.headers.host || "127.0.0.1"}`);
  try {
    if (url.pathname.startsWith("/api/")) {
      await handleApi(req, res, url.pathname);
      return;
    }
    await serveStatic(req, res, url.pathname);
  } catch (error) {
    const status = error.status || 500;
    sendError(res, status, "SERVER_ERROR", error.message || "服务器处理失败。");
  }
});

async function start() {
  await loadRoster();
  await loadState();
  server.listen(port, host, () => {
    const shownHost = host === "0.0.0.0" ? "127.0.0.1" : host;
    console.log(`A+在建客户动态管理系统 running at http://${shownHost}:${port}/prototype/index.html`);
  });
}

start().catch((error) => {
  console.error("Failed to start server", error);
  process.exit(1);
});
