'use strict';

const fs = require('node:fs');
const path = require('node:path');
const crypto = require('node:crypto');
const os = require('node:os');
const { spawn, execFileSync } = require('node:child_process');
const readline = require('node:readline');
const { executeCreatorSearch, executeCreatorDetail, executeCreatorClassification } = require('./page-tasks');

const VERSION = '0.1.4';
const TARGET_ORIGIN = 'https://buyin.jinritemai.com';
const TARGET_URL = `${TARGET_ORIGIN}/dashboard/servicehall/daren-square`;
const WORKER_ORIGIN = 'http://daren-worker.local';
const DEFAULT_SERVER = String(process.env.DAREN_SERVER || '').replace(/\/$/, '');
const ROOT = process.env.DAREN_WORKER_HOME || path.join(process.env.LOCALAPPDATA || os.homedir(), 'DarenWorkbench', 'Worker');
const STATE_FILE = path.join(ROOT, 'worker-state.json');
const PROFILE_DIR = process.env.DAREN_CHROME_PROFILE || path.join(ROOT, 'chrome-profile');
const CDP_PORT = Number(process.env.DAREN_CDP_PORT || 9223);
const CDP_ORIGIN = `http://127.0.0.1:${CDP_PORT}`;

function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
async function withTimeout(promise, timeoutMs, message) {
  let timer;
  try {
    return await Promise.race([
      promise,
      new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(message)), timeoutMs); })
    ]);
  } finally { clearTimeout(timer); }
}
function json(value) { return JSON.stringify(value); }
function readJson(file) { return JSON.parse(fs.readFileSync(file, 'utf8')); }
function writeJson(file, value) { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, JSON.stringify(value, null, 2), { mode: 0o600 }); }
function log(message) { process.stdout.write(`[daren-worker ${new Date().toLocaleTimeString()}] ${message}\n`); }

function parseArgs(argv) {
  const result = { command: argv[2] || 'run' };
  for (let i = 3; i < argv.length; i += 1) {
    const token = argv[i];
    if (!token.startsWith('--')) continue;
    const key = token.slice(2);
    result[key] = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[++i] : true;
  }
  return result;
}

function credentialsFromPairingCode(value) {
  const normalized = String(value || '').replace(/-/g, '').trim().toUpperCase();
  if (!/^[A-Z2-9]{32}$/.test(normalized)) throw new Error('配对码格式不正确，应为 8+24 位字母数字');
  return { entryId: normalized.slice(0, 8), secret: normalized.slice(8) };
}

function bridgeKey(credentials, info) {
  return Buffer.from(crypto.hkdfSync('sha256', Buffer.from(credentials.secret), Buffer.from(`daren-session-bridge:${credentials.entryId}`), Buffer.from(info), 32));
}

function encryptPayload(credentials, payload, info) {
  const iv = crypto.randomBytes(12);
  const cipher = crypto.createCipheriv('aes-256-gcm', bridgeKey(credentials, info), iv);
  cipher.setAAD(Buffer.from(credentials.entryId));
  const ciphertext = Buffer.concat([cipher.update(JSON.stringify(payload), 'utf8'), cipher.final(), cipher.getAuthTag()]);
  return { version: 1, iv: iv.toString('base64'), ciphertext: ciphertext.toString('base64') };
}

function decryptPayload(credentials, payload, info) {
  const packed = Buffer.from(String(payload?.ciphertext || ''), 'base64');
  const decipher = crypto.createDecipheriv('aes-256-gcm', bridgeKey(credentials, info), Buffer.from(String(payload?.iv || ''), 'base64'));
  decipher.setAAD(Buffer.from(credentials.entryId));
  decipher.setAuthTag(packed.subarray(-16));
  return JSON.parse(Buffer.concat([decipher.update(packed.subarray(0, -16)), decipher.final()]).toString('utf8'));
}

function digestPayload(payload) {
  return crypto.createHash('sha256').update(`${payload?.version || ''}.${payload?.iv || ''}.${payload?.ciphertext || ''}`).digest('hex');
}

function authBody(device, action, extra = '') {
  const timestamp = Date.now();
  const nonce = crypto.randomBytes(16).toString('hex');
  const signature = crypto.createHmac('sha256', device.secret).update(`${action}\n${device.deviceId}\n${timestamp}\n${nonce}\n${extra}`).digest('hex');
  return { deviceId: device.deviceId, timestamp, nonce, signature };
}

async function apiPost(server, pathname, body) {
  const response = await fetch(`${server}${pathname}`, { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: WORKER_ORIGIN, 'User-Agent': `daren-worker/${VERSION}` }, body: json(body) });
  const payload = await response.json().catch(() => ({}));
  if (!response.ok) throw new Error(payload.error || `服务器请求失败 HTTP ${response.status}`);
  return payload;
}

class CdpClient {
  constructor(url) { this.url = url; this.id = 0; this.pending = new Map(); this.ws = null; }
  async connect() {
    if (typeof WebSocket !== 'function') throw new Error('需要 Node.js 22 或更高版本以连接 Chrome');
    this.ws = new WebSocket(this.url);
    await new Promise((resolve, reject) => {
      const timer = setTimeout(() => reject(new Error('连接 Chrome 调试端口超时')), 10000);
      this.ws.addEventListener('open', () => { clearTimeout(timer); resolve(); });
      this.ws.addEventListener('error', (event) => { clearTimeout(timer); reject(new Error(event?.message || 'Chrome 调试连接失败')); });
    });
    this.ws.addEventListener('message', (event) => {
      let message;
      try { message = JSON.parse(String(event.data)); } catch (_) { return; }
      const pending = this.pending.get(message.id);
      if (!pending) return;
      this.pending.delete(message.id);
      if (message.error) pending.reject(new Error(message.error.message || 'Chrome DevTools 调用失败'));
      else pending.resolve(message.result);
    });
  }
  send(method, params = {}) {
    const id = ++this.id;
    return new Promise((resolve, reject) => {
      this.pending.set(id, { resolve, reject });
      this.ws.send(JSON.stringify({ id, method, params }));
    });
  }
  close() {
    for (const pending of this.pending.values()) pending.reject(new Error('Chrome DevTools 连接已关闭'));
    this.pending.clear();
    try { this.ws?.close(); } catch (_) {}
  }
}

async function cdpJson(pathname, options = {}) {
  const response = await fetch(`${CDP_ORIGIN}${pathname}`, options);
  if (!response.ok) throw new Error(`Chrome 调试接口 HTTP ${response.status}`);
  return response.json();
}

function commandOutput(command, args) {
  try { return execFileSync(command, args, { encoding: 'utf8', windowsHide: true, timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] }).trim(); }
  catch (_) { return ''; }
}

function registryAppPaths(executable) {
  const keys = [
    `HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\${executable}`,
    `HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\${executable}`,
    `HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\App Paths\\${executable}`
  ];
  return keys.flatMap((key) => {
    const output = commandOutput('reg.exe', ['query', key, '/ve']);
    const match = output.match(/REG_(?:EXPAND_)?SZ\s+(.+)$/im);
    return match ? [match[1].trim().replace(/^"|"$/g, '')] : [];
  });
}

function pathExecutables(executable) {
  return commandOutput('where.exe', [executable]).split(/\r?\n/).map((item) => item.trim()).filter(Boolean);
}

function desktopShortcutTargets() {
  const script = "[Console]::OutputEncoding=[Text.UTF8Encoding]::new(); $w=New-Object -ComObject WScript.Shell; @([Environment]::GetFolderPath('Desktop'),[Environment]::GetFolderPath('CommonDesktopDirectory')) | Select-Object -Unique | ForEach-Object { Get-ChildItem -LiteralPath $_ -Filter '*.lnk' -ErrorAction SilentlyContinue } | ForEach-Object { $w.CreateShortcut($_.FullName).TargetPath }";
  return commandOutput('powershell.exe', ['-NoProfile', '-Command', script]).split(/\r?\n/).map((item) => item.trim()).filter((item) => /(?:chrome|msedge)\.exe$/i.test(item));
}

function chromeCandidates() {
  const local = process.env.LOCALAPPDATA || '';
  const programFiles = [process.env.ProgramW6432, process.env.ProgramFiles, process.env['ProgramFiles(x86)']].filter(Boolean);
  const common = programFiles.flatMap((root) => [path.join(root, 'Google', 'Chrome', 'Application', 'chrome.exe'), path.join(root, 'Microsoft', 'Edge', 'Application', 'msedge.exe')]);
  return [...new Set([
    process.env.CHROME_PATH,
    path.join(local, 'Google', 'Chrome', 'Application', 'chrome.exe'),
    path.join(local, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
    ...common,
    ...registryAppPaths('chrome.exe'),
    ...registryAppPaths('msedge.exe'),
    ...pathExecutables('chrome.exe'),
    ...pathExecutables('msedge.exe'),
    ...desktopShortcutTargets()
  ].filter(Boolean))];
}

async function cdpReady() {
  try { await cdpJson('/json/version'); return true; } catch (_) { return false; }
}

async function ensureChrome() {
  if (await cdpReady()) return;
  const candidates = chromeCandidates();
  const executable = candidates.find((candidate) => fs.existsSync(candidate));
  if (!executable) throw new Error(`找不到 Chrome 或 Edge；已检查 ${candidates.length} 个位置，请在 Chrome 快捷方式属性中确认目标路径或设置 CHROME_PATH`);
  fs.mkdirSync(PROFILE_DIR, { recursive: true });
  const child = spawn(executable, [`--remote-debugging-port=${CDP_PORT}`, `--user-data-dir=${PROFILE_DIR}`, '--no-first-run', '--no-default-browser-check', TARGET_URL], { detached: true, stdio: 'ignore', windowsHide: false });
  child.unref();
  for (let i = 0; i < 30; i += 1) { if (await cdpReady()) return; await sleep(500); }
  throw new Error(`Chrome 调试端口未启动: ${CDP_PORT}`);
}

async function listPages() { return (await cdpJson('/json/list')).filter((item) => item.type === 'page' && item.webSocketDebuggerUrl); }

async function openTarget(url) {
  try { return await cdpJson(`/json/new?${encodeURIComponent(url)}`, { method: 'PUT' }); }
  catch (_) { return cdpJson(`/json/new?${encodeURIComponent(url)}`); }
}

async function selectPage() {
  let pages = await listPages();
  let page = pages.find((item) => String(item.url || '').startsWith(`${TARGET_ORIGIN}/`));
  if (page) return page;
  page = await openTarget(TARGET_URL);
  await sleep(1200);
  pages = await listPages();
  return pages.find((item) => item.id === page.id) || pages.find((item) => String(item.url || '').startsWith(`${TARGET_ORIGIN}/`));
}

async function withPage(page, callback) {
  const client = new CdpClient(page.webSocketDebuggerUrl);
  await client.connect();
  try { return await callback(client); } finally { client.close(); }
}

async function evaluate(page, fn, value, timeoutMs = 0) {
  return withPage(page, async (client) => {
    const evaluation = client.send('Runtime.evaluate', { expression: `(${fn.toString()})(${json(value)})`, awaitPromise: true, returnByValue: true, userGesture: true });
    const result = timeoutMs > 0 ? await withTimeout(evaluation, timeoutMs, `Chrome 页面任务超过 ${timeoutMs}ms`) : await evaluation;
    if (result?.exceptionDetails) throw new Error(result.exceptionDetails.text || '精选联盟页面执行失败');
    return result?.result?.value;
  });
}

async function navigate(page, url) {
  await withPage(page, (client) => client.send('Page.navigate', { url }));
  await sleep(1800);
}

async function browserUserAgent(page) { return evaluate(page, () => navigator.userAgent, null); }

async function browserCookies(page) {
  return withPage(page, async (client) => {
    await client.send('Network.enable');
    const result = await client.send('Network.getAllCookies');
    return (result.cookies || []).filter((item) => /(^|\.)jinritemai\.com$/i.test(String(item.domain || ''))).map((item) => ({ name: item.name, value: item.value, domain: item.domain, path: item.path || '/' }));
  });
}

async function waitForEnter(prompt) {
  const input = readline.createInterface({ input: process.stdin, output: process.stdout });
  await new Promise((resolve) => input.question(`${prompt}\n> `, () => resolve()));
  input.close();
}

async function setup(config) {
  if (!config.server) throw new Error('请使用 --server https://<实际子域名> 指定部署服务器');
  await ensureChrome();
  const page = await selectPage();
  if (!page) throw new Error('无法打开精选联盟页面');
  log(`Chrome 已启动: ${page.url}`);
  await waitForEnter('请在打开的 Chrome 中登录精选联盟并进入达人广场，完成后按回车');
  const cookies = await browserCookies(page);
  if (!cookies.length) throw new Error('没有读取到精选联盟 Cookie，请确认已经登录');
  const credentials = credentialsFromPairingCode(config.pairingCode);
  const encryptedSession = encryptPayload(credentials, { sourceUrl: String(page.url || TARGET_URL), browser: await browserUserAgent(page), clientType: 'windows_worker', clientVersion: VERSION, cookies }, 'cookie-sync-v1');
  const synced = await apiPost(config.server, '/api/douyin/worker-sync', { pairingId: credentials.entryId, encryptedSession });
  const registered = decryptPayload(credentials, synced.encryptedDevice, 'device-registration-v1');
  const state = { version: 1, server: config.server, deviceId: registered.deviceId, entryId: registered.deviceId, secret: registered.secret, profileDir: PROFILE_DIR, cdpPort: CDP_PORT, workerVersion: VERSION, registeredAt: new Date().toISOString() };
  writeJson(STATE_FILE, state);
  log(`工作节点已配对，Cookie 数量 ${synced.cookieCount || cookies.length}`);
  log(`状态文件: ${STATE_FILE}`);
}

async function pollOnce(state) {
  const page = await selectPage();
  if (!page) throw new Error('没有找到精选联盟页面，请保持 Chrome 打开');
  const auth = authBody(state, 'next');
  const claimed = await apiPost(state.server, '/api/creator-search/worker-next', auth);
  if (claimed.status !== 'claimed') return false;
  const taskInfo = claimed.encryptedTask?.taskKind === 'creator_detail'
    ? 'creator-detail-device-task-v1'
    : claimed.encryptedTask?.taskKind === 'wps_creator_classify' ? 'wps-creator-classify-device-task-v1' : 'creator-search-device-task-v1';
  let task;
  try { task = decryptPayload(state, claimed.encryptedTask, taskInfo); }
  catch (_) { task = decryptPayload(state, claimed.encryptedTask, 'creator-search-device-task-v1'); }
  if (task.jobId !== claimed.jobId) throw new Error('浏览器任务编号校验失败');
  log(`领取任务 ${task.jobId} (${task.kind === 'creator_detail' ? '详情筛选' : '达人搜索'})`);
  let result;
  if (task.kind === 'creator_detail') {
    const originalUrl = page.url;
    const results = [];
    for (const candidate of task.candidates || []) {
      const profileUrl = String(candidate.profileUrl || '').startsWith(TARGET_ORIGIN) ? candidate.profileUrl : `${TARGET_ORIGIN}/dashboard/servicehall/daren-profile?uid=${encodeURIComponent(candidate.id)}&enter_from=1&scene=1&author_type=1`;
      await navigate(page, profileUrl);
      const one = await evaluate(page, executeCreatorDetail, { ...task, candidates: [candidate] });
      results.push(...(one?.results || []));
    }
    if (originalUrl.startsWith(TARGET_ORIGIN)) await navigate(page, originalUrl);
    result = { version: 1, kind: 'creator_detail', jobId: task.jobId, sourceUrl: originalUrl, browser: await browserUserAgent(page), latencyMs: 0, results };
  } else if (task.kind === 'wps_creator_classify') {
    const originalUrl = page.url;
    const profileUrl = String(task.profileUrl || '').startsWith(TARGET_ORIGIN)
      ? task.profileUrl
      : `${TARGET_ORIGIN}/dashboard/servicehall/daren-profile?uid=${encodeURIComponent(task.uid || '')}&enter_from=1&scene=1&author_type=1`;
    await navigate(page, profileUrl);
    result = await evaluate(page, executeCreatorClassification, task, 45000);
    if (originalUrl.startsWith(TARGET_ORIGIN) && originalUrl !== profileUrl) await navigate(page, originalUrl);
  } else {
    const taskTimeoutMs = Math.max(20000, Math.min(75000, Number(task.requestTimeoutMs) || 30000) + 15000);
    result = await evaluate(page, executeCreatorSearch, task, taskTimeoutMs);
  }
  const resultInfo = task.kind === 'creator_detail'
    ? 'creator-detail-device-result-v1'
    : task.kind === 'wps_creator_classify' ? 'wps-creator-classify-device-result-v1' : 'creator-search-device-result-v1';
  const encryptedResult = encryptPayload(state, result, resultInfo);
  const digest = digestPayload(encryptedResult);
  const complete = await apiPost(state.server, '/api/creator-search/worker-complete', { jobId: task.jobId, encryptedResult, ...authBody(state, 'complete', `${task.jobId}\n${digest}`) });
  log(`任务完成 ${task.jobId}: ${complete.itemCount || complete.total || 0} 条`);
  return true;
}

async function run(config) {
  let state;
  try { state = readJson(STATE_FILE); } catch (_) { throw new Error(`找不到工作节点状态，请先运行 setup: ${STATE_FILE}`); }
  state.entryId = state.entryId || state.deviceId;
  if (config.server && config.server !== state.server) {
    state.server = config.server;
    writeJson(STATE_FILE, state);
  }
  if (!state.server) throw new Error('没有配置服务器地址，请使用 --server https://<实际子域名> 启动一次');
  await ensureChrome();
  log(`工作节点 ${VERSION} 已启动，服务器 ${state.server}`);
  for (;;) {
    try { await pollOnce(state); }
    catch (error) { log(`等待或执行任务失败: ${String(error?.message || error).slice(0, 240)}`); }
    await sleep(4000);
  }
}

async function main() {
  const args = parseArgs(process.argv);
  const config = { server: String(args.server || DEFAULT_SERVER).replace(/\/$/, ''), pairingCode: args['pairing-code'] || '' };
  if (args.command === 'setup') {
    if (!config.pairingCode) { const input = readline.createInterface({ input: process.stdin, output: process.stdout }); config.pairingCode = await new Promise((resolve) => input.question('请输入管理后台生成的一次性配对码: ', resolve)); input.close(); }
    await setup(config);
    return;
  }
  await run(config);
}

if (require.main === module) main().catch((error) => { console.error(`[daren-worker] ${error.message}`); process.exitCode = 1; });

module.exports = { VERSION, WORKER_ORIGIN, credentialsFromPairingCode, encryptPayload, decryptPayload, digestPayload, authBody, chromeCandidates, withTimeout };
