/**
 * Audit 覆盖度扫描
 *
 * 静态层：扫描所有 controller 中的 @Auditable，关联其 HTTP 装饰器和路径
 * 动态层：用 itadmin token 调用 GET 类 @Auditable 接口，统计 audit_log 增量
 *
 * 用法：
 *   npx ts-node testing/scripts/audit-coverage-scan.ts
 */

import * as fs from 'fs';
import * as path from 'path';
import { execSync } from 'child_process';

const REPO_ROOT = path.resolve(__dirname, '../..');
const BACKEND_SRC = path.join(REPO_ROOT, 'backend/src');
const REPORT_DIR = path.join(REPO_ROOT, 'testing/reports');
const API_BASE = process.env.AUDIT_SCAN_API || 'http://localhost:3001/api/v1';
const USERNAME = process.env.AUDIT_SCAN_USER || 'itadmin';
const PASSWORD = process.env.AUDIT_SCAN_PASSWORD || 'Admin@2024';

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('/');
  // src/modules/<m>/.. or src/modules/<m>/<sub>/.. or src/core/<m>/<sub>/.. or src/engines/<m>/..
  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');

  // Controller prefix
  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;

    // Look upward up to 8 lines for HTTP verb decorator
    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;

    // Look downward for method name and sibling decorators (Sensitive/Financial)
    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;
}

async function login(): Promise<string> {
  const res = await fetch(`${API_BASE}/auth/login`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ username: USERNAME, password: PASSWORD }),
  });
  const json: any = await res.json();
  if (!json?.data?.accessToken) {
    throw new Error('Login failed: ' + JSON.stringify(json).slice(0, 200));
  }
  return json.data.accessToken;
}

function fillPathParams(p: string): string {
  // Replace :id, :uuid -> a fixed UUID
  // Replace other :name -> a placeholder string
  return p
    .replace(/:[a-zA-Z0-9_]+\?/g, '') // drop optional params from path
    .replace(/:(id|uuid|.*Id)/g, '00000000-0000-0000-0000-000000000000')
    .replace(/:[a-zA-Z0-9_]+/g, 'placeholder');
}

const PG_USER = process.env.AUDIT_SCAN_PG_USER || 'ffoa_dev';
const PG_DB = process.env.AUDIT_SCAN_PG_DB || 'ffoa_development';
const PG_CONTAINER = process.env.AUDIT_SCAN_PG_CONTAINER || 'ffoa-dev-postgres';

async function snapshotAuditCount(): Promise<number> {
  const out = execSync(
    `docker exec ${PG_CONTAINER} psql -U ${PG_USER} -d ${PG_DB} -tAc "SELECT COUNT(*) FROM platform_audit.audit_log"`,
    { encoding: 'utf8' },
  ).trim();
  return parseInt(out, 10) || 0;
}

async function snapshotByModule(): Promise<Map<string, number>> {
  const out = execSync(
    `docker exec ${PG_CONTAINER} psql -U ${PG_USER} -d ${PG_DB} -tAc "SELECT module, COUNT(*) FROM platform_audit.audit_log GROUP BY module"`,
    { encoding: 'utf8' },
  );
  const m = new Map<string, number>();
  for (const line of out.split('\n')) {
    const [mod, cnt] = line.split('|');
    if (mod && cnt) m.set(mod.trim(), parseInt(cnt, 10) || 0);
  }
  return m;
}

async function main() {
  console.log('🔍 Phase 1: Static scan of @Auditable endpoints\n');
  const files = findControllerFiles(BACKEND_SRC);
  const allEndpoints: Endpoint[] = [];
  for (const f of files) {
    allEndpoints.push(...parseFile(f));
  }
  console.log(`  total controllers scanned: ${files.length}`);
  console.log(`  total @Auditable endpoints: ${allEndpoints.length}\n`);

  // Group by module
  const byModule = new Map<string, Endpoint[]>();
  for (const e of allEndpoints) {
    if (!byModule.has(e.module)) byModule.set(e.module, []);
    byModule.get(e.module)!.push(e);
  }

  console.log('📦 Per-module @Auditable counts:');
  const modules = [...byModule.keys()].sort();
  for (const m of modules) {
    const eps = byModule.get(m)!;
    const verbs = eps.reduce((acc, e) => ((acc[e.verb] = (acc[e.verb] || 0) + 1), acc), {} as Record<string, number>);
    console.log(`  ${m.padEnd(40)} total=${eps.length}  ${JSON.stringify(verbs)}`);
  }

  console.log('\n🔐 Phase 2: Login and probe GET endpoints\n');
  const token = await login();
  console.log('  ✅ logged in as ' + USERNAME);

  const before = await snapshotByModule();
  const beforeTotal = [...before.values()].reduce((a, b) => a + b, 0);

  // Probe GET endpoints only — safe
  const getEndpoints = allEndpoints.filter((e) => e.verb === 'GET');
  console.log(`  GET probes to run: ${getEndpoints.length}\n`);

  const results: Array<{ ep: Endpoint; status: number; ok: boolean }> = [];
  let probed = 0;
  for (const ep of getEndpoints) {
    const url = API_BASE + fillPathParams(ep.fullPath);
    try {
      const res = await fetch(url, {
        headers: { Authorization: `Bearer ${token}`, 'X-Audit-Scan': '1' },
      });
      results.push({ ep, status: res.status, ok: res.ok });
    } catch (err: any) {
      results.push({ ep, status: -1, ok: false });
    }
    probed++;
    if (probed % 20 === 0) console.log(`    probed ${probed}/${getEndpoints.length}`);
  }
  console.log(`    probed ${probed}/${getEndpoints.length}\n`);

  // Wait for async audit writes to flush
  await new Promise((r) => setTimeout(r, 2000));

  const after = await snapshotByModule();
  const afterTotal = [...after.values()].reduce((a, b) => a + b, 0);

  // Map controller name → module for cross-reference
  const controllerToModule = new Map<string, string>();
  for (const e of allEndpoints) {
    controllerToModule.set(e.controller.replace('Controller', ''), e.module);
  }

  // Aggregate audit_log delta by inferred module
  const deltaByAuditModule = new Map<string, number>();
  for (const [mod, cnt] of after.entries()) {
    deltaByAuditModule.set(mod, cnt - (before.get(mod) ?? 0));
  }

  // Summarize per source module
  const summary: Array<{
    module: string;
    auditableTotal: number;
    getCount: number;
    getOk: number;
    getFail: number;
    auditDelta: number;
  }> = [];

  for (const m of modules) {
    const eps = byModule.get(m)!;
    const moduleResults = results.filter((r) => r.ep.module === m);
    const getCount = moduleResults.length;
    const getOk = moduleResults.filter((r) => r.ok).length;
    const getFail = getCount - getOk;

    // delta per module: sum of deltas of audit modules whose controller's source-module == m
    let delta = 0;
    for (const [auditMod, d] of deltaByAuditModule.entries()) {
      const sourceMod = controllerToModule.get(auditMod);
      if (sourceMod === m && d > 0) delta += d;
    }
    summary.push({ module: m, auditableTotal: eps.length, getCount, getOk, getFail, auditDelta: delta });
  }

  console.log('📊 Per-module results:\n');
  console.log('  module                                   | total | GET | ok  | fail | delta');
  console.log('  -----------------------------------------+-------+-----+-----+------+------');
  for (const s of summary) {
    console.log(
      `  ${s.module.padEnd(40)} | ${String(s.auditableTotal).padStart(5)} | ${String(s.getCount).padStart(3)} | ${String(s.getOk).padStart(3)} | ${String(s.getFail).padStart(4)} | ${String(s.auditDelta).padStart(5)}`,
    );
  }
  console.log(`\n  audit_log total:  before=${beforeTotal}  after=${afterTotal}  delta=${afterTotal - beforeTotal}`);

  // 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-coverage-${ts}.md`);

  const lines: string[] = [];
  lines.push(`# Audit 覆盖度扫描报告\n`);
  lines.push(`- 时间: ${new Date().toISOString()}`);
  lines.push(`- 后端: ${API_BASE}`);
  lines.push(`- 用户: ${USERNAME}`);
  lines.push(`- 控制器文件数: ${files.length}`);
  lines.push(`- @Auditable 端点总数: ${allEndpoints.length}`);
  lines.push(`- audit_log 增量: ${afterTotal - beforeTotal} (before=${beforeTotal}, after=${afterTotal})\n`);

  lines.push(`## 静态扫描\n`);
  lines.push(`| 模块 | @Auditable 总数 | GET | POST | PUT | PATCH | DELETE | 敏感 | 财务 |`);
  lines.push(`|---|---:|---:|---:|---:|---:|---:|---:|---:|`);
  for (const m of modules) {
    const eps = byModule.get(m)!;
    const v = (vb: Verb) => eps.filter((e) => e.verb === vb).length;
    const sens = eps.filter((e) => e.isSensitive).length;
    const fin = eps.filter((e) => e.isFinancial).length;
    lines.push(`| ${m} | ${eps.length} | ${v('GET')} | ${v('POST')} | ${v('PUT')} | ${v('PATCH')} | ${v('DELETE')} | ${sens} | ${fin} |`);
  }

  lines.push(`\n## 动态层 GET 探测结果\n`);
  lines.push(`| 模块 | @Auditable | GET 探测 | 200/2xx | 失败 | audit_log 增量 |`);
  lines.push(`|---|---:|---:|---:|---:|---:|`);
  for (const s of summary) {
    lines.push(`| ${s.module} | ${s.auditableTotal} | ${s.getCount} | ${s.getOk} | ${s.getFail} | ${s.auditDelta} |`);
  }

  lines.push(`\n## 完整端点清单\n`);
  lines.push(`| 模块 | Verb | Path | Controller.method | 敏感 | 财务 |`);
  lines.push(`|---|---|---|---|---|---|`);
  for (const e of allEndpoints) {
    lines.push(
      `| ${e.module} | ${e.verb} | \`${e.fullPath}\` | ${e.controller}.${e.methodName} | ${e.isSensitive ? '✓' : ''} | ${e.isFinancial ? '✓' : ''} |`,
    );
  }

  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);
});
