/**
 * File-backed heartbeat task repository.
 *
 * Owns task, checkpoint, and run-record file I/O for heartbeat scheduling.
 * Scheduler and host code should depend on this repository contract instead of
 * reading heartbeat JSON files directly.
 */
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { basename, dirname, join } from 'node:path';
import dayjs from 'dayjs';
import { AgentLoopCheckpointSchema, HeartbeatTaskRunRecordSchema, HeartbeatTaskSchema } from './schemas.js';
import { HeartbeatTaskStateProjector } from './task-state.js';
export class FileHeartbeatTaskRepository {
    tasksDir;
    checkpointsDir;
    runsDir;
    constructor(options) {
        this.tasksDir = join(options.dir, 'tasks');
        this.checkpointsDir = join(options.dir, 'checkpoints');
        this.runsDir = join(options.dir, 'runs');
    }
    async listTasks() {
        if (!existsSync(this.tasksDir)) {
            return [];
        }
        return readdirSync(this.tasksDir)
            .filter((entry) => entry.endsWith('.json'))
            .flatMap((entry) => this.readTaskFile(join(this.tasksDir, entry)))
            .sort((left, right) => left.id.localeCompare(right.id));
    }
    async saveTask(task) {
        const path = join(this.tasksDir, `${FileHeartbeatTaskRepository.safeTaskFileName(task.id)}.json`);
        mkdirSync(dirname(path), { recursive: true });
        writeFileSync(path, `${JSON.stringify(HeartbeatTaskSchema.parse(HeartbeatTaskStateProjector.normalize(task)), null, 2)}\n`);
    }
    async deleteTask(task) {
        const safeTaskId = FileHeartbeatTaskRepository.safeTaskFileName(task.id);
        rmSync(join(this.tasksDir, `${safeTaskId}.json`), { force: true });
        rmSync(this.checkpointPathForTask(task), { force: true });
        await this.deleteRunRecordsForTask(task.id);
    }
    async loadCheckpoint(task) {
        const path = this.checkpointPathForTask(task);
        if (!existsSync(path)) {
            return undefined;
        }
        try {
            const parsed = AgentLoopCheckpointSchema.safeParse(JSON.parse(readFileSync(path, 'utf8')));
            return parsed.success ? parsed.data : undefined;
        }
        catch {
            return undefined;
        }
    }
    async saveCheckpoint(task, checkpoint) {
        const path = this.checkpointPathForTask(task);
        mkdirSync(dirname(path), { recursive: true });
        writeFileSync(path, `${JSON.stringify(AgentLoopCheckpointSchema.parse(checkpoint), null, 2)}\n`);
    }
    async saveRunRecord(record) {
        const timestamp = dayjs().toISOString().replaceAll(':', '-');
        const path = join(this.runsDir, `${timestamp}-${FileHeartbeatTaskRepository.safeTaskFileName(record.task.id)}.json`);
        mkdirSync(dirname(path), { recursive: true });
        writeFileSync(path, `${JSON.stringify(HeartbeatTaskRunRecordSchema.parse(record), null, 2)}\n`);
    }
    async listRunRecords(options = {}) {
        if (!existsSync(this.runsDir)) {
            return [];
        }
        const entries = readdirSync(this.runsDir)
            .filter((entry) => entry.endsWith('.json'))
            .flatMap((entry) => this.readRunRecordFile(join(this.runsDir, entry), options))
            .sort((left, right) => right.createdAt.localeCompare(left.createdAt));
        return options.limit ? entries.slice(0, options.limit) : entries;
    }
    async loadRunRecord(id) {
        const entries = await this.listRunRecords();
        return entries.find((entry) => entry.id === id || entry.runId === id);
    }
    async deleteRunRecordsForTask(taskId) {
        const entries = await this.listRunRecords({ taskId });
        entries.forEach((entry) => rmSync(entry.path, { force: true }));
    }
    readTaskFile(path) {
        try {
            const parsed = HeartbeatTaskSchema.safeParse(JSON.parse(readFileSync(path, 'utf8')));
            return parsed.success ? [HeartbeatTaskStateProjector.normalize(parsed.data)] : [];
        }
        catch {
            return [];
        }
    }
    readRunRecordFile(path, options) {
        try {
            const parsed = HeartbeatTaskRunRecordSchema.safeParse(JSON.parse(readFileSync(path, 'utf8')));
            if (!parsed.success || (options.taskId && parsed.data.task.id !== options.taskId)) {
                return [];
            }
            return [FileHeartbeatTaskRepository.runRecordEntryFromPath(path, parsed.data)];
        }
        catch {
            return [];
        }
    }
    checkpointPathForTask(task) {
        return task.checkpointPath ?? join(this.checkpointsDir, `${FileHeartbeatTaskRepository.safeTaskFileName(task.id)}.json`);
    }
    static safeTaskFileName(id) {
        if (!/^[a-zA-Z0-9._-]+$/.test(id)) {
            throw new Error(`Invalid heartbeat task id "${id}". Use only letters, numbers, dots, underscores, and hyphens.`);
        }
        return id;
    }
    static runRecordEntryFromPath(path, record) {
        const id = basename(path, '.json');
        return {
            id,
            path,
            taskId: record.task.id,
            workspaceId: record.task.workspaceId,
            runId: record.result.state.runId,
            createdAt: record.result.state.finishedAt,
            record,
        };
    }
}
//# sourceMappingURL=repository.js.map