/**
 * WalkGuide — k6 JSON Result Parser (Node.js)
 * Parse output dari k6 --out json=output.ndjson dan generate readable report.
 *
 * Usage:
 *   node result-parser.js <input.ndjson> [output-report.json]
 *
 * k6 JSON format: satu JSON object per baris (newline-delimited JSON / NDJSON)
 * Setiap baris adalah metric data point atau test info.
 */

const fs = require("fs");
const path = require("path");

// ── Parse args ────────────────────────────────────────────────────────────────
const inputFile = process.argv[2];
const outputFile = process.argv[3] || "k6-results/parsed-report.json";
const benchProfile = process.env.BENCH_PROFILE || "default";
const thresholdPreset =
  benchProfile === "local"
    ? {
        overallP95: 35000,
        errorRate: 0.1,
        sosP95: 3000,
        locationP95: 3000,
        obstacleP95: 5000,
        authP95: 35000,
        timelineP95: 5000,
        notificationP95: 5000,
      }
    : {
        overallP95: 500,
        errorRate: 0.01,
        sosP95: 200,
        locationP95: 300,
        obstacleP95: 400,
        authP95: 800,
        timelineP95: 1000,
        notificationP95: 500,
      };

if (!inputFile) {
  console.error(
    "Usage: node result-parser.js <k6-output.ndjson> [output.json]",
  );
  process.exit(1);
}

if (!fs.existsSync(inputFile)) {
  console.error(`Input file not found: ${inputFile}`);
  process.exit(1);
}

// ── Read and parse NDJSON ─────────────────────────────────────────────────────
const raw = fs.readFileSync(inputFile, "utf8");
const lines = raw.trim().split("\n").filter(Boolean);

const dataPoints = [];
const errors = [];

for (const line of lines) {
  try {
    dataPoints.push(JSON.parse(line));
  } catch (e) {
    errors.push({ line: line.slice(0, 100), error: e.message });
  }
}

console.log(
  `Parsed ${dataPoints.length} data points, ${errors.length} parse errors.`,
);

// ── Aggregate metrics ─────────────────────────────────────────────────────────
const metricAggregates = {};

for (const dp of dataPoints) {
  if (dp.type !== "Point") continue;

  const metricName = dp.metric;
  const value = dp.data && dp.data.value !== undefined ? dp.data.value : 0;
  const tags = dp.data && dp.data.tags ? dp.data.tags : {};

  if (!metricAggregates[metricName]) {
    metricAggregates[metricName] = {
      values: [],
      byEndpoint: {},
    };
  }

  metricAggregates[metricName].values.push(value);

  // Group by endpoint tag
  const endpoint = tags.endpoint || "unknown";
  if (!metricAggregates[metricName].byEndpoint[endpoint]) {
    metricAggregates[metricName].byEndpoint[endpoint] = [];
  }
  metricAggregates[metricName].byEndpoint[endpoint].push(value);
}

// ── Statistical helpers ───────────────────────────────────────────────────────
function percentile(arr, p) {
  if (!arr || arr.length === 0) return 0;
  const sorted = [...arr].sort((a, b) => a - b);
  const idx = Math.ceil((p / 100) * sorted.length) - 1;
  return sorted[Math.max(0, idx)];
}

function avg(arr) {
  if (!arr || arr.length === 0) return 0;
  return arr.reduce((a, b) => a + b, 0) / arr.length;
}

function summarizeSeries(arr) {
  if (!arr || arr.length === 0) return null;
  return {
    count: arr.length,
    min: Math.min(...arr).toFixed(2),
    max: Math.max(...arr).toFixed(2),
    avg: avg(arr).toFixed(2),
    p50: percentile(arr, 50).toFixed(2),
    p90: percentile(arr, 90).toFixed(2),
    p95: percentile(arr, 95).toFixed(2),
    p99: percentile(arr, 99).toFixed(2),
  };
}

// ── Build structured report ───────────────────────────────────────────────────
const report = {
  generatedAt: new Date().toISOString(),
  inputFile: path.resolve(inputFile),
  benchProfile,
  totalPoints: dataPoints.length,
  parseErrors: errors.length,

  // 5 KEY METRICS sesuai ketentuan pillar 3:
  keyMetrics: {
    // 1. Throughput — request per second
    throughput: {
      label: "Throughput (req/s)",
      description: "Total HTTP requests divided by test duration",
      value: null, // dihitung di bawah
    },
    // 2. p95 Latency — overall
    p95Latency: {
      label: "p95 Response Time (ms)",
      description: "95th percentile of all HTTP request durations",
      value: null,
    },
    // 3. Error Rate
    errorRate: {
      label: "Error Rate (%)",
      description: "Percentage of non-2xx HTTP responses",
      value: null,
      passFail: null,
    },
    // 4. DB Query Time (estimated via response time)
    dbQueryTime: {
      label: "Estimated DB Query Time (ms)",
      description: "p95 of write endpoints (location + obstacle) as DB proxy",
      value: null,
      note: "Actual DB time requires Spring Actuator or DB metrics",
    },
    // 5. JVM Heap
    jvmHeap: {
      label: "JVM Heap Used (MB)",
      description: "From Spring Actuator metric, collected during test",
      value: null,
    },
  },

  // Per-metric detailed stats
  metrics: {},

  // Per-endpoint breakdown
  endpoints: {},

  // WalkGuide-specific breakdown
  walkguideMetrics: {
    authLatency: null,
    locationLatency: null,
    obstacleLatency: null,
    sosLatency: null,
    notifLatency: null,
    timelineLatency: null,
    pairingLatency: null,
  },

  // Threshold evaluation
  thresholdResults: [],
};

// ── Populate metrics ──────────────────────────────────────────────────────────
for (const [name, data] of Object.entries(metricAggregates)) {
  report.metrics[name] = summarizeSeries(data.values);

  // Per-endpoint
  for (const [ep, vals] of Object.entries(data.byEndpoint)) {
    if (!report.endpoints[ep]) report.endpoints[ep] = {};
    report.endpoints[ep][name] = summarizeSeries(vals);
  }
}

// ── Fill key metrics ──────────────────────────────────────────────────────────
if (report.metrics["http_req_duration"]) {
  report.keyMetrics.p95Latency.value =
    report.metrics["http_req_duration"].p95 + " ms";
}

if (report.metrics["http_req_failed"]) {
  const rate = (
    parseFloat(report.metrics["http_req_failed"].avg) * 100
  ).toFixed(2);
  report.keyMetrics.errorRate.value = rate + "%";
  report.keyMetrics.errorRate.passFail =
    parseFloat(rate) / 100 < thresholdPreset.errorRate ? "PASS" : "FAIL";
}

if (report.metrics["walkguide_jvm_heap_used_mb"]) {
  report.keyMetrics.jvmHeap.value =
    report.metrics["walkguide_jvm_heap_used_mb"].avg + " MB";
}

// DB estimate: avg of location_update + obstacle_log p95
const dbMetrics = [
  "walkguide_location_latency_ms",
  "walkguide_obstacle_latency_ms",
];
const dbVals = dbMetrics
  .filter((m) => report.metrics[m])
  .map((m) => parseFloat(report.metrics[m].p95));
if (dbVals.length > 0) {
  report.keyMetrics.dbQueryTime.value =
    (dbVals.reduce((a, b) => a + b, 0) / dbVals.length).toFixed(2) + " ms";
}

// Throughput: dari http_reqs total / test duration (estimasi dari data points)
if (report.metrics["http_reqs"]) {
  const totalReqs = report.metrics["http_reqs"].count;
  // Estimasi durasi dari timestamps
  const timestamps = dataPoints
    .filter((dp) => dp.type === "Point" && dp.metric === "http_reqs")
    .map((dp) => new Date(dp.data.time).getTime());
  if (timestamps.length > 1) {
    const durationS =
      (Math.max(...timestamps) - Math.min(...timestamps)) / 1000;
    report.keyMetrics.throughput.value =
      (totalReqs / durationS).toFixed(1) + " req/s";
  } else {
    report.keyMetrics.throughput.value = totalReqs + " total requests";
  }
}

// ── WalkGuide-specific ────────────────────────────────────────────────────────
const wgMetricMap = {
  authLatency: "walkguide_auth_latency_ms",
  locationLatency: "walkguide_location_latency_ms",
  obstacleLatency: "walkguide_obstacle_latency_ms",
  sosLatency: "walkguide_sos_latency_ms",
  notifLatency: "walkguide_notif_latency_ms",
  timelineLatency: "walkguide_timeline_latency_ms",
  pairingLatency: "walkguide_pairing_latency_ms",
};

for (const [key, metricName] of Object.entries(wgMetricMap)) {
  if (report.metrics[metricName]) {
    report.walkguideMetrics[key] = {
      p95: report.metrics[metricName].p95 + " ms",
      p99: report.metrics[metricName].p99 + " ms",
      avg: report.metrics[metricName].avg + " ms",
    };
  }
}

// ── Threshold checks ──────────────────────────────────────────────────────────
const thresholdChecks = [
  {
    name: `p95 overall < ${thresholdPreset.overallP95}ms`,
    metric: "http_req_duration",
    stat: "p95",
    threshold: thresholdPreset.overallP95,
    unit: "ms",
    operator: "<",
  },
  {
    name: `error rate < ${thresholdPreset.errorRate * 100}%`,
    metric: "http_req_failed",
    stat: "avg",
    threshold: thresholdPreset.errorRate,
    unit: "rate",
    operator: "<",
  },
  {
    name: `SOS p95 < ${thresholdPreset.sosP95}ms`,
    metric: "walkguide_sos_latency_ms",
    stat: "p95",
    threshold: thresholdPreset.sosP95,
    unit: "ms",
    operator: "<",
  },
  {
    name: `Location p95 < ${thresholdPreset.locationP95}ms`,
    metric: "walkguide_location_latency_ms",
    stat: "p95",
    threshold: thresholdPreset.locationP95,
    unit: "ms",
    operator: "<",
  },
  {
    name: `Obstacle p95 < ${thresholdPreset.obstacleP95}ms`,
    metric: "walkguide_obstacle_latency_ms",
    stat: "p95",
    threshold: thresholdPreset.obstacleP95,
    unit: "ms",
    operator: "<",
  },
  {
    name: `Auth p95 < ${thresholdPreset.authP95}ms`,
    metric: "walkguide_auth_latency_ms",
    stat: "p95",
    threshold: thresholdPreset.authP95,
    unit: "ms",
    operator: "<",
  },
  {
    name: `Timeline p95 < ${thresholdPreset.timelineP95}ms`,
    metric: "walkguide_timeline_latency_ms",
    stat: "p95",
    threshold: thresholdPreset.timelineP95,
    unit: "ms",
    operator: "<",
  },
  {
    name: `Notification p95 < ${thresholdPreset.notificationP95}ms`,
    metric: "walkguide_notif_latency_ms",
    stat: "p95",
    threshold: thresholdPreset.notificationP95,
    unit: "ms",
    operator: "<",
  },
];

for (const check of thresholdChecks) {
  const m = report.metrics[check.metric];
  if (!m) {
    report.thresholdResults.push({ ...check, actual: "N/A", result: "SKIP" });
    continue;
  }
  const actual = parseFloat(m[check.stat]);
  const pass =
    check.operator === "<"
      ? actual < check.threshold
      : actual > check.threshold;
  report.thresholdResults.push({
    name: check.name,
    metric: check.metric,
    threshold: check.threshold + " " + check.unit,
    actual: actual.toFixed(2) + " " + check.unit,
    result: pass ? "✅ PASS" : "❌ FAIL",
  });
}

// ── Write output ──────────────────────────────────────────────────────────────
const outDir = path.dirname(outputFile);
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });

fs.writeFileSync(outputFile, JSON.stringify(report, null, 2));
console.log(`\n✅ Report written to: ${outputFile}`);

// ── Print summary to stdout ───────────────────────────────────────────────────
console.log("\n═══════════════════════════════════════════════════════");
console.log("  WalkGuide k6 — Parsed Result Summary");
console.log("═══════════════════════════════════════════════════════");
console.log(`  Throughput:    ${report.keyMetrics.throughput.value || "N/A"}`);
console.log(`  p95 Latency:   ${report.keyMetrics.p95Latency.value || "N/A"}`);
console.log(
  `  Error Rate:    ${report.keyMetrics.errorRate.value || "N/A"} [${report.keyMetrics.errorRate.passFail || "N/A"}]`,
);
console.log(`  DB Est. Time:  ${report.keyMetrics.dbQueryTime.value || "N/A"}`);
console.log(`  JVM Heap:      ${report.keyMetrics.jvmHeap.value || "N/A"}`);
console.log("\n  Threshold Results:");
for (const t of report.thresholdResults) {
  console.log(`    ${t.result} ${t.name} (actual: ${t.actual})`);
}
console.log("═══════════════════════════════════════════════════════\n");
