"use strict";
// =============================================================================
// Pure Agent - Cron Job Management
// Ported from Hermes cron/jobs.py
// =============================================================================
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.parseDuration = parseDuration;
exports.parseSchedule = parseSchedule;
exports.computeNextRun = computeNextRun;
exports.loadJobs = loadJobs;
exports.saveJobs = saveJobs;
exports.createJob = createJob;
exports.getJob = getJob;
exports.resolveJobRef = resolveJobRef;
exports.listJobs = listJobs;
exports.updateJob = updateJob;
exports.pauseJob = pauseJob;
exports.resumeJob = resumeJob;
exports.triggerJob = triggerJob;
exports.removeJob = removeJob;
exports.markJobRun = markJobRun;
exports.advanceNextRun = advanceNextRun;
exports.getDueJobs = getDueJobs;
exports.saveJobOutput = saveJobOutput;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const crypto = __importStar(require("crypto"));
const constants_1 = require("../constants");
const utils_1 = require("../utils");
const logger_1 = require("../logger");
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
function getCronDir() { return path.join((0, constants_1.getPureAgentHome)(), 'cron'); }
function getJobsFile() { return path.join(getCronDir(), 'jobs.json'); }
function getOutputDir() { return path.join(getCronDir(), 'output'); }
const IMMUTABLE_FIELDS = new Set(['id']);
// ---------------------------------------------------------------------------
// Schedule parsing
// ---------------------------------------------------------------------------
function parseDuration(s) {
    const match = s.match(/^(\d+)\s*(m|min|h|hr|hour|d|day|day)s?$/i);
    if (!match)
        return 0;
    const n = parseInt(match[1], 10);
    const unit = match[2].toLowerCase();
    if (unit.startsWith('m'))
        return n;
    if (unit.startsWith('h'))
        return n * 60;
    if (unit.startsWith('d'))
        return n * 1440;
    return 0;
}
function parseSchedule(scheduleStr) {
    const trimmed = scheduleStr.trim();
    // ISO datetime (once)
    const isoMatch = trimmed.match(/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2})?)?$/);
    if (isoMatch) {
        const dt = new Date(trimmed.length === 10 ? trimmed + 'T00:00:00' : trimmed);
        return { kind: 'once', value: trimmed, runAt: dt.toISOString() };
    }
    // Duration (once: "30m", "2h", "1d")
    const durMinutes = parseDuration(trimmed);
    if (durMinutes > 0 && !trimmed.toLowerCase().startsWith('every')) {
        const runAt = new Date(Date.now() + durMinutes * 60000);
        return { kind: 'once', value: trimmed, runAt: runAt.toISOString() };
    }
    // Interval: "every 30m", "every 2h"
    const everyMatch = trimmed.match(/^every\s+(.+)$/i);
    if (everyMatch) {
        const intervalMinutes = parseDuration(everyMatch[1]);
        if (intervalMinutes > 0) {
            return { kind: 'interval', value: trimmed, intervalMs: intervalMinutes * 60000 };
        }
    }
    // Cron expression: "0 9 * * *", "*/5 * * * *"
    if (trimmed.includes('*') || trimmed.match(/^\d+\s+\d+/)) {
        return { kind: 'cron', value: trimmed, cronExpr: trimmed };
    }
    // Fallback: treat as one-time, 1 minute from now
    return { kind: 'once', value: trimmed, runAt: new Date(Date.now() + 60000).toISOString() };
}
function computeNextRun(schedule, lastRunAt) {
    const now = new Date();
    switch (schedule.kind) {
        case 'once': {
            if (lastRunAt)
                return null; // Already ran
            return schedule.runAt || new Date(Date.now() + 60000).toISOString();
        }
        case 'interval': {
            if (!schedule.intervalMs)
                return null;
            if (lastRunAt) {
                const next = new Date(new Date(lastRunAt).getTime() + schedule.intervalMs);
                return next > now ? next.toISOString() : now.toISOString();
            }
            return new Date(Date.now() + schedule.intervalMs).toISOString();
        }
        case 'cron': {
            if (!schedule.cronExpr)
                return null;
            // Simple cron evaluation (minute, hour, dom, month, dow)
            try {
                return evaluateCronExpression(schedule.cronExpr, lastRunAt ? new Date(lastRunAt) : now);
            }
            catch {
                return null;
            }
        }
        default:
            return null;
    }
}
// Simple cron expression evaluator
function evaluateCronExpression(expr, after) {
    const parts = expr.split(/\s+/);
    if (parts.length !== 5)
        return null;
    const [minExpr, hourExpr, domExpr, monthExpr, dowExpr] = parts;
    // Try up to 366 days into the future
    const candidate = new Date(after.getTime() + 60000); // Start 1 minute after
    candidate.setSeconds(0, 0);
    for (let i = 0; i < 525960; i++) { // ~1 year of minutes
        if (matchCronField(minExpr, candidate.getMinutes()) &&
            matchCronField(hourExpr, candidate.getHours()) &&
            matchCronField(domExpr, candidate.getDate()) &&
            matchCronField(monthExpr, candidate.getMonth() + 1) &&
            matchCronField(dowExpr, candidate.getDay())) {
            return candidate.toISOString();
        }
        candidate.setTime(candidate.getTime() + 60000);
    }
    return null;
}
function matchCronField(expr, value) {
    if (expr === '*')
        return true;
    // Handle */N (step)
    const stepMatch = expr.match(/^\*\/(\d+)$/);
    if (stepMatch)
        return value % parseInt(stepMatch[1], 10) === 0;
    // Handle comma-separated values
    if (expr.includes(',')) {
        return expr.split(',').some(v => matchCronField(v.trim(), value));
    }
    // Handle range: N-M
    const rangeMatch = expr.match(/^(\d+)-(\d+)$/);
    if (rangeMatch) {
        const lo = parseInt(rangeMatch[1], 10);
        const hi = parseInt(rangeMatch[2], 10);
        return value >= lo && value <= hi;
    }
    // Handle range with step: N-M/S
    const rangeStepMatch = expr.match(/^(\d+)-(\d+)\/(\d+)$/);
    if (rangeStepMatch) {
        const lo = parseInt(rangeStepMatch[1], 10);
        const hi = parseInt(rangeStepMatch[2], 10);
        const step = parseInt(rangeStepMatch[3], 10);
        return value >= lo && value <= hi && (value - lo) % step === 0;
    }
    // Literal number
    return parseInt(expr, 10) === value;
}
function formatScheduleDisplay(schedule) {
    switch (schedule.kind) {
        case 'once': return `Once at ${schedule.runAt || 'unknown'}`;
        case 'interval': return `Every ${Math.round((schedule.intervalMs || 0) / 60000)} minutes`;
        case 'cron': return `Cron: ${schedule.cronExpr}`;
        default: return 'Unknown';
    }
}
// ---------------------------------------------------------------------------
// Job CRUD
// ---------------------------------------------------------------------------
function ensureDirs() {
    for (const dir of [getCronDir(), getOutputDir()]) {
        if (!fs.existsSync(dir))
            fs.mkdirSync(dir, { recursive: true });
    }
}
function loadJobs() {
    ensureDirs();
    const filePath = getJobsFile();
    try {
        if (!fs.existsSync(filePath))
            return [];
        const raw = fs.readFileSync(filePath, 'utf-8').trim();
        if (!raw)
            return [];
        const parsed = JSON.parse(raw);
        return Array.isArray(parsed) ? parsed : [];
    }
    catch (err) {
        (0, logger_1.getLogger)().warn(`Failed to load cron jobs: ${err.message}`);
        return [];
    }
}
function saveJobs(jobs) {
    ensureDirs();
    (0, utils_1.atomicJsonWrite)(getJobsFile(), jobs, 2);
}
function generateJobId() {
    return crypto.randomBytes(6).toString('hex');
}
function createJob(options) {
    const schedule = parseSchedule(options.schedule);
    const nextRun = computeNextRun(schedule);
    const id = generateJobId();
    const job = {
        id,
        name: options.name || options.prompt?.slice(0, 60) || id,
        prompt: options.prompt || '',
        skills: options.skills || [],
        skill: options.skills?.[0] || null,
        model: options.model || null,
        provider: null,
        base_url: null,
        script: options.script || null,
        no_agent: options.no_agent || false,
        context_from: null,
        schedule,
        schedule_display: formatScheduleDisplay(schedule),
        repeat: { times: options.repeat ?? null, completed: 0 },
        enabled: true,
        state: 'scheduled',
        paused_at: null,
        paused_reason: null,
        created_at: new Date().toISOString(),
        next_run_at: nextRun,
        last_run_at: null,
        last_status: null,
        last_error: null,
        last_delivery_error: null,
        deliver: options.deliver || 'local',
        origin: null,
        enabled_toolsets: null,
        workdir: options.workdir || null,
        profile: null,
    };
    const jobs = loadJobs();
    jobs.push(job);
    saveJobs(jobs);
    (0, logger_1.getLogger)().info(`Created cron job ${id}: ${job.name} (${job.schedule_display})`);
    return job;
}
function getJob(jobId) {
    const jobs = loadJobs();
    return jobs.find(j => j.id === jobId) || null;
}
function resolveJobRef(ref) {
    const jobs = loadJobs();
    // Try exact ID match first
    const byId = jobs.find(j => j.id === ref);
    if (byId)
        return byId;
    // Try name match (case-insensitive)
    const byName = jobs.filter(j => j.name.toLowerCase() === ref.toLowerCase());
    if (byName.length === 1)
        return byName[0];
    if (byName.length > 1) {
        (0, logger_1.getLogger)().warn(`Ambiguous job reference "${ref}" matches ${byName.length} jobs`);
        return byName[0];
    }
    return null;
}
function listJobs(includeDisabled = false) {
    const jobs = loadJobs();
    return includeDisabled ? jobs : jobs.filter(j => j.enabled);
}
function updateJob(jobId, updates) {
    const jobs = loadJobs();
    const idx = jobs.findIndex(j => j.id === jobId);
    if (idx < 0)
        return null;
    // Block immutable fields
    for (const key of IMMUTABLE_FIELDS) {
        if (key in updates)
            delete updates[key];
    }
    Object.assign(jobs[idx], updates);
    // Refresh schedule if changed
    if (updates.schedule || updates.next_run_at === undefined) {
        const schedule = jobs[idx].schedule;
        jobs[idx].next_run_at = computeNextRun(schedule, jobs[idx].last_run_at);
        jobs[idx].schedule_display = formatScheduleDisplay(schedule);
    }
    saveJobs(jobs);
    return jobs[idx];
}
function pauseJob(jobId, reason) {
    const ref = resolveJobRef(jobId);
    if (!ref)
        return null;
    return updateJob(ref.id, {
        enabled: false,
        state: 'paused',
        paused_at: new Date().toISOString(),
        paused_reason: reason || 'Paused by user',
    });
}
function resumeJob(jobId) {
    const ref = resolveJobRef(jobId);
    if (!ref)
        return null;
    const nextRun = computeNextRun(ref.schedule, ref.last_run_at);
    return updateJob(ref.id, {
        enabled: true,
        state: 'scheduled',
        paused_at: null,
        paused_reason: null,
        next_run_at: nextRun,
    });
}
function triggerJob(jobId) {
    const ref = resolveJobRef(jobId);
    if (!ref)
        return null;
    return updateJob(ref.id, { next_run_at: new Date().toISOString() });
}
function removeJob(jobId) {
    const ref = resolveJobRef(jobId);
    if (!ref)
        return false;
    const jobs = loadJobs();
    const filtered = jobs.filter(j => j.id !== ref.id);
    if (filtered.length === jobs.length)
        return false;
    saveJobs(filtered);
    // Cleanup output directory
    const outputDir = path.join(getOutputDir(), ref.id);
    try {
        if (fs.existsSync(outputDir))
            fs.rmSync(outputDir, { recursive: true, force: true });
    }
    catch { /* ignore */ }
    (0, logger_1.getLogger)().info(`Removed cron job ${ref.id}: ${ref.name}`);
    return true;
}
// ---------------------------------------------------------------------------
// Job execution tracking
// ---------------------------------------------------------------------------
function markJobRun(jobId, success, error, deliveryError) {
    const jobs = loadJobs();
    const job = jobs.find(j => j.id === jobId);
    if (!job)
        return;
    job.last_run_at = new Date().toISOString();
    job.last_status = success ? 'ok' : 'error';
    job.last_error = error || null;
    job.last_delivery_error = deliveryError || null;
    job.repeat.completed++;
    // Compute next run
    if (job.repeat.times !== null && job.repeat.completed >= job.repeat.times) {
        job.state = 'completed';
        job.enabled = false;
        job.next_run_at = null;
    }
    else {
        job.next_run_at = computeNextRun(job.schedule, job.last_run_at);
    }
    saveJobs(jobs);
}
function advanceNextRun(jobId) {
    const jobs = loadJobs();
    const job = jobs.find(j => j.id === jobId);
    if (!job || !job.next_run_at)
        return false;
    // Move next_run forward before execution to prevent double-run
    job.next_run_at = computeNextRun(job.schedule, job.last_run_at);
    saveJobs(jobs);
    return true;
}
function getDueJobs() {
    const jobs = loadJobs();
    const now = new Date();
    return jobs.filter(j => {
        if (!j.enabled)
            return false;
        if (j.state === 'paused' || j.state === 'completed')
            return false;
        if (!j.next_run_at)
            return false;
        return new Date(j.next_run_at) <= now;
    });
}
// ---------------------------------------------------------------------------
// Job output
// ---------------------------------------------------------------------------
function saveJobOutput(jobId, output) {
    const dir = path.join(getOutputDir(), jobId);
    if (!fs.existsSync(dir))
        fs.mkdirSync(dir, { recursive: true });
    const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
    const filePath = path.join(dir, `${timestamp}.md`);
    fs.writeFileSync(filePath, output, 'utf-8');
}
//# sourceMappingURL=jobs.js.map