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

function parseArgs(argv) {
  const options = {
    url: 'http://127.0.0.1:4000/api/v1/health',
    method: 'GET',
    duration: 15,
    concurrency: 20,
    timeout: 5000,
    warmup: 5,
    headers: {},
    body: undefined,
    gates: {
      minSuccessRate: undefined,
      minRequestsPerSecond: undefined,
      maxP95Ms: undefined,
      maxP99Ms: undefined,
    },
  };

  for (let index = 0; index < argv.length; index += 1) {
    const arg = argv[index];
    const next = argv[index + 1];

    if (arg === '--url' && next) {
      options.url = next;
      index += 1;
      continue;
    }

    if (arg === '--method' && next) {
      options.method = next.toUpperCase();
      index += 1;
      continue;
    }

    if (arg === '--duration' && next) {
      options.duration = Number(next);
      index += 1;
      continue;
    }

    if (arg === '--concurrency' && next) {
      options.concurrency = Number(next);
      index += 1;
      continue;
    }

    if (arg === '--timeout' && next) {
      options.timeout = Number(next);
      index += 1;
      continue;
    }

    if (arg === '--warmup' && next) {
      options.warmup = Number(next);
      index += 1;
      continue;
    }

    if (arg === '--min-success-rate') {
      assertOptionValue(arg, next);
      options.gates.minSuccessRate = Number(next);
      index += 1;
      continue;
    }

    if (arg === '--min-rps') {
      assertOptionValue(arg, next);
      options.gates.minRequestsPerSecond = Number(next);
      index += 1;
      continue;
    }

    if (arg === '--max-p95') {
      assertOptionValue(arg, next);
      options.gates.maxP95Ms = Number(next);
      index += 1;
      continue;
    }

    if (arg === '--max-p99') {
      assertOptionValue(arg, next);
      options.gates.maxP99Ms = Number(next);
      index += 1;
      continue;
    }

    if (arg === '--header' && next) {
      const separatorIndex = next.indexOf(':');
      if (separatorIndex === -1) {
        throw new Error(`Invalid header format: ${next}`);
      }

      const key = next.slice(0, separatorIndex).trim();
      const value = next.slice(separatorIndex + 1).trim();
      options.headers[key] = value;
      index += 1;
      continue;
    }

    if (arg === '--body' && next) {
      options.body = next;
      index += 1;
      continue;
    }

    if (arg === '--body-file' && next) {
      options.body = readFileSync(next, 'utf8');
      index += 1;
      continue;
    }
  }

  if (!Number.isFinite(options.duration) || options.duration <= 0) {
    throw new Error('duration must be a positive number of seconds');
  }

  if (!Number.isInteger(options.concurrency) || options.concurrency <= 0) {
    throw new Error('concurrency must be a positive integer');
  }

  if (!Number.isFinite(options.timeout) || options.timeout <= 0) {
    throw new Error('timeout must be a positive number of milliseconds');
  }

  if (!Number.isInteger(options.warmup) || options.warmup < 0) {
    throw new Error('warmup must be zero or a positive integer');
  }

  validateGate(options.gates.minSuccessRate, 'min-success-rate', {
    min: 0,
    max: 100,
  });
  validateGate(options.gates.minRequestsPerSecond, 'min-rps', { min: 0 });
  validateGate(options.gates.maxP95Ms, 'max-p95', { min: 0 });
  validateGate(options.gates.maxP99Ms, 'max-p99', { min: 0 });

  if (options.body && !options.headers['Content-Type']) {
    options.headers['Content-Type'] = 'application/json';
  }

  return options;
}

function assertOptionValue(option, value) {
  if (value === undefined) {
    throw new Error(`${option} requires a value`);
  }
}

function validateGate(value, name, limits) {
  if (value === undefined) {
    return;
  }

  const aboveMaximum = limits.max !== undefined && value > limits.max;
  if (!Number.isFinite(value) || value < limits.min || aboveMaximum) {
    const range =
      limits.max === undefined
        ? `at least ${limits.min}`
        : `between ${limits.min} and ${limits.max}`;
    throw new Error(`${name} must be a finite number ${range}`);
  }
}

function percentile(sortedValues, p) {
  if (!sortedValues.length) {
    return 0;
  }

  const rank = Math.ceil((p / 100) * sortedValues.length) - 1;
  const index = Math.min(sortedValues.length - 1, Math.max(0, rank));
  return sortedValues[index];
}

function average(values) {
  if (!values.length) {
    return 0;
  }

  const total = values.reduce((sum, value) => sum + value, 0);
  return total / values.length;
}

function printUsage() {
  console.log('Usage: node scripts/load-test.js --url <url> [--duration 15] [--concurrency 20]');
  console.log('Optional: --method POST --header "Authorization: Bearer <token>" --body "{\"key\":\"value\"}"');
  console.log(
    'Performance gates: --min-success-rate 99.9 --min-rps 100 --max-p95 250 --max-p99 500',
  );
}

async function warmup(options) {
  if (options.warmup === 0) {
    return;
  }

  for (let index = 0; index < options.warmup; index += 1) {
    const response = await fetch(options.url, {
      method: options.method,
      headers: options.headers,
      body: options.body,
    });

    await response.arrayBuffer();
  }
}

async function runWorker(options, results, endAt) {
  while (Date.now() < endAt) {
    const startedAt = performance.now();
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), options.timeout);

    try {
      const response = await fetch(options.url, {
        method: options.method,
        headers: options.headers,
        body: options.body,
        signal: controller.signal,
      });

      const payload = await response.arrayBuffer();
      const durationMs = performance.now() - startedAt;

      results.latencies.push(durationMs);
      results.totalRequests += 1;
      results.totalBytes += payload.byteLength;

      const statusKey = String(response.status);
      results.statusCounts[statusKey] = (results.statusCounts[statusKey] ?? 0) + 1;

      if (response.ok) {
        results.successCount += 1;
      } else {
        results.non2xxCount += 1;
      }
    } catch (error) {
      const durationMs = performance.now() - startedAt;
      results.latencies.push(durationMs);
      results.totalRequests += 1;

      if (error && typeof error === 'object' && error.name === 'AbortError') {
        results.timeoutCount += 1;
      } else {
        results.networkErrorCount += 1;
      }
    } finally {
      clearTimeout(timeoutId);
    }
  }
}

function buildSummary(options, results, totalDurationMs) {
  const latencies = [...results.latencies].sort((left, right) => left - right);
  const requestsPerSecond = results.totalRequests / (totalDurationMs / 1000);
  const successRate = results.totalRequests === 0 ? 0 : (results.successCount / results.totalRequests) * 100;

  return {
    target: options.url,
    method: options.method,
    durationSeconds: Number((totalDurationMs / 1000).toFixed(2)),
    concurrency: options.concurrency,
    totalRequests: results.totalRequests,
    successCount: results.successCount,
    non2xxCount: results.non2xxCount,
    timeoutCount: results.timeoutCount,
    networkErrorCount: results.networkErrorCount,
    requestsPerSecond: Number(requestsPerSecond.toFixed(2)),
    successRate: Number(successRate.toFixed(2)),
    transferredBytes: results.totalBytes,
    latencyMs: {
      min: Number((latencies[0] ?? 0).toFixed(2)),
      avg: Number(average(latencies).toFixed(2)),
      p50: Number(percentile(latencies, 50).toFixed(2)),
      p90: Number(percentile(latencies, 90).toFixed(2)),
      p95: Number(percentile(latencies, 95).toFixed(2)),
      p99: Number(percentile(latencies, 99).toFixed(2)),
      max: Number((latencies[latencies.length - 1] ?? 0).toFixed(2)),
    },
    statusCounts: results.statusCounts,
  };
}

function evaluatePerformanceGates(summary, gates) {
  const definitions = [
    {
      configuredValue: gates.minSuccessRate,
      metric: 'successRate',
      label: 'Success rate',
      actual: summary.successRate,
      operator: '>=',
      unit: '%',
      passes: (actual, threshold) => actual >= threshold,
    },
    {
      configuredValue: gates.minRequestsPerSecond,
      metric: 'requestsPerSecond',
      label: 'Requests/second',
      actual: summary.requestsPerSecond,
      operator: '>=',
      unit: ' req/s',
      passes: (actual, threshold) => actual >= threshold,
    },
    {
      configuredValue: gates.maxP95Ms,
      metric: 'latencyMs.p95',
      label: 'Latency p95',
      actual: summary.latencyMs.p95,
      operator: '<=',
      unit: ' ms',
      passes: (actual, threshold) => actual <= threshold,
    },
    {
      configuredValue: gates.maxP99Ms,
      metric: 'latencyMs.p99',
      label: 'Latency p99',
      actual: summary.latencyMs.p99,
      operator: '<=',
      unit: ' ms',
      passes: (actual, threshold) => actual <= threshold,
    },
  ];

  const checks = definitions
    .filter((definition) => definition.configuredValue !== undefined)
    .map((definition) => ({
      metric: definition.metric,
      label: definition.label,
      actual: definition.actual,
      operator: definition.operator,
      threshold: definition.configuredValue,
      unit: definition.unit,
      passed: definition.passes(definition.actual, definition.configuredValue),
    }));

  return {
    configured: checks.length > 0,
    passed: checks.every((check) => check.passed),
    checks,
  };
}

function printGateReport(gateResult) {
  console.log('');

  if (!gateResult.configured) {
    console.log('Performance gates: not configured');
    return;
  }

  const passedChecks = gateResult.checks.filter((check) => check.passed).length;
  console.log(
    `Performance gates: ${gateResult.passed ? 'PASSED' : 'FAILED'} (${passedChecks}/${gateResult.checks.length})`,
  );

  for (const check of gateResult.checks) {
    const status = check.passed ? 'PASS' : 'FAIL';
    console.log(
      `[${status}] ${check.label}: ${check.actual}${check.unit} ${check.operator} ${check.threshold}${check.unit}`,
    );
  }
}

async function main() {
  if (process.argv.includes('--help')) {
    printUsage();
    return;
  }

  const options = parseArgs(process.argv.slice(2));

  console.log(`Warmup: ${options.warmup} request(s) to ${options.url}`);
  await warmup(options);

  const results = {
    latencies: [],
    totalRequests: 0,
    successCount: 0,
    non2xxCount: 0,
    timeoutCount: 0,
    networkErrorCount: 0,
    totalBytes: 0,
    statusCounts: {},
  };

  const startedAt = performance.now();
  const endAt = Date.now() + options.duration * 1000;

  await Promise.all(
    Array.from({ length: options.concurrency }, () => runWorker(options, results, endAt)),
  );

  const totalDurationMs = performance.now() - startedAt;
  const summary = buildSummary(options, results, totalDurationMs);
  const performanceGates = evaluatePerformanceGates(summary, options.gates);
  summary.performanceGates = performanceGates;

  console.log('');
  console.log(JSON.stringify(summary, null, 2));
  printGateReport(performanceGates);

  if (!performanceGates.passed) {
    process.exitCode = 2;
  }
}

if (require.main === module) {
  main().catch((error) => {
    console.error(error instanceof Error ? error.message : String(error));
    process.exitCode = 1;
  });
}

module.exports = {
  buildSummary,
  evaluatePerformanceGates,
  parseArgs,
};
