import fs from "fs";
import path from "path";
import { execa } from "execa";
import { bundleK6File } from "../bundler/index.mjs";
import { resolveK6Binary } from "../config/index.mjs";
import {
  buildFileTimeoutEnv,
  formatFileTimeoutBudgetError,
} from "../shared/file-timeout.mjs";
import { persistTaskArtifacts, persistTaskOutputArtifacts } from "./artifacts.mjs";
import { determineDefaultRuntimeFailure } from "./default-runtime-errors.mjs";
import { collectFailureDetailsFromRuntimeArtifacts } from "./failure-details.mjs";
import { RUNTIME_ARTIFACT_MARKER } from "../runtime-src/k6/artifacts.js";
import { readDatabaseUrl } from "./state-io.mjs";
import { buildTaskExecutionEnv } from "./template.mjs";
import { killChildProcess } from "./processes.mjs";

export async function runHttpK6Task(targetConfig, task, lifecycle, lease, reporter = null) {
  const baseUrl = targetConfig.testkit.local?.baseUrl;
  if (!baseUrl) {
    throw new Error(`Service "${targetConfig.name}" requires local.baseUrl for HTTP suites`);
  }

  const bundledFile = await bundleK6File({
    productDir: targetConfig.productDir,
    serviceName: targetConfig.name,
    sourceFile: path.join(targetConfig.productDir, task.file),
  });
  if (lifecycle.isStopRequested()) {
    throw new Error(`testkit run interrupted before starting ${task.file}`);
  }
  return runDefaultRuntimeTask(
    targetConfig,
    task,
    lease,
    ["run", "--address", "127.0.0.1:0", "-e", `BASE_URL=${baseUrl}`, bundledFile],
    lifecycle,
    reporter
  );
}

export async function runDalTask(targetConfig, task, lifecycle, lease, reporter = null) {
  const databaseUrl = readDatabaseUrl(targetConfig.stateDir);
  if (!databaseUrl) {
    throw new Error(`Service "${targetConfig.name}" requires a database for DAL suites`);
  }

  const bundledFile = await bundleK6File({
    productDir: targetConfig.productDir,
    serviceName: targetConfig.name,
    sourceFile: path.join(targetConfig.productDir, task.file),
  });
  if (lifecycle.isStopRequested()) {
    throw new Error(`testkit run interrupted before starting ${task.file}`);
  }
  return runDefaultRuntimeTask(
    targetConfig,
    task,
    lease,
    ["run", "--address", "127.0.0.1:0", "-e", `DATABASE_URL=${databaseUrl}`, bundledFile],
    lifecycle,
    reporter
  );
}

export async function runDefaultRuntimeTask(
  targetConfig,
  task,
  lease,
  args,
  lifecycle,
  reporterOrFirstLine,
  maybeFirstLine
) {
  const k6Binary = resolveK6Binary();
  const reporter =
    reporterOrFirstLine && typeof reporterOrFirstLine === "object"
      ? reporterOrFirstLine
      : null;
  const firstLine =
    typeof reporterOrFirstLine === "function" ? reporterOrFirstLine : maybeFirstLine;
  const getFirstLine = firstLine || defaultFirstLine;
  const summaryFile = buildDefaultRuntimeSummaryPath(lease, task);
  fs.mkdirSync(path.dirname(summaryFile), { recursive: true });
  const startedAt = Date.now();
  const fileTimeoutSeconds = targetConfig.testkit.execution.fileTimeoutSeconds;
  const subprocess = execa(
    k6Binary,
    [...args.slice(0, 1), "--summary-export", summaryFile, ...args.slice(1)],
    {
      cwd: targetConfig.productDir,
      env: buildTaskExecutionEnv(
        targetConfig,
        lease,
        buildFileTimeoutEnv(fileTimeoutSeconds, startedAt),
        process.env
      ),
      reject: false,
      forceKillAfterDelay: 5_000,
    }
  );
  lifecycle.registerProcess(subprocess, () => {
    killChildProcess(subprocess, "SIGINT");
  });
  if (lifecycle.isStopRequested()) {
    const interruptSubprocess = () => killChildProcess(subprocess, "SIGINT");
    if (subprocess.pid) interruptSubprocess();
    else subprocess.once?.("spawn", interruptSubprocess);
  }
  reporter?.taskStarted?.(task, targetConfig);
  let result;
  let timedOut;
  try {
    ({ result, timedOut } = await settleSubprocess(subprocess, fileTimeoutSeconds));
  } finally {
    lifecycle.unregisterProcess(subprocess.pid);
  }

  const stdout = parseDefaultRuntimeOutput(result.stdout || "");
  const stderr = parseDefaultRuntimeOutput(result.stderr || "");

  const summary = readDefaultRuntimeSummary(summaryFile);
  const rawRuntimeArtifacts = [...stdout.artifacts, ...stderr.artifacts];
  const runtimeArtifacts = persistTaskArtifacts(
    targetConfig.productDir,
    task,
    rawRuntimeArtifacts
  );
  const outputArtifacts = persistTaskOutputArtifacts(targetConfig.productDir, task, [
    stdout.visibleOutput
      ? {
          name: "default-runtime-stdout",
          kind: "runtime.output",
          summary: defaultFirstLine(stdout.visibleOutput) || "captured stdout",
          stream: "stdout",
          text: stdout.visibleOutput,
        }
      : null,
    stderr.visibleOutput
      ? {
          name: "default-runtime-stderr",
          kind: "runtime.output",
          summary: defaultFirstLine(stderr.visibleOutput) || "captured stderr",
          stream: "stderr",
          text: stderr.visibleOutput,
        }
      : null,
  ]);
  const failureDetails = collectFailureDetailsFromRuntimeArtifacts(rawRuntimeArtifacts);
  const runtimeError = timedOut
    ? formatFileTimeoutBudgetError(fileTimeoutSeconds)
    : determineDefaultRuntimeFailure(result, summary, getFirstLine);
  const finishedAt = Date.now();

  return {
    task,
    failed: runtimeError !== null,
    error: runtimeError,
    durationMs: finishedAt - startedAt,
    startedAt,
    finishedAt,
    artifacts: [...runtimeArtifacts, ...outputArtifacts],
    failureDetails,
  };
}

export async function settleSubprocess(subprocess, fileTimeoutSeconds) {
  const timeoutMs = fileTimeoutSeconds * 1000 + 1_000;
  let timeoutHandle = null;
  let timedOut = false;

  try {
    return await Promise.race([
      subprocess.then((result) => ({ result, timedOut })),
      new Promise((resolve) => {
        timeoutHandle = setTimeout(async () => {
          timedOut = true;
          killChildProcess(subprocess, "SIGTERM");
          const result = await subprocess.catch((error) => error);
          resolve({ result, timedOut: true });
        }, timeoutMs);
      }),
    ]);
  } finally {
    if (timeoutHandle) clearTimeout(timeoutHandle);
  }
}

export function buildDefaultRuntimeSummaryPath(lease, task) {
  return path.join(lease.leaseDir, "default-runtime", `task-${task.id}.summary.json`);
}

export function readDefaultRuntimeSummary(filePath) {
  try {
    return JSON.parse(fs.readFileSync(filePath, "utf8"));
  } catch {
    return null;
  }
}

function defaultFirstLine(output) {
  return String(output || "")
    .split(/\r?\n/)
    .map((line) => line.trim())
    .find(Boolean) || null;
}

function parseDefaultRuntimeOutput(output) {
  if (!output) {
    return {
      visibleOutput: "",
      artifacts: [],
    };
  }

  const visibleLines = [];
  const artifacts = [];
  for (const line of output.split(/\r?\n/)) {
    const rawPayload = extractArtifactPayload(line);
    if (rawPayload !== null) {
      try {
        artifacts.push(JSON.parse(decodeURIComponent(rawPayload)));
      } catch {
        visibleLines.push(line);
      }
      continue;
    }
    visibleLines.push(line);
  }

  return {
    visibleOutput: visibleLines.join("\n"),
    artifacts,
  };
}

function extractArtifactPayload(line) {
  if (line.startsWith(RUNTIME_ARTIFACT_MARKER)) {
    return line.slice(RUNTIME_ARTIFACT_MARKER.length);
  }

  const k6ConsoleMatch = line.match(/msg="TESTKIT_ARTIFACT:(.*)"(?:\s+source=console)?$/);
  if (!k6ConsoleMatch) return null;
  return k6ConsoleMatch[1];
}
