const SECONDS_PER_MINUTE = 60;
const MILLISECONDS_PER_SECOND = 1000;
const MILLISECONDS_PER_MINUTE = SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND;
const DEFAULT_RUN_HISTORY_LIMIT = 200;
export class AsyncCronSchedulerError extends Error {
    code;
    constructor(code, message) {
        super(message);
        this.name = "AsyncCronSchedulerError";
        this.code = code;
    }
}
function parsePositiveInteger(value, fieldName) {
    if (!/^\d+$/.test(value)) {
        throw new Error(`invalid ${fieldName}: expected a positive integer, got '${value}'`);
    }
    const parsed = Number.parseInt(value, 10);
    if (!Number.isSafeInteger(parsed) || parsed <= 0) {
        throw new Error(`invalid ${fieldName}: expected a positive integer, got '${value}'`);
    }
    return parsed;
}
function parseInteger(value, fieldName) {
    if (!/^\d+$/.test(value)) {
        throw new Error(`invalid ${fieldName}: expected an integer, got '${value}'`);
    }
    const parsed = Number.parseInt(value, 10);
    if (!Number.isSafeInteger(parsed)) {
        throw new Error(`invalid ${fieldName}: expected an integer, got '${value}'`);
    }
    return parsed;
}
function normalizeDayOfWeek(value) {
    return value === 7 ? 0 : value;
}
function validateBounds(value, min, max, fieldName) {
    if (value < min || value > max) {
        throw new Error(`invalid ${fieldName}: value '${value}' is out of range (${min}-${max})`);
    }
    return value;
}
function parseCronField(rawField, options) {
    const field = rawField.trim();
    if (!field) {
        throw new Error(`invalid ${options.fieldName}: field cannot be empty`);
    }
    const values = new Set();
    const segments = field.split(",");
    for (const rawSegment of segments) {
        const segment = rawSegment.trim();
        if (!segment) {
            throw new Error(`invalid ${options.fieldName}: empty list segment`);
        }
        const slashParts = segment.split("/");
        if (slashParts.length > 2) {
            throw new Error(`invalid ${options.fieldName}: too many '/' in '${segment}'`);
        }
        const base = slashParts[0];
        const step = slashParts.length === 2
            ? parsePositiveInteger(slashParts[1], `${options.fieldName} step in segment '${segment}'`)
            : 1;
        let start;
        let end;
        if (base === "*") {
            start = options.min;
            end = options.max;
        }
        else if (base.includes("-")) {
            const rangeParts = base.split("-");
            if (rangeParts.length !== 2) {
                throw new Error(`invalid ${options.fieldName}: invalid range '${base}'`);
            }
            const parsedStart = validateBounds(parseInteger(rangeParts[0], `${options.fieldName} range start`), options.min, options.max, options.fieldName);
            const parsedEnd = validateBounds(parseInteger(rangeParts[1], `${options.fieldName} range end`), options.min, options.max, options.fieldName);
            if (parsedStart > parsedEnd) {
                throw new Error(`invalid ${options.fieldName}: range start must be <= range end in '${base}'`);
            }
            start = parsedStart;
            end = parsedEnd;
        }
        else {
            const parsedValue = validateBounds(parseInteger(base, options.fieldName), options.min, options.max, options.fieldName);
            start = parsedValue;
            end = parsedValue;
        }
        for (let value = start; value <= end; value += step) {
            const normalized = options.normalize ? options.normalize(value) : value;
            values.add(normalized);
        }
    }
    if (values.size === 0) {
        throw new Error(`invalid ${options.fieldName}: no values were selected`);
    }
    return {
        values,
        isWildcard: field.startsWith("*"),
    };
}
export function parseCronSchedule(expression) {
    const trimmed = expression.trim();
    if (!trimmed) {
        throw new Error("cron expression cannot be empty");
    }
    const parts = trimmed.split(/\s+/);
    if (parts.length !== 5) {
        throw new Error("cron expression must have five fields: minute hour day-of-month month day-of-week");
    }
    return {
        expression: trimmed,
        minute: parseCronField(parts[0], {
            fieldName: "minute",
            min: 0,
            max: 59,
        }),
        hour: parseCronField(parts[1], {
            fieldName: "hour",
            min: 0,
            max: 23,
        }),
        dayOfMonth: parseCronField(parts[2], {
            fieldName: "day-of-month",
            min: 1,
            max: 31,
        }),
        month: parseCronField(parts[3], {
            fieldName: "month",
            min: 1,
            max: 12,
        }),
        dayOfWeek: parseCronField(parts[4], {
            fieldName: "day-of-week",
            min: 0,
            max: 7,
            normalize: normalizeDayOfWeek,
        }),
    };
}
function resolveDayMatch(schedule, date) {
    const domMatches = schedule.dayOfMonth.values.has(date.getDate());
    const dowMatches = schedule.dayOfWeek.values.has(date.getDay());
    if (schedule.dayOfMonth.isWildcard || schedule.dayOfWeek.isWildcard) {
        return domMatches && dowMatches;
    }
    return domMatches || dowMatches;
}
export function matchesCronSchedule(schedule, date) {
    if (!schedule.minute.values.has(date.getMinutes())) {
        return false;
    }
    if (!schedule.hour.values.has(date.getHours())) {
        return false;
    }
    if (!schedule.month.values.has(date.getMonth() + 1)) {
        return false;
    }
    return resolveDayMatch(schedule, date);
}
function minuteBucket(now) {
    return Math.floor(now.getTime() / MILLISECONDS_PER_MINUTE);
}
function readErrorCode(error) {
    if (typeof error !== "object" || error === null) {
        return undefined;
    }
    const record = error;
    return typeof record.code === "string" ? record.code : undefined;
}
class AsyncCronSchedulerImpl {
    jobs;
    jobsById;
    sessionManager;
    additionalSystemMessage;
    pollIntervalMs;
    maxRunHistory;
    now;
    onLog;
    runHistory = [];
    timer;
    lastCheckedMinute;
    runSequence = 0;
    closed = false;
    constructor(options) {
        this.jobs = Object.entries(options.jobs).map(([id, config]) => ({
            id,
            config,
            schedule: parseCronSchedule(config.schedule),
        }));
        this.jobsById = new Map(this.jobs.map((job) => [job.id, job]));
        this.sessionManager = options.sessionManager;
        this.additionalSystemMessage = options.additionalSystemMessage?.trim() || undefined;
        this.pollIntervalMs = options.pollIntervalMs ?? MILLISECONDS_PER_SECOND;
        this.maxRunHistory = options.maxRunHistory ?? DEFAULT_RUN_HISTORY_LIMIT;
        this.now = options.now ?? (() => new Date());
        this.onLog = options.onLog;
        if (!Number.isInteger(this.pollIntervalMs) || this.pollIntervalMs <= 0) {
            throw new Error("cron scheduler pollIntervalMs must be a positive integer");
        }
        if (!Number.isInteger(this.maxRunHistory) || this.maxRunHistory <= 0) {
            throw new Error("cron scheduler maxRunHistory must be a positive integer");
        }
        this.tick();
        this.timer = setInterval(() => {
            this.tick();
        }, this.pollIntervalMs);
        this.timer.unref?.();
        this.log("info", "cron scheduler started", { jobCount: this.jobs.length });
    }
    listJobs() {
        return this.jobs.map((job) => ({
            id: job.id,
            projectId: job.config.projectId,
            schedule: job.config.schedule,
            prompt: job.config.prompt,
        }));
    }
    listRuns(options) {
        const jobId = options?.jobId?.trim();
        const limit = options?.limit;
        const filtered = [...this.runHistory]
            .reverse()
            .filter((run) => (jobId ? run.jobId === jobId : true));
        if (limit === undefined) {
            return filtered.map((run) => ({ ...run }));
        }
        if (!Number.isInteger(limit) || limit <= 0) {
            return [];
        }
        return filtered.slice(0, limit).map((run) => ({ ...run }));
    }
    async triggerJobNow(jobId) {
        const normalizedJobId = jobId.trim();
        const job = this.jobsById.get(normalizedJobId);
        if (!job) {
            throw new AsyncCronSchedulerError("not_found", `cron job '${normalizedJobId}' not found`);
        }
        this.log("info", "cron job triggered", {
            jobId: job.id,
            projectId: job.config.projectId,
            schedule: job.config.schedule,
            trigger: "manual",
        });
        return await this.runJob(job, { trigger: "manual" });
    }
    async close() {
        if (this.closed) {
            return;
        }
        this.closed = true;
        if (this.timer) {
            clearInterval(this.timer);
            this.timer = undefined;
        }
        this.log("info", "cron scheduler stopped");
    }
    tick() {
        if (this.closed || this.jobs.length === 0) {
            return;
        }
        const currentMinute = minuteBucket(this.now());
        if (this.lastCheckedMinute === undefined) {
            this.lastCheckedMinute = currentMinute - 1;
        }
        if (currentMinute < this.lastCheckedMinute) {
            this.log("warn", "system clock moved backwards, resetting cron cursor", {
                currentMinute,
                previousMinute: this.lastCheckedMinute,
            });
            this.lastCheckedMinute = currentMinute - 1;
        }
        for (let minute = (this.lastCheckedMinute ?? currentMinute) + 1; minute <= currentMinute; minute += 1) {
            const slotDate = new Date(minute * MILLISECONDS_PER_MINUTE);
            for (const job of this.jobs) {
                if (!matchesCronSchedule(job.schedule, slotDate)) {
                    continue;
                }
                this.log("info", "cron job triggered", {
                    jobId: job.id,
                    projectId: job.config.projectId,
                    schedule: job.config.schedule,
                    trigger: "scheduled",
                    scheduledFor: slotDate.toISOString(),
                });
                void this.runJob(job, {
                    trigger: "scheduled",
                    scheduledFor: slotDate.toISOString(),
                });
            }
        }
        this.lastCheckedMinute = currentMinute;
    }
    async runJob(job, trigger) {
        const triggeredAt = this.now().toISOString();
        const runId = `${triggeredAt}:${this.runSequence}`;
        this.runSequence += 1;
        try {
            const session = await this.sessionManager.createSession({
                projectId: job.config.projectId,
                prompt: job.config.prompt,
                ...(this.additionalSystemMessage
                    ? { additionalSystemMessage: this.additionalSystemMessage }
                    : {}),
            });
            const run = {
                id: runId,
                jobId: job.id,
                projectId: job.config.projectId,
                schedule: job.config.schedule,
                trigger: trigger.trigger,
                triggeredAt,
                ...(trigger.trigger === "scheduled" ? { scheduledFor: trigger.scheduledFor } : {}),
                status: "session-created",
                sessionId: session.id,
                sessionState: session.state,
            };
            this.pushRun(run);
            this.log("info", "cron job session created", {
                jobId: job.id,
                sessionId: session.id,
                projectId: job.config.projectId,
                state: session.state,
                trigger: trigger.trigger,
            });
            return { ...run };
        }
        catch (error) {
            const cause = error instanceof Error ? error.message : String(error);
            const code = readErrorCode(error);
            const run = {
                id: runId,
                jobId: job.id,
                projectId: job.config.projectId,
                schedule: job.config.schedule,
                trigger: trigger.trigger,
                triggeredAt,
                ...(trigger.trigger === "scheduled" ? { scheduledFor: trigger.scheduledFor } : {}),
                status: "failed",
                error: cause,
                ...(code ? { errorCode: code } : {}),
            };
            this.pushRun(run);
            this.log("error", "cron job failed to create session", {
                jobId: job.id,
                projectId: job.config.projectId,
                trigger: trigger.trigger,
                ...(code ? { code } : {}),
                cause,
            });
            return { ...run };
        }
    }
    pushRun(run) {
        this.runHistory.push(run);
        if (this.runHistory.length <= this.maxRunHistory) {
            return;
        }
        this.runHistory.splice(0, this.runHistory.length - this.maxRunHistory);
    }
    log(level, message, data) {
        this.onLog?.({
            timestamp: this.now().toISOString(),
            level,
            message,
            ...(data === undefined ? {} : { data }),
        });
    }
}
export function startAsyncCronScheduler(options) {
    return new AsyncCronSchedulerImpl(options);
}
//# sourceMappingURL=cron.js.map