import { randomUUID } from "node:crypto";
import { join } from "node:path";
import { curateAgentActivity } from "../agent/activity-curator.js";
import { ensureAgentLoaded } from "../agent/agent-loading.js";
import { formatSystemNotificationPrompt } from "../agent/agent-prompt.js";
import { resolveCreateAgentTitles } from "../agent/create-agent-title.js";
import { ScheduleStore } from "./store.js";
import { computeNextRunAt, validateScheduleCadence } from "./cron.js";
const SCHEDULE_TICK_INTERVAL_MS = 1000;
function trimOptionalName(value) {
    if (typeof value !== "string") {
        return null;
    }
    const trimmed = value.trim();
    return trimmed.length > 0 ? trimmed : null;
}
function buildScheduleFireBody(schedule, runId) {
    const heading = schedule.name
        ? `Schedule "${schedule.name}" fired (id=${schedule.id}, run=${runId}).`
        : `Schedule fired (id=${schedule.id}, run=${runId}).`;
    return `${heading}\n${schedule.prompt}`;
}
function normalizePrompt(prompt) {
    const trimmed = prompt.trim();
    if (!trimmed) {
        throw new Error("Schedule prompt is required");
    }
    return trimmed;
}
function applyNewAgentConfig(target, patch) {
    const config = { ...target.config };
    if (patch.provider !== undefined) {
        const trimmed = patch.provider.trim();
        if (!trimmed) {
            throw new Error("provider cannot be empty");
        }
        config.provider = trimmed;
    }
    if (patch.cwd !== undefined) {
        const trimmed = patch.cwd.trim();
        if (!trimmed) {
            throw new Error("cwd cannot be empty");
        }
        config.cwd = trimmed;
    }
    if (patch.model !== undefined) {
        const trimmed = patch.model?.trim();
        if (trimmed) {
            config.model = trimmed;
        }
        else {
            delete config.model;
        }
    }
    if (patch.modeId !== undefined) {
        const trimmed = patch.modeId?.trim();
        if (trimmed) {
            config.modeId = trimmed;
        }
        else {
            delete config.modeId;
        }
    }
    return { ...target, config };
}
function normalizeMaxRuns(value) {
    if (value == null) {
        return null;
    }
    if (!Number.isInteger(value) || value <= 0) {
        throw new Error("maxRuns must be a positive integer");
    }
    return value;
}
function countCompletedRuns(schedule) {
    return schedule.runs.filter((run) => run.status !== "running").length;
}
function shouldCompleteSchedule(schedule, now) {
    if (schedule.expiresAt && new Date(schedule.expiresAt).getTime() <= now.getTime()) {
        return true;
    }
    if (schedule.maxRuns == null) {
        return false;
    }
    return countCompletedRuns(schedule) >= schedule.maxRuns;
}
function completeSchedule(schedule, now) {
    return {
        ...schedule,
        status: "completed",
        nextRunAt: null,
        pausedAt: null,
        updatedAt: now.toISOString(),
    };
}
function buildRunOutput(params) {
    if (params.output && params.output.trim().length > 0) {
        return params.output;
    }
    if (params.finalText.trim().length > 0) {
        return params.finalText.trim();
    }
    if (params.timelineText.trim().length > 0) {
        return params.timelineText.trim();
    }
    return null;
}
export class ScheduleService {
    constructor(options) {
        this.runningScheduleIds = new Set();
        this.tickTimer = null;
        this.store = new ScheduleStore(join(options.paseoHome, "schedules"));
        this.logger = options.logger.child({ module: "schedule-service" });
        this.agentManager = options.agentManager;
        this.agentStorage = options.agentStorage;
        this.createConfigResolver = options.providerSnapshotManager;
        this.now = options.now ?? (() => new Date());
        this.runner = options.runner ?? ((schedule, runId) => this.executeSchedule(schedule, runId));
    }
    async start() {
        await this.recoverInterruptedRuns();
        if (this.tickTimer) {
            return;
        }
        const timer = setInterval(() => {
            void this.tick().catch((error) => {
                this.logger.error({ err: error }, "Failed to process schedule tick");
            });
        }, SCHEDULE_TICK_INTERVAL_MS);
        timer.unref?.();
        this.tickTimer = timer;
    }
    async stop() {
        if (this.tickTimer) {
            clearInterval(this.tickTimer);
            this.tickTimer = null;
        }
    }
    async create(input) {
        const now = this.now();
        const prompt = normalizePrompt(input.prompt);
        validateScheduleCadence(input.cadence);
        const runOnCreate = input.runOnCreate ?? input.cadence.type === "every";
        const nextRunAt = runOnCreate ? now : computeNextRunAt(input.cadence, now);
        const schedule = await this.store.create({
            name: trimOptionalName(input.name),
            prompt,
            cadence: input.cadence,
            target: input.target,
            status: "active",
            createdAt: now.toISOString(),
            updatedAt: now.toISOString(),
            nextRunAt: nextRunAt.toISOString(),
            lastRunAt: null,
            pausedAt: null,
            expiresAt: input.expiresAt ?? null,
            maxRuns: normalizeMaxRuns(input.maxRuns),
            runs: [],
        });
        return schedule;
    }
    async list() {
        return this.store.list();
    }
    async inspect(id) {
        const schedule = await this.store.get(id);
        if (!schedule) {
            throw new Error(`Schedule not found: ${id}`);
        }
        return schedule;
    }
    async logs(id) {
        const schedule = await this.inspect(id);
        return [...schedule.runs].sort((left, right) => left.startedAt.localeCompare(right.startedAt));
    }
    async pause(id) {
        const schedule = await this.inspect(id);
        if (schedule.status === "completed") {
            throw new Error(`Schedule ${id} is already completed`);
        }
        if (schedule.status === "paused") {
            return schedule;
        }
        const now = this.now();
        const paused = {
            ...schedule,
            status: "paused",
            nextRunAt: null,
            pausedAt: now.toISOString(),
            updatedAt: now.toISOString(),
        };
        await this.store.put(paused);
        return paused;
    }
    async resume(id) {
        const schedule = await this.inspect(id);
        if (schedule.status === "completed") {
            throw new Error(`Schedule ${id} is already completed`);
        }
        if (schedule.status === "active") {
            return schedule;
        }
        const now = this.now();
        const resumed = {
            ...schedule,
            status: "active",
            pausedAt: null,
            nextRunAt: computeNextRunAt(schedule.cadence, now).toISOString(),
            updatedAt: now.toISOString(),
        };
        await this.store.put(resumed);
        return resumed;
    }
    async update(input) {
        const schedule = await this.inspect(input.id);
        const now = this.now();
        let updated = schedule;
        if (input.prompt !== undefined) {
            updated = { ...updated, prompt: normalizePrompt(input.prompt) };
        }
        if (input.name !== undefined) {
            updated = { ...updated, name: trimOptionalName(input.name) };
        }
        if (input.cadence !== undefined) {
            validateScheduleCadence(input.cadence);
            const nextRunAt = updated.status === "active" ? computeNextRunAt(input.cadence, now).toISOString() : null;
            updated = { ...updated, cadence: input.cadence, nextRunAt };
        }
        if (input.newAgentConfig !== undefined) {
            if (updated.target.type !== "new-agent") {
                throw new Error("new-agent config updates are only valid for new-agent target schedules");
            }
            updated = { ...updated, target: applyNewAgentConfig(updated.target, input.newAgentConfig) };
        }
        if (input.maxRuns !== undefined) {
            updated = { ...updated, maxRuns: normalizeMaxRuns(input.maxRuns) };
        }
        if (input.expiresAt !== undefined) {
            updated = { ...updated, expiresAt: input.expiresAt };
        }
        updated = { ...updated, updatedAt: now.toISOString() };
        await this.store.put(updated);
        return updated;
    }
    async delete(id) {
        await this.store.delete(id);
    }
    async deleteForAgent(agentId) {
        const schedules = await this.store.list();
        const matches = schedules.filter((schedule) => schedule.target.type === "agent" && schedule.target.agentId === agentId);
        const results = await Promise.allSettled(matches.map((schedule) => this.store.delete(schedule.id)));
        let deleted = 0;
        for (const [index, result] of results.entries()) {
            if (result.status === "fulfilled") {
                deleted += 1;
            }
            else {
                this.logger.warn({ err: result.reason, scheduleId: matches[index].id, agentId }, "Failed to delete schedule for archived agent; continuing");
            }
        }
        return deleted;
    }
    async runOnce(id) {
        const schedule = await this.inspect(id);
        if (schedule.status === "completed") {
            throw new Error(`Schedule ${id} is already completed`);
        }
        if (this.runningScheduleIds.has(id)) {
            throw new Error(`Schedule ${id} is already running`);
        }
        await this.runSchedule(schedule, this.now(), { manual: true });
        return this.inspect(id);
    }
    async tick() {
        const now = this.now();
        const schedules = await this.store.list();
        for (const schedule of schedules) {
            if (schedule.status !== "active" || !schedule.nextRunAt) {
                continue;
            }
            if (this.runningScheduleIds.has(schedule.id)) {
                continue;
            }
            if (shouldCompleteSchedule(schedule, now)) {
                await this.store.put(completeSchedule(schedule, now));
                continue;
            }
            if (new Date(schedule.nextRunAt).getTime() > now.getTime()) {
                continue;
            }
            await this.runSchedule(schedule, now);
        }
    }
    async recoverInterruptedRuns() {
        const schedules = await this.store.list();
        const now = this.now();
        await Promise.all(schedules.map(async (schedule) => {
            let updated = { ...schedule };
            let dirty = false;
            // Mark any in-flight runs as failed
            const runningIndex = updated.runs.findIndex((run) => run.status === "running");
            if (runningIndex !== -1) {
                const runs = [...updated.runs];
                runs[runningIndex] = {
                    ...runs[runningIndex],
                    status: "failed",
                    endedAt: now.toISOString(),
                    error: "Daemon restarted before the scheduled run completed",
                };
                updated = { ...updated, runs };
                dirty = true;
            }
            // Advance stale nextRunAt for active schedules
            if (updated.status === "active" &&
                updated.nextRunAt &&
                new Date(updated.nextRunAt).getTime() <= now.getTime()) {
                let nextRunAt = computeNextRunAt(updated.cadence, new Date(updated.nextRunAt));
                while (nextRunAt.getTime() <= now.getTime()) {
                    nextRunAt = computeNextRunAt(updated.cadence, nextRunAt);
                }
                updated = { ...updated, nextRunAt: nextRunAt.toISOString() };
                dirty = true;
            }
            if (dirty) {
                updated = { ...updated, updatedAt: now.toISOString() };
                await this.store.put(updated);
            }
        }));
    }
    async runSchedule(schedule, now, options) {
        const manual = options?.manual === true;
        this.runningScheduleIds.add(schedule.id);
        const runId = randomUUID();
        const runningRun = {
            id: runId,
            scheduledFor: manual ? now.toISOString() : (schedule.nextRunAt ?? now.toISOString()),
            startedAt: now.toISOString(),
            endedAt: null,
            status: "running",
            agentId: null,
            output: null,
            error: null,
        };
        const scheduleWithRun = {
            ...schedule,
            updatedAt: now.toISOString(),
            runs: [...schedule.runs, runningRun],
        };
        await this.store.put(scheduleWithRun);
        try {
            const result = await this.runner(scheduleWithRun, runId);
            await this.finishRun({
                scheduleId: schedule.id,
                runId,
                status: "succeeded",
                agentId: result.agentId,
                output: result.output,
                error: null,
                manual,
            });
        }
        catch (error) {
            await this.finishRun({
                scheduleId: schedule.id,
                runId,
                status: "failed",
                agentId: null,
                output: null,
                error: error instanceof Error ? error.message : String(error),
                manual,
            });
        }
        finally {
            this.runningScheduleIds.delete(schedule.id);
        }
    }
    async finishRun(params) {
        const schedule = await this.inspect(params.scheduleId);
        const now = this.now();
        const completedRuns = schedule.runs.map((run) => run.id === params.runId
            ? {
                ...run,
                status: params.status,
                endedAt: now.toISOString(),
                agentId: params.agentId,
                output: params.output,
                error: params.error,
            }
            : run);
        let updated = {
            ...schedule,
            runs: completedRuns,
            lastRunAt: now.toISOString(),
            updatedAt: now.toISOString(),
        };
        if (params.manual) {
            // Manual one-shot runs do not advance the cadence or recompute completion.
        }
        else if (shouldCompleteSchedule(updated, now)) {
            updated = completeSchedule(updated, now);
        }
        else if (updated.status === "paused") {
            updated = {
                ...updated,
                nextRunAt: null,
            };
        }
        else {
            const after = new Date(schedule.nextRunAt ?? now.toISOString());
            let nextRunAt = computeNextRunAt(updated.cadence, after);
            while (nextRunAt.getTime() <= now.getTime()) {
                nextRunAt = computeNextRunAt(updated.cadence, nextRunAt);
            }
            updated = {
                ...updated,
                nextRunAt: nextRunAt.toISOString(),
            };
        }
        await this.store.put(updated);
    }
    async executeSchedule(schedule, runId) {
        if (schedule.target.type === "agent") {
            const wrappedPrompt = formatSystemNotificationPrompt(buildScheduleFireBody(schedule, runId));
            const record = await this.agentStorage.get(schedule.target.agentId);
            if (record?.archivedAt) {
                throw new Error(`Agent ${schedule.target.agentId} is archived`);
            }
            const agent = await ensureAgentLoaded(schedule.target.agentId, {
                agentManager: this.agentManager,
                agentStorage: this.agentStorage,
                logger: this.logger,
            });
            if (this.agentManager.hasInFlightRun(agent.id)) {
                throw new Error(`Agent ${agent.id} already has an active run`);
            }
            const result = await this.agentManager.runAgent(agent.id, wrappedPrompt);
            const timelineText = curateAgentActivity(result.timeline);
            return {
                agentId: agent.id,
                output: buildRunOutput({
                    output: null,
                    timelineText,
                    finalText: result.finalText,
                }),
            };
        }
        const targetConfig = schedule.target.config;
        const resolvedUnattendedConfig = targetConfig.modeId
            ? { modeId: targetConfig.modeId, featureValues: targetConfig.featureValues }
            : await this.resolveProviderCreateConfig({
                provider: targetConfig.provider,
                cwd: targetConfig.cwd,
                requestedMode: undefined,
                featureValues: targetConfig.featureValues,
                parent: null,
                unattended: true,
            });
        const config = {
            provider: targetConfig.provider,
            cwd: targetConfig.cwd,
            modeId: resolvedUnattendedConfig.modeId,
            model: targetConfig.model,
            thinkingOptionId: targetConfig.thinkingOptionId,
            title: targetConfig.title,
            approvalPolicy: targetConfig.approvalPolicy,
            sandboxMode: targetConfig.sandboxMode,
            networkAccess: targetConfig.networkAccess,
            webSearch: targetConfig.webSearch,
            featureValues: resolvedUnattendedConfig.featureValues,
            extra: targetConfig.extra,
            systemPrompt: targetConfig.systemPrompt,
            mcpServers: targetConfig.mcpServers,
        };
        const { provisionalTitle } = resolveCreateAgentTitles({
            configTitle: config.title,
            initialPrompt: schedule.prompt,
        });
        const labels = {
            "paseo.schedule-id": schedule.id,
            "paseo.schedule-run": runId,
        };
        const agent = await this.agentManager.createAgent(config, undefined, {
            labels,
            initialPrompt: schedule.prompt,
            initialTitle: provisionalTitle,
        });
        let result;
        try {
            result = await this.agentManager.runAgent(agent.id, schedule.prompt);
        }
        catch (error) {
            try {
                await this.agentManager.archiveAgent(agent.id);
            }
            catch (archiveError) {
                this.logger.warn({ err: archiveError, agentId: agent.id, scheduleId: schedule.id, runId }, "Failed to archive scheduled agent after failed run");
            }
            throw error;
        }
        await this.agentManager.archiveAgent(agent.id);
        const timelineText = curateAgentActivity(result.timeline);
        return {
            agentId: agent.id,
            output: buildRunOutput({
                output: null,
                timelineText,
                finalText: result.finalText,
            }),
        };
    }
    async resolveProviderCreateConfig(input) {
        return this.createConfigResolver.resolveCreateConfig(input);
    }
}
//# sourceMappingURL=service.js.map