// Copyright (c) 2026 Hellmai Ltd
// SPDX-License-Identifier: AGPL-3.0-only
/**
 * Claim Queue (WU-2668)
 *
 * Append-only JSONL registry at `.lumenflow/state/wu-claim-queue.jsonl` that
 * records `wu:claim --wait-for-lane` intents. `wu:claim` uses this queue to
 * defer a claim until every blocker WU has merged (observed via a
 * `{ type: 'complete' }` event in the existing `.lumenflow/state/wu-events.jsonl`).
 *
 * Design notes:
 *
 * - Persistence follows the same pattern as `wu-events.jsonl`: append-only,
 *   one JSON object per line, lenient reader (malformed lines are silently
 *   skipped so a single bad record cannot mask legitimate entries).
 * - Blocker-merge detection reuses the wu-events log — no new file-watcher or
 *   subscriber process. We poll the events log at a configurable cadence; the
 *   same technique the control-plane sidecar (WU-2642) uses.
 * - Concurrency: two agents queueing claims for the same WU are both recorded;
 *   the first to satisfy blockers and acquire the lane lock wins. The second
 *   falls through to the normal wu:claim error path (branch/lock already held)
 *   which surfaces a clear error rather than silently dropping.
 * - Timeout: `waitForBlockersResolved` returns `{ cleared: false, timedOut: true }`
 *   so the caller can surface a clear "gave up after N seconds" error instead
 *   of silently abandoning the queued claim.
 * - Cancellation: `cancelActiveEntry` appends a `cancelled` event so the queue
 *   file remains append-only and auditable.
 *
 * @see {@link ./wu-claim.ts} — orchestrator consumer
 * @see {@link ../../core/src/wu-event-sourcer.ts} — wu-events.jsonl conventions
 */
import fs from 'node:fs';
import path from 'node:path';
import { WU_STATUS } from '@lumenflow/core/wu-constants';
import { resolveStateDir } from './state-path-resolvers.js';
export const CLAIM_QUEUE_FILE_NAME = 'wu-claim-queue.jsonl';
const WU_EVENTS_FILE_NAME = 'wu-events.jsonl';
const DEFAULT_POLL_INTERVAL_MS = 5_000;
const CANCELLED_QUEUE_EVENT_TYPE = WU_STATUS.CANCELLED;
function queueFilePath(baseDir) {
    return path.join(resolveStateDir(baseDir), CLAIM_QUEUE_FILE_NAME);
}
function eventsFilePath(baseDir) {
    return path.join(resolveStateDir(baseDir), WU_EVENTS_FILE_NAME);
}
function ensureStateDir(baseDir) {
    fs.mkdirSync(resolveStateDir(baseDir), { recursive: true });
}
function parseJsonLines(content, keep) {
    const result = [];
    for (const rawLine of content.split('\n')) {
        const line = rawLine.trim();
        if (line.length === 0)
            continue;
        let parsed;
        try {
            parsed = JSON.parse(line);
        }
        catch {
            continue;
        }
        if (keep(parsed)) {
            result.push(parsed);
        }
    }
    return result;
}
function isQueueEvent(value) {
    if (!value || typeof value !== 'object')
        return false;
    const record = value;
    if (typeof record.wuId !== 'string' || typeof record.timestamp !== 'string')
        return false;
    switch (record.type) {
        case 'queued':
            return (typeof record.lane === 'string' &&
                Array.isArray(record.blockers) &&
                record.blockers.every((b) => typeof b === 'string') &&
                typeof record.timeoutSeconds === 'number');
        case 'fired':
        case CANCELLED_QUEUE_EVENT_TYPE:
        case 'timed_out':
            return true;
        default:
            return false;
    }
}
function isCompleteEvent(value) {
    if (!value || typeof value !== 'object')
        return false;
    const record = value;
    return record.type === 'complete' && typeof record.wuId === 'string';
}
/**
 * Append a queue event to the append-only JSONL file. Creates the state
 * directory and file on first use.
 */
export function appendQueueEvent(baseDir, event) {
    ensureStateDir(baseDir);
    const line = `${JSON.stringify(event)}\n`;
    fs.appendFileSync(queueFilePath(baseDir), line, 'utf-8');
}
/**
 * Read the full queue event log. Returns [] when the file is missing. Lenient:
 * malformed JSON lines are skipped, not thrown, so a single bad record cannot
 * poison the queue.
 */
export function readQueueEvents(baseDir) {
    const filePath = queueFilePath(baseDir);
    let content;
    try {
        content = fs.readFileSync(filePath, 'utf-8');
    }
    catch {
        return [];
    }
    return parseJsonLines(content, isQueueEvent);
}
/**
 * Fold the event log down to the current status of a WU's queue entry.
 * Terminal events (`fired`, `cancelled`, `timed_out`) always win over a prior
 * `queued` event; any subsequent `queued` would represent a new entry.
 */
export function computeEntryStatus(baseDir, wuId) {
    const events = readQueueEvents(baseDir);
    let status = 'none';
    for (const event of events) {
        if (event.wuId !== wuId)
            continue;
        if (event.type === 'queued') {
            status = 'queued';
        }
        else {
            // Terminal event for the most recent queued entry.
            status = event.type;
        }
    }
    return status;
}
/**
 * Return the most recent queued entry for a WU iff it has not been terminated
 * (fired/cancelled/timed_out). Returns null otherwise.
 */
export function findActiveEntry(baseDir, wuId) {
    const events = readQueueEvents(baseDir);
    let active = null;
    for (const event of events) {
        if (event.wuId !== wuId)
            continue;
        if (event.type === 'queued') {
            active = {
                wuId: event.wuId,
                lane: event.lane,
                blockers: [...event.blockers],
                timeoutSeconds: event.timeoutSeconds,
                timestamp: event.timestamp,
                ...(event.worktree ? { worktree: event.worktree } : {}),
                ...(event.branch ? { branch: event.branch } : {}),
            };
        }
        else {
            // Terminal — clear the active entry.
            active = null;
        }
    }
    return active;
}
/**
 * List every WU currently in the `queued` state (not yet fired/cancelled/
 * timed-out). Order matches insertion in the event log.
 */
export function listActiveEntries(baseDir) {
    const events = readQueueEvents(baseDir);
    const activeByWu = new Map();
    const insertionOrder = [];
    for (const event of events) {
        if (event.type === 'queued') {
            if (!activeByWu.has(event.wuId)) {
                insertionOrder.push(event.wuId);
            }
            activeByWu.set(event.wuId, {
                wuId: event.wuId,
                lane: event.lane,
                blockers: [...event.blockers],
                timeoutSeconds: event.timeoutSeconds,
                timestamp: event.timestamp,
                ...(event.worktree ? { worktree: event.worktree } : {}),
                ...(event.branch ? { branch: event.branch } : {}),
            });
        }
        else {
            activeByWu.delete(event.wuId);
        }
    }
    return insertionOrder
        .map((wuId) => activeByWu.get(wuId))
        .filter((entry) => Boolean(entry));
}
/**
 * Cancel the active queue entry for a WU by appending a `cancelled` event.
 * Returns `{ cancelled: false }` when no active entry exists (no-op).
 */
export function cancelActiveEntry(baseDir, wuId, timestamp = new Date().toISOString()) {
    const active = findActiveEntry(baseDir, wuId);
    if (!active) {
        return { cancelled: false };
    }
    appendQueueEvent(baseDir, { type: CANCELLED_QUEUE_EVENT_TYPE, wuId, timestamp });
    return { cancelled: true };
}
/**
 * Inspect `wu-events.jsonl` and return the subset of `blockers` that have not
 * yet emitted a `complete` event. Returns the full list when the events file
 * is missing — callers should treat "unknown" as "still blocking" to avoid
 * firing against an empty log.
 */
export function blockersStillOpen(baseDir, blockers) {
    if (blockers.length === 0)
        return [];
    let content;
    try {
        content = fs.readFileSync(eventsFilePath(baseDir), 'utf-8');
    }
    catch {
        return [...blockers];
    }
    const completed = new Set();
    for (const event of parseJsonLines(content, isCompleteEvent)) {
        completed.add(event.wuId);
    }
    return blockers.filter((wuId) => !completed.has(wuId));
}
/**
 * Poll `wu-events.jsonl` until every blocker has a `complete` event, the
 * timeout elapses, or `isCancelled()` returns true. Resolves with the final
 * state so the caller can surface a precise error when firing fails.
 */
export async function waitForBlockersResolved(options) {
    const { baseDir, blockers, timeoutMs, pollIntervalMs = DEFAULT_POLL_INTERVAL_MS, isCancelled, now = () => Date.now(), sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), } = options;
    if (blockers.length === 0) {
        return { cleared: true, remainingBlockers: [] };
    }
    const deadline = now() + timeoutMs;
    // Initial check — fire immediately if already clear.
    let remaining = blockersStillOpen(baseDir, blockers);
    if (remaining.length === 0) {
        return { cleared: true, remainingBlockers: [] };
    }
    while (now() < deadline) {
        if (isCancelled?.()) {
            return {
                cleared: false,
                cancelled: true,
                remainingBlockers: blockersStillOpen(baseDir, blockers),
            };
        }
        await sleep(pollIntervalMs);
        if (isCancelled?.()) {
            return {
                cleared: false,
                cancelled: true,
                remainingBlockers: blockersStillOpen(baseDir, blockers),
            };
        }
        remaining = blockersStillOpen(baseDir, blockers);
        if (remaining.length === 0) {
            return { cleared: true, remainingBlockers: [] };
        }
    }
    return {
        cleared: false,
        timedOut: true,
        remainingBlockers: blockersStillOpen(baseDir, blockers),
    };
}
//# sourceMappingURL=claim-queue.js.map