/**
 * Unified Chat Monitor
 *
 * Watches local SQLite message databases for multiple chat applications
 * (WeChat, Feishu/Lark, etc.) and triggers VisionClaw wake-ups when new
 * messages arrive.
 *
 * Each chat app is defined as an adapter with:
 *   - detect()       — locate the local database directory
 *   - watchTargets() — list of files to watch for changes
 *   - channel        — queue channel name
 *   - sender         — display name for the injected wake message
 *   - wakeText       — message text injected into the queue
 *
 * Usage:
 *   node scripts/chat-monitor.mjs
 *
 * Run under pm2 (recommended):
 *   pm2 start scripts/chat-monitor.mjs --name chat-monitor
 *   pm2 save
 *
 * Environment variables:
 *   WECHAT_WXID           — override WeChat account ID (e.g. wxid_abc123)
 *   FEISHU_ACCOUNT_HASH   — override Feishu account directory name
 *   VISIONCLAW_PROFILE    — VisionClaw profile name (default: "default")
 *   CHAT_MONITOR_APPS     — comma-separated list of apps to monitor
 *                           (default: all detected). e.g. "wechat,feishu"
 */

import { watch, watchFile } from "node:fs";
import { readdirSync, statSync, existsSync } from "node:fs";
import { DatabaseSync } from "node:sqlite";
import { randomUUID } from "node:crypto";
import { execSync } from "node:child_process";
import path from "node:path";
import os from "node:os";

const HOME = os.homedir();
const LOG_PREFIX = "[chat-monitor]";

// VisionClaw queue database
const PROFILE = process.env.VISIONCLAW_PROFILE || "default";
const QUEUE_DB = path.join(HOME, `.visionclaw/profiles/${PROFILE}/queue.db`);

// Debounce per adapter (keyed by adapter name)
const debounceTimers = new Map();
const DEBOUNCE_MS = 500;

// Cached VisionClaw PID
let cachedPid = null;

// ---------------------------------------------------------------------------
// Chat App Adapters
// ---------------------------------------------------------------------------

const XWECHAT_ROOT = path.join(
  HOME,
  "Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files"
);

const LARKSHELL_ROOT = path.join(
  HOME,
  "Library/Application Support/LarkShell/sdk_storage"
);

const adapters = [
  {
    name: "wechat",
    channel: "wechat-monitor",
    sender: "WeChat Monitor",
    wakeText: "[WeChat] New message detected \u2014 please check WeChat for unread messages.",

    detect() {
      const wxidOverride = process.env.WECHAT_WXID;
      if (wxidOverride) {
        const dbPath = path.join(XWECHAT_ROOT, wxidOverride, "db_storage/message/message_0.db");
        if (existsSync(dbPath)) return { dir: path.dirname(dbPath), label: wxidOverride };
        console.error(`${LOG_PREFIX} WECHAT_WXID set to '${wxidOverride}' but db not found at: ${dbPath}`);
        return null;
      }

      const candidates = [];
      try {
        const entries = readdirSync(XWECHAT_ROOT, { withFileTypes: true });
        for (const entry of entries) {
          if (!entry.isDirectory() || !entry.name.startsWith("wxid_")) continue;
          const dbPath = path.join(XWECHAT_ROOT, entry.name, "db_storage/message/message_0.db");
          try {
            const st = statSync(dbPath);
            candidates.push({ dir: path.dirname(dbPath), label: entry.name, mtimeMs: st.mtimeMs });
          } catch { /* no db */ }
        }
      } catch {
        return null;
      }

      if (candidates.length === 0) return null;
      if (candidates.length > 1) candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
      return candidates[0];
    },

    watchTargets(detected) {
      const db = path.join(detected.dir, "message_0.db");
      return {
        event: [db, db + "-wal", db + "-last.material"],
        poll: [db, db + "-wal"],
      };
    },
  },

  {
    name: "feishu",
    channel: "feishu-monitor",
    sender: "Feishu Monitor",
    wakeText: "[Feishu] New message detected \u2014 please check Feishu for unread messages.",

    detect() {
      const hashOverride = process.env.FEISHU_ACCOUNT_HASH;
      if (hashOverride) {
        const dir = path.join(LARKSHELL_ROOT, hashOverride);
        if (existsSync(path.join(dir, "messages.db"))) return { dir, label: hashOverride };
        console.error(`${LOG_PREFIX} FEISHU_ACCOUNT_HASH set to '${hashOverride}' but messages.db not found`);
        return null;
      }

      const candidates = [];
      try {
        const entries = readdirSync(LARKSHELL_ROOT, { withFileTypes: true });
        for (const entry of entries) {
          if (!entry.isDirectory()) continue;
          const messagesDb = path.join(LARKSHELL_ROOT, entry.name, "messages.db");
          try {
            const st = statSync(messagesDb);
            candidates.push({ dir: path.join(LARKSHELL_ROOT, entry.name), label: entry.name, mtimeMs: st.mtimeMs });
          } catch { /* no db */ }
        }
      } catch {
        return null;
      }

      if (candidates.length === 0) return null;
      if (candidates.length > 1) candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
      return candidates[0];
    },

    watchTargets(detected) {
      const messagesDb = path.join(detected.dir, "messages.db");
      const imDb = path.join(detected.dir, "im.db");
      return {
        event: [messagesDb, messagesDb + "-wal", imDb, imDb + "-wal"],
        poll: [messagesDb, messagesDb + "-wal", imDb, imDb + "-wal"],
      };
    },
  },
];

// ---------------------------------------------------------------------------
// Core logic (shared across all adapters)
// ---------------------------------------------------------------------------

function getVisionClawPid() {
  if (cachedPid !== null) {
    try {
      process.kill(cachedPid, 0);
      return cachedPid;
    } catch {
      cachedPid = null;
    }
  }

  try {
    const out = execSync("pm2 pid visionclaw", { encoding: "utf-8" }).trim();
    const pid = parseInt(out, 10);
    if (!isNaN(pid) && pid > 0) { cachedPid = pid; return pid; }
  } catch { /* pm2 not available */ }

  try {
    const out = execSync("pgrep -f 'bundle\\.cjs.*--profile'", { encoding: "utf-8" }).trim();
    const pid = parseInt(out.split("\n")[0], 10);
    if (!isNaN(pid) && pid > 0) { cachedPid = pid; return pid; }
  } catch { /* no process */ }

  return null;
}

function injectWakeMessage(adapter) {
  let db;
  try {
    db = new DatabaseSync(QUEUE_DB);

    const pending = db.prepare(
      `SELECT COUNT(*) as count FROM messages
       WHERE sender = ?
       AND json_extract(meta, '$.kind') = 'external-wake'`
    ).get(adapter.sender);
    if (pending.count > 0) {
      console.log(`${LOG_PREFIX} [${adapter.name}] Skipped \u2014 ${pending.count} pending message(s) already in queue`);
      return;
    }

    const stmt = db.prepare(
      `INSERT OR IGNORE INTO messages (id, channel, sender, text, attachments, timestamp, meta)
       VALUES (?, ?, ?, ?, ?, ?, ?)`
    );
    stmt.run(
      randomUUID(),
      adapter.channel,
      adapter.sender,
      adapter.wakeText,
      "[]",
      new Date().toISOString(),
      JSON.stringify({ source: adapter.channel, kind: "external-wake" })
    );
    console.log(`${LOG_PREFIX} [${adapter.name}] Injected wake message at ${new Date().toISOString()}`);
  } catch (err) {
    console.error(`${LOG_PREFIX} [${adapter.name}] Failed to inject message: ${err.message}`);
  } finally {
    if (db) { try { db.close(); } catch { /* best-effort */ } }
  }
}

function sendSigusr2() {
  const pid = getVisionClawPid();
  if (!pid) {
    console.warn(`${LOG_PREFIX} VisionClaw PID not found \u2014 skipping SIGUSR2`);
    return;
  }
  try {
    process.kill(pid, "SIGUSR2");
    console.log(`${LOG_PREFIX} Sent SIGUSR2 to VisionClaw (pid ${pid})`);
  } catch (err) {
    cachedPid = null;
    console.error(`${LOG_PREFIX} Failed to send SIGUSR2: ${err.message}`);
  }
}

function onDatabaseChanged(adapter) {
  const existing = debounceTimers.get(adapter.name);
  if (existing) clearTimeout(existing);
  debounceTimers.set(adapter.name, setTimeout(() => {
    debounceTimers.delete(adapter.name);
    console.log(`${LOG_PREFIX} [${adapter.name}] Database changed at ${new Date().toISOString()}`);
    injectWakeMessage(adapter);
    sendSigusr2();
  }, DEBOUNCE_MS));
}

// ---------------------------------------------------------------------------
// Start — detect and watch all available adapters
// ---------------------------------------------------------------------------

const allowedApps = process.env.CHAT_MONITOR_APPS
  ? process.env.CHAT_MONITOR_APPS.split(",").map((s) => s.trim().toLowerCase())
  : null;

const POLL_INTERVAL_MS = 2000;
let totalEventWatchers = 0;
let totalPollWatchers = 0;
let activeAdapters = 0;

for (const adapter of adapters) {
  if (allowedApps && !allowedApps.includes(adapter.name)) {
    console.log(`${LOG_PREFIX} [${adapter.name}] Skipped (not in CHAT_MONITOR_APPS)`);
    continue;
  }

  const detected = adapter.detect();
  if (!detected) {
    console.log(`${LOG_PREFIX} [${adapter.name}] Not detected \u2014 skipping`);
    continue;
  }

  const targets = adapter.watchTargets(detected);
  let adapterEventCount = 0;
  let adapterPollCount = 0;

  for (const file of targets.event) {
    try {
      watch(file, { persistent: true }, (eventType) => {
        if (eventType === "change") onDatabaseChanged(adapter);
      });
      adapterEventCount++;
    } catch { /* file may not exist yet */ }
  }

  for (const file of targets.poll) {
    try {
      watchFile(file, { persistent: true, interval: POLL_INTERVAL_MS }, (curr, prev) => {
        if (curr.mtimeMs !== prev.mtimeMs || curr.size !== prev.size) {
          onDatabaseChanged(adapter);
        }
      });
      adapterPollCount++;
    } catch { /* file may not exist yet */ }
  }

  if (adapterEventCount + adapterPollCount === 0) {
    console.warn(`${LOG_PREFIX} [${adapter.name}] No files could be watched \u2014 skipping`);
    continue;
  }

  activeAdapters++;
  totalEventWatchers += adapterEventCount;
  totalPollWatchers += adapterPollCount;
  console.log(`${LOG_PREFIX} [${adapter.name}] Active \u2014 ${adapterEventCount} event + ${adapterPollCount} poll watchers (account: ${detected.label})`);
}

if (activeAdapters === 0) {
  console.error(`${LOG_PREFIX} No chat apps detected. Exiting.`);
  process.exit(1);
}

console.log(`${LOG_PREFIX} Started. Monitoring ${activeAdapters} app(s) with ${totalEventWatchers} event + ${totalPollWatchers} poll watchers.`);
console.log(`${LOG_PREFIX} Profile: ${PROFILE}`);
console.log(`${LOG_PREFIX} Queue DB: ${QUEUE_DB}`);
