import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { spawnProcess, waitForChildProcess } from "../../utils/child-process.js";
import { createAgentSessionFromServices, createAgentSessionServices } from "../agent-session-services.js";
import { SkillManager } from "../learning/skill-manager.js";
import { SessionManager } from "../session-manager.js";
import { loadSkills } from "../skills.js";
import { deliverCronResult } from "./delivery.js";
import { CronJobStore } from "./job-store.js";
import { assertSafeCronContent } from "./prompt-scan.js";
function truncate(value, maxChars) {
    if (value.length <= maxChars)
        return value;
    return `${value.slice(0, maxChars)}\n\n[... truncated ...]`;
}
function extractAssistantText(state) {
    const last = [...state.messages].reverse().find((message) => message.role === "assistant");
    if (!last)
        return { text: "", error: "No assistant response was produced." };
    const content = Array.isArray(last.content) ? last.content : [];
    const text = content
        .map((item) => typeof item === "object" && item !== null && "type" in item && item.type === "text" && "text" in item
        ? String(item.text)
        : "")
        .filter(Boolean)
        .join("\n");
    return { text, error: last.errorMessage };
}
function applySchedulerLearningPolicy(settingsManager, settings) {
    const learning = settingsManager.getLearningSettings();
    settingsManager.applyOverrides({
        learning: {
            ...learning,
            enabled: settings.learning.enabled,
            review: {
                ...learning.review,
                mode: settings.learning.reviewEnabled ? learning.review.mode : "off",
            },
        },
    });
}
function createSchedulerUsageStore(base, job) {
    return {
        recordUsage: (record) => base.recordUsage({
            ...record,
            clientOrigin: record.clientOrigin ?? "pie-cron",
            endpoint: record.endpoint ?? `pie-cron:${job.id}`,
            agentRunId: record.agentRunId ?? job.id,
        }),
    };
}
/**
 * Wake-gate: parse the last non-empty stdout line as JSON.
 * If it contains `{ "wakeAgent": false }`, the agent LLM call is skipped.
 * Returns the clean stdout (with the JSON gate line stripped) and the wakeAgent flag.
 */
function parseWakeGate(stdout) {
    const lines = stdout.split("\n");
    // Find the last non-empty line
    let gateLineIndex = -1;
    for (let i = lines.length - 1; i >= 0; i--) {
        if (lines[i].trim()) {
            gateLineIndex = i;
            break;
        }
    }
    if (gateLineIndex < 0)
        return { wakeAgent: true, cleanStdout: stdout };
    try {
        const parsed = JSON.parse(lines[gateLineIndex].trim());
        if (typeof parsed === "object" && parsed !== null && "wakeAgent" in parsed) {
            const wakeAgent = parsed.wakeAgent !== false;
            const cleanLines = [...lines.slice(0, gateLineIndex), ...lines.slice(gateLineIndex + 1)];
            return { wakeAgent, cleanStdout: cleanLines.join("\n") };
        }
    }
    catch {
        // Not JSON — not a gate line
    }
    return { wakeAgent: true, cleanStdout: stdout };
}
async function runScript(job, store, scriptTimeoutMs) {
    if (!job.script) {
        return { success: true, stdout: "", stderr: "" };
    }
    const scriptPath = await store.resolveScriptPath(job.script);
    const child = spawnProcess(scriptPath, [], {
        cwd: job.workdir ?? process.cwd(),
        env: {
            ...process.env,
            PIE_CRON_JOB_ID: job.id,
            PIE_CRON_JOB_NAME: job.name,
        },
        stdio: ["ignore", "pipe", "pipe"],
    });
    let stdout = "";
    let stderr = "";
    child.stdout?.setEncoding("utf-8");
    child.stderr?.setEncoding("utf-8");
    child.stdout?.on("data", (chunk) => {
        stdout += String(chunk);
    });
    child.stderr?.on("data", (chunk) => {
        stderr += String(chunk);
    });
    let timedOut = false;
    const timer = setTimeout(() => {
        timedOut = true;
        child.kill("SIGTERM");
    }, scriptTimeoutMs);
    try {
        const code = await waitForChildProcess(child);
        if (timedOut) {
            return { success: false, stdout, stderr, error: `Script timed out after ${scriptTimeoutMs / 1000}s.` };
        }
        if (code !== 0) {
            return { success: false, stdout, stderr, error: `Script exited with code ${code ?? "unknown"}.` };
        }
        return { success: true, stdout, stderr };
    }
    catch (error) {
        return {
            success: false,
            stdout,
            stderr,
            error: error instanceof Error ? error.message : String(error),
        };
    }
    finally {
        clearTimeout(timer);
    }
}
/**
 * Assemble the prompt for an agent-backed scheduled run.
 *
 * Content gathered at fire time (other jobs' output, script output, skill
 * files) is injection-scanned — it can carry text the user never reviewed,
 * such as fetched web content. A scan match fails the run.
 */
export async function buildCronJobPrompt(job, store, env, scriptOutput) {
    const blocks = [];
    if (job.contextFrom?.length) {
        for (const sourceJobId of job.contextFrom) {
            const output = await store.latestOutput(sourceJobId);
            if (output?.trim()) {
                assertSafeCronContent(output, `output of job "${sourceJobId}"`);
                blocks.push(`The following is the latest output from scheduled job "${sourceJobId}":\n\n${truncate(output.trim(), 20_000)}`);
            }
        }
    }
    if (scriptOutput?.trim()) {
        assertSafeCronContent(scriptOutput, "pre-run script output");
        blocks.push(`The pre-run script produced this output:\n\n\`\`\`text\n${truncate(scriptOutput.trim(), 20_000)}\n\`\`\``);
    }
    if (job.skills?.length) {
        const cwd = job.workdir ?? env.cwd;
        const { skills } = loadSkills({ cwd, agentDir: env.agentDir, skillPaths: [], includeDefaults: true });
        const byName = new Map(skills.map((skill) => [skill.name, skill]));
        const skillManager = new SkillManager({ agentDir: env.agentDir, cwd });
        for (const name of job.skills) {
            const skill = byName.get(name);
            if (!skill) {
                blocks.push(`Note: this job requested the skill "${name}", but it is not installed. Proceed without it.`);
                continue;
            }
            const content = await readFile(skill.filePath, "utf-8");
            assertSafeCronContent(content, `skill "${name}"`);
            try {
                skillManager.recordUseByPath(skill.filePath);
            }
            catch {
                // Usage tracking is best-effort.
            }
            blocks.push(`[IMPORTANT: This scheduled job invokes the skill "${name}". Follow its instructions.]\n\n${truncate(content.trim(), 20_000)}`);
        }
    }
    blocks.push(`Scheduled job prompt:\n\n${job.prompt}`);
    blocks.push("Return only the final scheduled job output. Do not describe the scheduler internals.");
    return blocks.join("\n\n---\n\n");
}
function buildOutputDocument(job, result) {
    const lines = [
        `# Cron Job: ${job.name}`,
        "",
        `- Job ID: ${job.id}`,
        `- Status: ${result.status}`,
        `- Ran At: ${result.ranAt.toISOString()}`,
        `- Schedule: ${job.scheduleDisplay}`,
        "",
        "## Prompt",
        "",
        job.prompt,
        "",
    ];
    if (result.scriptOutput || result.scriptError) {
        lines.push("## Script", "");
        if (result.scriptOutput) {
            lines.push("```text", result.scriptOutput.trim(), "```", "");
        }
        if (result.scriptError) {
            lines.push(`Script error: ${result.scriptError}`, "");
        }
    }
    if (result.error) {
        lines.push("## Error", "", result.error, "");
    }
    lines.push("## Response", "", result.finalResponse || "(no response)", "");
    return lines.join("\n");
}
async function finalizeRun(job, options, store, result) {
    const outputPath = await store.saveOutput(job.id, result.output, result.ranAt);
    const delivery = result.skipDelivery
        ? { delivered: 0, targets: [], errors: [] }
        : await deliverCronResult({
            agentDir: options.agentDir,
            deliver: job.deliver,
            origin: job.origin,
            content: result.finalResponse || result.error || result.output,
        });
    const deliveryError = delivery.errors.length > 0 ? delivery.errors.join("; ") : undefined;
    return {
        jobId: job.id,
        success: result.success,
        status: result.status,
        output: result.output,
        finalResponse: result.finalResponse,
        error: result.error,
        outputPath,
        delivered: delivery.delivered,
        deliveryTargets: delivery.targets,
        deliveryError,
    };
}
export async function runCronJob(job, options) {
    const store = options.store ?? new CronJobStore({ agentDir: options.agentDir, cwd: options.cwd });
    const ranAt = new Date();
    // Agent timeout (LLM call) and script timeout are separate
    const agentTimeoutMs = options.settings.timeoutSeconds * 1000;
    const scriptTimeoutMs = options.settings.scriptTimeoutSeconds * 1000;
    if (job.noAgent && !options.settings.noAgentEnabled) {
        const error = "noAgent scheduled jobs are disabled by scheduler settings.";
        const output = buildOutputDocument(job, { status: "failed", finalResponse: "", error, ranAt });
        return finalizeRun(job, options, store, {
            success: false,
            status: "failed",
            output,
            finalResponse: "",
            error,
            ranAt,
        });
    }
    if (job.script && !options.settings.scriptsEnabled) {
        const error = "Scheduled job scripts are disabled by scheduler settings.";
        const output = buildOutputDocument(job, { status: "failed", finalResponse: "", error, ranAt });
        return finalizeRun(job, options, store, {
            success: false,
            status: "failed",
            output,
            finalResponse: "",
            error,
            ranAt,
        });
    }
    const scriptResult = await runScript(job, store, scriptTimeoutMs);
    // Wake-gate: parse last stdout line — {"wakeAgent": false} skips the LLM entirely
    const { wakeAgent, cleanStdout } = parseWakeGate(scriptResult.stdout);
    const scriptCombined = [cleanStdout, scriptResult.stderr ? `stderr:\n${scriptResult.stderr}` : ""]
        .filter(Boolean)
        .join("\n\n");
    if (!scriptResult.success) {
        const output = buildOutputDocument(job, {
            status: "failed",
            finalResponse: "",
            error: scriptResult.error,
            scriptOutput: scriptCombined,
            scriptError: scriptResult.error,
            ranAt,
        });
        return finalizeRun(job, options, store, {
            success: false,
            status: "failed",
            output,
            finalResponse: "",
            error: scriptResult.error,
            ranAt,
        });
    }
    // noAgent jobs deliver script stdout directly; wake-gate skip also goes here
    if (job.noAgent || !wakeAgent) {
        const finalResponse = cleanStdout.trim();
        // Empty stdout is a soft failure — nothing was produced
        const status = finalResponse ? "success" : "failed";
        const output = buildOutputDocument(job, {
            status,
            finalResponse,
            scriptOutput: scriptCombined,
            ranAt,
        });
        return finalizeRun(job, options, store, {
            success: status !== "failed",
            status,
            output,
            finalResponse,
            ranAt,
        });
    }
    const services = await createAgentSessionServices({
        cwd: job.workdir ?? options.cwd,
        agentDir: options.agentDir,
        usageFilePath: undefined,
        resourceLoaderOptions: {
            noExtensions: true,
            noPromptTemplates: true,
        },
    });
    applySchedulerLearningPolicy(services.settingsManager, options.settings);
    services.usageStore = createSchedulerUsageStore(services.usageStore, job);
    let output = "";
    try {
        const model = job.model?.provider && job.model.id
            ? services.modelRegistry.find(job.model.provider, job.model.id)
            : undefined;
        const sessionManager = SessionManager.create(job.workdir ?? options.cwd, join(options.agentDir, "cron", "sessions", job.id));
        const { session } = await createAgentSessionFromServices({
            services,
            sessionManager,
            model,
            tools: job.tools ?? ["read", "bash", "edit", "write"],
            sessionStartEvent: { type: "session_start", reason: "startup" },
        });
        const timer = setTimeout(() => {
            session.agent.abort();
        }, agentTimeoutMs);
        try {
            const prompt = await buildCronJobPrompt(job, store, { agentDir: options.agentDir, cwd: options.cwd }, scriptCombined);
            await session.prompt(prompt, {
                expandPromptTemplates: false,
                source: "extension",
            });
        }
        finally {
            clearTimeout(timer);
        }
        const { text, error } = extractAssistantText(session.state);
        // [SILENT] marker at the start of response → skip delivery, strip marker
        const isSilent = text.trimStart().startsWith("[SILENT]");
        const finalText = isSilent ? text.replace(/^\[SILENT\]\s*/i, "") : text;
        // Empty response (after stripping [SILENT]) is a soft failure — agent produced nothing
        const status = error ? "failed" : !finalText.trim() ? "failed" : "success";
        output = buildOutputDocument(job, {
            status,
            finalResponse: finalText,
            error,
            scriptOutput: scriptCombined,
            ranAt,
        });
        session.dispose();
        return finalizeRun(job, options, store, {
            success: status !== "failed",
            status,
            output,
            finalResponse: finalText,
            error,
            ranAt,
            skipDelivery: isSilent,
        });
    }
    catch (error) {
        const message = error instanceof Error ? error.message : String(error);
        output = buildOutputDocument(job, {
            status: "failed",
            finalResponse: "",
            error: message,
            scriptOutput: scriptCombined,
            ranAt,
        });
        return finalizeRun(job, options, store, {
            success: false,
            status: "failed",
            output,
            finalResponse: "",
            error: message,
            ranAt,
        });
    }
}
export async function tickCronScheduler(options) {
    const store = options.store ?? new CronJobStore({ agentDir: options.agentDir, cwd: options.cwd });
    // Reset jobs stuck in "running" from a previous crashed run
    // Use agent timeout + a buffer as the stale detection threshold
    const staleThresholdMs = (options.settings.timeoutSeconds + 60) * 1000;
    await store.resetStaleRunning(staleThresholdMs).catch(() => undefined);
    const due = await store.getDueJobs();
    const results = [];
    const parallel = Math.max(1, options.settings.maxParallelJobs);
    let index = 0;
    async function worker() {
        while (index < due.length) {
            const job = due[index++];
            const claimed = await store.claimDueJob(job.id);
            if (!claimed)
                continue;
            const result = await runCronJob(claimed, { ...options, store });
            await store.markRun(claimed.id, {
                status: result.status,
                outputPath: result.outputPath,
                error: result.error,
            });
            await store.markDelivery(claimed.id, result.deliveryError);
            results.push(result);
        }
    }
    await Promise.all(Array.from({ length: Math.min(parallel, due.length) }, () => worker()));
    return results;
}
//# sourceMappingURL=runner.js.map