#!/usr/bin/env node

import { execFileSync, spawnSync } from "node:child_process";
import { existsSync } from "node:fs";
import { mkdir, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";

const docRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");

const config = {
  baseUrl: process.env.BASE_URL || "https://dev.manwe.qzz.io",
  sshHost: process.env.SSH_HOST || "root@5.78.213.189",
  sshKey: expandHome(process.env.SSH_KEY || "~/CascadeProjects/wzp"),
  mongoContainer: process.env.MONGO_CONTAINER || "amanat-dev-mongodb",
  mongoDb: process.env.MONGO_DB || "marketplace",
  mongoUser: process.env.MONGO_USER || "admin",
  mongoPassword: process.env.MONGO_PASSWORD || "password123",
  mongoAuthDb: process.env.MONGO_AUTH_DB || "admin",
  backendContainer: process.env.BACKEND_CONTAINER || "amanat-dev-backend",
  resetBackendLimiter: ["1", "true", "yes"].includes(
    String(process.env.RESET_BACKEND_LIMITER || "").toLowerCase(),
  ),
  npxBin: process.env.NPX_BIN || "npx",
  buyerEmail: process.env.BUYER_EMAIL || "buyer@marketplace.com",
  buyerPassword: process.env.BUYER_PASSWORD || "Moji6364",
  templateShareableLink: process.env.TEMPLATE_SHAREABLE_LINK || "logo-design-template",
  outputDir:
    process.env.OUT_DIR ||
    path.join(
      docRoot,
      "09 - Audits",
      "Mongo API Profiles",
      new Date().toISOString().replace(/[:.]/g, "-"),
    ),
};

const containers = (process.env.PROFILE_CONTAINERS || [
  "amanat-dev-nginx",
  "amanat-dev-backend",
  "amanat-dev-frontend",
  "amanat-dev-postgres",
  "amanat-dev-mongodb",
  "amanat-dev-redis",
  "amanat-dev-scanner",
].join(","))
  .split(",")
  .map((value) => value.trim())
  .filter(Boolean);

const endpointMatrix = [
  { name: "health", path: "/api/health", concurrency: 1, amount: 5 },
  { name: "categories", path: "/api/marketplace/categories", concurrency: 2, amount: 10 },
  { name: "categories_tree", path: "/api/marketplace/categories/tree", concurrency: 2, amount: 10 },
  { name: "sellers", path: "/api/marketplace/sellers", concurrency: 2, amount: 10 },
  {
    name: "template_public",
    path: `/api/marketplace/request-templates/public/${encodeURIComponent(config.templateShareableLink)}`,
    concurrency: 2,
    amount: 10,
  },
  {
    name: "payment_options_template",
    path: null,
    concurrency: 5,
    amount: 50,
    auth: true,
  },
  { name: "addresses_me", path: "/api/addresses", concurrency: 2, amount: 10, auth: true },
  {
    name: "purchase_requests_my",
    path: "/api/marketplace/purchase-requests/my",
    concurrency: 2,
    amount: 10,
    auth: true,
  },
  {
    name: "auth_login",
    path: "/api/auth/login",
    concurrency: 1,
    amount: 5,
    method: "POST",
    headers: ["Content-Type: application/json"],
    body: () => ({ email: config.buyerEmail, password: config.buyerPassword }),
  },
];

const sshBaseArgs = ["-i", config.sshKey, "-o", "BatchMode=yes", "-o", "ConnectTimeout=10", config.sshHost];

if (!existsSync(config.sshKey)) {
  throw new Error(`SSH key not found: ${config.sshKey}`);
}

await mkdir(config.outputDir, { recursive: true });

let profilerEnabled = false;

try {
  if (config.resetBackendLimiter) {
    console.error(`restarting ${config.backendContainer} to reset process-local rate limits`);
    restartBackendContainer();
    await waitForHealth();
  }

  const authToken = await login();
  const template = getTemplateContext();

  const matrix = endpointMatrix.map((test, index) => {
    const pathValue =
      test.name === "payment_options_template"
        ? `/api/payment/request-network/options?currency=USD&amount=0.01&sellerId=${template.sellerId}&templateId=${template.templateId}`
        : test.path;

    return {
      ...test,
      path: pathValue,
      headers: [
        ...(test.headers || []),
        // Counts are intentionally low to avoid profiling the in-memory global limiter.
        `X-Forwarded-For: 203.0.113.${10 + index}`,
      ],
    };
  });

  const results = [];

  for (const test of matrix) {
    console.error(`profiling ${test.name} ${test.path}`);
    enableProfiler();
    const beforeBlockIo = readDockerBlockIo();
    const bench = runAutocannon(test, authToken);
    const afterBlockIo = readDockerBlockIo();
    const mongoProfile = collectMongoProfile();

    results.push({
      name: test.name,
      method: test.method || "GET",
      path: test.path,
      requestCount: bench.requests.total,
      rps: bench.requests.average,
      latency: {
        averageMs: bench.latency.average,
        p50Ms: bench.latency.p50,
        p90Ms: bench.latency.p90,
        p95Ms: bench.latency.p95 ?? bench.latency.p97_5,
        p99Ms: bench.latency.p99,
        maxMs: bench.latency.max,
      },
      non2xx: bench.non2xx || 0,
      statusCodeStats: bench.statusCodeStats || {},
      mongoProfile,
      blockIoDelta: diffBlockIo(beforeBlockIo, afterBlockIo),
    });
  }

  disableProfiler();

  const report = {
    generatedAt: new Date().toISOString(),
    config: {
      baseUrl: config.baseUrl,
      sshHost: config.sshHost,
      mongoContainer: config.mongoContainer,
      mongoDb: config.mongoDb,
      mongoAuthDb: config.mongoAuthDb,
      backendContainer: config.backendContainer,
      resetBackendLimiter: config.resetBackendLimiter,
      containers,
      templateShareableLink: config.templateShareableLink,
      outputDir: config.outputDir,
    },
    results,
  };

  const jsonPath = path.join(config.outputDir, "mongo-api-profile.json");
  const markdownPath = path.join(config.outputDir, "summary.md");
  await writeFile(jsonPath, `${JSON.stringify(report, null, 2)}\n`);
  await writeFile(markdownPath, renderMarkdown(report));

  console.log(`Wrote ${path.relative(docRoot, jsonPath)}`);
  console.log(`Wrote ${path.relative(docRoot, markdownPath)}`);
} catch (error) {
  if (profilerEnabled) {
    try {
      disableProfiler();
    } catch (disableError) {
      console.error(`failed to disable profiler: ${disableError.message}`);
    }
  }
  throw error;
}

function expandHome(value) {
  if (!value.startsWith("~/")) return value;
  return path.join(os.homedir(), value.slice(2));
}

function shellQuote(value) {
  return `'${String(value).replace(/'/g, "'\\''")}'`;
}

function ssh(command, options = {}) {
  return execFileSync("ssh", [...sshBaseArgs, command], {
    encoding: "utf8",
    maxBuffer: options.maxBuffer || 100 * 1024 * 1024,
  });
}

function mongoEval(js) {
  const command = [
    "docker exec",
    shellQuote(config.mongoContainer),
    "mongosh --quiet",
    "-u",
    shellQuote(config.mongoUser),
    "-p",
    shellQuote(config.mongoPassword),
    "--authenticationDatabase",
    shellQuote(config.mongoAuthDb),
    shellQuote(config.mongoDb),
    "--eval",
    shellQuote(js),
  ].join(" ");
  return ssh(command);
}

function restartBackendContainer() {
  ssh(`docker restart ${shellQuote(config.backendContainer)}`, { maxBuffer: 1024 * 1024 });
}

async function waitForHealth() {
  const deadline = Date.now() + 90_000;
  let lastError = "";

  while (Date.now() < deadline) {
    try {
      const response = await fetch(`${config.baseUrl}/api/health`);
      const body = await response.json();
      if (response.ok && body?.status === "ok") return;
      lastError = `status=${response.status} body=${JSON.stringify(body)}`;
    } catch (error) {
      lastError = error.message;
    }
    await new Promise((resolve) => setTimeout(resolve, 2_000));
  }

  throw new Error(`backend did not become healthy after restart: ${lastError}`);
}

async function login() {
  const response = await fetch(`${config.baseUrl}/api/auth/login`, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ email: config.buyerEmail, password: config.buyerPassword }),
  });
  const body = await response.json();
  if (!body?.success || !body?.data?.tokens?.accessToken) {
    throw new Error(`login failed with status ${response.status}: ${JSON.stringify(body)}`);
  }
  return body.data.tokens.accessToken;
}

function getTemplateContext() {
  const raw = mongoEval(`
    const doc = db.requesttemplates.findOne(
      { shareableLink: ${JSON.stringify(config.templateShareableLink)} },
      { _id: 1, sellerId: 1 }
    );
    print(JSON.stringify(doc));
  `).trim();

  if (!raw || raw === "null") {
    throw new Error(`template not found: ${config.templateShareableLink}`);
  }

  const doc = JSON.parse(raw);
  const templateId = doc._id?.$oid || doc._id;
  const sellerId = doc.sellerId?.$oid || doc.sellerId;
  if (!templateId || !sellerId) {
    throw new Error(`template missing _id/sellerId: ${raw}`);
  }
  return { templateId, sellerId };
}

function enableProfiler() {
  mongoEval(`
    db.setProfilingLevel(0);
    try { db.system.profile.drop(); } catch (error) {}
    db.setProfilingLevel(2, { slowms: 0, sampleRate: 1 });
    print(JSON.stringify(db.getProfilingStatus()));
  `);
  profilerEnabled = true;
}

function disableProfiler() {
  mongoEval(`db.setProfilingLevel(0); print(JSON.stringify(db.getProfilingStatus()));`);
  profilerEnabled = false;
}

function collectMongoProfile() {
  const output = mongoEval(`
    db.setProfilingLevel(0);
    const docs = db.system.profile.find({ ns: /^${escapeRegExp(config.mongoDb)}\\./ }).toArray();

    function commandName(doc) {
      const command = doc.command || {};
      for (const key of Object.keys(command)) {
        if (!['lsid', '$db', '$clusterTime', 'readConcern', 'writeConcern', 'maxTimeMS'].includes(key)) {
          return key;
        }
      }
      return doc.op || 'unknown';
    }

    function collectionName(doc) {
      const command = doc.command || {};
      return command.find ||
        command.aggregate ||
        command.count ||
        command.distinct ||
        command.update ||
        command.delete ||
        command.findAndModify ||
        command.insert ||
        doc.ns.replace(/^${escapeRegExp(config.mongoDb)}\\./, '');
    }

    function shapeValue(value) {
      if (value === null) return 'null';
      if (value === undefined) return 'undefined';
      if (Array.isArray(value)) return '[' + value.map(shapeValue).join(',') + ']';
      if (typeof value === 'object') {
        if (value._bsontype) return value._bsontype;
        return '{' + Object.keys(value).sort().map((key) => key + ':' + shapeValue(value[key])).join(',') + '}';
      }
      return typeof value;
    }

    function queryShape(doc) {
      const command = doc.command || {};
      const parts = [];
      if (command.filter) parts.push('filter=' + shapeValue(command.filter));
      if (command.query) parts.push('query=' + shapeValue(command.query));
      if (command.pipeline) parts.push('pipeline=' + shapeValue(command.pipeline));
      if (command.sort) parts.push('sort=' + shapeValue(command.sort));
      if (command.projection) parts.push('projection=' + shapeValue(command.projection));
      if (command.update) parts.push('update=' + shapeValue(command.update));
      return parts.join(' ');
    }

    const groups = new Map();
    for (const doc of docs) {
      const key = [
        doc.ns,
        doc.op,
        commandName(doc),
        collectionName(doc),
        doc.planSummary || '',
        doc.queryHash || '',
        doc.planCacheKey || '',
        queryShape(doc),
      ].join(' | ');

      let group = groups.get(key);
      if (!group) {
        group = {
          namespace: doc.ns,
          operation: doc.op,
          command: commandName(doc),
          collection: collectionName(doc),
          planSummary: doc.planSummary || '',
          queryHash: doc.queryHash || '',
          planCacheKey: doc.planCacheKey || '',
          queryShape: queryShape(doc),
          count: 0,
          millisTotal: 0,
          millisMax: 0,
          millisValues: [],
          docsExamined: 0,
          keysExamined: 0,
          nreturned: 0,
          ninserted: 0,
          nMatched: 0,
          nModified: 0,
          responseLength: 0,
          numYield: 0,
        };
        groups.set(key, group);
      }

      const millis = Number(doc.millis || 0);
      group.count += 1;
      group.millisTotal += millis;
      group.millisMax = Math.max(group.millisMax, millis);
      group.millisValues.push(millis);
      group.docsExamined += Number(doc.docsExamined || 0);
      group.keysExamined += Number(doc.keysExamined || 0);
      group.nreturned += Number(doc.nreturned || 0);
      group.ninserted += Number(doc.ninserted || 0);
      group.nMatched += Number(doc.nMatched || 0);
      group.nModified += Number(doc.nModified || 0);
      group.responseLength += Number(doc.responseLength || 0);
      group.numYield += Number(doc.numYield || 0);
    }

    function percentile(values, p) {
      if (!values.length) return 0;
      values.sort((a, b) => a - b);
      return values[Math.min(values.length - 1, Math.floor((p / 100) * values.length))];
    }

    const groupsOut = Array.from(groups.values())
      .map((group) => ({
        namespace: group.namespace,
        operation: group.operation,
        command: group.command,
        collection: group.collection,
        planSummary: group.planSummary,
        queryHash: group.queryHash,
        planCacheKey: group.planCacheKey,
        queryShape: group.queryShape,
        count: group.count,
        millisTotal: group.millisTotal,
        millisAverage: group.count ? group.millisTotal / group.count : 0,
        millisP50: percentile(group.millisValues, 50),
        millisP95: percentile(group.millisValues, 95),
        millisMax: group.millisMax,
        docsExamined: group.docsExamined,
        keysExamined: group.keysExamined,
        nreturned: group.nreturned,
        ninserted: group.ninserted,
        nMatched: group.nMatched,
        nModified: group.nModified,
        responseLength: group.responseLength,
        numYield: group.numYield,
      }))
      .sort((a, b) => b.millisTotal - a.millisTotal || b.count - a.count);

    print(JSON.stringify({
      totalOperations: docs.length,
      totalMillis: groupsOut.reduce((sum, group) => sum + group.millisTotal, 0),
      groups: groupsOut,
    }));
  `);

  return JSON.parse(output.trim().split(/\n/).pop());
}

function runAutocannon(test, authToken) {
  const args = ["-y", "autocannon@8.0.0"];
  if (test.amount) args.push("-a", String(test.amount));
  if (test.duration) args.push("-d", String(test.duration));
  args.push("-c", String(test.concurrency || 1), "--json");
  for (const header of test.headers || []) args.push("-H", header);
  if (test.auth) args.push("-H", `Authorization: Bearer ${authToken}`);
  if (test.method) args.push("-m", test.method);
  const body = typeof test.body === "function" ? test.body() : test.body;
  if (body) args.push("-b", JSON.stringify(body));
  args.push(`${config.baseUrl}${test.path}`);

  const result = spawnSync(config.npxBin, args, {
    encoding: "utf8",
    maxBuffer: 100 * 1024 * 1024,
  });

  if (result.status !== 0) {
    throw new Error(`autocannon failed for ${test.name}\n${result.stderr}\n${result.stdout}`);
  }

  return JSON.parse(result.stdout);
}

function readDockerBlockIo() {
  const output = ssh(
    `docker stats --no-stream --format '{{json .}}' ${containers.map(shellQuote).join(" ")}`,
  );
  const rows = output.trim().split(/\n/).filter(Boolean).map((line) => JSON.parse(line));
  const map = {};
  for (const row of rows) {
    const [readRaw, writeRaw] = String(row.BlockIO || "0B / 0B").split("/").map((item) => item.trim());
    map[row.Name] = {
      readBytes: parseBytes(readRaw),
      writeBytes: parseBytes(writeRaw),
      raw: row.BlockIO,
    };
  }
  return map;
}

function diffBlockIo(before, after) {
  const diff = {};
  for (const [name, value] of Object.entries(after)) {
    diff[name] = {
      readBytes: Math.max(0, value.readBytes - (before[name]?.readBytes || 0)),
      writeBytes: Math.max(0, value.writeBytes - (before[name]?.writeBytes || 0)),
    };
  }
  return diff;
}

function parseBytes(value) {
  const match = String(value || "").trim().match(/^([0-9.]+)\s*([KMGT]?i?B|B)$/i);
  if (!match) return 0;
  const number = Number(match[1]);
  const unit = match[2].toLowerCase();
  const multiplier = {
    b: 1,
    kb: 1_000,
    mb: 1_000_000,
    gb: 1_000_000_000,
    tb: 1_000_000_000_000,
    kib: 1024,
    mib: 1024 ** 2,
    gib: 1024 ** 3,
    tib: 1024 ** 4,
  }[unit] || 1;
  return number * multiplier;
}

function formatBytes(value) {
  if (!value) return "0 B";
  const units = ["B", "KB", "MB", "GB"];
  let next = value;
  let index = 0;
  while (next >= 1000 && index < units.length - 1) {
    next /= 1000;
    index += 1;
  }
  return `${next.toFixed(next >= 10 || index === 0 ? 0 : 1)} ${units[index]}`;
}

function renderMarkdown(report) {
  const lines = [];
  lines.push("# Mongo API Query Profile");
  lines.push("");
  lines.push(`Generated: ${report.generatedAt}`);
  lines.push(`Base URL: \`${report.config.baseUrl}\``);
  lines.push(`Mongo: \`${report.config.mongoContainer}/${report.config.mongoDb}\``);
  lines.push("");
  lines.push("This is a query-shape profile, not a max-throughput test. Request counts are intentionally small so the backend rate limiter does not dominate the profile.");
  lines.push("");
  lines.push("## Endpoint Summary");
  lines.push("");
  lines.push("| Endpoint | Requests | Avg | P95 | P99 | Non-2xx | Mongo ops | Top Mongo query |");
  lines.push("|---|---:|---:|---:|---:|---:|---:|---|");
  for (const result of report.results) {
    const top = result.mongoProfile.groups[0];
    lines.push(
      [
        `\`${result.method} ${result.path}\``,
        result.requestCount,
        `${result.latency.averageMs}ms`,
        `${result.latency.p95Ms}ms`,
        `${result.latency.p99Ms}ms`,
        result.non2xx,
        result.mongoProfile.totalOperations,
        top ? `\`${top.collection}\` ${top.command} (${top.count}x, ${top.planSummary || "no plan"})` : "-",
      ].join(" | ").replace(/^/, "| ").replace(/$/, " |"),
    );
  }
  lines.push("");
  lines.push("## Query Groups");
  for (const result of report.results) {
    lines.push("");
    lines.push(`### ${result.name}`);
    lines.push("");
    lines.push(`Path: \`${result.method} ${result.path}\``);
    lines.push(`Status codes: \`${JSON.stringify(result.statusCodeStats)}\``);
    lines.push("");
    if (!result.mongoProfile.groups.length) {
      lines.push("No Mongo operations captured in this endpoint window.");
      continue;
    }
    lines.push("| Collection | Command | Count | Total ms | Avg ms | P95 ms | Plan | Docs | Keys | Returned | Shape |");
    lines.push("|---|---|---:|---:|---:|---:|---|---:|---:|---:|---|");
    for (const group of result.mongoProfile.groups.slice(0, 12)) {
      lines.push(
        [
          `\`${group.collection}\``,
          `\`${group.command}\``,
          group.count,
          group.millisTotal,
          round(group.millisAverage),
          group.millisP95,
          `\`${group.planSummary || "-"}\``,
          group.docsExamined,
          group.keysExamined,
          group.nreturned,
          `\`${truncate(group.queryShape || "-", 140)}\``,
        ].join(" | ").replace(/^/, "| ").replace(/$/, " |"),
      );
    }
  }
  lines.push("");
  lines.push("## Block I/O Deltas");
  for (const result of report.results) {
    const active = Object.entries(result.blockIoDelta)
      .filter(([, value]) => value.readBytes || value.writeBytes)
      .map(([name, value]) => `${name}: read ${formatBytes(value.readBytes)}, write ${formatBytes(value.writeBytes)}`);
    lines.push(`- ${result.name}: ${active.length ? active.join("; ") : "no container block I/O delta"}`);
  }
  lines.push("");
  return `${lines.join("\n")}\n`;
}

function truncate(value, max) {
  return value.length > max ? `${value.slice(0, max - 3)}...` : value;
}

function round(value) {
  return Math.round(value * 1000) / 1000;
}

function escapeRegExp(value) {
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
