import { DatabaseSync } from "node:sqlite";
import path from "node:path";
import { getConfigDir } from "../config/index.js";
import { sanitizeJsonString, sanitizeJsonValue } from "../utils/json-sanitize.js";
import { getMessageDedupKey } from "./message-dedup.js";
import { logger } from "../logger.js";
/**
 * Persistent message queue backed by SQLite (node:sqlite built-in).
 * Incoming channel messages are enqueued here and dequeued by the agent loop.
 */
/**
 * The `meta.kind` value that external monitor scripts must include
 * in queue messages to be recognized as legitimate wake triggers.
 * Any monitor (WeChat, WhatsApp, Feishu, etc.) can use this protocol:
 *   1. Insert a queue row with `meta: { source: "<monitor-name>", kind: "external-wake" }`
 *   2. Send SIGUSR2 to the VisionClaw process
 * The SIGUSR2 handler verifies this marker before waking the loop.
 */
export const EXTERNAL_WAKE_KIND = "external-wake";
export class MessageQueue {
    db;
    stmtEnqueue;
    stmtSelectFirst;
    stmtDeleteById;
    stmtCount;
    stmtClear;
    stmtHasExternalWake;
    stmtSelectProcessed;
    stmtMarkHandled;
    constructor(customDbPath) {
        const dbPath = customDbPath ?? path.join(getConfigDir(), "queue.db");
        this.db = new DatabaseSync(dbPath);
        this.db.exec("PRAGMA journal_mode = WAL");
        this.db.exec(`
      CREATE TABLE IF NOT EXISTS messages (
        id TEXT PRIMARY KEY,
        channel TEXT NOT NULL,
        sender TEXT NOT NULL,
        text TEXT NOT NULL,
        attachments TEXT NOT NULL DEFAULT '[]',
        timestamp TEXT NOT NULL,
        meta TEXT,
        created_at INTEGER NOT NULL DEFAULT (unixepoch())
      )
    `);
        this.db.exec(`
      CREATE TABLE IF NOT EXISTS processed_messages (
        dedup_key TEXT PRIMARY KEY,
        message_id TEXT NOT NULL,
        channel TEXT NOT NULL,
        sender TEXT NOT NULL,
        first_seen_at INTEGER NOT NULL DEFAULT (unixepoch()),
        handled_at INTEGER
      )
    `);
        this.stmtEnqueue = this.db.prepare(`INSERT OR IGNORE INTO messages (id, channel, sender, text, attachments, timestamp, meta)
       VALUES (?, ?, ?, ?, ?, ?, ?)`);
        this.stmtSelectFirst = this.db.prepare(`SELECT * FROM messages ORDER BY created_at ASC LIMIT 1`);
        this.stmtDeleteById = this.db.prepare(`DELETE FROM messages WHERE id = ?`);
        this.stmtCount = this.db.prepare(`SELECT COUNT(*) as count FROM messages`);
        this.stmtClear = this.db.prepare(`DELETE FROM messages`);
        this.stmtHasExternalWake = this.db.prepare(`SELECT COUNT(*) as count FROM messages
       WHERE json_extract(meta, '$.kind') = '${EXTERNAL_WAKE_KIND}'
       AND created_at >= unixepoch() - 10`);
        this.stmtSelectProcessed = this.db.prepare(`SELECT handled_at FROM processed_messages WHERE dedup_key = ?`);
        this.stmtMarkHandled = this.db.prepare(`INSERT INTO processed_messages (dedup_key, message_id, channel, sender, handled_at)
       VALUES (?, ?, ?, ?, unixepoch())
       ON CONFLICT(dedup_key) DO UPDATE SET handled_at = COALESCE(processed_messages.handled_at, unixepoch())`);
    }
    acceptMessage(msg) {
        const dedupKey = getMessageDedupKey(msg);
        logger.log("info", "message_dedup", "message_received", {
            dedupKey,
            messageId: msg.id,
            channel: msg.channel,
            sender: msg.sender,
            timestamp: msg.timestamp,
        });
        const existing = this.stmtSelectProcessed.get(dedupKey);
        const accepted = !existing?.handled_at;
        logger.log("info", "message_dedup", "message_dedup_decision", {
            dedupKey,
            messageId: msg.id,
            channel: msg.channel,
            sender: msg.sender,
            decision: accepted ? "accepted" : "duplicate_skipped",
        });
        return accepted;
    }
    markHandled(msg) {
        const dedupKey = getMessageDedupKey(msg);
        this.stmtMarkHandled.run(dedupKey, msg.id, msg.channel, msg.sender);
        logger.log("info", "message_dedup", "message_handled", {
            dedupKey,
            messageId: msg.id,
            channel: msg.channel,
            sender: msg.sender,
        });
    }
    enqueue(msg) {
        this.stmtEnqueue.run(sanitizeJsonString(msg.id), sanitizeJsonString(msg.channel), sanitizeJsonString(msg.sender), sanitizeJsonString(msg.text), JSON.stringify(sanitizeJsonValue(msg.attachments)), sanitizeJsonString(msg.timestamp), msg.meta ? JSON.stringify(sanitizeJsonValue(msg.meta)) : null);
    }
    dequeue() {
        const row = this.stmtSelectFirst.get();
        if (!row)
            return null;
        this.stmtDeleteById.run(row.id);
        return {
            id: row.id,
            channel: row.channel,
            sender: row.sender,
            text: row.text,
            attachments: JSON.parse(row.attachments),
            timestamp: row.timestamp,
            meta: row.meta ? JSON.parse(row.meta) : undefined,
        };
    }
    peek() {
        const row = this.stmtSelectFirst.get();
        if (!row)
            return null;
        return {
            id: row.id,
            channel: row.channel,
            sender: row.sender,
            text: row.text,
            attachments: JSON.parse(row.attachments),
            timestamp: row.timestamp,
            meta: row.meta ? JSON.parse(row.meta) : undefined,
        };
    }
    isEmpty() {
        const row = this.stmtCount.get();
        return row.count === 0;
    }
    size() {
        const row = this.stmtCount.get();
        return row.count;
    }
    clear() {
        this.stmtClear.run();
    }
    /**
     * Check whether the queue contains a recent external monitor wake message.
     * Used by the SIGUSR2 handler to verify the signal was triggered by a
     * legitimate monitor script (WeChat, WhatsApp, Feishu, etc.) rather than
     * an unrelated source. Any message with `meta.kind = "external-wake"`
     * created within the last 10 seconds qualifies.
     */
    hasExternalWakeMessage() {
        const row = this.stmtHasExternalWake.get();
        return row.count > 0;
    }
    closed = false;
    close() {
        if (this.closed)
            return;
        this.closed = true;
        this.db.close();
    }
}
//# sourceMappingURL=queue.js.map