#!/usr/bin/env node
#!/usr/bin/env node

// src/cli.ts
import { writeFileSync, readFileSync, existsSync } from "fs";
import { resolve, extname } from "path";
function parseArgs(argv) {
  const args = {
    url: null,
    duration: 30,
    network: "wifi",
    output: null,
    format: "console",
    session: null,
    headless: true,
    interact: false,
    help: false,
    version: false
  };
  const positional = [];
  for (let i = 0; i < argv.length; i++) {
    const arg = argv[i];
    const next = argv[i + 1];
    switch (arg) {
      case "--duration":
      case "-d":
        args.duration = parseInt(next, 10) || 30;
        i++;
        break;
      case "--network":
      case "-n":
        args.network = next;
        i++;
        break;
      case "--output":
      case "-o":
        args.output = next;
        i++;
        break;
      case "--format":
      case "-f":
        args.format = next;
        i++;
        break;
      case "--session":
      case "-s":
        args.session = next;
        i++;
        break;
      case "--no-headless":
        args.headless = false;
        break;
      case "--interact":
        args.interact = true;
        break;
      case "--help":
      case "-h":
        args.help = true;
        break;
      case "--version":
      case "-v":
        args.version = true;
        break;
      default:
        if (!arg.startsWith("-")) positional.push(arg);
    }
  }
  args.url = positional[0] || null;
  if (args.output && args.format === "console") {
    const ext = extname(args.output).toLowerCase();
    if (ext === ".html") args.format = "html";
    else if (ext === ".json") args.format = "json";
  }
  return args;
}
var HELP = `
\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
\u2551  \u26A1 FluxAPI Scanner CLI               \u2551
\u2551  Lighthouse for your API calls        \u2551
\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D

USAGE
  npx flux-scan <url> [options]

EXAMPLES
  npx flux-scan https://myapp.com
  npx flux-scan https://myapp.com -d 60 -n jio-4g
  npx flux-scan https://myapp.com -o report.html
  npx flux-scan --session scan.json -o report.json

OPTIONS
  -d, --duration <sec>   Scan duration in seconds (default: 30)
  -n, --network <name>   Network profile for scoring adjustment
                         wifi | jio-4g | airtel-4g | airtel-3g | bsnl-2g | slow-3g
  -o, --output <file>    Output file path (.html or .json)
  -f, --format <fmt>     Output format: console | html | json
  -s, --session <file>   Analyze saved session JSON (skip live scan)
      --no-headless      Show browser window during scan
      --interact         Wait for user to browse (press Enter to stop)
  -h, --help             Show this help
  -v, --version          Show version

SCORING
  90-100  \u{1F7E2} Excellent \u2014 API layer is well optimized
  70-89   \u{1F535} Good \u2014 Minor improvements possible
  50-69   \u{1F7E1} Needs Work \u2014 Several optimization opportunities
  0-49    \u{1F534} Poor \u2014 Significant API anti-patterns detected
`;
async function liveScan(url, args) {
  let puppeteer;
  try {
    puppeteer = await import("./puppeteer-URNPE4CJ.js");
  } catch {
    console.error("");
    console.error("  \u274C Puppeteer is required for live scanning.");
    console.error("");
    console.error("  Install it:");
    console.error("    npm install -g puppeteer");
    console.error("    # or");
    console.error("    npx flux-scan --session saved-session.json");
    console.error("");
    console.error("  To save a session from the browser:");
    console.error('    1. Add <script src="https://unpkg.com/@fluxiapi/scan"></script> to your page');
    console.error("    2. Open DevTools console");
    console.error("    3. Run: const s = new FluxScanner(); s.start();");
    console.error("    4. Browse for 30-60 seconds");
    console.error("    5. Run: copy(JSON.stringify(s.stop()))");
    console.error("    6. Paste into a .json file");
    console.error("    7. Run: npx flux-scan --session file.json");
    console.error("");
    process.exit(1);
  }
  const spinner = createSpinner();
  spinner.start(`Launching browser for ${url}`);
  const browser = await puppeteer.default.launch({
    headless: args.headless ? "new" : false,
    args: ["--no-sandbox", "--disable-setuid-sandbox"]
  });
  const page = await browser.newPage();
  const requests = [];
  let reqSeq = 0;
  await page.setRequestInterception(true);
  page.on("request", (req) => {
    req.continue();
  });
  const client = await page.createCDPSession();
  await client.send("Network.enable");
  const pendingRequests = /* @__PURE__ */ new Map();
  client.on("Network.requestWillBeSent", (params) => {
    reqSeq++;
    const { requestId, request, timestamp, initiator } = params;
    pendingRequests.set(requestId, {
      id: `fx_cli_${reqSeq}`,
      url: request.url,
      method: request.method,
      urlParts: parseUrlSimple(request.url),
      headers: sanitizeHeaders(request.headers || {}),
      bodySize: request.postData ? request.postData.length : 0,
      bodyHash: null,
      startTime: timestamp * 1e3,
      ttfb: null,
      endTime: null,
      duration: null,
      response: null,
      initiator: {
        stackTrace: [],
        componentName: extractComponentFromStack(initiator?.stack?.callFrames),
        componentFile: null,
        rawStack: ""
      },
      navigationContext: {
        currentRoute: new URL(request.url, url).pathname,
        previousRoute: null,
        timeSinceNavigation: 0,
        pageState: "complete"
      },
      type: classifyUrl(request.url, request.method),
      source: "fetch",
      error: null,
      sequence: reqSeq
    });
  });
  client.on("Network.responseReceived", (params) => {
    const { requestId, response, timestamp } = params;
    const req = pendingRequests.get(requestId);
    if (!req) return;
    req.ttfb = timestamp * 1e3;
    req.response = {
      status: response.status,
      statusText: response.statusText || "",
      headers: response.headers || {},
      bodySize: response.encodedDataLength || 0,
      contentType: response.mimeType || null,
      cacheHeaders: {
        cacheControl: response.headers?.["cache-control"] || null,
        etag: response.headers?.["etag"] || null,
        lastModified: response.headers?.["last-modified"] || null,
        expires: response.headers?.["expires"] || null,
        age: response.headers?.["age"] || null,
        acceptEncoding: false,
        contentEncoding: response.headers?.["content-encoding"] || null
      },
      bodyHash: `cdp_${requestId.slice(0, 8)}`,
      jsonFieldCount: null,
      fromCache: response.fromDiskCache || response.fromServiceWorker || false
    };
  });
  client.on("Network.loadingFinished", (params) => {
    const { requestId, timestamp, encodedDataLength } = params;
    const req = pendingRequests.get(requestId);
    if (!req) return;
    req.endTime = timestamp * 1e3;
    req.duration = req.endTime - req.startTime;
    if (req.response) req.response.bodySize = encodedDataLength || req.response.bodySize;
    requests.push(req);
    pendingRequests.delete(requestId);
  });
  client.on("Network.loadingFailed", (params) => {
    const { requestId, errorText, timestamp } = params;
    const req = pendingRequests.get(requestId);
    if (!req) return;
    req.endTime = timestamp * 1e3;
    req.duration = req.endTime - req.startTime;
    req.error = errorText;
    requests.push(req);
    pendingRequests.delete(requestId);
  });
  spinner.update(`Navigating to ${url}`);
  await page.goto(url, { waitUntil: "networkidle2", timeout: 3e4 });
  spinner.update(`Scanning... (${args.duration}s)`);
  if (args.interact) {
    spinner.stop();
    console.log("");
    console.log("  \u{1F5B1}\uFE0F  Browse the app in the opened window.");
    console.log("  Press Enter when done...");
    await waitForEnter();
  } else {
    await countdown(args.duration, spinner);
  }
  spinner.update("Closing browser...");
  await browser.close();
  spinner.stop();
  const apiRequests = requests.filter(
    (r) => r.type === "api-rest" || r.type === "api-graphql" || r.type === "api-grpc"
  );
  const session = {
    id: `cli_${Date.now()}`,
    startTime: requests[0]?.startTime || 0,
    endTime: requests[requests.length - 1]?.endTime || 0,
    requests,
    navigations: [],
    stack: { framework: null, dataLibrary: null, apiType: "rest", backendHints: detectBackend(requests) },
    config: {
      duration: args.duration,
      network: args.network,
      ignore: [],
      captureFields: false,
      maxRequests: 5e3,
      minDuration: 0,
      verbose: false
    },
    metadata: {
      pageUrl: url,
      userAgent: "FluxAPI CLI",
      scanDuration: args.duration * 1e3,
      totalRequests: requests.length,
      apiRequests: apiRequests.length,
      uniqueEndpoints: new Set(requests.map((r) => r.urlParts.pathPattern)).size,
      uniqueHosts: [...new Set(requests.map((r) => r.urlParts.host))]
    }
  };
  return session;
}
function loadSession(path) {
  const abs = resolve(path);
  if (!existsSync(abs)) {
    console.error(`  \u274C Session file not found: ${abs}`);
    process.exit(1);
  }
  try {
    return JSON.parse(readFileSync(abs, "utf-8"));
  } catch (e) {
    console.error(`  \u274C Invalid JSON in session file: ${abs}`);
    process.exit(1);
  }
}
async function main() {
  const args = parseArgs(process.argv.slice(2));
  if (args.version) {
    console.log("flux-scan v0.1.0");
    process.exit(0);
  }
  if (args.help || !args.url && !args.session) {
    console.log(HELP);
    process.exit(0);
  }
  const { FluxAnalyzer, generateHtmlReport, exportReportJson, printReport } = await import("@fluxiapi/scan");
  let session;
  if (args.session) {
    console.log(`  \u{1F4C2} Loading session from ${args.session}`);
    session = loadSession(args.session);
  } else {
    session = await liveScan(args.url, args);
  }
  console.log(`  \u{1F4CA} Analyzing ${session.metadata?.apiRequests || 0} API requests...`);
  const analyzer = new FluxAnalyzer({
    network: args.network
  });
  const report = analyzer.analyze(session);
  if (args.format === "console" || !args.output) {
    console.log(printReport(report));
  }
  if (args.output) {
    const outPath = resolve(args.output);
    const ext = extname(outPath).toLowerCase();
    if (ext === ".html" || args.format === "html") {
      const html = generateHtmlReport(report);
      writeFileSync(outPath, html, "utf-8");
      console.log(`  \u{1F4C4} HTML report: ${outPath}`);
    } else if (ext === ".json" || args.format === "json") {
      const json = exportReportJson(report);
      writeFileSync(outPath, json, "utf-8");
      console.log(`  \u{1F4C4} JSON report: ${outPath}`);
    }
  }
  const exitCode = report.score.overall < 50 ? 1 : 0;
  if (exitCode === 1) {
    console.log("  \u26A0\uFE0F  Score below 50 \u2014 exiting with code 1 (CI failure)");
  }
  process.exit(exitCode);
}
function parseUrlSimple(rawUrl) {
  try {
    const u = new URL(rawUrl);
    const segments = u.pathname.split("/").filter(Boolean);
    const pattern = "/" + segments.map((s) => {
      if (/^\d+$/.test(s)) return ":id";
      if (/^[0-9a-f]{8}-[0-9a-f]{4}-/.test(s)) return ":uuid";
      if (/^[a-f0-9]{24}$/.test(s)) return ":objectId";
      return s;
    }).join("/");
    return { protocol: u.protocol, host: u.host, pathSegments: segments, pathPattern: pattern, queryParams: Object.fromEntries(u.searchParams), pathname: u.pathname };
  } catch {
    return { protocol: "", host: "", pathSegments: [], pathPattern: rawUrl, queryParams: {}, pathname: rawUrl };
  }
}
function classifyUrl(url, method) {
  const lower = url.toLowerCase();
  if (lower.includes("/graphql")) return "api-graphql";
  if (lower.includes("grpc")) return "api-grpc";
  if (/\.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot)(\?|$)/.test(lower)) return "static";
  if (/\.(html?)(\?|$)/.test(lower)) return "document";
  if (lower.includes("/api/") || method !== "GET" || lower.includes(".json")) return "api-rest";
  return "other";
}
function sanitizeHeaders(headers) {
  const clean = {};
  const sensitive = ["authorization", "cookie", "set-cookie", "x-api-key"];
  for (const [k, v] of Object.entries(headers)) {
    clean[k] = sensitive.includes(k.toLowerCase()) ? "[REDACTED]" : v;
  }
  return clean;
}
function extractComponentFromStack(frames) {
  if (!frames) return null;
  for (const frame of frames) {
    const name = frame.functionName || "";
    if (/^[A-Z][a-zA-Z]+$/.test(name) && !["Error", "Promise", "Object"].includes(name)) {
      return name;
    }
  }
  return null;
}
function detectBackend(requests) {
  let poweredBy = null;
  let server = null;
  for (const r of requests) {
    if (r.response?.headers) {
      if (r.response.headers["x-powered-by"]) poweredBy = r.response.headers["x-powered-by"];
      if (r.response.headers["server"]) server = r.response.headers["server"];
    }
  }
  return { poweredBy, server, detectedFramework: null };
}
function createSpinner() {
  const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
  let i = 0;
  let timer = null;
  let msg = "";
  return {
    start(text) {
      msg = text;
      timer = setInterval(() => {
        process.stdout.write(`\r  ${frames[i++ % frames.length]} ${msg}`);
      }, 80);
    },
    update(text) {
      msg = text;
    },
    stop() {
      if (timer) {
        clearInterval(timer);
        timer = null;
      }
      process.stdout.write("\r" + " ".repeat(80) + "\r");
    }
  };
}
async function countdown(seconds, spinner) {
  for (let s = seconds; s > 0; s--) {
    const pct = (seconds - s) / seconds * 100;
    const filled = Math.round(pct / 5);
    const bar = "\u2588".repeat(filled) + "\u2591".repeat(20 - filled);
    spinner.update(`Scanning [${bar}] ${s}s remaining`);
    await new Promise((r) => setTimeout(r, 1e3));
  }
}
function waitForEnter() {
  return new Promise((resolve2) => {
    process.stdin.setRawMode?.(false);
    process.stdin.resume();
    process.stdin.once("data", () => {
      process.stdin.pause();
      resolve2();
    });
  });
}
main().catch((err) => {
  console.error("  \u274C Fatal error:", err.message);
  process.exit(2);
});
