"use strict";
/**
 * CliInbox — file-queue for daemon → core CLI messages.
 *
 * v1.11.0 — Core ↔ Agent Bridge (Phase 4).
 *
 * Why file-queue (not HTTP): the core CLI is short-lived. It isn't always
 * listening on a port. The daemon needs a "leave a note" mechanism that
 * survives the core not being up at the time of the message.
 *
 * Layout:
 *   ~/.openlife/cli-inbox/
 *   ├── <ulid>.json        # CoreInboxMessage
 *   ├── <ulid>.json
 *   └── replies/
 *       └── <ulid>.json    # written by core after handling
 */
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.CliInbox = void 0;
const crypto = __importStar(require("crypto"));
const fs = __importStar(require("fs"));
const os = __importStar(require("os"));
const path = __importStar(require("path"));
const AtomicWriter_1 = require("../orchestrator/util/AtomicWriter");
const DEFAULT_TTL_S = 86_400;
function inboxDir() {
    return path.join(os.homedir(), '.openlife', 'cli-inbox');
}
function repliesDir() {
    return path.join(inboxDir(), 'replies');
}
function ensureDirs() {
    fs.mkdirSync(repliesDir(), { recursive: true });
}
/**
 * Crockford-base32 ULID-ish: 10-char timestamp + 16-char randomness.
 * Not cryptographically perfect but adequate for unique inbox IDs.
 */
function newUlid() {
    const t = Date.now().toString(36).padStart(10, '0').slice(-10);
    const r = crypto.randomBytes(8).toString('hex');
    return `${t}${r}`.toUpperCase();
}
function readJsonSafe(file) {
    try {
        return JSON.parse(fs.readFileSync(file, 'utf-8'));
    }
    catch {
        return null;
    }
}
function isExpired(msg) {
    const ts = Date.parse(msg.createdAt);
    if (!Number.isFinite(ts))
        return false;
    const ageS = (Date.now() - ts) / 1000;
    return ageS > (msg.ttlSeconds || DEFAULT_TTL_S);
}
exports.CliInbox = {
    /**
     * Append a new message to the inbox. Returns the ULID assigned.
     * Used by the daemon (and Telegram /core router in Phase 4).
     */
    enqueue(msg) {
        ensureDirs();
        const id = msg.id || newUlid();
        const full = {
            id,
            createdAt: msg.createdAt || new Date().toISOString(),
            ttlSeconds: msg.ttlSeconds ?? DEFAULT_TTL_S,
            kind: msg.kind,
            origin: msg.origin,
            verb: msg.verb,
            args: msg.args,
            text: msg.text,
            requireSync: msg.requireSync,
            expectedReplyKind: msg.expectedReplyKind,
            targetCwd: msg.targetCwd,
        };
        const file = path.join(inboxDir(), `${id}.json`);
        (0, AtomicWriter_1.writeJsonAtomic)(file, full);
        try {
            fs.chmodSync(file, 0o600);
        }
        catch { /* best-effort */ }
        return id;
    },
    /**
     * Drain pending messages. Deletes expired ones from disk. Returns
     * non-expired messages (caller decides which to consume + delete).
     */
    async drain() {
        ensureDirs();
        const out = [];
        const dir = inboxDir();
        if (!fs.existsSync(dir))
            return out;
        const entries = fs.readdirSync(dir).filter(name => name.endsWith('.json') && !name.startsWith('replies'));
        for (const name of entries) {
            const file = path.join(dir, name);
            if (!fs.statSync(file).isFile())
                continue;
            const msg = readJsonSafe(file);
            if (!msg || !msg.id) {
                // Corrupt — delete to avoid blocking the queue.
                try {
                    fs.unlinkSync(file);
                }
                catch { /* ignore */ }
                continue;
            }
            if (isExpired(msg)) {
                try {
                    fs.unlinkSync(file);
                }
                catch { /* ignore */ }
                continue;
            }
            out.push(msg);
        }
        // Stable order: oldest first.
        out.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
        return out;
    },
    /** Mark a message as handled by deleting its file. */
    acknowledge(id) {
        const file = path.join(inboxDir(), `${id}.json`);
        try {
            fs.unlinkSync(file);
        }
        catch { /* race or already gone */ }
    },
    /** Write a reply file for a synchronous message. */
    reply(id, reply) {
        ensureDirs();
        const file = path.join(repliesDir(), `${id}.json`);
        (0, AtomicWriter_1.writeJsonAtomic)(file, reply);
        try {
            fs.chmodSync(file, 0o600);
        }
        catch { /* best-effort */ }
    },
    /** Read a reply file (returns null if absent). */
    readReply(id) {
        const file = path.join(repliesDir(), `${id}.json`);
        if (!fs.existsSync(file))
            return null;
        return readJsonSafe(file);
    },
    /**
     * Wait for a reply file to appear (polling). Used when the daemon enqueued
     * a message with requireSync=true. Returns null on timeout.
     */
    async waitForReply(id, timeoutMs = 30_000, pollMs = 200) {
        const deadline = Date.now() + timeoutMs;
        while (Date.now() < deadline) {
            const r = this.readReply(id);
            if (r)
                return r;
            await new Promise(resolve => setTimeout(resolve, pollMs));
        }
        return null;
    },
    /** Exposed for tests so they can target the inbox dir. */
    _dirs() {
        return { inbox: inboxDir(), replies: repliesDir() };
    },
};
