'use strict';
/**
 * 通用解混淆脚本：对指定目录下的 JS 文件批量解码字符串、清理控制流并重命名 _0x 前缀标识符。
 * 使用：
 *   node tools/auto_deobf.js
 * 可配置 inputDir/outputDir/mapDir/rename 见下方常量。
 */
const fs = require('fs');
const path = require('path');
const vm = require('vm');
const beautify = require('js-beautify').js;
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;

// 配置：如有需要可修改
const inputDir = path.join(__dirname, '..', '原版本', 'extension', 'out');
const outputDir = path.join(__dirname, '..', 'codexfanbianyi', 'extension', 'out');
const mapDir = path.join(__dirname, '..'); // *_decoded_map.json 所在目录
const enableRename = true; // 是否批量重命名 _0x**** -> refN

function ensureDir(dir) {
  if (!fs.existsSync(dir)) {
    fs.mkdirSync(dir, { recursive: true });
  }
}

function listJsFiles(dir) {
  let files = [];
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
    const full = path.join(dir, entry.name);
    if (entry.isDirectory()) {
      files = files.concat(listJsFiles(full));
    } else if (entry.isFile() && entry.name.endsWith('.js')) {
      files.push(full);
    }
  }
  return files;
}

function extractFunctionWithBody(code, nameRegex) {
  const match = nameRegex.exec(code);
  if (!match) return null;
  const start = match.index;
  const braceStart = code.indexOf('{', start);
  if (braceStart === -1) return null;
  let depth = 0;
  for (let i = braceStart; i < code.length; i++) {
    const ch = code[i];
    if (ch === '{') depth++;
    else if (ch === '}') {
      depth--;
      if (depth === 0) {
        return { name: match[1], code: code.slice(start, i + 1), end: i + 1 };
      }
    }
  }
  return null;
}

function buildSandbox() {
  const sandbox = {
    console: { log: () => {}, warn: () => {}, error: () => {} },
    vip: 'cursor',
    Buffer,
    atob: (str) => Buffer.from(str, 'base64').toString('binary'),
    btoa: (str) => Buffer.from(str, 'binary').toString('base64'),
    decodeURIComponent,
    encodeURIComponent,
    parseInt,
    String,
    exports: {},
    module: { exports: {} },
    require: () => ({})
  };
  vm.createContext(sandbox);
  return sandbox;
}

function buildDecoder(code) {
  const arrayFunc = extractFunctionWithBody(code, /function\s+(_0x[a-f0-9]+)\s*\(\s*\)/i);
  const decoderFunc = extractFunctionWithBody(code, /function\s+(_0x[a-f0-9]+)\s*\(\s*_0x[a-f0-9]+\s*,\s*_0x[a-f0-9]+\s*\)/i);
  if (!arrayFunc || !decoderFunc) return null;
  const sandbox = buildSandbox();
  try {
    // 仅执行字符串数组和解码函数，避免触发业务逻辑
    vm.runInContext(`${arrayFunc.code}\n${decoderFunc.code}`, sandbox, { timeout: 4000 });
    // 启动段：取 exports 之前的代码，通常包含数组旋转
    const stopMarkers = ['var __createBinding', 'Object.defineProperty(exports', 'exports.', 'module.exports'];
    let bootstrapEnd = code.length;
    for (const marker of stopMarkers) {
      const idx = code.indexOf(marker);
      if (idx !== -1 && idx < bootstrapEnd) bootstrapEnd = idx;
    }
    const bootstrapCode = code.slice(0, bootstrapEnd);
    vm.runInContext(bootstrapCode, sandbox, { timeout: 4000 });
    const decoder = sandbox[decoderFunc.name];
    if (typeof decoder !== 'function') return null;
    return decoder.bind(sandbox);
  } catch {
    return null;
  }
}

function literalFor(decoded) {
  if (decoded == null) return 'undefined';
  if (typeof decoded !== 'string') return JSON.stringify(decoded);
  if (decoded.includes('\n')) return '`' + decoded.replace(/`/g, '\\`') + '`';
  return JSON.stringify(decoded);
}

function replaceWithMap(code, filePath) {
  const mapName = path.basename(filePath).replace('.js', '_decoded_map.json');
  const mapPath = path.join(mapDir, mapName);
  if (!fs.existsSync(mapPath)) return { code, replaced: 0, entries: 0 };
  const decodeMap = JSON.parse(fs.readFileSync(mapPath, 'utf8'));
  let replaced = 0;
  for (const [pattern, decoded] of Object.entries(decodeMap)) {
    const literal = literalFor(decoded);
    const newCode = code.split(pattern).join(literal);
    if (newCode !== code) {
      replaced++;
      code = newCode;
    }
  }
  return { code, replaced, entries: Object.keys(decodeMap).length };
}

function replaceWithDecoder(code, decoder) {
  if (!decoder) return { code, replaced: 0, found: 0 };
  const callPattern = /_0x[a-f0-9]+\s*\(\s*(0x[a-f0-9]+|\d+)\s*,\s*['"]([^'"]+)['"]\s*\)/gi;
  const replacements = new Map();
  let match;
  while ((match = callPattern.exec(code)) !== null) {
    const full = match[0];
    if (replacements.has(full)) continue;
    const index = match[1].startsWith('0x') ? parseInt(match[1], 16) : parseInt(match[1], 10);
    const key = match[2];
    try {
      const decoded = decoder(index, key);
      if (typeof decoded === 'string') {
        replacements.set(full, literalFor(decoded));
      }
    } catch {
      /* ignore individual decode errors */
    }
  }
  let replaced = 0;
  for (const [call, lit] of replacements) {
    const newCode = code.split(call).join(lit);
    if (newCode !== code) {
      replaced++;
      code = newCode;
    }
  }
  return { code, replaced, found: replacements.size };
}

function renameObfuscatedIdentifiers(code) {
  if (!enableRename) return code;
  const ast = parser.parse(code, { sourceType: 'unambiguous', plugins: ['jsx'] });
  let counter = 1;
  const renameMap = new Map();
  traverse(ast, {
    Identifier(path) {
      const name = path.node.name;
      if (!/^_0x[a-f0-9]+$/i.test(name)) return;
      const binding = path.scope.getBinding(name);
      if (!binding) return;
      if (renameMap.has(name)) return;
      const newName = `ref${counter++}`;
      binding.scope.rename(name, newName);
      renameMap.set(name, newName);
    }
  });
  return generate(ast, { compact: false }).code;
}

function processFile(inFile) {
  const rel = path.relative(inputDir, inFile);
  const outFile = path.join(outputDir, rel);
  console.log(`\n[process] ${rel}`);
  let code = fs.readFileSync(inFile, 'utf8');

  const mapResult = replaceWithMap(code, inFile);
  if (mapResult.entries > 0) console.log(`  map: ${mapResult.replaced}/${mapResult.entries}`);
  code = mapResult.code;

  const decoder = buildDecoder(code);
  if (!decoder) {
    console.log('  decoder: none');
  } else {
    const decRes = replaceWithDecoder(code, decoder);
    console.log(`  decoder replace: ${decRes.replaced}/${decRes.found}`);
    code = decRes.code;
  }

  code = renameObfuscatedIdentifiers(code);
  code = beautify(code, { indent_size: 2, max_preserve_newlines: 2, end_with_newline: true });

  ensureDir(path.dirname(outFile));
  fs.writeFileSync(outFile, code, 'utf8');
  console.log(`  output: ${outFile}`);
}

function main() {
  const files = listJsFiles(inputDir);
  console.log(`Found ${files.length} JS files under ${inputDir}`);
  files.forEach(processFile);
  console.log('\nAll done.');
}

main();
