"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseAutomationSchedule = parseAutomationSchedule;
exports.computeNextAutomationRun = computeNextAutomationRun;
exports.createAutomationJob = createAutomationJob;
exports.listAutomationJobs = listAutomationJobs;
exports.saveAutomationJobs = saveAutomationJobs;
exports.listDueAutomationJobs = listDueAutomationJobs;
exports.markAutomationJobRun = markAutomationJobRun;
exports.readAutomationRunLog = readAutomationRunLog;
exports.deleteAutomationJob = deleteAutomationJob;
exports.updateAutomationJob = updateAutomationJob;
exports.executeDueJobs = executeDueJobs;
const crypto = __importStar(require("crypto"));
const fs = __importStar(require("fs/promises"));
const path = __importStar(require("path"));
const atomicFile_1 = require("../persistence/atomicFile");
const runner_1 = require("./runner");
function parseAutomationSchedule(value, now = new Date()) {
    const schedule = value.trim();
    const lower = schedule.toLowerCase();
    if (!schedule)
        throw new Error('Schedule is required.');
    if (lower.startsWith('every ')) {
        const minutes = parseDurationMinutes(schedule.slice(6));
        return { kind: 'interval', minutes, display: `every ${minutes}m` };
    }
    if (/^(\d+\s*)?(m|min|mins|minute|minutes|h|hr|hrs|hour|hours|d|day|days)$/i.test(schedule) || /^\d+\s*(m|min|mins|minute|minutes|h|hr|hrs|hour|hours|d|day|days)$/i.test(schedule)) {
        const minutes = parseDurationMinutes(schedule);
        return { kind: 'once', runAt: new Date(now.getTime() + minutes * 60_000).toISOString(), display: `once in ${schedule}` };
    }
    const parts = schedule.split(/\s+/);
    if (parts.length >= 5 && parts.every((part) => /^[\d*,-/]+$/.test(part))) {
        return { kind: 'cron', expr: schedule, display: schedule };
    }
    const timestamp = Date.parse(schedule);
    if (Number.isFinite(timestamp)) {
        return { kind: 'once', runAt: new Date(timestamp).toISOString(), display: `once at ${new Date(timestamp).toISOString()}` };
    }
    throw new Error(`Invalid schedule: ${value}`);
}
function computeNextAutomationRun(schedule, lastRunAt, now = new Date()) {
    if (schedule.kind === 'once')
        return lastRunAt ? undefined : schedule.runAt;
    if (schedule.kind === 'interval') {
        const base = lastRunAt ? new Date(lastRunAt) : now;
        return new Date(base.getTime() + (schedule.minutes ?? 1) * 60_000).toISOString();
    }
    if (schedule.kind === 'cron' && schedule.expr)
        return computeNextCronRun(schedule.expr, now);
    return undefined;
}
async function createAutomationJob(projectDir, input, now = new Date()) {
    const schedule = parseAutomationSchedule(input.schedule, now);
    const job = {
        id: crypto.randomUUID(),
        name: input.name.trim() || 'Untitled automation',
        prompt: input.prompt,
        schedule,
        scriptCommand: input.scriptCommand?.trim() || undefined,
        createdAt: now.toISOString(),
        updatedAt: now.toISOString(),
        enabled: true,
        nextRunAt: computeNextAutomationRun(schedule, undefined, now),
    };
    await (0, atomicFile_1.withFileLock)(jobsPath(projectDir), async () => {
        const jobs = await listAutomationJobs(projectDir);
        jobs.push(job);
        await writeJobsFileUnlocked(projectDir, jobs);
    });
    return job;
}
async function listAutomationJobs(projectDir) {
    try {
        const raw = await fs.readFile(jobsPath(projectDir), 'utf-8');
        const parsed = JSON.parse(raw);
        return Array.isArray(parsed.jobs) ? parsed.jobs : [];
    }
    catch {
        return [];
    }
}
async function saveAutomationJobs(projectDir, jobs) {
    await (0, atomicFile_1.withFileLock)(jobsPath(projectDir), () => writeJobsFileUnlocked(projectDir, jobs));
}
// Internal: write the jobs file without taking the lock. Callers must
// already hold the lock for jobsPath(projectDir).
async function writeJobsFileUnlocked(projectDir, jobs) {
    const filePath = jobsPath(projectDir);
    await (0, atomicFile_1.atomicWriteFile)(filePath, JSON.stringify({ jobs }, null, 2));
}
async function listDueAutomationJobs(projectDir, now = new Date()) {
    const jobs = await listAutomationJobs(projectDir);
    return jobs.filter((job) => job.enabled && job.nextRunAt !== undefined && Date.parse(job.nextRunAt) <= now.getTime());
}
async function markAutomationJobRun(projectDir, jobId, update = {}, now = new Date()) {
    const updated = await (0, atomicFile_1.withFileLock)(jobsPath(projectDir), async () => {
        const jobs = await listAutomationJobs(projectDir);
        const jobIndex = jobs.findIndex((job) => job.id === jobId);
        if (jobIndex === -1)
            throw new Error(`Automation job not found: ${jobId}`);
        const existing = jobs[jobIndex];
        const lastRunAt = now.toISOString();
        const next = {
            ...existing,
            lastRunAt,
            updatedAt: lastRunAt,
            nextRunAt: computeNextAutomationRun(existing.schedule, lastRunAt, now),
        };
        if (existing.schedule.kind === 'once')
            next.enabled = false;
        jobs[jobIndex] = next;
        await writeJobsFileUnlocked(projectDir, jobs);
        return next;
    });
    // The run log is a separate append-only file; it's safe to write
    // outside the jobs.json lock.
    await appendAutomationRunLog(projectDir, updated, update, now);
    return updated;
}
function parseDurationMinutes(value) {
    const match = value.trim().toLowerCase().match(/^(\d+)\s*(m|min|mins|minute|minutes|h|hr|hrs|hour|hours|d|day|days)$/);
    if (!match)
        throw new Error(`Invalid duration: ${value}`);
    const amount = Number(match[1]);
    const unit = match[2][0];
    if (unit === 'h')
        return amount * 60;
    if (unit === 'd')
        return amount * 1440;
    return amount;
}
async function appendAutomationRunLog(projectDir, job, update, now) {
    const logPath = automationRunLogPath(projectDir);
    await fs.mkdir(path.dirname(logPath), { recursive: true });
    await fs.appendFile(logPath, JSON.stringify({ jobId: job.id, name: job.name, ranAt: now.toISOString(), ...update }) + '\n', 'utf-8');
}
async function readAutomationRunLog(projectDir, limit = 50) {
    try {
        const raw = await fs.readFile(automationRunLogPath(projectDir), 'utf-8');
        return raw.split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line)).slice(-limit).reverse();
    }
    catch {
        return [];
    }
}
function automationRunLogPath(projectDir) {
    return path.join(projectDir, '.harness', 'automations', 'runs.jsonl');
}
function computeNextCronRun(expr, now) {
    const parts = expr.trim().split(/\s+/).slice(0, 5);
    if (parts.length !== 5)
        return undefined;
    const start = new Date(now.getTime());
    start.setUTCSeconds(0, 0);
    for (let offset = 1; offset <= 525_600; offset++) {
        const candidate = new Date(start.getTime() + offset * 60_000);
        if (cronPartMatches(parts[0], candidate.getUTCMinutes(), 0, 59)
            && cronPartMatches(parts[1], candidate.getUTCHours(), 0, 23)
            && cronPartMatches(parts[2], candidate.getUTCDate(), 1, 31)
            && cronPartMatches(parts[3], candidate.getUTCMonth() + 1, 1, 12)
            && cronPartMatches(parts[4], candidate.getUTCDay(), 0, 6)) {
            return candidate.toISOString();
        }
    }
    return undefined;
}
function cronPartMatches(part, value, min, max) {
    return part.split(',').some((segment) => cronSegmentMatches(segment, value, min, max));
}
function cronSegmentMatches(segment, value, min, max) {
    const [rangePart, stepPart] = segment.split('/');
    const step = stepPart ? Number(stepPart) : 1;
    if (!Number.isInteger(step) || step < 1)
        return false;
    let rangeMin = min;
    let rangeMax = max;
    if (rangePart !== '*') {
        const rangeMatch = rangePart.match(/^(\d+)(?:-(\d+))?$/);
        if (!rangeMatch)
            return false;
        rangeMin = Number(rangeMatch[1]);
        rangeMax = rangeMatch[2] ? Number(rangeMatch[2]) : rangeMin;
    }
    if (rangeMin < min || rangeMax > max || value < rangeMin || value > rangeMax)
        return false;
    return (value - rangeMin) % step === 0;
}
function jobsPath(projectDir) {
    return path.join(projectDir, '.harness', 'automations', 'jobs.json');
}
async function deleteAutomationJob(projectDir, jobId) {
    return (0, atomicFile_1.withFileLock)(jobsPath(projectDir), async () => {
        const jobs = await listAutomationJobs(projectDir);
        const filtered = jobs.filter((job) => job.id !== jobId);
        if (filtered.length === jobs.length)
            return false;
        await writeJobsFileUnlocked(projectDir, filtered);
        return true;
    });
}
async function updateAutomationJob(projectDir, jobId, input, now = new Date()) {
    return (0, atomicFile_1.withFileLock)(jobsPath(projectDir), async () => {
        const jobs = await listAutomationJobs(projectDir);
        const index = jobs.findIndex((job) => job.id === jobId);
        if (index === -1)
            return null;
        const existing = jobs[index];
        const updated = { ...existing, updatedAt: now.toISOString() };
        if (input.enabled !== undefined)
            updated.enabled = input.enabled;
        if (typeof input.name === 'string' && input.name.trim())
            updated.name = input.name.trim();
        if (typeof input.prompt === 'string' && input.prompt.trim())
            updated.prompt = input.prompt.trim();
        if (typeof input.schedule === 'string' && input.schedule.trim()) {
            updated.schedule = parseAutomationSchedule(input.schedule.trim(), now);
            updated.nextRunAt = computeNextAutomationRun(updated.schedule, updated.lastRunAt, now);
        }
        if (input.scriptCommand === null)
            updated.scriptCommand = undefined;
        else if (typeof input.scriptCommand === 'string')
            updated.scriptCommand = input.scriptCommand.trim() || undefined;
        jobs[index] = updated;
        await writeJobsFileUnlocked(projectDir, jobs);
        return updated;
    });
}
async function executeDueJobs(projectDir, policy = {}, now = new Date()) {
    const due = await listDueAutomationJobs(projectDir, now);
    const results = [];
    for (const job of due) {
        const run = await (0, runner_1.prepareAutomationRun)(projectDir, job, now, policy);
        const markedJob = await markAutomationJobRun(projectDir, job.id, { success: true, outputPath: run.outputPath }, now);
        results.push({ jobId: job.id, name: job.name, run, markedJob });
    }
    return results;
}
//# sourceMappingURL=jobs.js.map