import { applyJobPatch, computeJobNextRunAtMs, createJob, findJobOrThrow, isJobDue, nextWakeAtMs, recomputeNextRuns, recomputeNextRunsForMaintenance, } from "./jobs.js";
import { locked } from "./locked.js";
import { ensureLoaded, persist, warnIfDisabled } from "./store.js";
import { armTimer, emit, executeJob, runMissedJobs, stopTimer, wake } from "./timer.js";
export async function start(state) {
    await locked(state, async () => {
        if (!state.deps.cronEnabled) {
            state.deps.log.info({ enabled: false }, "cron: disabled");
            return;
        }
        // @ts-ignore -- cherry-pick upstream type mismatch
        // @ts-ignore -- cherry-pick upstream type mismatch
        await ensureLoaded(state, { skipRecompute: true });
        const jobs = state.store?.jobs ?? [];
        const startupInterruptedJobIds = new Set();
        for (const job of jobs) {
            if (typeof job.state.runningAtMs === "number") {
                state.deps.log.warn({ jobId: job.id, runningAtMs: job.state.runningAtMs }, "cron: clearing stale running marker on startup");
                job.state.runningAtMs = undefined;
                startupInterruptedJobIds.add(job.id);
            }
            // @ts-ignore -- cherry-pick upstream type mismatch
        }
        // @ts-ignore -- cherry-pick upstream type mismatch
        await runMissedJobs(state, { skipJobIds: startupInterruptedJobIds });
        recomputeNextRuns(state);
        await persist(state);
        armTimer(state);
        state.deps.log.info({
            enabled: true,
            jobs: state.store?.jobs.length ?? 0,
            nextWakeAtMs: nextWakeAtMs(state) ?? null,
        }, "cron: started");
    });
}
export function stop(state) {
    stopTimer(state);
}
// @ts-ignore -- cherry-pick upstream type mismatch
export async function status(state) {
    return await locked(state, async () => {
        // @ts-ignore -- cherry-pick upstream type mismatch
        await ensureLoaded(state, { skipRecompute: true });
        if (state.store) {
            // Use the maintenance-only version so that read-only operations never
            // advance a past-due nextRunAtMs without executing the job (#16156).
            const changed = recomputeNextRunsForMaintenance(state);
            if (changed) {
                await persist(state);
            }
        }
        return {
            enabled: state.deps.cronEnabled,
            storePath: state.deps.storePath,
            jobs: state.store?.jobs.length ?? 0,
            nextWakeAtMs: state.deps.cronEnabled ? (nextWakeAtMs(state) ?? null) : null,
        };
    });
}
// @ts-ignore -- cherry-pick upstream type mismatch
export async function list(state, opts) {
    return await locked(state, async () => {
        // @ts-ignore -- cherry-pick upstream type mismatch
        await ensureLoaded(state, { skipRecompute: true });
        if (state.store) {
            // Use the maintenance-only version so that read-only operations never
            // advance a past-due nextRunAtMs without executing the job (#16156).
            const changed = recomputeNextRunsForMaintenance(state);
            if (changed) {
                await persist(state);
            }
        }
        const includeDisabled = opts?.includeDisabled === true;
        const jobs = (state.store?.jobs ?? []).filter((j) => includeDisabled || j.enabled);
        return jobs.toSorted((a, b) => (a.state.nextRunAtMs ?? 0) - (b.state.nextRunAtMs ?? 0));
    });
}
export async function add(state, input) {
    return await locked(state, async () => {
        warnIfDisabled(state, "add");
        await ensureLoaded(state);
        const job = createJob(state, input);
        state.store?.jobs.push(job);
        // Defensive: recompute all next-run times to ensure consistency
        recomputeNextRuns(state);
        await persist(state);
        armTimer(state);
        state.deps.log.info({
            jobId: job.id,
            jobName: job.name,
            nextRunAtMs: job.state.nextRunAtMs,
            schedulerNextWakeAtMs: nextWakeAtMs(state) ?? null,
            timerArmed: state.timer !== null,
            cronEnabled: state.deps.cronEnabled,
        }, "cron: job added");
        emit(state, {
            jobId: job.id,
            action: "added",
            nextRunAtMs: job.state.nextRunAtMs,
        });
        return job;
    });
}
// @ts-ignore -- cherry-pick upstream type mismatch
export async function update(state, id, patch) {
    return await locked(state, async () => {
        warnIfDisabled(state, "update");
        // @ts-ignore -- cherry-pick upstream type mismatch
        await ensureLoaded(state, { skipRecompute: true });
        const job = findJobOrThrow(state, id);
        const now = state.deps.nowMs();
        applyJobPatch(job, patch);
        if (job.schedule.kind === "every") {
            const anchor = job.schedule.anchorMs;
            if (typeof anchor !== "number" || !Number.isFinite(anchor)) {
                const patchSchedule = patch.schedule;
                const fallbackAnchorMs = patchSchedule?.kind === "every"
                    ? now
                    : typeof job.createdAtMs === "number" && Number.isFinite(job.createdAtMs)
                        ? job.createdAtMs
                        : now;
                job.schedule = {
                    ...job.schedule,
                    anchorMs: Math.max(0, Math.floor(fallbackAnchorMs)),
                };
            }
        }
        const scheduleChanged = patch.schedule !== undefined;
        const enabledChanged = patch.enabled !== undefined;
        job.updatedAtMs = now;
        if (scheduleChanged || enabledChanged) {
            if (job.enabled) {
                job.state.nextRunAtMs = computeJobNextRunAtMs(job, now);
            }
            else {
                job.state.nextRunAtMs = undefined;
                job.state.runningAtMs = undefined;
            }
        }
        else if (job.enabled) {
            // Non-schedule edits should not mutate other jobs, but still repair a
            // missing/corrupt nextRunAtMs for the updated job.
            const nextRun = job.state.nextRunAtMs;
            if (typeof nextRun !== "number" || !Number.isFinite(nextRun)) {
                job.state.nextRunAtMs = computeJobNextRunAtMs(job, now);
            }
        }
        await persist(state);
        armTimer(state);
        emit(state, {
            jobId: id,
            action: "updated",
            nextRunAtMs: job.state.nextRunAtMs,
        });
        return job;
    });
}
export async function remove(state, id) {
    return await locked(state, async () => {
        warnIfDisabled(state, "remove");
        await ensureLoaded(state);
        const before = state.store?.jobs.length ?? 0;
        if (!state.store) {
            return { ok: false, removed: false };
        }
        state.store.jobs = state.store.jobs.filter((j) => j.id !== id);
        const removed = (state.store.jobs.length ?? 0) !== before;
        await persist(state);
        armTimer(state);
        if (removed) {
            emit(state, { jobId: id, action: "removed" });
        }
        return { ok: true, removed };
    });
    // @ts-ignore -- cherry-pick upstream type mismatch
}
export async function run(state, id, mode) {
    return await locked(state, async () => {
        warnIfDisabled(state, "run");
        // @ts-ignore -- cherry-pick upstream type mismatch
        await ensureLoaded(state, { skipRecompute: true });
        const job = findJobOrThrow(state, id);
        if (typeof job.state.runningAtMs === "number") {
            return { ok: true, ran: false, reason: "already-running" };
        }
        const now = state.deps.nowMs();
        const due = isJobDue(job, now, { forced: mode === "force" });
        if (!due) {
            return { ok: true, ran: false, reason: "not-due" };
        }
        await executeJob(state, job, now, { forced: mode === "force" });
        recomputeNextRuns(state);
        await persist(state);
        armTimer(state);
        return { ok: true, ran: true };
    });
}
export function wakeNow(state, opts) {
    return wake(state, opts);
}
