import fs from 'node:fs/promises';
import path from 'node:path';
import crypto from 'node:crypto';
import { ensureDir, readSessionDirEnv, resolveClockDir, resolveClockStateFile } from './paths.js';
import { cleanExpiredTasks, nowMs } from './state.js';
import { writeJsonFileAtomic } from './io.js';
import { clearClockSession, loadClockSessionState } from './session-store.js';
function safeJson(value) {
    try {
        return JSON.stringify(value ?? {});
    }
    catch {
        return '{}';
    }
}
function safeQuoted(text) {
    const normalized = String(text ?? '');
    const escaped = normalized.replace(/"/g, '\\"');
    return `"${escaped}"`;
}
function buildTaskId() {
    try {
        return crypto.randomUUID();
    }
    catch {
        return `task_${Math.random().toString(16).slice(2)}_${Date.now()}`;
    }
}
function normalizeRecurrence(raw) {
    if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
        return undefined;
    }
    const record = raw;
    const rawKind = typeof record.kind === 'string' ? record.kind.trim().toLowerCase() : '';
    const kind = rawKind === 'daily' || rawKind === 'weekly' || rawKind === 'interval'
        ? rawKind
        : undefined;
    const maxRunsRaw = Number(record.maxRuns);
    const maxRuns = Number.isFinite(maxRunsRaw) ? Math.floor(maxRunsRaw) : NaN;
    if (!kind || !Number.isFinite(maxRuns) || maxRuns <= 0) {
        return undefined;
    }
    if (kind === 'interval') {
        const everyRaw = Number(record.everyMinutes);
        const everyMinutes = Number.isFinite(everyRaw) ? Math.floor(everyRaw) : NaN;
        if (!Number.isFinite(everyMinutes) || everyMinutes <= 0) {
            return undefined;
        }
        return { kind, maxRuns, everyMinutes };
    }
    return { kind, maxRuns };
}
function normalizeTaskSetter(raw) {
    const value = typeof raw === 'string' ? raw.trim().toLowerCase() : '';
    if (value === 'agent') {
        return 'agent';
    }
    return 'user';
}
function resolveRecurringStepMs(recurrence) {
    if (recurrence.kind === 'daily') {
        return 24 * 60 * 60_000;
    }
    if (recurrence.kind === 'weekly') {
        return 7 * 24 * 60 * 60_000;
    }
    if (recurrence.kind === 'interval') {
        const minutes = Number(recurrence.everyMinutes);
        if (!Number.isFinite(minutes) || minutes <= 0) {
            return null;
        }
        return Math.floor(minutes) * 60_000;
    }
    return null;
}
function computeNextRecurringDueAtMs(currentDueAtMs, recurrence, atMs) {
    if (!Number.isFinite(currentDueAtMs)) {
        return null;
    }
    const stepMs = resolveRecurringStepMs(recurrence);
    if (!stepMs || stepMs <= 0) {
        return null;
    }
    let next = Math.floor(currentDueAtMs) + stepMs;
    const ceiling = Math.max(atMs, Math.floor(currentDueAtMs)) + stepMs * 100_000;
    while (next <= atMs && next < ceiling) {
        next += stepMs;
    }
    if (!Number.isFinite(next) || next <= atMs) {
        return null;
    }
    return Math.floor(next);
}
export function parseDueAtMs(value) {
    if (typeof value !== 'string')
        return null;
    const t = value.trim();
    if (!t)
        return null;
    const ms = Date.parse(t);
    if (!Number.isFinite(ms))
        return null;
    return ms;
}
export async function listClockSessionIds() {
    const sessionDir = readSessionDirEnv();
    if (!sessionDir) {
        return [];
    }
    const clockDir = resolveClockDir(sessionDir);
    const entries = await fs.readdir(clockDir, { withFileTypes: true }).catch(() => []);
    const sessionIds = entries
        .filter((entry) => entry && typeof entry.name === 'string' && entry.name.endsWith('.json'))
        .filter((entry) => entry.name !== 'ntp-state.json')
        .filter((entry) => (typeof entry.isFile === 'function' ? entry.isFile() : true))
        .map((entry) => entry.name.slice(0, -'.json'.length).trim())
        .filter(Boolean)
        .sort((a, b) => a.localeCompare(b));
    return sessionIds;
}
export async function listClockTasks(sessionId, config) {
    const state = await loadClockSessionState(sessionId, config);
    return state.tasks.slice();
}
export async function scheduleClockTasks(sessionId, items, config) {
    const sessionDir = readSessionDirEnv();
    if (!sessionDir) {
        throw new Error('clock: missing ROUTECODEX_SESSION_DIR');
    }
    const filePath = resolveClockStateFile(sessionDir, sessionId);
    if (!filePath) {
        throw new Error('clock: invalid sessionId');
    }
    await ensureDir(path.dirname(filePath));
    const at = nowMs();
    const existing = await loadClockSessionState(sessionId, config);
    const cleaned = cleanExpiredTasks(existing.tasks, config, at);
    const scheduled = [];
    for (const item of items) {
        const text = typeof item.task === 'string' ? item.task.trim() : '';
        const dueAtMs = item.dueAtMs;
        if (!text || !Number.isFinite(dueAtMs)) {
            continue;
        }
        const recurrence = normalizeRecurrence(item.recurrence);
        if (item.recurrence && !recurrence) {
            continue;
        }
        const setBy = normalizeTaskSetter(item.setBy);
        const taskId = buildTaskId();
        scheduled.push({
            taskId,
            sessionId,
            dueAtMs: Math.floor(dueAtMs),
            createdAtMs: at,
            updatedAtMs: at,
            setBy,
            task: text,
            ...(item.tool ? { tool: item.tool } : {}),
            ...(item.arguments ? { arguments: item.arguments } : {}),
            ...(item.notBeforeRequestId ? { notBeforeRequestId: item.notBeforeRequestId } : {}),
            ...(recurrence ? { recurrence } : {}),
            deliveryCount: 0
        });
    }
    const nextTasks = cleanExpiredTasks([...cleaned, ...scheduled], config, at);
    const next = {
        version: 1,
        sessionId,
        tasks: nextTasks,
        updatedAtMs: at
    };
    if (!next.tasks.length) {
        await fs.rm(filePath, { force: true });
    }
    else {
        await writeJsonFileAtomic(filePath, next);
    }
    return scheduled;
}
export async function updateClockTask(sessionId, taskId, patch, config) {
    const sessionDir = readSessionDirEnv();
    if (!sessionDir) {
        throw new Error('clock: missing ROUTECODEX_SESSION_DIR');
    }
    const filePath = resolveClockStateFile(sessionDir, sessionId);
    if (!filePath) {
        throw new Error('clock: invalid sessionId');
    }
    await ensureDir(path.dirname(filePath));
    const at = nowMs();
    const state = await loadClockSessionState(sessionId, config);
    const cleaned = cleanExpiredTasks(state.tasks, config, at);
    const index = cleaned.findIndex((item) => item.taskId === taskId);
    if (index < 0) {
        return null;
    }
    const current = cleaned[index];
    const nextTask = {
        ...current,
        updatedAtMs: at
    };
    if (typeof patch.dueAtMs === 'number' && Number.isFinite(patch.dueAtMs)) {
        nextTask.dueAtMs = Math.floor(patch.dueAtMs);
        delete nextTask.deliveredAtMs;
    }
    if (typeof patch.task === 'string' && patch.task.trim()) {
        nextTask.task = patch.task.trim();
    }
    if (patch.tool === null) {
        delete nextTask.tool;
    }
    else if (typeof patch.tool === 'string' && patch.tool.trim()) {
        nextTask.tool = patch.tool.trim();
    }
    if (patch.arguments === null) {
        delete nextTask.arguments;
    }
    else if (patch.arguments && typeof patch.arguments === 'object' && !Array.isArray(patch.arguments)) {
        nextTask.arguments = patch.arguments;
    }
    if (patch.notBeforeRequestId === null) {
        delete nextTask.notBeforeRequestId;
    }
    else if (typeof patch.notBeforeRequestId === 'string' && patch.notBeforeRequestId.trim()) {
        nextTask.notBeforeRequestId = patch.notBeforeRequestId.trim();
    }
    if (patch.recurrence === null) {
        delete nextTask.recurrence;
    }
    else if (patch.recurrence) {
        const recurrence = normalizeRecurrence(patch.recurrence);
        if (!recurrence) {
            return null;
        }
        nextTask.recurrence = recurrence;
        delete nextTask.deliveredAtMs;
    }
    if (patch.resetDelivery) {
        nextTask.deliveryCount = 0;
        delete nextTask.deliveredAtMs;
    }
    cleaned[index] = nextTask;
    const next = { version: 1, sessionId, tasks: cleaned, updatedAtMs: at };
    if (!next.tasks.length) {
        await fs.rm(filePath, { force: true });
    }
    else {
        await writeJsonFileAtomic(filePath, next);
    }
    return nextTask;
}
export async function cancelClockTask(sessionId, taskId, config) {
    const sessionDir = readSessionDirEnv();
    if (!sessionDir) {
        throw new Error('clock: missing ROUTECODEX_SESSION_DIR');
    }
    const filePath = resolveClockStateFile(sessionDir, sessionId);
    if (!filePath) {
        throw new Error('clock: invalid sessionId');
    }
    await ensureDir(path.dirname(filePath));
    const at = nowMs();
    const state = await loadClockSessionState(sessionId, config);
    const cleaned = cleanExpiredTasks(state.tasks, config, at);
    const nextTasks = cleaned.filter((t) => t.taskId !== taskId);
    const removed = nextTasks.length !== cleaned.length;
    const next = { version: 1, sessionId, tasks: nextTasks, updatedAtMs: at };
    if (!next.tasks.length) {
        await fs.rm(filePath, { force: true });
    }
    else {
        await writeJsonFileAtomic(filePath, next);
    }
    return removed;
}
export async function clearClockTasks(sessionId, config) {
    const items = await listClockTasks(sessionId, config);
    await clearClockSession(sessionId);
    return items.length;
}
function isRecurringTaskPending(task) {
    const recurrence = normalizeRecurrence(task.recurrence);
    if (!recurrence) {
        return false;
    }
    const deliveryCount = typeof task.deliveryCount === 'number' && Number.isFinite(task.deliveryCount)
        ? Math.max(0, Math.floor(task.deliveryCount))
        : 0;
    return deliveryCount < recurrence.maxRuns;
}
export function selectDueUndeliveredTasks(tasks, config, atMs) {
    const due = [];
    for (const task of tasks) {
        if (!task || typeof task !== 'object')
            continue;
        const recurringPending = isRecurringTaskPending(task);
        if (!recurringPending && task.deliveredAtMs !== undefined)
            continue;
        if (typeof task.notBeforeRequestId === 'string' && task.notBeforeRequestId.trim().length) {
            // notBeforeRequestId is evaluated by reserveDueTasksForRequest, which passes requestId.
            // Keep legacy callers working by ignoring this guard here (default behavior).
        }
        if (!Number.isFinite(task.dueAtMs))
            continue;
        if (atMs < task.dueAtMs - config.dueWindowMs)
            continue;
        if (!recurringPending && atMs > task.dueAtMs + config.retentionMs)
            continue;
        due.push(task);
    }
    due.sort((a, b) => a.dueAtMs - b.dueAtMs);
    return due;
}
export function findNextUndeliveredDueAtMs(tasks, atMs) {
    let next = null;
    for (const task of tasks) {
        if (!task || typeof task !== 'object')
            continue;
        const recurringPending = isRecurringTaskPending(task);
        if (!recurringPending && task.deliveredAtMs !== undefined)
            continue;
        if (!Number.isFinite(task.dueAtMs))
            continue;
        if (task.dueAtMs < atMs) {
            // keep past due tasks as "next" too (will be due immediately)
        }
        if (next === null || task.dueAtMs < next) {
            next = task.dueAtMs;
        }
    }
    return next;
}
export async function reserveDueTasksForRequest(args) {
    const state = await loadClockSessionState(args.sessionId, args.config);
    const at = nowMs();
    const dueAll = selectDueUndeliveredTasks(state.tasks, args.config, at);
    const requestId = typeof args.requestId === 'string' && args.requestId.trim().length ? args.requestId.trim() : '';
    const isSameRequestChain = (guardedRequestId, currentRequestId) => {
        const guarded = guardedRequestId.trim();
        if (!guarded)
            return false;
        return currentRequestId === guarded || currentRequestId.startsWith(`${guarded}:`);
    };
    const due = requestId
        ? dueAll.filter((t) => {
            const guarded = typeof t.notBeforeRequestId === 'string' ? String(t.notBeforeRequestId).trim() : '';
            if (!guarded) {
                return true;
            }
            return !isSameRequestChain(guarded, requestId);
        })
        : dueAll;
    if (!due.length) {
        return { reservation: null };
    }
    const taskIds = due.map((t) => t.taskId);
    const reservation = {
        reservationId: args.reservationId,
        sessionId: args.sessionId,
        taskIds,
        reservedAtMs: at
    };
    const injectText = due
        .map((t) => {
        const dueAtIso = new Date(t.dueAtMs).toISOString();
        const setAtIso = new Date(t.createdAtMs).toISOString();
        const toolLabel = t.tool ? ` tool=${t.tool}` : '';
        const argsLabel = t.arguments ? ` args=${safeJson(t.arguments)}` : '';
        const setByLabel = t.setBy === 'agent' ? 'agent' : 'user';
        const recurrence = normalizeRecurrence(t.recurrence);
        const recurrenceLabel = recurrence
            ? ` recurrence=${recurrence.kind}${recurrence.kind === 'interval' ? `(${recurrence.everyMinutes}m)` : ''} maxRuns=${recurrence.maxRuns} run=${Math.max(0, t.deliveryCount)}/${recurrence.maxRuns}`
            : '';
        return `[scheduled task:${safeQuoted(t.task)}${toolLabel}${argsLabel}${recurrenceLabel} dueAt=${dueAtIso} setBy=${setByLabel} setAt=${setAtIso}]`;
    })
        .join('\n');
    return { reservation, injectText };
}
export async function commitClockReservation(reservation, config) {
    const sessionDir = readSessionDirEnv();
    if (!sessionDir) {
        return;
    }
    const filePath = resolveClockStateFile(sessionDir, reservation.sessionId);
    if (!filePath) {
        return;
    }
    const at = nowMs();
    const state = await loadClockSessionState(reservation.sessionId, config);
    const cleaned = cleanExpiredTasks(state.tasks, config, at);
    const reservedSet = new Set(reservation.taskIds);
    let touched = false;
    const nextTasks = [];
    for (const task of cleaned) {
        if (!reservedSet.has(task.taskId)) {
            nextTasks.push(task);
            continue;
        }
        touched = true;
        const currentCount = typeof task.deliveryCount === 'number' && Number.isFinite(task.deliveryCount)
            ? Math.max(0, Math.floor(task.deliveryCount))
            : 0;
        const nextCount = currentCount + 1;
        const recurrence = normalizeRecurrence(task.recurrence);
        if (recurrence) {
            if (nextCount >= recurrence.maxRuns) {
                continue;
            }
            const nextDueAtMs = computeNextRecurringDueAtMs(task.dueAtMs, recurrence, at);
            if (!Number.isFinite(nextDueAtMs)) {
                continue;
            }
            nextTasks.push({
                ...task,
                dueAtMs: nextDueAtMs,
                deliveryCount: nextCount,
                updatedAtMs: at,
                recurrence,
                deliveredAtMs: undefined
            });
            continue;
        }
        if (task.deliveredAtMs !== undefined) {
            nextTasks.push(task);
            continue;
        }
        nextTasks.push({
            ...task,
            deliveredAtMs: at,
            deliveryCount: nextCount,
            updatedAtMs: at
        });
    }
    if (!touched) {
        return;
    }
    const next = { version: 1, sessionId: reservation.sessionId, tasks: nextTasks, updatedAtMs: at };
    await ensureDir(path.dirname(filePath));
    if (!next.tasks.length) {
        await fs.rm(filePath, { force: true });
    }
    else {
        await writeJsonFileAtomic(filePath, next);
    }
}
