/**
 * Load Profiler
 *
 * Runs load tests using k6, Artillery, or other tools.
 * Generates k6 scripts from YAML profile definitions.
 *
 * @module scanners/scale/load-profiler
 */
import { spawn } from "child_process";
import { readFile, readdir, writeFile, mkdir, rm } from "fs/promises";
import { join } from "path";
import { tmpdir } from "os";
import { parse as parseYaml } from "yaml";
import { logger } from "../../logger.js";
import { LoadProfileSchema, } from "./types.js";
const PROFILES_DIR = ".vaspera/load";
/**
 * Check if k6 is installed
 */
export async function isK6Available() {
    return new Promise((resolve) => {
        const child = spawn("k6", ["version"]);
        child.on("error", () => resolve(false));
        child.on("close", (code) => resolve(code === 0));
    });
}
/**
 * Check if Artillery is installed
 */
export async function isArtilleryAvailable() {
    return new Promise((resolve) => {
        const child = spawn("artillery", ["version"]);
        child.on("error", () => resolve(false));
        child.on("close", (code) => resolve(code === 0));
    });
}
/**
 * Detect best available load testing tool
 */
export async function detectLoadTool() {
    if (await isK6Available())
        return "k6";
    if (await isArtilleryAvailable())
        return "artillery";
    return null;
}
/**
 * Parse duration string to milliseconds
 */
function parseDuration(duration) {
    const match = duration.match(/^(\d+)(s|m|h)$/);
    if (!match)
        return 60000;
    const value = parseInt(match[1], 10);
    const unit = match[2];
    switch (unit) {
        case "s":
            return value * 1000;
        case "m":
            return value * 60 * 1000;
        case "h":
            return value * 60 * 60 * 1000;
        default:
            return value * 1000;
    }
}
/**
 * Generate k6 script from load profile
 */
export function generateK6Script(profile, baseUrl) {
    const endpoints = profile.endpoints || [{ path: "/", method: "GET", weight: 100 }];
    const scenarios = profile.scenarios.map((scenario) => {
        const vus = typeof scenario.vus === "number"
            ? { start: scenario.vus, end: scenario.vus }
            : scenario.vus;
        return `
    ${scenario.name.replace(/[^a-zA-Z0-9]/g, "_")}: {
      executor: '${scenario.type === "ramp" ? "ramping-vus" : "constant-vus"}',
      ${scenario.type === "ramp" ? `
      stages: [
        { duration: '${scenario.duration}', target: ${vus.end} },
      ],
      startVUs: ${vus.start},` : `
      vus: ${vus.start},
      duration: '${scenario.duration}',`}
    }`;
    });
    const endpointChecks = endpoints.map((ep, i) => `
    // ${ep.path} (weight: ${ep.weight})
    if (Math.random() * 100 < ${ep.weight}) {
      const res${i} = http.${ep.method.toLowerCase()}(\`\${BASE_URL}${ep.path}\`${ep.body ? `, JSON.stringify(${JSON.stringify(ep.body)}), { headers: { 'Content-Type': 'application/json' } }` : ""});
      check(res${i}, {
        '${ep.path} status is 2xx': (r) => r.status >= 200 && r.status < 300,
        '${ep.path} duration < ${ep.thresholds?.p95 || 500}ms': (r) => r.timings.duration < ${ep.thresholds?.p95 || 500},
      });
    }`);
    const thresholds = profile.thresholds || {};
    return `
import http from 'k6/http';
import { check, sleep } from 'k6';

const BASE_URL = '${baseUrl}';

export const options = {
  scenarios: {${scenarios.join(",")}
  },
  thresholds: {
    http_req_duration: ['p(95)<${thresholds.p95 || 500}', 'p(99)<${thresholds.p99 || 1000}'],
    http_req_failed: ['rate<${thresholds.errorRate || 0.01}'],
  },
};

export default function() {
  ${endpointChecks.join("\n")}
  sleep(1);
}
`;
}
/**
 * Load a profile from YAML file
 */
export async function loadProfile(profilePath) {
    const content = await readFile(profilePath, "utf-8");
    const parsed = parseYaml(content);
    return LoadProfileSchema.parse(parsed);
}
/**
 * Discover all load profiles in a project
 */
export async function discoverProfiles(projectPath) {
    const profilesDir = join(projectPath, PROFILES_DIR);
    const profiles = [];
    try {
        const files = await readdir(profilesDir);
        for (const file of files) {
            if (file.endsWith(".yaml") || file.endsWith(".yml")) {
                try {
                    const profilePath = join(profilesDir, file);
                    const profile = await loadProfile(profilePath);
                    profiles.push({ path: profilePath, profile });
                }
                catch (error) {
                    logger.warn("scale.profile_parse_error", {
                        file,
                        error: error.message,
                    });
                }
            }
        }
    }
    catch {
        logger.debug("scale.no_profiles_directory", { path: profilesDir });
    }
    return profiles;
}
/**
 * Parse k6 JSON output
 */
function parseK6Output(output) {
    try {
        const lines = output.split("\n").filter((l) => l.trim());
        const metrics = lines
            .filter((l) => l.includes("http_req_duration") || l.includes("http_reqs") || l.includes("http_req_failed"))
            .join("\n");
        // Parse summary metrics from k6 output
        const durationMatch = metrics.match(/http_req_duration[^:]*:\s*avg=([0-9.]+)ms\s*min=([0-9.]+)ms\s*med=([0-9.]+)ms\s*max=([0-9.]+)ms\s*p\(90\)=([0-9.]+)ms\s*p\(95\)=([0-9.]+)ms/);
        const reqsMatch = metrics.match(/http_reqs[^:]*:\s*(\d+)/);
        const failedMatch = metrics.match(/http_req_failed[^:]*:\s*([0-9.]+)%/);
        return {
            latency: durationMatch ? {
                min: parseFloat(durationMatch[2]),
                max: parseFloat(durationMatch[4]),
                mean: parseFloat(durationMatch[1]),
                median: parseFloat(durationMatch[3]),
                p90: parseFloat(durationMatch[5]),
                p95: parseFloat(durationMatch[6]),
                p99: parseFloat(durationMatch[6]) * 1.2, // Estimate
            } : {
                min: 0, max: 0, mean: 0, median: 0, p90: 0, p95: 0, p99: 0,
            },
            requests: reqsMatch ? parseInt(reqsMatch[1], 10) : 0,
            failures: failedMatch ? Math.round(parseInt(reqsMatch?.[1] || "0", 10) * parseFloat(failedMatch[1]) / 100) : 0,
            throughput: 0, // Calculated from duration
        };
    }
    catch {
        return {
            latency: { min: 0, max: 0, mean: 0, median: 0, p90: 0, p95: 0, p99: 0 },
            requests: 0,
            failures: 0,
            throughput: 0,
        };
    }
}
/**
 * Run a load test with k6
 */
export async function runK6Test(profile, baseUrl, options = {}) {
    const startTime = new Date();
    const script = generateK6Script(profile, baseUrl);
    // Write script to temp file
    const tmpDir = options.outputDir || join(tmpdir(), `vaspera-k6-${Date.now()}`);
    await mkdir(tmpDir, { recursive: true });
    const scriptPath = join(tmpDir, "script.js");
    await writeFile(scriptPath, script);
    logger.info("scale.k6_test_started", {
        profile: profile.name,
        baseUrl,
        scenarios: profile.scenarios.length,
    });
    return new Promise((resolve) => {
        const args = ["run", "--summary-export", join(tmpDir, "summary.json"), scriptPath];
        const child = spawn("k6", args, {
            timeout: options.timeout || 600000,
        });
        let stdout = "";
        let stderr = "";
        child.stdout?.on("data", (data) => {
            stdout += data.toString();
        });
        child.stderr?.on("data", (data) => {
            stderr += data.toString();
        });
        child.on("close", async (code) => {
            const endTime = new Date();
            const duration = endTime.getTime() - startTime.getTime();
            const parsed = parseK6Output(stdout);
            // Calculate score based on error rate and latency
            let score = 100;
            if (parsed.latency.p95 > (profile.thresholds?.p95 || 500)) {
                score -= 20;
            }
            if (parsed.latency.p99 > (profile.thresholds?.p99 || 1000)) {
                score -= 15;
            }
            const errorRate = parsed.failures / (parsed.requests || 1);
            if (errorRate > (profile.thresholds?.errorRate || 0.01)) {
                score -= 30;
            }
            const result = {
                profile: profile.name,
                tool: "k6",
                startTime: startTime.toISOString(),
                endTime: endTime.toISOString(),
                duration,
                scenarios: profile.scenarios.map((s) => ({
                    name: s.name,
                    type: s.type || "ramp",
                    duration: parseDuration(s.duration),
                    vusMax: typeof s.vus === "number" ? s.vus : s.vus.end,
                    totalRequests: parsed.requests,
                    failedRequests: parsed.failures,
                    errorRate,
                    latency: parsed.latency,
                    throughput: parsed.requests / (duration / 1000),
                    endpoints: [],
                    passed: code === 0,
                })),
                summary: {
                    totalRequests: parsed.requests,
                    failedRequests: parsed.failures,
                    errorRate,
                    avgThroughput: parsed.requests / (duration / 1000),
                    peakThroughput: parsed.requests / (duration / 1000),
                    latency: parsed.latency,
                },
                passed: code === 0 && score >= 70,
                score: Math.max(0, score),
            };
            // Cleanup temp files
            try {
                await rm(tmpDir, { recursive: true, force: true });
            }
            catch {
                // Ignore cleanup errors
            }
            logger.info("scale.k6_test_completed", {
                profile: profile.name,
                passed: result.passed,
                score: result.score,
                duration,
            });
            resolve(result);
        });
        child.on("error", async (error) => {
            const endTime = new Date();
            // Cleanup temp files
            try {
                await rm(tmpDir, { recursive: true, force: true });
            }
            catch {
                // Ignore cleanup errors
            }
            resolve({
                profile: profile.name,
                tool: "k6",
                startTime: startTime.toISOString(),
                endTime: endTime.toISOString(),
                duration: endTime.getTime() - startTime.getTime(),
                scenarios: [],
                summary: {
                    totalRequests: 0,
                    failedRequests: 0,
                    errorRate: 0,
                    avgThroughput: 0,
                    peakThroughput: 0,
                    latency: { min: 0, max: 0, mean: 0, median: 0, p90: 0, p95: 0, p99: 0 },
                },
                passed: false,
                score: 0,
            });
        });
    });
}
/**
 * Generate a sample load profile
 */
export async function generateSampleProfile(projectPath, name = "production") {
    const profilesDir = join(projectPath, PROFILES_DIR);
    await mkdir(profilesDir, { recursive: true });
    const sampleProfile = `# ${name} Load Profile
# Define load testing scenarios for your app

name: "${name}"
description: "Production-like load test profile"
tool: k6

# Default endpoints for all scenarios
endpoints:
  - path: "/"
    method: GET
    weight: 50
  - path: "/api/health"
    method: GET
    weight: 20
  # Add your endpoints:
  # - path: "/api/products"
  #   method: GET
  #   weight: 30

# Global thresholds
thresholds:
  p95: 500      # 95th percentile < 500ms
  p99: 1000     # 99th percentile < 1000ms
  errorRate: 0.01  # < 1% errors

scenarios:
  # Ramp up test
  - name: "Ramp Up"
    type: ramp
    duration: "2m"
    vus:
      start: 1
      end: 50

  # Steady state
  - name: "Steady State"
    type: ramp
    duration: "5m"
    vus:
      start: 50
      end: 50

  # Spike test (uncomment to enable)
  # - name: "Spike"
  #   type: spike
  #   duration: "30s"
  #   vus:
  #     start: 50
  #     end: 200
`;
    const profilePath = join(profilesDir, `${name}.yaml`);
    await writeFile(profilePath, sampleProfile, "utf-8");
    logger.info("scale.sample_profile_created", { path: profilePath });
    return profilePath;
}
//# sourceMappingURL=load-profiler.js.map