import { randomUUID } from "node:crypto";
const DEFAULT_CONSUMED_TERMINAL_TTL_MS = 5 * 60 * 1000;
const DEFAULT_DETACHED_TERMINAL_TTL_MS = 60 * 1000;
function now() {
    return new Date().toISOString();
}
function snapshot(record) {
    const output = {
        runId: record.runId,
        kind: record.kind,
        ownerPiboSessionId: record.ownerPiboSessionId,
        status: record.status,
        completionPolicy: record.completionPolicy,
        consumed: record.consumed,
        toolName: record.toolName,
        createdAt: record.createdAt,
        updatedAt: record.updatedAt,
    };
    if (record.summary)
        output.summary = record.summary;
    if (record.completedAt)
        output.completedAt = record.completedAt;
    return output;
}
function terminal(status) {
    return status === "completed" || status === "failed" || status === "cancelled";
}
export class PiboRunRegistry {
    options;
    runs = new Map();
    waiters = new Map();
    constructor(options = {}) {
        this.options = options;
        if (this.options.store) {
            this.options.store.recoverInterruptedRuns();
            for (const record of this.options.store.listRuns({ includeConsumed: true, includeDetached: true })) {
                this.runs.set(record.runId, recordFromStored(record));
            }
        }
    }
    startToolRun(input) {
        this.prune();
        if (this.options.store) {
            const stored = this.options.store.createRun({
                ownerPiboSessionId: input.ownerPiboSessionId,
                toolName: input.toolName,
                completionPolicy: input.completionPolicy ?? "tracked",
                params: input.params,
                retryable: input.retryable ?? false,
                maxAttempts: input.maxAttempts ?? 1,
            });
            const record = recordFromStored(stored);
            this.runs.set(record.runId, record);
            return snapshot(record);
        }
        const timestamp = now();
        const runId = `run_${randomUUID()}`;
        const record = {
            runId,
            kind: "tool",
            ownerPiboSessionId: input.ownerPiboSessionId,
            status: "running",
            completionPolicy: input.completionPolicy ?? "tracked",
            consumed: false,
            toolName: input.toolName,
            createdAt: timestamp,
            updatedAt: timestamp,
            summary: `${input.toolName} run is running.`,
            retryable: input.retryable ?? false,
            maxAttempts: Math.max(1, input.maxAttempts ?? 1),
        };
        this.runs.set(runId, record);
        return snapshot(record);
    }
    complete(runId, result) {
        const record = this.runs.get(runId);
        if (!record || terminal(record.status))
            return undefined;
        record.status = "completed";
        record.result = result;
        record.summary = `${record.toolName} run completed.`;
        this.finish(record);
        this.options.store?.updateRun(runId, record);
        if (record.jobId)
            this.options.store?.ack(record.jobId, `run-registry:${process.pid}`);
        return snapshot(record);
    }
    fail(runId, error) {
        const record = this.runs.get(runId);
        if (!record || terminal(record.status))
            return undefined;
        record.status = "failed";
        record.error = error;
        record.summary = `${record.toolName} run failed.`;
        this.finish(record);
        this.options.store?.updateRun(runId, record);
        if (record.jobId)
            this.options.store?.fail(record.jobId, `run-registry:${process.pid}`, error);
        return snapshot(record);
    }
    list(ownerPiboSessionId, options = {}) {
        this.prune();
        return [...this.runs.values()]
            .filter((record) => record.ownerPiboSessionId === ownerPiboSessionId)
            .filter((record) => options.includeConsumed || !record.consumed)
            .filter((record) => options.includeDetached || record.completionPolicy !== "detached")
            .map(snapshot);
    }
    status(ownerPiboSessionId, runId) {
        return snapshot(this.requireOwned(ownerPiboSessionId, runId));
    }
    async wait(ownerPiboSessionId, runId, timeoutMs) {
        const record = this.requireOwned(ownerPiboSessionId, runId);
        if (terminal(record.status))
            return { ...snapshot(record), timedOut: false };
        const boundedTimeoutMs = Math.max(0, Math.min(timeoutMs, 300000));
        const completed = await new Promise((resolve) => {
            const timeout = setTimeout(() => {
                removeWaiter();
                resolve(undefined);
            }, boundedTimeoutMs);
            const waiter = {
                resolve: (updated) => {
                    clearTimeout(timeout);
                    resolve(updated);
                },
            };
            const removeWaiter = () => {
                const waiters = this.waiters.get(runId);
                if (!waiters)
                    return;
                const index = waiters.indexOf(waiter);
                if (index >= 0)
                    waiters.splice(index, 1);
                if (waiters.length === 0)
                    this.waiters.delete(runId);
            };
            const waiters = this.waiters.get(runId) ?? [];
            waiters.push(waiter);
            this.waiters.set(runId, waiters);
        });
        if (!completed)
            return { ...snapshot(record), timedOut: true };
        return { ...snapshot(completed), timedOut: false };
    }
    read(ownerPiboSessionId, runId) {
        const record = this.requireOwned(ownerPiboSessionId, runId);
        if (terminal(record.status)) {
            record.consumed = true;
            record.updatedAt = now();
            this.options.store?.updateRun(runId, record);
        }
        const output = { ...snapshot(record) };
        if (record.result)
            output.result = record.result;
        if (record.error)
            output.error = record.error;
        return output;
    }
    cancel(ownerPiboSessionId, runId) {
        const record = this.requireOwned(ownerPiboSessionId, runId);
        if (!terminal(record.status)) {
            record.status = "cancelled";
            record.summary = `${record.toolName} run cancelled.`;
            this.finish(record);
            if (record.jobId)
                this.options.store?.fail(record.jobId, `run-registry:${process.pid}`, "Run was cancelled.");
        }
        record.consumed = true;
        record.updatedAt = now();
        this.options.store?.updateRun(runId, record);
        return snapshot(record);
    }
    ack(ownerPiboSessionId, runId) {
        const record = this.requireOwned(ownerPiboSessionId, runId);
        record.acknowledgedStatus = record.status;
        if (terminal(record.status))
            record.consumed = true;
        record.updatedAt = now();
        this.options.store?.updateRun(runId, record);
        return snapshot(record);
    }
    createNotification(ownerPiboSessionId, options = {}) {
        const records = [...this.runs.values()].filter((record) => this.needsNotification(record, ownerPiboSessionId, options));
        if (records.length === 0)
            return undefined;
        for (const record of records) {
            record.notifiedStatus = record.status;
            this.options.store?.updateRun(record.runId, record);
        }
        const notification = {
            completed: [],
            failed: [],
            cancelled: [],
            running: [],
        };
        for (const record of records) {
            const item = snapshot(record);
            if (record.status === "completed")
                notification.completed.push(item);
            else if (record.status === "failed")
                notification.failed.push(item);
            else if (record.status === "cancelled")
                notification.cancelled.push(item);
            else
                notification.running.push(item);
        }
        return notification;
    }
    hasPendingNotification(ownerPiboSessionId, options = {}) {
        return [...this.runs.values()].some((record) => this.needsNotification(record, ownerPiboSessionId, options));
    }
    cancelOwnerRuns(ownerPiboSessionId, reason = "Owner Pibo session was disposed.") {
        const cancelled = [];
        for (const record of this.runs.values()) {
            if (record.ownerPiboSessionId !== ownerPiboSessionId || terminal(record.status))
                continue;
            record.status = "cancelled";
            record.error = reason;
            record.consumed = true;
            record.summary = `${record.toolName} run cancelled.`;
            this.finish(record);
            this.options.store?.updateRun(record.runId, record);
            if (record.jobId)
                this.options.store?.fail(record.jobId, `run-registry:${process.pid}`, reason);
            cancelled.push(snapshot(record));
        }
        return cancelled;
    }
    cancelAll(reason = "Run registry was disposed.") {
        const cancelled = [];
        for (const record of this.runs.values()) {
            if (terminal(record.status))
                continue;
            record.status = "cancelled";
            record.error = reason;
            record.consumed = true;
            record.summary = `${record.toolName} run cancelled.`;
            this.finish(record);
            this.options.store?.updateRun(record.runId, record);
            if (record.jobId)
                this.options.store?.fail(record.jobId, `run-registry:${process.pid}`, reason);
            cancelled.push(snapshot(record));
        }
        return cancelled;
    }
    prune(options = {}) {
        const nowMs = options.nowMs ?? Date.now();
        const consumedTerminalTtlMs = options.consumedTerminalTtlMs ??
            this.options.consumedTerminalTtlMs ??
            DEFAULT_CONSUMED_TERMINAL_TTL_MS;
        const detachedTerminalTtlMs = options.detachedTerminalTtlMs ??
            this.options.detachedTerminalTtlMs ??
            DEFAULT_DETACHED_TERMINAL_TTL_MS;
        let pruned = 0;
        for (const [runId, record] of this.runs) {
            if (!terminal(record.status) || !record.completedAt)
                continue;
            const ageMs = nowMs - Date.parse(record.completedAt);
            const shouldPrune = (record.completionPolicy === "detached" && ageMs >= detachedTerminalTtlMs) ||
                (record.completionPolicy === "tracked" && record.consumed && ageMs >= consumedTerminalTtlMs);
            if (!shouldPrune)
                continue;
            this.runs.delete(runId);
            pruned += 1;
        }
        if (this.options.store) {
            this.options.store.pruneRuns({ consumedTerminalTtlMs, detachedTerminalTtlMs, nowMs });
        }
        return pruned;
    }
    requireOwned(ownerPiboSessionId, runId) {
        const record = this.runs.get(runId);
        if (!record || record.ownerPiboSessionId !== ownerPiboSessionId) {
            throw new Error(`Unknown run "${runId}" for session "${ownerPiboSessionId}"`);
        }
        return record;
    }
    needsNotification(record, ownerPiboSessionId, options = {}) {
        return (record.ownerPiboSessionId === ownerPiboSessionId &&
            record.completionPolicy === "tracked" &&
            !record.consumed &&
            record.acknowledgedStatus !== record.status &&
            (options.includeAlreadyNotified || record.notifiedStatus !== record.status));
    }
    finish(record) {
        const timestamp = now();
        record.updatedAt = timestamp;
        record.completedAt = timestamp;
        this.resolveWaiters(record);
    }
    resolveWaiters(record) {
        const waiters = this.waiters.get(record.runId);
        if (!waiters)
            return;
        this.waiters.delete(record.runId);
        for (const waiter of waiters) {
            waiter.resolve(record);
        }
    }
}
function recordFromStored(record) {
    return {
        runId: record.runId,
        kind: record.kind,
        ownerPiboSessionId: record.ownerPiboSessionId,
        status: record.status,
        completionPolicy: record.completionPolicy,
        consumed: record.consumed,
        toolName: record.toolName,
        createdAt: record.createdAt,
        updatedAt: record.updatedAt,
        summary: record.summary,
        completedAt: record.completedAt,
        result: record.result,
        error: record.error,
        notifiedStatus: record.notifiedStatus,
        acknowledgedStatus: record.acknowledgedStatus,
        jobId: record.jobId,
        retryable: record.retryable,
        maxAttempts: record.maxAttempts,
    };
}
