import fs from "fs";
import path from "path";
import crypto from "crypto";
import { execFileSync } from "child_process";
import { destroyRuntimeDatabase, cleanupOrphanedLocalInfrastructure } from "../database/index.mjs";

const RUN_SCHEMA_VERSION = 1;
const RUNS_DIRNAME = path.join(".testkit", "_runs");
const TERMINATION_TIMEOUT_MS = 5_000;
const SHUTDOWN_HOLD_TIMEOUT_MS = 10_000;

export function createRunLifecycle(productDir) {
  const runId = buildRunId();
  const manifestPath = path.join(getRunsDir(productDir), `${runId}.json`);
  const abortController = new AbortController();
  const state = {
    schemaVersion: RUN_SCHEMA_VERSION,
    runId,
    productDir,
    pid: process.pid,
    status: "starting",
    startedAt: new Date().toISOString(),
    interruptReason: null,
    services: [],
    graphDirs: [],
    runtimeDirs: [],
    runtimeStateDirs: [],
  };
  const signalListeners = [];
  const managedProcesses = new Set();
  let shutdownHold = null;
  let shutdownHoldTimeout = null;

  function persist() {
    fs.mkdirSync(getRunsDir(productDir), { recursive: true });
    fs.writeFileSync(manifestPath, `${JSON.stringify(state, null, 2)}\n`);
  }

  function mutate(mutator) {
    mutator(state);
    persist();
  }

  function ensureShutdownHold() {
    if (shutdownHold) return;
    shutdownHold = setInterval(() => {}, 1_000);
    shutdownHoldTimeout = setTimeout(() => {
      releaseShutdownHold();
    }, SHUTDOWN_HOLD_TIMEOUT_MS);
  }

  function releaseShutdownHold() {
    if (!shutdownHold) return;
    clearInterval(shutdownHold);
    shutdownHold = null;
    if (shutdownHoldTimeout) {
      clearTimeout(shutdownHoldTimeout);
      shutdownHoldTimeout = null;
    }
  }

  const api = {
    runId,
    manifestPath,
    signal: abortController.signal,
    isStopRequested() {
      return abortController.signal.aborted;
    },
    markRunning() {
      mutate((draft) => {
        draft.status = "running";
      });
    },
    markFinished(status = "finished") {
      mutate((draft) => {
        draft.status = status;
        draft.finishedAt = new Date().toISOString();
      });
    },
    requestStop(reason = "interrupted") {
      ensureShutdownHold();
      if (!abortController.signal.aborted) {
        abortController.abort(new Error(`testkit run interrupted (${reason})`));
      }
      for (const entry of managedProcesses) {
        try {
          entry.terminate?.();
        } catch {
          // Best-effort interruption only.
        }
      }
      terminateDescendantProcesses(process.pid, "SIGINT");
      mutate((draft) => {
        draft.status = "interrupting";
        draft.interruptReason = reason;
      });
    },
    trackGraphContext(context) {
      mutate((draft) => {
        pushUnique(draft.graphDirs, context.graphDir);
        pushUnique(draft.runtimeDirs, context.runtimeDir);
        for (const runtimeConfig of context.runtimeConfigs || []) {
          if (runtimeConfig.stateDir) pushUnique(draft.runtimeStateDirs, runtimeConfig.stateDir);
        }
      });
    },
    registerProcess(child, terminate) {
      if (!child) return;
      managedProcesses.add({
        child,
        terminate,
      });
    },
    unregisterProcess(childPid) {
      for (const entry of managedProcesses) {
        if (entry.child?.pid === childPid) {
          managedProcesses.delete(entry);
        }
      }
    },
    registerService(config, child, cwd, terminate) {
      api.registerProcess(child, terminate);
      const ports = collectConfigPorts(config);
      mutate((draft) => {
        draft.services = draft.services.filter((service) => service.pid !== child.pid);
        draft.services.push({
          serviceName: config.name,
          runtimeLabel: config.runtimeLabel,
          command: config.testkit.local?.start || null,
          cwd,
          pid: child.pid,
          processGroupId: child.pid,
          ports,
          startedAt: new Date().toISOString(),
        });
      });
    },
    unregisterService(childPid) {
      api.unregisterProcess(childPid);
      mutate((draft) => {
        draft.services = draft.services.filter((service) => service.pid !== childPid);
      });
    },
    installSignalHandlers() {
      const install = (eventName, handler) => {
        process.on(eventName, handler);
        signalListeners.push([eventName, handler]);
      };

      const handleSignal = (signalName) => {
        api.requestStop(signalName);
      };

      install("SIGINT", () => handleSignal("SIGINT"));
      install("SIGTERM", () => handleSignal("SIGTERM"));
      install("SIGHUP", () => handleSignal("SIGHUP"));
    },
    removeSignalHandlers() {
      while (signalListeners.length > 0) {
        const [eventName, handler] = signalListeners.pop();
        process.off(eventName, handler);
      }
    },
    removeManifest() {
      removeManifestFile(productDir, runId);
    },
    dispose() {
      releaseShutdownHold();
    },
  };

  return api;
}

export async function cleanupRuns(productDir, { includeActive = false } = {}) {
  const manifests = listRunManifests(productDir);
  const summary = {
    cleaned: [],
    skippedActive: [],
  };

  for (const manifest of manifests) {
    const isActive = isPidRunning(manifest.pid);
    if (isActive && !includeActive) {
      summary.skippedActive.push(manifest);
      continue;
    }
    await cleanupRunManifest(productDir, manifest, { removeRuntimeState: false });
    summary.cleaned.push(manifest);
  }

  await cleanupOrphanedLocalInfrastructure(productDir);
  return summary;
}

export async function cleanupStaleRuns(productDir) {
  const manifests = listRunManifests(productDir);
  const cleaned = [];

  for (const manifest of manifests) {
    if (isPidRunning(manifest.pid)) continue;
    await cleanupRunManifest(productDir, manifest, { removeRuntimeState: false });
    cleaned.push(manifest);
  }

  await cleanupOrphanedLocalInfrastructure(productDir);
  return cleaned;
}

export async function cleanupRunById(productDir, runId) {
  const manifest = listRunManifests(productDir).find((entry) => entry.runId === runId);
  if (!manifest) return false;
  await cleanupRunManifest(productDir, manifest, { removeRuntimeState: false });
  await cleanupOrphanedLocalInfrastructure(productDir);
  return true;
}

export function listRunManifests(productDir) {
  const runsDir = getRunsDir(productDir);
  if (!fs.existsSync(runsDir)) return [];

  return fs
    .readdirSync(runsDir)
    .filter((entry) => entry.endsWith(".json"))
    .map((entry) => readRunManifest(path.join(runsDir, entry)))
    .filter(Boolean)
    .sort((left, right) => String(left.startedAt || "").localeCompare(String(right.startedAt || "")));
}

export function findPortOwner(productDir, { host, port }) {
  const manifests = listRunManifests(productDir);
  for (const manifest of manifests) {
    for (const service of manifest.services || []) {
      for (const socket of service.ports || []) {
        if (normalizeHost(socket.host) === normalizeHost(host) && Number(socket.port) === Number(port)) {
          return {
            manifest,
            service,
            active: isPidRunning(manifest.pid),
          };
        }
      }
    }
  }
  return null;
}

export function formatRunSummary(manifest) {
  const runtimeLabels = [...new Set((manifest.services || []).map((service) => service.runtimeLabel).filter(Boolean))];
  return `${manifest.runId} pid=${manifest.pid}${runtimeLabels.length > 0 ? ` runtimes=${runtimeLabels.join(",")}` : ""}`;
}

export function isPidRunning(pid) {
  if (!Number.isInteger(Number(pid)) || Number(pid) <= 0) return false;
  try {
    process.kill(Number(pid), 0);
    return true;
  } catch (error) {
    return error?.code === "EPERM";
  }
}

async function cleanupRunManifest(productDir, manifest, { removeRuntimeState = false } = {}) {
  for (const service of [...(manifest.services || [])].reverse()) {
    await terminateOwnedProcess(service);
  }

  if (removeRuntimeState) {
    const runtimeStateDirs = [...new Set(manifest.runtimeStateDirs || [])].sort((a, b) => b.length - a.length);
    for (const stateDir of runtimeStateDirs) {
      await destroyRuntimeDatabase({ productDir, stateDir });
    }

    for (const runtimeDir of [...new Set(manifest.runtimeDirs || [])].sort((a, b) => b.length - a.length)) {
      fs.rmSync(runtimeDir, { recursive: true, force: true });
    }

    for (const graphDir of [...new Set(manifest.graphDirs || [])].sort((a, b) => b.length - a.length)) {
      pruneEmptyParents(graphDir, path.join(productDir, ".testkit", "_graphs"));
    }
  }

  removeManifestFile(productDir, manifest.runId);
}

async function terminateOwnedProcess(service) {
  const pid = Number(service.processGroupId || service.pid);
  if (!Number.isInteger(pid) || pid <= 0) return;
  if (!isPidRunning(pid)) return;

  killProcessGroup(pid, "SIGTERM");
  const exited = await waitForPidExit(pid, TERMINATION_TIMEOUT_MS);
  if (!exited) {
    killProcessGroup(pid, "SIGKILL");
    await waitForPidExit(pid, TERMINATION_TIMEOUT_MS);
  }
}

function killProcessGroup(pid, signal) {
  try {
    process.kill(-pid, signal);
    return;
  } catch (error) {
    if (error?.code !== "ESRCH") {
      // Fall through and try the direct pid.
    } else {
      return;
    }
  }

  try {
    process.kill(pid, signal);
  } catch (error) {
    if (error?.code !== "ESRCH") throw error;
  }
}

function terminateDescendantProcesses(rootPid, signal) {
  const descendants = listDescendantPids(rootPid);
  for (const pid of descendants) {
    try {
      process.kill(pid, signal);
    } catch (error) {
      if (error?.code !== "ESRCH") {
        // Best-effort interruption only.
      }
    }
  }
}

function listDescendantPids(rootPid) {
  try {
    const output = execFileSync("ps", ["-eo", "pid=,ppid="], {
      encoding: "utf8",
      stdio: ["ignore", "pipe", "ignore"],
    });
    const childrenByParent = new Map();
    for (const line of output.split(/\r?\n/)) {
      const trimmed = line.trim();
      if (!trimmed) continue;
      const [pidRaw, parentRaw] = trimmed.split(/\s+/);
      const pid = Number(pidRaw);
      const parentPid = Number(parentRaw);
      if (!Number.isInteger(pid) || !Number.isInteger(parentPid)) continue;
      const siblings = childrenByParent.get(parentPid) || [];
      siblings.push(pid);
      childrenByParent.set(parentPid, siblings);
    }

    const descendants = [];
    const stack = [...(childrenByParent.get(rootPid) || [])];
    while (stack.length > 0) {
      const pid = stack.pop();
      descendants.push(pid);
      for (const childPid of childrenByParent.get(pid) || []) {
        stack.push(childPid);
      }
    }
    return descendants;
  } catch {
    return [];
  }
}

async function waitForPidExit(pid, timeoutMs) {
  const startedAt = Date.now();
  while (Date.now() - startedAt < timeoutMs) {
    if (!isPidRunning(pid)) return true;
    await sleep(100);
  }
  return !isPidRunning(pid);
}

function readRunManifest(filePath) {
  try {
    const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
    if (!parsed?.runId) return null;
    return parsed;
  } catch {
    return null;
  }
}

function collectConfigPorts(config) {
  const seen = new Set();
  const ports = [];
  for (const rawUrl of [config.testkit.local?.baseUrl, config.testkit.local?.readyUrl]) {
    if (!rawUrl) continue;
    try {
      const parsed = new URL(rawUrl);
      const host = normalizeHost(parsed.hostname);
      const port = Number(parsed.port || (parsed.protocol === "https:" ? 443 : 80));
      const key = `${host}:${port}`;
      if (seen.has(key)) continue;
      seen.add(key);
      ports.push({ host, port });
    } catch {
      // Ignore malformed URLs here; startup validation handles them elsewhere.
    }
  }
  return ports;
}

function pushUnique(list, value) {
  if (!value) return;
  if (!list.includes(value)) list.push(value);
}

function getRunsDir(productDir) {
  return path.join(productDir, RUNS_DIRNAME);
}

function buildRunId() {
  return `${Date.now()}-${process.pid}-${crypto.randomBytes(4).toString("hex")}`;
}

function normalizeHost(host) {
  if (!host) return "127.0.0.1";
  return host === "::1" ? "127.0.0.1" : host;
}

function pruneEmptyParents(startDir, stopDir) {
  let current = startDir;
  while (current && current.startsWith(stopDir)) {
    if (!fs.existsSync(current)) {
      current = path.dirname(current);
      continue;
    }
    const entries = fs.readdirSync(current);
    if (entries.length > 0) break;
    fs.rmSync(current, { recursive: true, force: true });
    if (current === stopDir) break;
    current = path.dirname(current);
  }
}

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function removeManifestFile(productDir, runId) {
  const runsDir = getRunsDir(productDir);
  fs.rmSync(path.join(runsDir, `${runId}.json`), { force: true });
  if (fs.existsSync(runsDir) && fs.readdirSync(runsDir).length === 0) {
    fs.rmSync(runsDir, { recursive: true, force: true });
  }
}
