const { performance } = require('perf_hooks');

const TARGET = String(process.env.MAZEX_PUBLIC_STRESS_TARGET || 'http://13.159.208.92').replace(/\/+$/, '');
const CONFIG = {
  readRequests: Math.max(1, Math.trunc(Number(process.env.MAZEX_PUBLIC_STRESS_READS || 420))),
  writeRequests: Math.max(0, Math.trunc(Number(process.env.MAZEX_PUBLIC_STRESS_WRITES || 80))),
  concurrency: Math.max(1, Math.trunc(Number(process.env.MAZEX_PUBLIC_STRESS_CONCURRENCY || 24))),
  timeoutMs: Math.max(1000, Math.trunc(Number(process.env.MAZEX_PUBLIC_STRESS_TIMEOUT_MS || 10000))),
};

const READ_ENDPOINTS = [
  { method: 'GET', path: '/' },
  { method: 'GET', path: '/api/health' },
  { method: 'GET', path: '/api/base1/current' },
  { method: 'GET', path: '/api/base1/chain/summary' },
  { method: 'GET', path: '/api/base1/security/status' },
  { method: 'GET', path: '/api/base1/storage/status' },
  { method: 'GET', path: '/api/base1/time' },
];

function percentile(values, ratio) {
  if (!values.length) return 0;
  const sorted = values.slice().sort((a, b) => a - b);
  const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(sorted.length * ratio) - 1));
  return Number(sorted[index].toFixed(2));
}

function summarize(results) {
  const durations = results.map((item) => item.ms).filter((item) => Number.isFinite(item));
  const statusCounts = {};
  const endpointCounts = {};
  let ok = 0;
  let failed = 0;
  for (const item of results) {
    const statusKey = item.error ? `ERR_${item.error}` : String(item.status);
    statusCounts[statusKey] = (statusCounts[statusKey] || 0) + 1;
    endpointCounts[item.endpoint] ||= {};
    endpointCounts[item.endpoint][statusKey] = (endpointCounts[item.endpoint][statusKey] || 0) + 1;
    if (item.status >= 200 && item.status < 400) ok += 1;
    else failed += 1;
  }
  return {
    total: results.length,
    ok,
    failed,
    status_counts: statusCounts,
    latency_ms: {
      min: durations.length ? Number(Math.min(...durations).toFixed(2)) : 0,
      p50: percentile(durations, 0.5),
      p95: percentile(durations, 0.95),
      p99: percentile(durations, 0.99),
      max: durations.length ? Number(Math.max(...durations).toFixed(2)) : 0,
      avg: durations.length
        ? Number((durations.reduce((sum, value) => sum + value, 0) / durations.length).toFixed(2))
        : 0,
    },
    endpoints: endpointCounts,
  };
}

async function withTimeout(url, options = {}) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), CONFIG.timeoutMs);
  const started = performance.now();
  try {
    const response = await fetch(url, {
      ...options,
      signal: controller.signal,
    });
    await response.arrayBuffer();
    return {
      status: response.status,
      ms: Number((performance.now() - started).toFixed(2)),
      bytes: Number(response.headers.get('content-length') || 0),
    };
  } catch (error) {
    return {
      status: 0,
      error: error.name || 'REQUEST_FAILED',
      ms: Number((performance.now() - started).toFixed(2)),
    };
  } finally {
    clearTimeout(timer);
  }
}

function makeJobs() {
  const jobs = [];
  for (let index = 0; index < CONFIG.readRequests; index += 1) {
    const endpoint = READ_ENDPOINTS[index % READ_ENDPOINTS.length];
    jobs.push({
      phase: 'read',
      endpoint: `${endpoint.method} ${endpoint.path}`,
      run: () => withTimeout(`${TARGET}${endpoint.path}`, { method: endpoint.method }),
    });
  }
  for (let index = 0; index < CONFIG.writeRequests; index += 1) {
    const x = (index * 7) % 20;
    const y = (index * 11) % 20;
    jobs.push({
      phase: 'write',
      endpoint: 'POST /api/base1/sample',
      run: () =>
        withTimeout(`${TARGET}/api/base1/sample`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            userId: `public-stress-${Date.now()}-${index}`,
            x,
            y,
          }),
        }),
    });
  }
  return jobs;
}

async function runQueue(jobs) {
  const results = [];
  let cursor = 0;
  async function worker(workerId) {
    while (cursor < jobs.length) {
      const index = cursor;
      cursor += 1;
      const job = jobs[index];
      const result = await job.run();
      results.push({
        index,
        worker: workerId,
        phase: job.phase,
        endpoint: job.endpoint,
        ...result,
      });
    }
  }
  await Promise.all(Array.from({ length: CONFIG.concurrency }, (_, index) => worker(index + 1)));
  results.sort((a, b) => a.index - b.index);
  return results;
}

async function main() {
  const started = performance.now();
  const before = await withTimeout(`${TARGET}/api/health`);
  const jobs = makeJobs();
  const results = await runQueue(jobs);
  const after = await withTimeout(`${TARGET}/api/health`);
  const summary = summarize(results);
  const report = {
    ok: after.status === 200 && summary.status_counts['0'] == null,
    target: TARGET,
    config: CONFIG,
    before_health: before,
    after_health: after,
    total_ms: Number((performance.now() - started).toFixed(2)),
    requests_per_second: Number((results.length / Math.max(0.001, (performance.now() - started) / 1000)).toFixed(2)),
    summary,
  };
  console.log(JSON.stringify(report, null, 2));
  console.table(
    Object.entries(summary.endpoints).map(([endpoint, statuses]) => ({
      endpoint,
      ...statuses,
    })),
  );
  if (after.status !== 200) process.exitCode = 1;
}

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