import fs from "node:fs";
import path from "path";
import { resolveStateDir } from "../config/paths.js";
import { diagnosticLogger as diag } from "../logging/diagnostic.js";
const DB_FILENAME = "command-queue.db";
let db = null;
export function initQueueDB(customDbPath) {
    if (db) {
        return db;
    }
    const dbPath = customDbPath ?? path.join(resolveStateDir(), DB_FILENAME);
    diag.debug(`Initializing command queue database at ${dbPath}`);
    // Ensure directory exists
    const dir = path.dirname(dbPath);
    if (!fs.existsSync(dir)) {
        fs.mkdirSync(dir, { recursive: true });
    }
    let DatabaseCtor;
    try {
        DatabaseCtor = require("better-sqlite3");
    }
    catch {
        throw new Error(`[openclaw] persistent queue requires better-sqlite3 which is not installed.\n` +
            `Run: npm install better-sqlite3\n` +
            `Or set queue.mode to "memory" in your openclaw.json to disable persistence.`);
    }
    db = new DatabaseCtor(dbPath /*, { verbose: console.log } */);
    db.pragma("journal_mode = WAL");
    // Schema creation
    db.exec(`
    CREATE TABLE IF NOT EXISTS task_queue (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      lane TEXT NOT NULL,
      task_type TEXT NOT NULL,
      payload TEXT NOT NULL,
      status TEXT NOT NULL,
      error_msg TEXT,
      result TEXT,
      retry_count INTEGER DEFAULT 0,
      created_at INTEGER NOT NULL,
      updated_at INTEGER NOT NULL
    );
    CREATE INDEX IF NOT EXISTS idx_task_queue_lane_status ON task_queue(lane, status);
  `);
    // Add result column for legacy tables (if not exists)
    try {
        db.exec(`ALTER TABLE task_queue ADD COLUMN result TEXT`);
    }
    catch {
        // Column already exists, ignore
    }
    // pending_followup table: preserves queued messages in the followup queue to prevent loss on process crash
    db.exec(`
    CREATE TABLE IF NOT EXISTS pending_followup (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      queue_key TEXT NOT NULL,
      payload TEXT NOT NULL,
      created_at INTEGER NOT NULL
    );
    CREATE INDEX IF NOT EXISTS idx_pending_followup_key ON pending_followup(queue_key);
  `);
    return db;
}
export function closeQueueDB() {
    if (db) {
        db.close();
        db = null;
    }
}
export function insertTask(lane, taskType, payload) {
    const conn = initQueueDB();
    const now = Date.now();
    const stmt = conn.prepare(`
    INSERT INTO task_queue (lane, task_type, payload, status, created_at, updated_at)
    VALUES (?, ?, ?, ?, ?, ?)
  `);
    const info = stmt.run(lane, taskType, JSON.stringify(payload), "PENDING", now, now);
    return info.lastInsertRowid;
}
// Record the time when this module is loaded (i.e. gateway startup time)
const moduleInitTime = Date.now();
// Atomically claim a task during dequeue to prevent concurrent runners from executing the same task
export function claimNextPendingTask(lane) {
    const conn = initQueueDB();
    const now = Date.now();
    let claimedTask = null;
    // Use a transaction to ensure atomicity of SELECT and UPDATE
    const transaction = conn.transaction(() => {
        // Find the oldest PENDING task in this lane
        const row = conn
            .prepare(`
      SELECT * FROM task_queue
      WHERE lane = ? AND status = 'PENDING'
      ORDER BY created_at ASC
      LIMIT 1
    `)
            .get(lane);
        if (row) {
            // Mark as RUNNING
            conn
                .prepare(`
        UPDATE task_queue
        SET status = 'RUNNING', updated_at = ?
        WHERE id = ?
      `)
                .run(now, row.id);
            // Inject __isRecoveredTask flag if this task is from a previous process execution
            let finalPayload = row.payload;
            if (row.created_at < moduleInitTime) {
                try {
                    const parsed = JSON.parse(finalPayload);
                    parsed.__isRecoveredTask = true;
                    finalPayload = JSON.stringify(parsed);
                }
                catch {
                    // ignore parsing error if it's not JSON
                }
            }
            claimedTask = { ...row, payload: finalPayload, status: "RUNNING", updated_at: now };
        }
    });
    transaction();
    return claimedTask;
}
export function resolveTask(id, result) {
    const conn = initQueueDB();
    const resultJson = result !== undefined ? JSON.stringify(result) : null;
    conn
        .prepare(`
    UPDATE task_queue
    SET status = 'COMPLETED', result = ?, updated_at = ?
    WHERE id = ?
  `)
        .run(resultJson, Date.now(), id);
}
/**
 * Query the result of a completed task, useful for result retrieval after process restart.
 * Returns null if the task does not exist or has not yet completed.
 */
export function getTaskResult(id) {
    const conn = initQueueDB();
    const row = conn
        .prepare("SELECT status, result, error_msg FROM task_queue WHERE id = ?")
        .get(id);
    if (!row)
        return null;
    return {
        status: row.status,
        result: row.result ? JSON.parse(row.result) : null,
        error_msg: row.error_msg,
    };
}
/**
 * Query the latest N completed/failed task results for a given taskType.
 * Useful for batch recovery after process restart.
 */
export function getRecentResults(taskType, limit = 20) {
    const conn = initQueueDB();
    const rows = conn
        .prepare(`SELECT id, status, result, error_msg, created_at FROM task_queue
       WHERE task_type = ? AND status IN ('COMPLETED', 'FAILED')
       ORDER BY updated_at DESC LIMIT ?`)
        .all(taskType, limit);
    return rows.map((r) => ({
        id: r.id,
        status: r.status,
        result: r.result ? JSON.parse(r.result) : null,
        error_msg: r.error_msg,
        created_at: r.created_at,
    }));
}
export function rejectTask(id, errorMsg) {
    const conn = initQueueDB();
    conn
        .prepare(`
    UPDATE task_queue
    SET status = 'FAILED', error_msg = ?, updated_at = ?
    WHERE id = ?
  `)
        .run(errorMsg, Date.now(), id);
}
// Counting functions
export function countQueueByStatus(lane, status) {
    const conn = initQueueDB();
    if (status) {
        const row = conn
            .prepare("SELECT COUNT(*) as cnt FROM task_queue WHERE lane = ? AND status = ?")
            .get(lane, status);
        return row.cnt;
    }
    else {
        const row = conn
            .prepare("SELECT COUNT(*) as cnt FROM task_queue WHERE lane = ? AND status IN ('PENDING', 'RUNNING')")
            .get(lane);
        return row.cnt;
    }
}
export function countTotalQueue() {
    const conn = initQueueDB();
    const row = conn
        .prepare("SELECT COUNT(*) as cnt FROM task_queue WHERE status IN ('PENDING', 'RUNNING')")
        .get();
    return row.cnt;
}
export function clearLaneTasks(lane) {
    const conn = initQueueDB();
    const info = conn
        .prepare("DELETE FROM task_queue WHERE lane = ? AND status = 'PENDING'")
        .run(lane);
    return info.changes;
}
/**
 * Get the list of PENDING task IDs for a given lane (called before clearLaneTasks to reject in-memory Promises).
 */
export function getPendingTaskIdsForLane(lane) {
    const conn = initQueueDB();
    const rows = conn
        .prepare("SELECT id FROM task_queue WHERE lane = ? AND status = 'PENDING'")
        .all(lane);
    return rows.map((r) => r.id);
}
// Recover deadlocked tasks
export function recoverRunningTasks() {
    const conn = initQueueDB();
    const affectedLanes = new Set();
    const transaction = conn.transaction(() => {
        // Find all tasks in RUNNING status
        const rows = conn
            .prepare("SELECT DISTINCT lane FROM task_queue WHERE status = 'RUNNING'")
            .all();
        rows.forEach((r) => affectedLanes.add(r.lane));
        // Reset them to PENDING
        conn
            .prepare("UPDATE task_queue SET status = 'PENDING', updated_at = ? WHERE status = 'RUNNING'")
            .run(Date.now());
    });
    transaction();
    return Array.from(affectedLanes);
}
// Check if there are any unfinished tasks (for graceful shutdown)
export function hasActiveTasks() {
    const conn = initQueueDB();
    const row = conn
        .prepare(`SELECT COUNT(*) as cnt FROM task_queue WHERE status = 'RUNNING'`)
        .get();
    return row.cnt > 0;
}
// Get all lanes that have PENDING tasks (used for recovery on startup)
export function getPendingLanes() {
    const conn = initQueueDB();
    const rows = conn
        .prepare("SELECT DISTINCT lane FROM task_queue WHERE status = 'PENDING'")
        .all();
    return rows.map((r) => r.lane);
}
// Mark stale PENDING/RUNNING tasks as FAILED (called on process restart)
export function markStaleTasks(reason = "stale: process restarted") {
    const conn = initQueueDB();
    const info = conn
        .prepare("UPDATE task_queue SET status = 'FAILED', error_msg = ?, updated_at = ? WHERE status IN ('PENDING', 'RUNNING')")
        .run(reason, Date.now());
    return info.changes;
}
// Get recoverable PENDING tasks (with full payload, used for channel recovery)
export function getRecoverableTasks() {
    const conn = initQueueDB();
    return conn
        .prepare("SELECT * FROM task_queue WHERE status = 'PENDING' ORDER BY id ASC")
        .all();
}
// ────────────────────────────────────────────────────────────────────────────
// pending_followup operations: preserve queued messages in the followup queue
// ────────────────────────────────────────────────────────────────────────────
/** Persist a followup queued item to SQLite */
export function insertPendingFollowup(queueKey, payload) {
    const conn = initQueueDB();
    const now = Date.now();
    const info = conn
        .prepare("INSERT INTO pending_followup (queue_key, payload, created_at) VALUES (?, ?, ?)")
        .run(queueKey, JSON.stringify(payload), now);
    return info.lastInsertRowid;
}
/** Query all pending followup items (used during startup recovery) */
export function getAllPendingFollowups() {
    const conn = initQueueDB();
    return conn
        .prepare("SELECT id, queue_key, payload, created_at FROM pending_followup ORDER BY id ASC")
        .all();
}
/** Delete all pending followup items for a given queue_key (called after drain completes) */
export function clearPendingFollowupsByKey(queueKey) {
    const conn = initQueueDB();
    const info = conn.prepare("DELETE FROM pending_followup WHERE queue_key = ?").run(queueKey);
    return info.changes;
}
/** Delete all pending followup items (called after full recovery is complete) */
export function clearAllPendingFollowups() {
    const conn = initQueueDB();
    const info = conn.prepare("DELETE FROM pending_followup").run();
    return info.changes;
}
