/**
 * Cron tool — manage scheduled jobs via the agent.
 *
 * Actions: status, list, add, update, remove, run, history, daemon_start, daemon_stop.
 * All actions read/write jobs.json directly (no daemon needed for CRUD).
 */
import { Type } from '@sinclair/typebox';
import { SHORTCUT_MODEL_REF } from '../../model-ids.js';
import { CRON } from '../../tool-names.js';
import { getDaemonStatus, isDaemonRunning, startDaemon, stopDaemon } from '../cron/daemon-ipc.js';
import { applyJobPatch, createJob, normalizeJobState } from '../cron/jobs.js';
import { readRunLog } from '../cron/run-log.js';
import { computeNextRunAtMs } from '../cron/schedule.js';
import { getCronStorePath, loadCronStore, saveCronStore } from '../cron/store.js';
// ---------------------------------------------------------------------------
// Schema
// ---------------------------------------------------------------------------
const ScheduleSchema = Type.Union([
    Type.Object({
        kind: Type.Literal('at'),
        at: Type.String({ description: 'ISO-8601 timestamp, e.g. "2026-03-06T17:00:00Z"' })
    }),
    Type.Object({
        kind: Type.Literal('every'),
        everyMs: Type.Number({ description: 'Interval in milliseconds, e.g. 1800000 for 30 minutes' })
    }),
    Type.Object({
        kind: Type.Literal('cron'),
        expr: Type.String({ description: 'Cron expression, e.g. "0 8 * * *" for daily at 8am' }),
        tz: Type.Optional(Type.String({
            description: 'IANA timezone, e.g. "America/New_York". Defaults to system timezone.'
        }))
    })
]);
const JobInputSchema = Type.Object({
    name: Type.String({ description: 'Short name for the job' }),
    schedule: ScheduleSchema,
    payload: Type.Object({
        message: Type.String({ description: 'The prompt/instruction for the agent to execute' }),
        model: Type.Optional(Type.String({ description: `Model override, e.g. "${SHORTCUT_MODEL_REF.ClaudeOpus48}"` }))
    }),
    enabled: Type.Optional(Type.Boolean({ description: 'Whether the job is enabled (default: true)' })),
    description: Type.Optional(Type.String({ description: 'Longer description of what the job does' }))
});
const PatchSchema = Type.Object({
    name: Type.Optional(Type.String()),
    description: Type.Optional(Type.String()),
    enabled: Type.Optional(Type.Boolean()),
    schedule: Type.Optional(ScheduleSchema),
    payload: Type.Optional(Type.Object({
        message: Type.Optional(Type.String()),
        model: Type.Optional(Type.String())
    }))
});
const schema = Type.Object({
    action: Type.Union([
        Type.Literal('list'),
        Type.Literal('add'),
        Type.Literal('update'),
        Type.Literal('remove'),
        Type.Literal('run'),
        Type.Literal('status'),
        Type.Literal('history'),
        Type.Literal('daemon_start'),
        Type.Literal('daemon_stop')
    ], { description: 'The action to perform' }),
    job: Type.Optional(JobInputSchema),
    jobId: Type.Optional(Type.String({ description: 'Job ID for update/remove/run' })),
    patch: Type.Optional(PatchSchema),
    includeDisabled: Type.Optional(Type.Boolean({ description: 'Include disabled jobs in list (default: false)' })),
    limit: Type.Optional(Type.Number({ description: 'Max entries to return for history (default: 5)' }))
});
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/** Render a schedule as a human-readable summary. */
function formatSchedule(schedule) {
    if (schedule.kind === 'at')
        return `at ${schedule.at}`;
    if (schedule.kind === 'every')
        return `every ${schedule.everyMs}ms`;
    return `cron "${schedule.expr}"${schedule.tz ? ` (${schedule.tz})` : ''}`;
}
/** Render a single cron job as a multi-line summary. */
function formatJob(job) {
    const status = job.enabled ? 'enabled' : 'disabled';
    const schedule = formatSchedule(job.schedule);
    const next = job.state.nextRunAtMs ? new Date(job.state.nextRunAtMs).toISOString() : 'none';
    const lastStatus = job.state.lastRunStatus ?? 'never run';
    const lastError = job.state.lastError ? ` — ${job.state.lastError}` : '';
    return [
        `  id: ${job.id}`,
        `  name: ${job.name}`,
        job.description ? `  description: ${job.description}` : null,
        `  status: ${status}`,
        `  schedule: ${schedule}`,
        `  next run: ${next}`,
        `  last: ${lastStatus}${lastError}`,
        `  payload: ${job.payload.message}`
    ]
        .filter(Boolean)
        .join('\n');
}
/** Try to recover flat params that weaker models produce (e.g. top-level name/schedule instead of nested job). */
function recoverFlatParams(params) {
    const p = { ...params };
    if (p.action === 'add' && !p.job && typeof p.name === 'string') {
        p.job = {
            name: p.name,
            schedule: p.schedule,
            payload: (p.payload ?? { message: p.message }),
            enabled: p.enabled,
            description: p.description
        };
    }
    if (p.action === 'update' && !p.jobId && typeof p.id === 'string') {
        p.jobId = p.id;
        // Recover flat patch fields
        if (!p.patch) {
            const patch = {};
            if (typeof p.name === 'string')
                patch.name = p.name;
            if (typeof p.description === 'string')
                patch.description = p.description;
            if (typeof p.enabled === 'boolean')
                patch.enabled = p.enabled;
            if (p.schedule)
                patch.schedule = p.schedule;
            if (Object.keys(patch).length > 0)
                p.patch = patch;
        }
    }
    if ((p.action === 'remove' || p.action === 'run') && !p.jobId && typeof p.id === 'string') {
        p.jobId = p.id;
    }
    return p;
}
// ---------------------------------------------------------------------------
// Action handlers
// ---------------------------------------------------------------------------
/** Handle the `status` action — daemon state plus job counts and next fire time. */
async function handleStatus(storePath) {
    const store = await loadCronStore(storePath);
    const now = Date.now();
    const enabled = store.jobs.filter((j) => j.enabled);
    const nextRun = enabled
        .map((j) => j.state.nextRunAtMs)
        .filter((ms) => typeof ms === 'number')
        .sort((a, b) => a - b)[0];
    const running = store.jobs.filter((j) => typeof j.state.runningAtMs === 'number');
    const daemon = getDaemonStatus();
    const lines = [
        `Daemon: ${daemon.running ? `running (pid ${daemon.pid})` : 'stopped'}`,
        `Jobs: ${enabled.length} enabled, ${store.jobs.length - enabled.length} disabled`
    ];
    if (nextRun) {
        const delta = Math.max(0, nextRun - now);
        const mins = Math.round(delta / 60_000);
        lines.push(`Next run: ${new Date(nextRun).toISOString()} (in ~${mins}m)`);
    }
    if (running.length > 0) {
        lines.push(`Running: ${running.length} job(s)`);
    }
    if (!daemon.running && enabled.length > 0) {
        lines.push('\nNote: daemon is not running. Use daemon_start to start it.');
    }
    return lines.join('\n');
}
/** Handle the `history` action — render recent run-log entries for a job. */
async function handleHistory(storePath, jobId, limit) {
    // Resolve job name for display
    const store = await loadCronStore(storePath);
    const job = store.jobs.find((j) => j.id === jobId);
    const jobLabel = job ? `"${job.name}" (${jobId})` : jobId;
    const entries = await readRunLog(jobId, limit);
    if (entries.length === 0) {
        return `No run history for ${jobLabel}.`;
    }
    const lines = [`Run history for ${jobLabel} (${entries.length} most recent):\n`];
    for (const entry of entries) {
        const date = new Date(entry.startedAtMs).toISOString();
        const duration = `${entry.durationMs}ms`;
        const status = entry.status === 'ok' ? '✓' : '✗';
        lines.push(`${status} ${date} — ${entry.status} (${duration})`);
        if (entry.error) {
            lines.push(`  error: ${entry.error}`);
        }
        if (entry.output) {
            // Show first ~500 chars of output inline
            const preview = entry.output.length > 500 ? entry.output.slice(0, 500) + '...' : entry.output;
            lines.push(`  output: ${preview}`);
        }
    }
    return lines.join('\n');
}
/** Handle the `daemon_start` action — start the cron daemon if not already running. */
async function handleDaemonStart() {
    const { pid, alreadyRunning } = await startDaemon();
    if (alreadyRunning) {
        return `Cron daemon is already running (pid ${pid}).`;
    }
    return `Cron daemon started (pid ${pid}).`;
}
/** Handle the `daemon_stop` action — stop the cron daemon. */
async function handleDaemonStop() {
    const stopped = await stopDaemon();
    return stopped ? 'Cron daemon stopped.' : 'Cron daemon was not running.';
}
/** Handle the `list` action — render enabled (or all) jobs after normalizing state. */
async function handleList(storePath, includeDisabled) {
    const store = await loadCronStore(storePath);
    const now = Date.now();
    let storeChanged = false;
    for (const j of store.jobs) {
        if (normalizeJobState(j, now))
            storeChanged = true;
    }
    if (storeChanged)
        await saveCronStore(storePath, store);
    const jobs = includeDisabled ? store.jobs : store.jobs.filter((j) => j.enabled);
    if (jobs.length === 0) {
        return includeDisabled
            ? 'No cron jobs.'
            : 'No enabled cron jobs. Use includeDisabled to see all.';
    }
    return jobs.map((j) => formatJob(j)).join('\n\n');
}
/** Handle the `add` action — validate the schedule and persist a new job. */
async function handleAdd(storePath, input) {
    const now = Date.now();
    // Validate schedule before creating
    try {
        const nextRun = computeNextRunAtMs(input.schedule, now);
        if (nextRun === undefined && input.schedule.kind === 'at') {
            return 'Error: the "at" timestamp is in the past. Use a future timestamp.';
        }
    }
    catch (err) {
        return `Error: invalid schedule — ${err instanceof Error ? err.message : String(err)}`;
    }
    const jobCreate = {
        name: input.name,
        schedule: input.schedule,
        payload: input.payload,
        enabled: input.enabled ?? true,
        description: input.description
    };
    const store = await loadCronStore(storePath);
    const job = createJob(jobCreate, now);
    store.jobs.push(job);
    await saveCronStore(storePath, store);
    let result = `Created job "${job.name}" (${job.id})\nSchedule: ${formatSchedule(job.schedule)}\nNext run: ${job.state.nextRunAtMs ? new Date(job.state.nextRunAtMs).toISOString() : 'none'}`;
    // Auto-start daemon if not running
    if (job.enabled && !isDaemonRunning()) {
        try {
            const { pid } = await startDaemon();
            result += `\nDaemon started (pid ${pid}).`;
        }
        catch {
            result += '\nNote: could not auto-start daemon. Use daemon_start manually.';
        }
    }
    return result;
}
/** Handle the `update` action — apply a partial patch to an existing job. */
async function handleUpdate(storePath, jobId, patch) {
    const store = await loadCronStore(storePath);
    const job = store.jobs.find((j) => j.id === jobId);
    if (!job)
        return `Error: no job with id "${jobId}". Use the list action to see available jobs.`;
    const now = Date.now();
    // Validate new schedule if provided
    if (patch.schedule) {
        try {
            const nextRun = computeNextRunAtMs(patch.schedule, now);
            if (nextRun === undefined && patch.schedule.kind === 'at') {
                return 'Error: the "at" timestamp is in the past. Use a future timestamp.';
            }
        }
        catch (err) {
            return `Error: invalid schedule — ${err instanceof Error ? err.message : String(err)}`;
        }
    }
    applyJobPatch(job, patch, now);
    await saveCronStore(storePath, store);
    return `Updated job "${job.name}" (${job.id})\n${formatJob(job)}`;
}
/** Handle the `remove` action — delete a job from the store. */
async function handleRemove(storePath, jobId) {
    const store = await loadCronStore(storePath);
    const idx = store.jobs.findIndex((j) => j.id === jobId);
    if (idx === -1)
        return `Error: no job with id "${jobId}".`;
    const removed = store.jobs.splice(idx, 1)[0];
    await saveCronStore(storePath, store);
    return `Removed job "${removed.name}" (${removed.id}).`;
}
/** Handle the `run` action — force a job to run on the next daemon tick. */
async function handleRun(storePath, jobId) {
    const store = await loadCronStore(storePath);
    const job = store.jobs.find((j) => j.id === jobId);
    if (!job)
        return `Error: no job with id "${jobId}".`;
    if (!job.enabled)
        return `Error: job "${job.name}" is disabled. Enable it first.`;
    if (typeof job.state.runningAtMs === 'number')
        return `Job "${job.name}" is already running.`;
    const now = Date.now();
    // Mark as due so the daemon picks it up on next tick
    job.state.runningAtMs = undefined;
    job.state.nextRunAtMs = now - 1; // Force due
    await saveCronStore(storePath, store);
    if (!isDaemonRunning()) {
        return `Triggered job "${job.name}" (${job.id}), but the daemon is not running. Use daemon_start first.`;
    }
    return `Triggered job "${job.name}" (${job.id}). It will execute on the next daemon tick.`;
}
// ---------------------------------------------------------------------------
// Tool description
// ---------------------------------------------------------------------------
const TOOL_DESCRIPTION = `\
Manage cron jobs — scheduled tasks that run automatically.

**Actions:**
- \`status\` — Show job count and next fire time
- \`list\` — List jobs (set includeDisabled=true to see disabled ones)
- \`add\` — Create a new scheduled job
- \`update\` — Modify an existing job (requires jobId + patch)
- \`remove\` — Delete a job (requires jobId)
- \`run\` — Force a job to run on the next daemon tick (requires jobId)
- \`history\` — View recent run results with output (requires jobId, optional limit)
- \`daemon_start\` — Start the background scheduler
- \`daemon_stop\` — Stop the scheduler

**Schedule types:**
- \`at\`: one-shot at a specific time — \`{ kind: "at", at: "2026-03-06T17:00:00Z" }\`
- \`every\`: recurring interval — \`{ kind: "every", everyMs: 1800000 }\` (30 minutes)
- \`cron\`: cron expression — \`{ kind: "cron", expr: "0 8 * * *" }\` (daily at 8am)

**Examples:**
- "every morning at 8am" → schedule: \`{ kind: "cron", expr: "0 8 * * *" }\`
- "every 30 minutes" → schedule: \`{ kind: "every", everyMs: 1800000 }\`
- "at 5pm today" → schedule: \`{ kind: "at", at: "<today>T17:00:00Z" }\` (use actual date)
- "every weekday at 9am ET" → schedule: \`{ kind: "cron", expr: "0 9 * * 1-5", tz: "America/New_York" }\`
`;
// ---------------------------------------------------------------------------
// Factory
// ---------------------------------------------------------------------------
/** Build the `cron` tool that exposes scheduled-job CRUD and daemon control. */
export function createCronTool(storePath = getCronStorePath()) {
    return {
        name: CRON,
        label: 'Cron',
        description: TOOL_DESCRIPTION,
        parameters: schema,
        async execute(_toolCallId, rawParams, _signal, _onUpdate, _ctx) {
            const params = recoverFlatParams(rawParams);
            let text;
            switch (params.action) {
                case 'status':
                    text = await handleStatus(storePath);
                    break;
                case 'list':
                    text = await handleList(storePath, params.includeDisabled ?? false);
                    break;
                case 'add': {
                    if (!params.job) {
                        text = 'Error: "add" action requires a "job" object with name, schedule, and payload.';
                        break;
                    }
                    text = await handleAdd(storePath, params.job);
                    break;
                }
                case 'update': {
                    if (!params.jobId) {
                        text = 'Error: "update" action requires "jobId".';
                        break;
                    }
                    if (!params.patch) {
                        text = 'Error: "update" action requires a "patch" object.';
                        break;
                    }
                    text = await handleUpdate(storePath, params.jobId, params.patch);
                    break;
                }
                case 'remove': {
                    if (!params.jobId) {
                        text = 'Error: "remove" action requires "jobId".';
                        break;
                    }
                    text = await handleRemove(storePath, params.jobId);
                    break;
                }
                case 'run': {
                    if (!params.jobId) {
                        text = 'Error: "run" action requires "jobId".';
                        break;
                    }
                    text = await handleRun(storePath, params.jobId);
                    break;
                }
                case 'history': {
                    if (!params.jobId) {
                        text = 'Error: "history" action requires "jobId".';
                        break;
                    }
                    text = await handleHistory(storePath, params.jobId, params.limit ?? 5);
                    break;
                }
                case 'daemon_start':
                    text = await handleDaemonStart();
                    break;
                case 'daemon_stop':
                    text = await handleDaemonStop();
                    break;
                default:
                    text = `Unknown action "${params.action}". Valid actions: status, list, add, update, remove, run, history, daemon_start, daemon_stop.`;
            }
            return {
                content: [{ type: 'text', text }],
                details: undefined
            };
        }
    };
}
//# sourceMappingURL=cron.js.map