// Repo + process helpers for the social-autoposter MCP wrapper.
// This MCP is a THIN client: it shells out to the existing pipeline scripts in
// the social-autoposter repo and never reimplements pipeline logic.
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
import path from "node:path";
import fs from "node:fs";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// dist/repo.js -> repo root is two levels up (mcp/dist -> mcp -> repo root).
// Override with SAPS_REPO_DIR for non-standard installs.
export const REPO_DIR = process.env.SAPS_REPO_DIR || path.resolve(__dirname, "..", "..");
// Python used by the pipeline (deps from requirements.txt). Override per-install.
export const PYTHON = process.env.SAPS_PYTHON || "python3";
// The locked pipeline script (run-twitter-cycle.sh) writes the draft plan to a
// HARDCODED /tmp path (`PLAN_FILE="/tmp/twitter_cycle_plan_<batch>.json"`), and the
// `DRAFT_ONLY_PLAN=` marker the wrapper parses is rooted at /tmp too. We MUST read
// and write plans from the same directory. os.tmpdir() is NOT /tmp on macOS (it
// honors $TMPDIR, e.g. /var/folders/.../T, or /tmp/claude-501 inside Claude Code),
// which silently stranded every draft and made draft_cycle always report
// "No drafts in batch ...". Default to /tmp to match the script; allow an explicit
// override for non-standard installs.
export const TMP_DIR = process.env.SAPS_TMP_DIR || "/tmp";
// Spawn a process inside the repo, inheriting the repo env (API base + keys
// come from the install's environment / .env loaded by the scripts themselves).
//
// `onLine` (optional) fires once per COMPLETE line as the child emits output,
// so a long-running script (e.g. run-twitter-cycle.sh, which can churn for
// minutes) can be followed live instead of going dark until it exits. The full
// buffered stdout/stderr are still returned unchanged, so existing callers are
// unaffected. A throwing sink never breaks the run.
export function run(cmd, args, opts = {}) {
    return new Promise((resolve) => {
        const child = spawn(cmd, args, {
            cwd: opts.cwd || REPO_DIR,
            env: { ...process.env, ...(opts.env || {}) },
        });
        let stdout = "";
        let stderr = "";
        // Per-stream partial-line buffers so onLine fires on whole lines only,
        // regardless of how the OS chunks the pipe reads.
        let outBuf = "";
        let errBuf = "";
        const pump = (chunk, which, buf) => {
            if (!opts.onLine)
                return buf;
            buf += chunk;
            let nl;
            while ((nl = buf.indexOf("\n")) !== -1) {
                const line = buf.slice(0, nl);
                buf = buf.slice(nl + 1);
                try {
                    opts.onLine(line, which);
                }
                catch {
                    /* a progress sink must never break the wrapped command */
                }
            }
            return buf;
        };
        let timer;
        if (opts.timeoutMs) {
            timer = setTimeout(() => {
                child.kill("SIGTERM");
            }, opts.timeoutMs);
        }
        child.stdout.on("data", (d) => {
            const s = d.toString();
            stdout += s;
            outBuf = pump(s, "stdout", outBuf);
        });
        child.stderr.on("data", (d) => {
            const s = d.toString();
            stderr += s;
            errBuf = pump(s, "stderr", errBuf);
        });
        child.on("close", (code) => {
            if (timer)
                clearTimeout(timer);
            // Flush any trailing partial line (output with no terminating newline).
            if (opts.onLine) {
                if (outBuf)
                    try {
                        opts.onLine(outBuf, "stdout");
                    }
                    catch {
                        /* ignore */
                    }
                if (errBuf)
                    try {
                        opts.onLine(errBuf, "stderr");
                    }
                    catch {
                        /* ignore */
                    }
            }
            resolve({ code: code ?? -1, stdout, stderr });
        });
        child.on("error", (err) => {
            if (timer)
                clearTimeout(timer);
            resolve({ code: -1, stdout, stderr: stderr + String(err) });
        });
    });
}
export function runPython(scriptRelPath, args, opts = {}) {
    return run(PYTHON, [scriptRelPath, ...args], opts);
}
export function planPath(batchId) {
    return path.join(TMP_DIR, `twitter_cycle_plan_${batchId}.json`);
}
export function readPlan(batchId) {
    const p = planPath(batchId);
    if (!fs.existsSync(p))
        return null;
    try {
        return JSON.parse(fs.readFileSync(p, "utf-8"));
    }
    catch {
        return null;
    }
}
export function writePlan(batchId, plan) {
    fs.writeFileSync(planPath(batchId), JSON.stringify(plan, null, 2), "utf-8");
}
// Find the newest plan file when no batch id is supplied.
export function latestBatchId() {
    let files;
    try {
        files = fs.readdirSync(TMP_DIR);
    }
    catch {
        return null;
    }
    const matches = files
        .map((f) => /^twitter_cycle_plan_(.+)\.json$/.exec(f))
        .filter((m) => !!m && !m[1].endsWith("_approved"))
        .map((m) => ({
        batchId: m[1],
        mtime: fs.statSync(path.join(TMP_DIR, m[0])).mtimeMs,
    }))
        .sort((a, b) => b.mtime - a.mtime);
    return matches.length ? matches[0].batchId : null;
}
