// src/providers/cli-provider.ts
import { spawn } from "child_process";
import { Readable } from "stream";
function killWithEscalation(proc) {
  proc.kill("SIGTERM");
  return setTimeout(() => {
    if (!proc.killed) proc.kill("SIGKILL");
  }, 3e3);
}
function createCliProvider(cfg) {
  const { binary, name } = cfg;
  function cliRun(args, opts) {
    return new Promise((resolve, reject) => {
      const proc = spawn(binary, args, {
        stdio: ["pipe", "pipe", "pipe"]
      });
      let stdout = "";
      let stderr = "";
      proc.stdout?.on("data", (buf) => {
        stdout += buf.toString();
      });
      proc.stderr?.on("data", (buf) => {
        stderr += buf.toString();
      });
      if (opts?.stdin) {
        proc.stdin?.write(opts.stdin);
      }
      proc.stdin?.end();
      const timer = opts?.timeoutMs ? setTimeout(() => {
        proc.kill("SIGKILL");
        reject(
          new Error(
            `${binary} command timed out after ${opts.timeoutMs}ms`
          )
        );
      }, opts.timeoutMs) : null;
      proc.on("close", (code) => {
        if (timer) clearTimeout(timer);
        if (code !== 0) {
          reject(
            new Error(
              `${binary} ${args[0]} failed (${code}): ${stderr.trim()}`
            )
          );
        } else {
          resolve(stdout);
        }
      });
      proc.on("error", (err) => {
        if (timer) clearTimeout(timer);
        reject(err);
      });
    });
  }
  function execOneShot(containerName, argv, stdin, timeoutMs) {
    return new Promise((resolve, reject) => {
      const proc = spawn(binary, cfg.execArgs(containerName, argv), {
        stdio: ["pipe", "pipe", "pipe"]
      });
      let stdout = "";
      let stderr = "";
      proc.stdout?.on("data", (buf) => {
        stdout += buf.toString();
      });
      proc.stderr?.on("data", (buf) => {
        stderr += buf.toString();
      });
      if (stdin) {
        proc.stdin?.write(stdin);
      }
      proc.stdin?.end();
      const timer = timeoutMs ? setTimeout(() => {
        proc.kill("SIGKILL");
        reject(
          new Error(`${binary} exec timed out after ${timeoutMs}ms`)
        );
      }, timeoutMs) : null;
      proc.on("close", (code) => {
        if (timer) clearTimeout(timer);
        resolve({ stdout, stderr, exit_code: code ?? 1 });
      });
      proc.on("error", (err) => {
        if (timer) clearTimeout(timer);
        reject(err);
      });
    });
  }
  function execStreaming(containerName, opts) {
    const args = cfg.execArgs(containerName, opts.argv);
    if (process.env.DEBUG_NDJSON) {
      console.log(`[exec] ${binary} ${args.map((a) => a.includes(" ") ? `"${a}"` : a).join(" ")}`);
      if (opts.stdin) {
        const sepIdx = opts.stdin.indexOf("\n\n");
        const safePreview = sepIdx >= 0 ? opts.stdin.slice(sepIdx + 2, sepIdx + 202) : "[env-only stdin]";
        console.log(`[exec] stdin: ${safePreview}...`);
      }
    }
    const proc = spawn(binary, args, {
      stdio: ["pipe", "pipe", "pipe"]
    });
    let stderrBuf = "";
    proc.stderr?.on("data", (chunk) => {
      stderrBuf += chunk.toString();
    });
    if (opts.stdin) {
      proc.stdin?.write(opts.stdin);
    }
    proc.stdin?.end();
    const stdout = Readable.toWeb(proc.stdout);
    let exitResolve;
    let exitReject;
    const exit = new Promise((res, rej) => {
      exitResolve = res;
      exitReject = rej;
    });
    let escalationTimer = null;
    proc.on("close", (code) => {
      if (escalationTimer) clearTimeout(escalationTimer);
      if (process.env.DEBUG_NDJSON && code !== 0 && stderrBuf) {
        console.log(`[exec] exit ${code}, stderr: ${stderrBuf.slice(0, 500)}`);
      }
      exitResolve({ code: code ?? 1 });
    });
    proc.on("error", (err) => {
      if (escalationTimer) clearTimeout(escalationTimer);
      exitReject(err);
    });
    let timer = null;
    if (opts.timeoutMs) {
      timer = setTimeout(() => {
        proc.kill("SIGKILL");
        exitReject(
          new Error(`${binary} exec timed out after ${opts.timeoutMs}ms`)
        );
      }, opts.timeoutMs);
    }
    exit.finally(() => {
      if (timer) clearTimeout(timer);
    });
    if (opts.signal) {
      if (opts.signal.aborted) {
        escalationTimer = killWithEscalation(proc);
      } else {
        opts.signal.addEventListener(
          "abort",
          () => {
            escalationTimer = killWithEscalation(proc);
          },
          { once: true }
        );
      }
    }
    return {
      stdout,
      exit,
      async kill() {
        escalationTimer = killWithEscalation(proc);
      }
    };
  }
  const provider = {
    name,
    stripControlChars: cfg.stripControlChars ?? false,
    async checkAvailability(_secrets) {
      if (cfg.checkAvailability) {
        return cfg.checkAvailability();
      }
      if (cfg.checkCmd) {
        try {
          await cliRun(cfg.checkCmd, {
            timeoutMs: cfg.checkTimeoutMs ?? 3e3
          });
          return { available: true };
        } catch (err) {
          const msg = err instanceof Error ? err.message : String(err);
          const failMsg = cfg.checkFailMsg ? cfg.checkFailMsg(msg) : `${binary} is not available: ${msg}`;
          return { available: false, message: failMsg };
        }
      }
      return { available: true };
    },
    async create({ name: containerName }) {
      const steps = cfg.createSteps(containerName);
      for (const step of steps) {
        await cliRun(step, { timeoutMs: 6e4 });
      }
      if (cfg.postCreate) {
        cfg.postCreate(
          containerName,
          (argv, stdin, timeoutMs) => execOneShot(containerName, argv, stdin, timeoutMs)
        );
      }
    },
    async delete(containerName) {
      await cliRun(cfg.deleteArgs(containerName)).catch(() => {
      });
    },
    async list(opts) {
      try {
        const prefix = opts?.prefix ?? "ca-sess-";
        const out = await cliRun(cfg.listArgs(prefix));
        const names = cfg.parseList(out, prefix);
        return names.map((n) => ({ name: n }));
      } catch {
        return [];
      }
    },
    async exec(containerName, argv, opts) {
      return execOneShot(containerName, argv, opts?.stdin, opts?.timeoutMs);
    },
    startExec(containerName, opts) {
      return Promise.resolve(execStreaming(containerName, opts));
    }
  };
  return provider;
}

export {
  createCliProvider
};
