/**
 * 审计敏感标记缺口分析
 *
 * 复用 audit-coverage-scan 的静态解析，加一层启发式：
 * 找出"按命名/动词应当 @Sensitive，但实际没有"的端点。
 *
 * 启发式（任一命中 = 应敏感）：
 *   1. DELETE 动词
 *   2. 路径或方法名含: password / reset / token / secret / key / credential
 *   3. 路径或方法名含: bulk / batch / mass
 *   4. 路径或方法名含: role / permission / data-scope / data_scope / access / scope / grant / revoke
 *   5. 路径或方法名含: lock / unlock / disable / enable / suspend / activate / archive / restore
 *   6. 路径或方法名含: approve / reject / publish / transfer / merge
 */

import * as fs from 'fs';
import * as path from 'path';

const REPO_ROOT = path.resolve(__dirname, '../..');
const BACKEND_SRC = path.join(REPO_ROOT, 'backend/src');
const REPORT_DIR = path.join(REPO_ROOT, 'testing/reports');

type Verb = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
interface Endpoint {
  file: string;
  module: string;
  controller: string;
  controllerPrefix: string;
  verb: Verb;
  subPath: string;
  fullPath: string;
  methodName: string;
  isSensitive: boolean;
  isFinancial: boolean;
}

function findControllerFiles(dir: string): string[] {
  const out: string[] = [];
  const stack = [dir];
  while (stack.length) {
    const d = stack.pop()!;
    for (const e of fs.readdirSync(d, { withFileTypes: true })) {
      const p = path.join(d, e.name);
      if (e.isDirectory()) stack.push(p);
      else if (e.isFile() && p.endsWith('.controller.ts')) out.push(p);
    }
  }
  return out;
}

function deriveModule(file: string): string {
  const rel = path.relative(BACKEND_SRC, file).replace(/\\/g, '/');
  const parts = rel.split('/');
  if (parts[0] === 'modules') return parts[1] + (parts[2] && !parts[2].endsWith('.ts') ? '/' + parts[2] : '');
  if (parts[0] === 'core') return 'core/' + parts[1];
  if (parts[0] === 'engines') return 'engines/' + parts[1];
  return parts[0];
}

function parseFile(file: string): Endpoint[] {
  const src = fs.readFileSync(file, 'utf8');
  const lines = src.split('\n');

  let controllerPrefix = '';
  let controllerName = '';
  for (let i = 0; i < lines.length; i++) {
    const m = lines[i].match(/^@Controller\(\s*(?:'([^']*)'|"([^"]*)"|`([^`]*)`)?\s*\)/);
    if (m) controllerPrefix = (m[1] ?? m[2] ?? m[3] ?? '').replace(/^\/+|\/+$/g, '');
    const cm = lines[i].match(/^export\s+class\s+(\w+)/);
    if (cm) {
      controllerName = cm[1];
      break;
    }
  }

  const endpoints: Endpoint[] = [];
  for (let i = 0; i < lines.length; i++) {
    if (!/^\s*@Auditable\(/.test(lines[i])) continue;

    let verb: Verb | null = null;
    let subPath = '';
    for (let j = i - 1; j >= Math.max(0, i - 8); j--) {
      const m = lines[j].match(/^\s*@(Get|Post|Put|Patch|Delete)\(\s*(?:'([^']*)'|"([^"]*)"|`([^`]*)`)?/);
      if (m) {
        verb = m[1].toUpperCase() as Verb;
        subPath = (m[2] ?? m[3] ?? m[4] ?? '').replace(/^\/+/, '');
        break;
      }
    }
    if (!verb) continue;

    let methodName = '?';
    let isSensitive = false;
    let isFinancial = false;
    for (let k = i + 1; k < Math.min(lines.length, i + 20); k++) {
      if (/^\s*@Sensitive\(/.test(lines[k])) isSensitive = true;
      if (/^\s*@Financial\(/.test(lines[k])) isFinancial = true;
      const mn = lines[k].match(/^\s*(?:async\s+)?(\w+)\s*\(/);
      if (mn && !lines[k].trim().startsWith('@')) {
        methodName = mn[1];
        break;
      }
    }

    const prefix = controllerPrefix ? '/' + controllerPrefix : '';
    const tail = subPath ? '/' + subPath : '';
    const fullPath = prefix + tail || '/';

    endpoints.push({
      file: path.relative(REPO_ROOT, file),
      module: deriveModule(file),
      controller: controllerName,
      controllerPrefix,
      verb,
      subPath,
      fullPath,
      methodName,
      isSensitive,
      isFinancial,
    });
  }
  return endpoints;
}

const SENSITIVE_PATTERNS: Array<{ name: string; re: RegExp }> = [
  { name: 'verb=DELETE', re: /__VERB_DELETE__/ }, // handled separately
  { name: 'auth/credential', re: /password|reset|token|secret|credential|api[-_]?key/i },
  { name: 'bulk/batch', re: /\b(bulk|batch|mass|all)\b/i },
  { name: 'permission/role', re: /\brole\b|\bpermission\b|data[-_]?scope|\bgrant\b|\brevoke\b|\baccess\b/i },
  { name: 'lifecycle', re: /\block\b|unlock|disable|enable|suspend|activate|archive|restore|deactivate/i },
  { name: 'approval/state', re: /\bapprove\b|\breject\b|\bpublish\b|\btransfer\b|\bmerge\b|\bemergency\b|\bbypass\b|impersonate|sudo/i },
];

function classifyShouldBeSensitive(ep: Endpoint): string[] {
  const reasons: string[] = [];
  if (ep.verb === 'DELETE') reasons.push('verb=DELETE');
  const haystack = `${ep.fullPath} ${ep.methodName}`;
  for (const p of SENSITIVE_PATTERNS) {
    if (p.name === 'verb=DELETE') continue;
    if (p.re.test(haystack)) reasons.push(p.name);
  }
  return reasons;
}

async function main() {
  const files = findControllerFiles(BACKEND_SRC);
  const all: Endpoint[] = [];
  for (const f of files) all.push(...parseFile(f));

  const totalAuditable = all.length;
  const totalSensitive = all.filter((e) => e.isSensitive).length;
  const totalFinancial = all.filter((e) => e.isFinancial).length;

  // Endpoints that should be sensitive but aren't
  const gaps: Array<{ ep: Endpoint; reasons: string[] }> = [];
  // Endpoints marked sensitive but heuristic doesn't match (likely fine, just informational)
  const overTagged: Endpoint[] = [];

  for (const ep of all) {
    const reasons = classifyShouldBeSensitive(ep);
    if (reasons.length > 0 && !ep.isSensitive) gaps.push({ ep, reasons });
    if (reasons.length === 0 && ep.isSensitive) overTagged.push(ep);
  }

  // Group gaps by module
  const gapsByModule = new Map<string, Array<{ ep: Endpoint; reasons: string[] }>>();
  for (const g of gaps) {
    if (!gapsByModule.has(g.ep.module)) gapsByModule.set(g.ep.module, []);
    gapsByModule.get(g.ep.module)!.push(g);
  }

  console.log('📊 @Sensitive 缺口分析\n');
  console.log(`  @Auditable 总数: ${totalAuditable}`);
  console.log(`  @Sensitive 已标: ${totalSensitive}`);
  console.log(`  @Financial 已标: ${totalFinancial}`);
  console.log(`  启发式判定应敏感但未标: ${gaps.length}`);
  console.log(`  标了但启发式未命中（可能多余/项目特定）: ${overTagged.length}\n`);

  console.log('🚨 缺口（按模块）:\n');
  for (const m of [...gapsByModule.keys()].sort()) {
    const list = gapsByModule.get(m)!;
    console.log(`  ${m}  (${list.length})`);
    for (const g of list) {
      console.log(`    ${g.ep.verb.padEnd(6)} ${g.ep.fullPath.padEnd(50)} ${g.ep.controller}.${g.ep.methodName}  [${g.reasons.join(',')}]`);
    }
  }

  // Write report
  if (!fs.existsSync(REPORT_DIR)) fs.mkdirSync(REPORT_DIR, { recursive: true });
  const ts = new Date().toISOString().replace(/[:.]/g, '-');
  const reportPath = path.join(REPORT_DIR, `audit-sensitive-gap-${ts}.md`);

  const lines: string[] = [];
  lines.push(`# @Sensitive 标记缺口分析报告\n`);
  lines.push(`- 时间: ${new Date().toISOString()}`);
  lines.push(`- @Auditable 端点总数: ${totalAuditable}`);
  lines.push(`- 已标 @Sensitive: ${totalSensitive}`);
  lines.push(`- 已标 @Financial: ${totalFinancial}`);
  lines.push(`- **启发式判定应敏感但未标: ${gaps.length}**`);
  lines.push(`- 启发式未命中但已标: ${overTagged.length}\n`);

  lines.push(`## 启发式规则\n`);
  lines.push(`命中以下任一即建议标 \`@Sensitive\`:\n`);
  lines.push(`- \`verb=DELETE\`：删除一律视为敏感`);
  for (const p of SENSITIVE_PATTERNS) {
    if (p.name === 'verb=DELETE') continue;
    lines.push(`- \`${p.name}\`：路径或方法名匹配 \`${p.re}\``);
  }

  lines.push(`\n## 缺口（按模块）\n`);
  for (const m of [...gapsByModule.keys()].sort()) {
    const list = gapsByModule.get(m)!;
    lines.push(`### ${m}  (${list.length})\n`);
    lines.push(`| Verb | Path | Controller.method | 命中规则 |`);
    lines.push(`|---|---|---|---|`);
    for (const g of list) {
      lines.push(`| ${g.ep.verb} | \`${g.ep.fullPath}\` | ${g.ep.controller}.${g.ep.methodName} | ${g.reasons.join(', ')} |`);
    }
    lines.push('');
  }

  if (overTagged.length > 0) {
    lines.push(`## 已标但启发式未命中（信息性，通常不需处理）\n`);
    lines.push(`| 模块 | Verb | Path | Controller.method |`);
    lines.push(`|---|---|---|---|`);
    for (const e of overTagged) {
      lines.push(`| ${e.module} | ${e.verb} | \`${e.fullPath}\` | ${e.controller}.${e.methodName} |`);
    }
  }

  fs.writeFileSync(reportPath, lines.join('\n'));
  console.log(`\n📝 Report written: ${path.relative(REPO_ROOT, reportPath)}`);
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});
