/**
 * 公司账号服务（内部通讯 app）
 *
 * 职责：
 *  - 员工账号：管理员导入/新增/停用/启用/重置密码（本地 JSON 文件存储，密码 scrypt 哈希）
 *  - 登录：校验工号+密码后，调用 OpenIM REST API 换取 IM token 返回给客户端
 *  - 管理页：GET / 直接返回内置的单文件管理页 admin.html
 *
 * 零 npm 依赖，Node >= 18（用到全局 fetch）。
 *
 * 环境变量：
 *  PORT             监听端口，默认 10010
 *  DATA_FILE        员工数据文件，默认 ./data/employees.json
 *  OPENIM_API_URL   OpenIM REST 地址，默认 http://127.0.0.1:10002
 *  OPENIM_SECRET    OpenIM 管理密钥（与服务端 IMENV_SHARE_SECRET 一致），默认 openIM123
 *  ADMIN_TOKEN      管理页/管理接口口令，默认 admin123（部署时务必修改）
 */

'use strict';

const http = require('http');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');

const PORT = parseInt(process.env.PORT || '10010', 10);
const DATA_FILE = process.env.DATA_FILE || path.join(__dirname, 'data', 'employees.json');
const OPENIM_API_URL = (process.env.OPENIM_API_URL || 'http://127.0.0.1:10002').replace(/\/+$/, '');
const OPENIM_SECRET = process.env.OPENIM_SECRET || 'openIM123';
const ADMIN_TOKEN = process.env.ADMIN_TOKEN || 'admin123';
// LiveKit 语音通话（与 livekit 容器同一对 key/secret，见 .env 的 LIVEKIT_API_KEY / LIVEKIT_API_SECRET）
const LIVEKIT_API_KEY = process.env.LIVEKIT_API_KEY || '';
const LIVEKIT_API_SECRET = process.env.LIVEKIT_API_SECRET || '';

// OpenIM 平台号：1 iOS, 2 Android, 3 Windows, 4 OSX, 5 Web... 停用时逐个踢下线
const ALL_PLATFORM_IDS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// ---------------- 员工数据存储（JSON 文件） ----------------

let employees = {}; // staffNo -> { name, salt, hash, status, createdAt, updatedAt }

function loadStore() {
  try {
    employees = JSON.parse(fs.readFileSync(DATA_FILE, 'utf8'));
  } catch {
    employees = {};
  }
}

function saveStore() {
  fs.mkdirSync(path.dirname(DATA_FILE), { recursive: true });
  const tmp = DATA_FILE + '.tmp';
  fs.writeFileSync(tmp, JSON.stringify(employees, null, 2));
  fs.renameSync(tmp, DATA_FILE);
}

function hashPassword(password, salt) {
  return crypto.scryptSync(password, salt, 32).toString('hex');
}

function verifyPassword(emp, password) {
  const a = Buffer.from(emp.hash, 'hex');
  const b = Buffer.from(hashPassword(password, emp.salt), 'hex');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

const STAFF_NO_RE = /^[A-Za-z0-9_-]{2,32}$/;

function upsertEmployee(staffNo, name, password) {
  const salt = crypto.randomBytes(16).toString('hex');
  const now = new Date().toISOString();
  employees[staffNo] = {
    name,
    salt,
    hash: hashPassword(password, salt),
    status: 'active',
    createdAt: employees[staffNo] ? employees[staffNo].createdAt : now,
    updatedAt: now,
  };
  saveStore();
}

// ---------------- OpenIM REST 客户端 ----------------

let adminTokenCache = { token: null, expireAt: 0 };

async function imApi(route, body, token) {
  const res = await fetch(OPENIM_API_URL + route, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      operationID: crypto.randomUUID(),
      ...(token ? { token } : {}),
    },
    body: JSON.stringify(body),
  });
  return res.json();
}

async function getAdminToken() {
  if (adminTokenCache.token && Date.now() < adminTokenCache.expireAt - 60000) {
    return adminTokenCache.token;
  }
  const r = await imApi('/auth/get_admin_token', {
    secret: OPENIM_SECRET,
    platformID: 10,
    userID: 'imAdmin',
  });
  if (r.errCode !== 0) throw new Error('OpenIM 管理员令牌获取失败: ' + (r.errMsg || r.errCode));
  adminTokenCache = {
    token: r.data.token,
    expireAt: Date.now() + r.data.expireTimeSeconds * 1000,
  };
  return adminTokenCache.token;
}

/** 确保 OpenIM 里存在该用户；返回 true 表示已存在 */
async function ensureImUser(staffNo, name) {
  const admin = await getAdminToken();
  await imApi('/user/user_register', {
    users: [{ userID: staffNo, nickname: name, faceURL: '' }],
  }, admin);
  // 注册接口对已存在用户的行为各版本不一，统一以查询结果为准
  const q = await imApi('/user/get_users_info', { userIDs: [staffNo] }, admin);
  return q.errCode === 0 && Array.isArray(q.data?.usersInfo) && q.data.usersInfo.length > 0;
}

async function getImUserToken(staffNo, platformID) {
  const admin = await getAdminToken();
  const r = await imApi('/auth/get_user_token', { userID: staffNo, platformID }, admin);
  if (r.errCode !== 0) throw new Error('OpenIM 用户令牌获取失败: ' + (r.errMsg || r.errCode));
  return r.data;
}

/** 停用后踢下线（逐平台，尽力而为，失败不阻断） */
async function forceLogoutAll(staffNo) {
  try {
    const admin = await getAdminToken();
    await Promise.all(ALL_PLATFORM_IDS.map((pid) =>
      imApi('/auth/force_logout', { platformID: pid, userID: staffNo }, admin).catch(() => {})
    ));
  } catch { /* 忽略 */ }
}

// ---------------- LiveKit 通话 token ----------------

/** 用 OpenIM parse_token 校验客户端带来的 IM token，返回对应的 userID（无效/过期返回 null） */
async function parseImToken(token) {
  try {
    const admin = await getAdminToken();
    const r = await imApi('/auth/parse_token', { token }, admin);
    if (r.errCode !== 0 || !r.data || !r.data.userID) return null;
    return String(r.data.userID);
  } catch {
    return null;
  }
}

/** 签 LiveKit 访问 token（HS256 JWT，零依赖手写；claims 格式与 livekit-server 约定一致） */
function signLivekitToken(identity, room) {
  const now = Math.floor(Date.now() / 1000);
  const b64 = (o) => Buffer.from(JSON.stringify(o)).toString('base64url');
  const data = b64({ alg: 'HS256', typ: 'JWT' }) + '.' + b64({
    iss: LIVEKIT_API_KEY,
    sub: identity,
    iat: now,
    nbf: now - 10,
    exp: now + 2 * 3600, // 2 小时，一场通话足够
    video: { roomJoin: true, room },
  });
  const sig = crypto.createHmac('sha256', LIVEKIT_API_SECRET).update(data).digest('base64url');
  return data + '.' + sig;
}

// ---------------- HTTP 服务 ----------------

function send(res, status, obj) {
  const body = JSON.stringify(obj);
  res.writeHead(status, {
    'Content-Type': 'application/json; charset=utf-8',
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Headers': 'Content-Type, admin-token, authorization',
    'Access-Control-Allow-Methods': 'GET,POST,OPTIONS',
  });
  res.end(body);
}

const ok = (res, data) => send(res, 200, { code: 0, msg: '', data });
const fail = (res, msg, httpStatus = 200) => send(res, httpStatus, { code: 1, msg });

function readBody(req) {
  return new Promise((resolve, reject) => {
    let buf = '';
    req.on('data', (c) => {
      buf += c;
      if (buf.length > 2 * 1024 * 1024) reject(new Error('请求体过大'));
    });
    req.on('end', () => {
      if (!buf) return resolve({});
      try { resolve(JSON.parse(buf)); } catch { reject(new Error('请求体不是合法 JSON')); }
    });
    req.on('error', reject);
  });
}

/** 从已解析的 URL 里取查询参数（原样返回，未解码处理由调用方负责） */
function urlParam(req, key) {
  return req.urlObj ? (req.urlObj.searchParams.get(key) || '') : '';
}

function checkAdmin(req, res) {
  const t = req.headers['admin-token'] || '';
  const a = Buffer.from(String(t));
  const b = Buffer.from(ADMIN_TOKEN);
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    fail(res, '管理口令错误或未提供', 401);
    return false;
  }
  return true;
}

function publicView(emp, staffNo) {
  return { staffNo, name: emp.name, status: emp.status, createdAt: emp.createdAt, updatedAt: emp.updatedAt };
}

const routes = {
  'GET /api/health': async (req, res) => ok(res, { status: 'up', employees: Object.keys(employees).length }),

  // 员工目录：登录用户可读的启用员工列表（供客户端「添加同事」搜索）。
  // 请求头 Authorization: Bearer <OpenIM imToken>（与 /api/rtc_token 一致，用 parse_token 校验身份）。
  // 只返回启用员工、排除自己，不返回密码/手机号/管理字段。
  'GET /api/directory': async (req, res) => {
    const m = /^Bearer\s+(.+)$/.exec(String(req.headers.authorization || ''));
    if (!m) return fail(res, '未登录或登录已过期', 401);
    const userID = await parseImToken(m[1]);
    if (!userID) return fail(res, '登录已过期，请重新登录', 401);
    const keyword = (urlParam(req, 'keyword') || '').trim();
    const limit = Math.min(Math.max(parseInt(urlParam(req, 'limit') || '20', 10) || 20, 1), 100);
    let list = Object.keys(employees)
      .filter((k) => employees[k].status === 'active' && k !== userID)
      .map((k) => ({ userID: k, nickname: employees[k].name, department: '', title: '', faceURL: '' }));
    if (keyword) {
      const kw = keyword.toLowerCase();
      list = list.filter((u) => u.userID.toLowerCase().includes(kw) || u.nickname.toLowerCase().includes(kw));
    }
    const items = list.slice(0, limit);
    ok(res, { items, nextCursor: null });
  },

  'POST /api/login': async (req, res, body) => {
    const { staffNo, password, platformID } = body;
    if (!staffNo || !password) return fail(res, '工号和密码不能为空');
    const emp = employees[staffNo];
    if (!emp || !verifyPassword(emp, String(password))) return fail(res, '工号或密码错误');
    if (emp.status !== 'active') return fail(res, '账号已停用，请联系管理员');
    const pid = Number.isInteger(platformID) && platformID >= 1 && platformID <= 10 ? platformID : 10;
    // 兜底：OpenIM 侧账号不存在（例如服务端重建过）时先补开户
    if (!(await ensureImUser(staffNo, emp.name))) return fail(res, 'IM 开户失败，请联系管理员', 500);
    const t = await getImUserToken(staffNo, pid);
    ok(res, { userID: staffNo, nickname: emp.name, imToken: t.token, expireTimeSeconds: t.expireTimeSeconds });
  },

  // 语音通话：用登录拿到的 IM token 换 LiveKit 进房 token
  // 请求头 Authorization: Bearer <imToken>，body {room, identity}
  'POST /api/rtc_token': async (req, res, body) => {
    if (!LIVEKIT_API_KEY || !LIVEKIT_API_SECRET) return fail(res, '语音通话服务未配置，请联系管理员', 500);
    const m = /^Bearer\s+(.+)$/.exec(String(req.headers.authorization || ''));
    if (!m) return fail(res, '未登录或登录已过期', 401);
    const { room, identity } = body;
    if (!room || typeof room !== 'string' || room.length > 128) return fail(res, '房间号不合法');
    if (!identity || typeof identity !== 'string') return fail(res, '缺少用户标识');
    const userID = await parseImToken(m[1]);
    if (!userID) return fail(res, '登录已过期，请重新登录', 401);
    if (userID !== identity) return fail(res, '用户标识与登录凭证不一致', 403);
    ok(res, { token: signLivekitToken(identity, room) });
  },

  'GET /api/admin/employees': async (req, res) => {
    const list = Object.keys(employees).sort().map((k) => publicView(employees[k], k));
    ok(res, { total: list.length, employees: list });
  },

  'POST /api/admin/employees': async (req, res, body) => {
    const { staffNo, name, password } = body;
    if (!STAFF_NO_RE.test(staffNo || '')) return fail(res, '工号需为 2-32 位字母/数字/中划线/下划线');
    if (!name || !String(name).trim()) return fail(res, '姓名不能为空');
    if (!password || String(password).length < 6) return fail(res, '初始密码至少 6 位');
    if (!(await ensureImUser(staffNo, String(name).trim()))) return fail(res, 'IM 开户失败', 500);
    upsertEmployee(staffNo, String(name).trim(), String(password));
    ok(res, publicView(employees[staffNo], staffNo));
  },

  // 批量导入：csv 文本，每行 "工号,姓名,初始密码"（也支持 Tab 分隔），首行为表头时自动跳过
  'POST /api/admin/import': async (req, res, body) => {
    const lines = String(body.csv || '').split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
    if (lines.length === 0) return fail(res, '名单为空');
    const results = [];
    for (const line of lines) {
      const parts = line.split(/[,\t]/).map((s) => s.trim());
      const [staffNo, name, password] = parts;
      if (!STAFF_NO_RE.test(staffNo || '') || /工号|staff/i.test(staffNo || '')) {
        results.push({ line, ok: false, msg: parts.length < 3 ? '格式错误或表头行' : '工号不合法' });
        continue;
      }
      if (parts.length < 3 || !name) { results.push({ line, ok: false, msg: '格式：工号,姓名,初始密码' }); continue; }
      if (String(password).length < 6) { results.push({ line, ok: false, msg: '密码至少 6 位' }); continue; }
      try {
        if (!(await ensureImUser(staffNo, name))) { results.push({ line, ok: false, msg: 'IM 开户失败' }); continue; }
        upsertEmployee(staffNo, name, String(password));
        results.push({ line, ok: true, staffNo, name });
      } catch (e) {
        results.push({ line, ok: false, msg: e.message });
      }
    }
    ok(res, { total: results.length, imported: results.filter((r) => r.ok).length, results });
  },

  'POST /api/admin/disable': async (req, res, body) => {
    const emp = employees[body.staffNo];
    if (!emp) return fail(res, '员工不存在');
    emp.status = 'disabled';
    emp.updatedAt = new Date().toISOString();
    saveStore();
    forceLogoutAll(body.staffNo); // 异步踢下线，不等结果
    ok(res, publicView(emp, body.staffNo));
  },

  'POST /api/admin/enable': async (req, res, body) => {
    const emp = employees[body.staffNo];
    if (!emp) return fail(res, '员工不存在');
    emp.status = 'active';
    emp.updatedAt = new Date().toISOString();
    saveStore();
    ok(res, publicView(emp, body.staffNo));
  },

  // 删除员工：本地账号直接移除（立即无法再登录并被踢下线）。
  // 注意：OpenIM 侧的同名用户不删（其公开 API 不提供删除），但删除后本服务不再为其签发令牌，等于不可用。
  'POST /api/admin/delete': async (req, res, body) => {
    const emp = employees[body.staffNo];
    if (!emp) return fail(res, '员工不存在');
    delete employees[body.staffNo];
    saveStore();
    forceLogoutAll(body.staffNo); // 异步踢下线，不等结果
    ok(res, { staffNo: body.staffNo, deleted: true });
  },

  'POST /api/admin/reset_password': async (req, res, body) => {
    const emp = employees[body.staffNo];
    if (!emp) return fail(res, '员工不存在');
    if (!body.password || String(body.password).length < 6) return fail(res, '新密码至少 6 位');
    upsertEmployee(body.staffNo, emp.name, String(body.password));
    if (employees[body.staffNo].status !== 'active') employees[body.staffNo].status = emp.status; // 保留停用状态
    saveStore();
    forceLogoutAll(body.staffNo); // 密码已改，立即踢掉该员工所有端，防止旧 token 继续使用
    ok(res, publicView(employees[body.staffNo], body.staffNo));
  },
};

const server = http.createServer(async (req, res) => {
  try {
    const url = new URL(req.url, 'http://localhost');
    req.urlObj = url;
    if (req.method === 'OPTIONS') return send(res, 204, {});
    // 管理页（免登录打开，页面内再输入管理口令）
    if (req.method === 'GET' && (url.pathname === '/' || url.pathname === '/admin')) {
      res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
      return fs.createReadStream(path.join(__dirname, 'admin.html')).pipe(res);
    }
    const key = `${req.method} ${url.pathname}`;
    const handler = routes[key];
    if (!handler) return fail(res, '接口不存在', 404);
    if (url.pathname.startsWith('/api/admin/') && !checkAdmin(req, res)) return;
    const body = req.method === 'POST' ? await readBody(req) : {};
    await handler(req, res, body);
  } catch (e) {
    fail(res, '服务内部错误: ' + e.message, 500);
  }
});

loadStore();
server.listen(PORT, () => {
  console.log(`account-server listening on :${PORT}, OpenIM API: ${OPENIM_API_URL}, employees: ${Object.keys(employees).length}`);
});
