import { pino } from "pino";
import os from "node:os";
import fs from "node:fs";
import { initializeDatabase } from "./database.ts";
import { startWhatsAppConnection, sendWhatsAppMessage, sendWhatsAppAudio, fetchAvatar, drainPendingAvatars, loadMoreHistory, backfillOnce, reactToMessage, retryMediaDownload, markChatRead, type WhatsAppSocket } from "./whatsapp.ts";
import { emitEvent } from "./events.ts";
import readline from "node:readline";

const account = process.env.WHATSAPP_ACCOUNT_NAME || "default";
const home = process.env.HOME || os.homedir();
const dataDir = process.env.WHATSAPP_MCP_DATA_DIR || `${home}/.whatsapp-baileys/${account}`;
fs.mkdirSync(dataDir, { recursive: true });

const waLogger = pino(
  { level: process.env.LOG_LEVEL || "info", timestamp: pino.stdTimeFunctions.isoTime },
  pino.destination(`${dataDir}/wa-logs.txt`),
);
const mcpLogger = pino(
  { level: process.env.LOG_LEVEL || "info", timestamp: pino.stdTimeFunctions.isoTime },
  pino.destination(`${dataDir}/mcp-logs.txt`),
);

async function main() {
  mcpLogger.info(`Starting DewLink engine for account "${account}"...`);
  let whatsappSocket: WhatsAppSocket | null = null;

  try {
    initializeDatabase();
    whatsappSocket = await startWhatsAppConnection(waLogger);
  } catch (error: any) {
    mcpLogger.fatal({ err: error }, "Failed during WhatsApp connection attempt");
    emitEvent({ type: "log", level: "error", msg: `init failed: ${error?.message}` });
    process.exit(1);
  }

  // NOTE: the per-account MCP server (mcp.ts) is intentionally NOT started here. The unified MCP
  // (dewx-mcp.ts, one process for the whole app) is the sole MCP surface — it covers inbox/CRM/send
  // across every channel. Running a second HTTP MCP per WhatsApp engine was redundant (extra memory +
  // a port each) and its only mutating tool (send_message) is already approval-gated there too.

  // Command channel: DewLink (Swift) sends newline-delimited JSON commands on stdin.
  // Supported: {"cmd":"send","chatId","text"} · {"cmd":"sendAudio","chatId","path"}
  const rl = readline.createInterface({ input: process.stdin });
  rl.on("line", async (line) => {
    const trimmed = line.trim();
    if (!trimmed) return;
    try {
      const cmd = JSON.parse(trimmed);
      if (cmd.cmd === "send" && cmd.chatId && typeof cmd.text === "string") {
        const result = await sendWhatsAppMessage(waLogger, whatsappSocket, cmd.chatId, cmd.text);
        const providerMsgId = result?.key?.id ? String(result.key.id) : undefined;
        emitEvent({
          type: "sendAck",
          draftId: typeof cmd.draftId === "string" ? cmd.draftId : undefined,
          providerMsgId,
          status: providerMsgId ? "sent" : "failed",
          error: providerMsgId ? undefined : "WhatsApp did not return a provider message id",
        });
        // The outgoing message echoes back via messages.upsert → "message" event (direction OUT).
      } else if (cmd.cmd === "sendAudio" && cmd.chatId && typeof cmd.path === "string") {
        await sendWhatsAppAudio(waLogger, whatsappSocket, cmd.chatId, cmd.path);
      } else if (cmd.cmd === "fetchAvatar" && typeof cmd.jid === "string") {
        waLogger.info(`[cmd] fetchAvatar ${cmd.jid}`);
        const p = await fetchAvatar(cmd.jid, waLogger);
        if (p) emitEvent({ type: "avatar", jid: cmd.jid, path: p });
      } else if (cmd.cmd === "loadMore" && typeof cmd.chatId === "string") {
        waLogger.info(`[cmd] loadMore ${cmd.chatId}`);
        await loadMoreHistory(cmd.chatId, typeof cmd.count === "number" ? cmd.count : 50, waLogger);
      } else if (cmd.cmd === "backfill") {
        waLogger.info(`[cmd] backfill (force)`);
        backfillOnce(waLogger, true);   // replay whatsapp.db → inbox on demand
      } else if (cmd.cmd === "react" && typeof cmd.chatId === "string" && typeof cmd.messageId === "string") {
        await reactToMessage(cmd.chatId, cmd.messageId, !!cmd.fromMe, typeof cmd.emoji === "string" ? cmd.emoji : "", waLogger);
      } else if (cmd.cmd === "markRead" && typeof cmd.chatId === "string") {
        const messageIds = Array.isArray(cmd.messageIds)
          ? cmd.messageIds.filter((id: unknown): id is string => typeof id === "string" && id.trim().length > 0)
          : undefined;
        await markChatRead(cmd.chatId, waLogger, messageIds);
      } else if (cmd.cmd === "downloadMedia" && typeof cmd.chatId === "string" && typeof cmd.messageId === "string") {
        await retryMediaDownload(cmd.chatId, cmd.messageId, !!cmd.fromMe, waLogger, typeof cmd.participant === "string" ? cmd.participant : null);
      }
    } catch (err: any) {
      emitEvent({ type: "log", level: "error", msg: `bad command: ${err?.message}` });
    }
  });

  emitEvent({ type: "ready" });
  mcpLogger.info("DewLink engine ready.");
  // Periodically retry unresolved avatars — the LID↔phone map fills over time.
  setInterval(() => drainPendingAvatars(waLogger), 30_000);
}

function shutdown(signal: string) {
  mcpLogger.info(`Received ${signal}. Shutting down...`);
  waLogger.flush();
  mcpLogger.flush();
  process.exit(0);
}
process.on("SIGINT", () => shutdown("SIGINT"));
process.on("SIGTERM", () => shutdown("SIGTERM"));

// Async-runtime safety net. Without these, an unhandled rejection in any
// background callback (a Baileys socket/message handler, the avatar setInterval,
// a media download) crashes the daemon silently — the "engine keeps dying with
// no trace" class. A rejected promise should NOT kill the comms daemon, so we
// log and keep running; an uncaught exception leaves the process in an undefined
// state, so we log + flush + exit(1) and let the Swift EngineManager restart it
// cleanly — same end-state as the old default crash, but now with a logged cause.
process.on("unhandledRejection", (reason: any) => {
  mcpLogger.error({ err: reason }, "unhandledRejection (kept alive)");
  emitEvent({ type: "log", level: "error", msg: `unhandledRejection: ${reason?.message ?? reason}` });
});
process.on("uncaughtException", (error: any) => {
  mcpLogger.fatal({ err: error }, "uncaughtException — flushing + exiting for restart");
  emitEvent({ type: "log", level: "error", msg: `uncaughtException: ${error?.message ?? error}` });
  waLogger.flush();
  mcpLogger.flush();
  process.exit(1);
});

main().catch((error) => {
  mcpLogger.fatal({ err: error }, "Unhandled error during startup");
  waLogger.flush();
  mcpLogger.flush();
  process.exit(1);
});
