"use strict";
/**
 * QueueScheduler — background mission queue poller for the autonomous
 * daemon profile.
 *
 * Story 6.3 — OpenLife v1.2 Royal Stack.
 *
 * Reads `.openlife/agent-queue.json`, picks the next `pending` mission,
 * acquires a per-mission DistributedLock, marks it `running`, runs an
 * injected MissionExecutor, marks the terminal state, and persists the
 * queue via AtomicWriter. Concurrency capped by maxConcurrent (default 3).
 *
 * Opt-in via OPENLIFE_QUEUE_POLL_INTERVAL_MS env var or explicit
 * `start({pollIntervalMs})`. Off by default — `start --daemon` gateway
 * passes `--enable-queue` to flip it on. Keeps the existing Gateway
 * event-driven path unchanged.
 *
 * Crash semantics: a process restart finds `running` items orphaned (no
 * lock holder, no executing process). `reconcileOrphans()` on start
 * resets those back to `pending` so the next worker picks them up.
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.QueueScheduler = exports.NoopMissionExecutor = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const AtomicWriter_1 = require("./util/AtomicWriter");
const DistributedLock_1 = require("./util/DistributedLock");
class NoopMissionExecutor {
    // eslint-disable-next-line @typescript-eslint/require-await
    async execute() {
        return { status: 'completed' };
    }
}
exports.NoopMissionExecutor = NoopMissionExecutor;
class QueueScheduler {
    queueFile;
    pollIntervalMs;
    maxConcurrent;
    executor;
    lock;
    maxAttempts;
    timer = null;
    running = new Set();
    inFlight = new Set();
    pollPromise = null;
    shuttingDown = false;
    constructor(opts = {}) {
        const stateDir = process.env.OPENLIFE_STATE_DIR || path.join(process.cwd(), '.openlife');
        this.queueFile = opts.queueFile || path.join(stateDir, 'agent-queue.json');
        this.pollIntervalMs = opts.pollIntervalMs ?? Number(process.env.OPENLIFE_QUEUE_POLL_INTERVAL_MS || 10_000);
        this.maxConcurrent = opts.maxConcurrent ?? 3;
        this.executor = opts.executor || new NoopMissionExecutor();
        this.lock = opts.lock || new DistributedLock_1.DistributedLock();
        this.maxAttempts = opts.maxAttempts ?? 5;
    }
    /** Add a mission to the queue. Atomic. */
    enqueue(spec) {
        const q = this.read();
        const mission = {
            ...spec,
            enqueuedAt: new Date().toISOString(),
            status: 'pending',
            attempts: 0,
        };
        q.items.push(mission);
        this.write(q);
        return mission;
    }
    /** Inspect the queue without modification. */
    list() {
        return this.read().items;
    }
    /**
     * Reset any 'running' missions back to 'pending'. Call on boot to
     * recover from a previous crash that left missions mid-flight.
     */
    reconcileOrphans() {
        const q = this.read();
        let reset = 0;
        for (const item of q.items) {
            if (item.status === 'running') {
                item.status = 'pending';
                item.startedAt = undefined;
                reset += 1;
            }
        }
        if (reset > 0)
            this.write(q);
        return reset;
    }
    /**
     * Start the poll loop. Performs an immediate reconcileOrphans() then
     * schedules periodic ticks. Idempotent.
     */
    start() {
        if (this.timer)
            return;
        this.reconcileOrphans();
        // Immediate tick + interval
        void this.tick();
        this.timer = setInterval(() => { void this.tick(); }, this.pollIntervalMs);
        if (typeof this.timer.unref === 'function')
            this.timer.unref();
    }
    /**
     * Gracefully stop. Always awaits the current tick to settle (no new
     * picks). With `{ drain: true }`, also waits for in-flight missions to
     * finish, bounded by `timeoutMs` (default 2500ms) — past the deadline,
     * remaining missions keep running on their own promises but the daemon
     * proceeds to exit (they'll be reconciled as orphans on next boot).
     * Idempotent.
     */
    async stop(opts = {}) {
        this.shuttingDown = true;
        if (this.timer) {
            clearInterval(this.timer);
            this.timer = null;
        }
        if (this.pollPromise) {
            try {
                await this.pollPromise;
            }
            catch { /* swallow */ }
        }
        if (opts.drain && this.inFlight.size > 0) {
            const timeoutMs = opts.timeoutMs ?? 2500;
            const all = Promise.allSettled(Array.from(this.inFlight));
            const deadline = new Promise((resolve) => {
                const t = setTimeout(resolve, timeoutMs);
                if (typeof t.unref === 'function')
                    t.unref();
            });
            await Promise.race([all.then(() => undefined), deadline]);
        }
    }
    /** In-flight mission count. Exposed for tests + observability. */
    inFlightCount() {
        return this.inFlight.size;
    }
    /** Force one poll iteration (used by tests). */
    async tick() {
        if (this.shuttingDown)
            return;
        if (this.pollPromise)
            return; // already polling
        this.pollPromise = (async () => {
            try {
                await this.poll();
            }
            finally {
                this.pollPromise = null;
            }
        })();
        return this.pollPromise;
    }
    // ─────────────────────────────────────────────────────────
    // Internals
    // ─────────────────────────────────────────────────────────
    async poll() {
        const q = this.read();
        const slots = this.maxConcurrent - this.running.size;
        if (slots <= 0)
            return;
        let picked = 0;
        for (const item of q.items) {
            if (picked >= slots)
                break;
            if (item.status !== 'pending')
                continue;
            if (this.running.has(item.id))
                continue;
            // Attempt to acquire the per-mission lock
            const acq = this.lock.acquire(`mission:${item.id}`, this.pollIntervalMs * 4, { startHeartbeat: true });
            if (!acq.ok)
                continue;
            item.status = 'running';
            item.startedAt = new Date().toISOString();
            item.attempts += 1;
            this.running.add(item.id);
            picked += 1;
            // Persist the running transition before dispatching
            this.writeItems(q.items);
            // Fire-and-track the execution without blocking the poll loop.
            // Track the promise so stop({ drain: true }) can await in-flight work.
            const p = this.runMission(item, acq.lockId).finally(() => {
                this.inFlight.delete(p);
            });
            this.inFlight.add(p);
        }
    }
    async runMission(item, lockId) {
        let result;
        try {
            result = await this.executor.execute(item);
        }
        catch (err) {
            result = { status: 'failed', error: err instanceof Error ? err.message : String(err) };
        }
        const q = this.read();
        const ref = q.items.find((m) => m.id === item.id);
        if (ref) {
            if (result.status === 'completed') {
                ref.status = 'completed';
                ref.completedAt = new Date().toISOString();
                ref.lastError = undefined;
            }
            else {
                ref.lastError = result.error;
                const nonRetryable = result.retryable === false;
                if (nonRetryable || ref.attempts >= this.maxAttempts) {
                    // Terminal: either explicitly non-retryable (e.g. governance
                    // block) or out of attempts.
                    ref.status = 'failed';
                    ref.completedAt = new Date().toISOString();
                }
                else {
                    // Re-queue for another attempt
                    ref.status = 'pending';
                    ref.startedAt = undefined;
                }
            }
            this.writeItems(q.items);
        }
        this.lock.release(lockId);
        this.running.delete(item.id);
    }
    read() {
        if (!fs.existsSync(this.queueFile))
            return { items: [] };
        try {
            const raw = JSON.parse(fs.readFileSync(this.queueFile, 'utf-8'));
            if (Array.isArray(raw.items))
                return raw;
            return { items: [] };
        }
        catch {
            return { items: [] };
        }
    }
    write(payload) {
        payload.lastFlushedAt = new Date().toISOString();
        (0, AtomicWriter_1.writeJsonAtomic)(this.queueFile, payload);
    }
    writeItems(items) {
        this.write({ items, lastFlushedAt: new Date().toISOString() });
    }
    /** Exposed for tests. */
    _queueFile() {
        return this.queueFile;
    }
}
exports.QueueScheduler = QueueScheduler;
