import { sanitizeAgentId } from "../routing/session-key.js";
import { parseAbsoluteTimeMs } from "./parse.js";
import { migrateLegacyCronPayload } from "./payload-migration.js";
const DEFAULT_OPTIONS = {
    applyDefaults: false,
};
function isRecord(value) {
    return typeof value === "object" && value !== null && !Array.isArray(value);
}
function coerceSchedule(schedule) {
    const next = { ...schedule };
    const kind = typeof schedule.kind === "string" ? schedule.kind : undefined;
    const atMsRaw = schedule.atMs;
    const atRaw = schedule.at;
    // Resolve atMs as a number: prefer already-numeric atMs, then parse string at/atMs.
    const atMsFromRaw = typeof atMsRaw === "number"
        ? atMsRaw
        : typeof atMsRaw === "string"
            ? parseAbsoluteTimeMs(atMsRaw)
            : typeof atRaw === "string"
                ? parseAbsoluteTimeMs(atRaw)
                : null;
    if (!kind) {
        if (typeof schedule.atMs === "number" ||
            typeof schedule.at === "string" ||
            typeof schedule.atMs === "string") {
            next.kind = "at";
        }
        else if (typeof schedule.everyMs === "number") {
            next.kind = "every";
        }
        else if (typeof schedule.expr === "string") {
            next.kind = "cron";
        }
    }
    // Normalize the `at` field: always keep/produce a normalized UTC ISO string
    // so the gateway schema (which requires `at: string`) passes validation and
    // `computeNextRunAtMs` can reliably use `schedule.at`.
    // When only atMs (numeric ms) was provided, synthesize the ISO string from it.
    if (atMsFromRaw !== null) {
        next.atMs = atMsFromRaw;
        // Keep normalized ISO string so schema validation and schedule.ts are happy.
        if (typeof next.at !== "string" || !next.at.trim()) {
            next.at = new Date(atMsFromRaw).toISOString();
        }
    }
    return next;
}
function coercePayload(payload) {
    const next = { ...payload };
    // Back-compat: older configs used `provider` for delivery channel.
    migrateLegacyCronPayload(next);
    return next;
}
function coerceDelivery(delivery) {
    const next = { ...delivery };
    if (typeof delivery.mode === "string") {
        next.mode = delivery.mode.trim().toLowerCase();
    }
    if (typeof delivery.channel === "string") {
        const trimmed = delivery.channel.trim().toLowerCase();
        if (trimmed) {
            next.channel = trimmed;
        }
        else {
            delete next.channel;
        }
    }
    if (typeof delivery.to === "string") {
        const trimmed = delivery.to.trim();
        if (trimmed) {
            next.to = trimmed;
        }
        else {
            delete next.to;
        }
    }
    return next;
}
function hasLegacyDeliveryHints(payload) {
    if (typeof payload.deliver === "boolean") {
        return true;
    }
    if (typeof payload.bestEffortDeliver === "boolean") {
        return true;
    }
    if (typeof payload.to === "string" && payload.to.trim()) {
        return true;
    }
    return false;
}
function unwrapJob(raw) {
    if (isRecord(raw.data)) {
        return raw.data;
    }
    if (isRecord(raw.job)) {
        return raw.job;
    }
    return raw;
}
export function normalizeCronJobInput(raw, options = DEFAULT_OPTIONS) {
    if (!isRecord(raw)) {
        return null;
    }
    const base = unwrapJob(raw);
    const next = { ...base };
    if ("agentId" in base) {
        const agentId = base.agentId;
        if (agentId === null) {
            next.agentId = null;
        }
        else if (typeof agentId === "string") {
            const trimmed = agentId.trim();
            if (trimmed) {
                next.agentId = sanitizeAgentId(trimmed);
            }
            else {
                delete next.agentId;
            }
        }
    }
    if ("enabled" in base) {
        const enabled = base.enabled;
        if (typeof enabled === "boolean") {
            next.enabled = enabled;
        }
        else if (typeof enabled === "string") {
            const trimmed = enabled.trim().toLowerCase();
            if (trimmed === "true") {
                next.enabled = true;
            }
            if (trimmed === "false") {
                next.enabled = false;
            }
        }
    }
    if (isRecord(base.schedule)) {
        next.schedule = coerceSchedule(base.schedule);
    }
    if (isRecord(base.payload)) {
        next.payload = coercePayload(base.payload);
    }
    if (isRecord(base.delivery)) {
        next.delivery = coerceDelivery(base.delivery);
    }
    if (options.applyDefaults) {
        if (!next.wakeMode) {
            next.wakeMode = "next-heartbeat";
        }
        if (!next.sessionTarget && isRecord(next.payload)) {
            const kind = typeof next.payload.kind === "string" ? next.payload.kind : "";
            if (kind === "systemEvent") {
                next.sessionTarget = "main";
            }
            if (kind === "agentTurn") {
                next.sessionTarget = "isolated";
            }
        }
        if ("schedule" in next &&
            isRecord(next.schedule) &&
            next.schedule.kind === "at" &&
            !("deleteAfterRun" in next)) {
            next.deleteAfterRun = true;
        }
        const hasDelivery = "delivery" in next && next.delivery !== undefined;
        const payload = isRecord(next.payload) ? next.payload : null;
        const payloadKind = payload && typeof payload.kind === "string" ? payload.kind : "";
        const sessionTarget = typeof next.sessionTarget === "string" ? next.sessionTarget : "";
        const hasLegacyIsolation = isRecord(next.isolation);
        const hasLegacyDelivery = payload ? hasLegacyDeliveryHints(payload) : false;
        if (!hasDelivery &&
            !hasLegacyIsolation &&
            !hasLegacyDelivery &&
            sessionTarget === "isolated" &&
            payloadKind === "agentTurn") {
            next.delivery = { mode: "announce" };
        }
    }
    return next;
}
export function normalizeCronJobCreate(raw, options) {
    return normalizeCronJobInput(raw, {
        applyDefaults: true,
        ...options,
    });
}
export function normalizeCronJobPatch(raw, options) {
    return normalizeCronJobInput(raw, {
        applyDefaults: false,
        ...options,
    });
}
