import crypto from "node:crypto";
import { parseAbsoluteTimeMs } from "../parse.js";
import { computeNextRunAtMs } from "../schedule.js";
import { normalizeOptionalAgentId, normalizeOptionalText, normalizePayloadToSystemText, normalizeRequiredName, } from "./normalize.js";
const STUCK_RUN_MS = 2 * 60 * 60 * 1000;
export function assertSupportedJobSpec(job) {
    if (job.sessionTarget === "main" && job.payload.kind !== "systemEvent") {
        throw new Error('main cron jobs require payload.kind="systemEvent"');
    }
    if (job.sessionTarget === "isolated" && job.payload.kind !== "agentTurn") {
        throw new Error('isolated cron jobs require payload.kind="agentTurn"');
    }
}
function assertDeliverySupport(job) {
    if (job.delivery && job.sessionTarget !== "isolated") {
        throw new Error('cron delivery config is only supported for sessionTarget="isolated"');
    }
}
export function findJobOrThrow(state, id) {
    const job = state.store?.jobs.find((j) => j.id === id);
    if (!job) {
        throw new Error(`unknown cron job id: ${id}`);
    }
    return job;
}
export function computeJobNextRunAtMs(job, nowMs) {
    if (!job.enabled) {
        return undefined;
    }
    if (job.schedule.kind === "at") {
        // One-shot jobs stay due until they successfully finish.
        if (job.state.lastStatus === "ok" && job.state.lastRunAtMs) {
            return undefined;
        }
        const atMs = parseAbsoluteTimeMs(job.schedule.at);
        return atMs !== null ? atMs : undefined;
    }
    const next = computeNextRunAtMs(job.schedule, nowMs);
    if (next === undefined && job.schedule.kind === "cron") {
        // Retry from the next whole second if the first attempt returned undefined
        const nextSecondMs = Math.floor(nowMs / 1000) * 1000 + 1000;
        return computeNextRunAtMs(job.schedule, nextSecondMs);
    }
    return next;
}
export function recomputeNextRuns(state) {
    if (!state.store) {
        return;
    }
    const now = state.deps.nowMs();
    for (const job of state.store.jobs) {
        if (!job.state) {
            job.state = {};
        }
        if (!job.enabled) {
            job.state.nextRunAtMs = undefined;
            job.state.runningAtMs = undefined;
            continue;
        }
        const runningAt = job.state.runningAtMs;
        if (typeof runningAt === "number" && now - runningAt > STUCK_RUN_MS) {
            state.deps.log.warn({ jobId: job.id, runningAtMs: runningAt }, "cron: clearing stuck running marker");
            job.state.runningAtMs = undefined;
        }
        job.state.nextRunAtMs = computeJobNextRunAtMs(job, now);
    }
}
export function nextWakeAtMs(state) {
    const jobs = state.store?.jobs ?? [];
    const enabled = jobs.filter((j) => j.enabled && typeof j.state.nextRunAtMs === "number");
    if (enabled.length === 0) {
        return undefined;
    }
    return enabled.reduce((min, j) => Math.min(min, j.state.nextRunAtMs), enabled[0].state.nextRunAtMs);
}
export function createJob(state, input) {
    const now = state.deps.nowMs();
    const id = crypto.randomUUID();
    const deleteAfterRun = typeof input.deleteAfterRun === "boolean"
        ? input.deleteAfterRun
        : input.schedule.kind === "at"
            ? true
            : undefined;
    const job = {
        id,
        agentId: normalizeOptionalAgentId(input.agentId),
        name: normalizeRequiredName(input.name),
        description: normalizeOptionalText(input.description),
        enabled: input.enabled,
        deleteAfterRun,
        createdAtMs: now,
        updatedAtMs: now,
        schedule: input.schedule,
        sessionTarget: input.sessionTarget,
        wakeMode: input.wakeMode,
        payload: input.payload,
        delivery: input.delivery,
        state: {
            ...input.state,
        },
    };
    assertSupportedJobSpec(job);
    assertDeliverySupport(job);
    job.state.nextRunAtMs = computeJobNextRunAtMs(job, now);
    return job;
}
export function applyJobPatch(job, patch) {
    if ("name" in patch) {
        job.name = normalizeRequiredName(patch.name);
    }
    if ("description" in patch) {
        job.description = normalizeOptionalText(patch.description);
    }
    if (typeof patch.enabled === "boolean") {
        job.enabled = patch.enabled;
    }
    if (typeof patch.deleteAfterRun === "boolean") {
        job.deleteAfterRun = patch.deleteAfterRun;
    }
    if (patch.schedule) {
        job.schedule = patch.schedule;
    }
    if (patch.sessionTarget) {
        job.sessionTarget = patch.sessionTarget;
    }
    if (patch.wakeMode) {
        job.wakeMode = patch.wakeMode;
    }
    if (patch.payload) {
        job.payload = mergeCronPayload(job.payload, patch.payload);
    }
    if (patch.delivery) {
        job.delivery = mergeCronDelivery(job.delivery, patch.delivery);
    }
    if (patch.state) {
        job.state = { ...job.state, ...patch.state };
    }
    if ("agentId" in patch) {
        job.agentId = normalizeOptionalAgentId(patch.agentId);
    }
    assertSupportedJobSpec(job);
    assertDeliverySupport(job);
}
function mergeCronPayload(existing, patch) {
    if (patch.kind !== existing.kind) {
        return buildPayloadFromPatch(patch);
    }
    if (patch.kind === "systemEvent") {
        if (existing.kind !== "systemEvent") {
            return buildPayloadFromPatch(patch);
        }
        const text = typeof patch.text === "string" ? patch.text : existing.text;
        return { kind: "systemEvent", text };
    }
    if (existing.kind !== "agentTurn") {
        return buildPayloadFromPatch(patch);
    }
    const next = { ...existing };
    if (typeof patch.message === "string") {
        next.message = patch.message;
    }
    if (typeof patch.model === "string") {
        next.model = patch.model;
    }
    if (typeof patch.thinking === "string") {
        next.thinking = patch.thinking;
    }
    if (typeof patch.timeoutSeconds === "number") {
        next.timeoutSeconds = patch.timeoutSeconds;
    }
    if (typeof patch.deliver === "boolean") {
        next.deliver = patch.deliver;
    }
    if (typeof patch.channel === "string") {
        next.channel = patch.channel;
    }
    if (typeof patch.to === "string") {
        next.to = patch.to;
    }
    if (typeof patch.bestEffortDeliver === "boolean") {
        next.bestEffortDeliver = patch.bestEffortDeliver;
    }
    return next;
}
function buildPayloadFromPatch(patch) {
    if (patch.kind === "systemEvent") {
        if (typeof patch.text !== "string" || patch.text.length === 0) {
            throw new Error('cron.update payload.kind="systemEvent" requires text');
        }
        return { kind: "systemEvent", text: patch.text };
    }
    if (typeof patch.message !== "string" || patch.message.length === 0) {
        throw new Error('cron.update payload.kind="agentTurn" requires message');
    }
    return {
        kind: "agentTurn",
        message: patch.message,
        model: patch.model,
        thinking: patch.thinking,
        timeoutSeconds: patch.timeoutSeconds,
        deliver: patch.deliver,
        channel: patch.channel,
        to: patch.to,
        bestEffortDeliver: patch.bestEffortDeliver,
    };
}
function mergeCronDelivery(existing, patch) {
    const next = {
        mode: existing?.mode ?? "none",
        channel: existing?.channel,
        to: existing?.to,
        bestEffort: existing?.bestEffort,
    };
    if (typeof patch.mode === "string") {
        // @ts-ignore -- cherry-pick upstream type mismatch
        // @ts-ignore -- cherry-pick upstream type mismatch
        next.mode = patch.mode === "deliver" ? "announce" : patch.mode;
    }
    if ("channel" in patch) {
        const channel = typeof patch.channel === "string" ? patch.channel.trim() : "";
        next.channel = channel ? channel : undefined;
    }
    if ("to" in patch) {
        const to = typeof patch.to === "string" ? patch.to.trim() : "";
        next.to = to ? to : undefined;
    }
    if (typeof patch.bestEffort === "boolean") {
        next.bestEffort = patch.bestEffort;
    }
    return next;
}
export function isJobDue(job, nowMs, opts) {
    if (opts.forced) {
        return true;
    }
    return job.enabled && typeof job.state.nextRunAtMs === "number" && nowMs >= job.state.nextRunAtMs;
}
export function resolveJobPayloadTextForMain(job) {
    if (job.payload.kind !== "systemEvent") {
        return undefined;
    }
    const text = normalizePayloadToSystemText(job.payload);
    return text.trim() ? text : undefined;
}
export function recomputeNextRunsForMaintenance(_state, _now) {
    // Stub: maintenance recompute is handled by the scheduler
    return false;
}
