// Slack adapter for DewLink — Socket Mode (receive over WSS) + Web API (send).
// Emits the same @@DEWLINK@@ events as the other channels; each Slack channel/DM = a conversation.
//
// Needs TWO tokens:
//   SLACK_APP_TOKEN  (xapp-…)  — app-level token, opens the Socket Mode websocket
//   SLACK_BOT_TOKEN  (xoxb-…)  — bot token, sends via chat.postMessage
//
// Socket Mode flow: POST apps.connections.open (app token) → {url} → connect WSS →
//   "hello" → events_api envelopes (ACK each by echoing envelope_id) → message events.

import WebSocket from "ws";
import { emitEvent } from "./events.ts";

const APP_TOKEN = process.env.SLACK_APP_TOKEN || "";
const BOT_TOKEN = process.env.SLACK_BOT_TOKEN || "";
const API = "https://slack.com/api";

let ws: WebSocket | null = null;
let running = false;
let botUserId: string | null = null;

function log(level: "info" | "warn" | "error", msg: string) {
  emitEvent({ type: "log", level, msg: `[slack] ${msg}` });
}

async function slackPost(method: string, token: string, body?: any) {
  const res = await fetch(`${API}/${method}`, {
    method: "POST",
    headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json; charset=utf-8" },
    body: body ? JSON.stringify(body) : undefined,
  });
  return res.json() as Promise<any>;
}

export async function startSlack() {
  if (!APP_TOKEN || !BOT_TOKEN) {
    log("error", "missing SLACK_APP_TOKEN or SLACK_BOT_TOKEN");
    emitEvent({ type: "status", state: "logged_out" });
    return;
  }
  running = true;
  // Learn our own bot user id so we can skip our own messages.
  try {
    const auth = await slackPost("auth.test", BOT_TOKEN);
    if (auth?.ok) { botUserId = auth.user_id; emitEvent({ type: "status", state: "open", user: auth.user ?? null }); }
  } catch (e: any) { log("warn", `auth.test: ${e?.message}`); }
  connect();
}

async function connect() {
  emitEvent({ type: "status", state: "connecting" });
  let url: string;
  try {
    const open = await slackPost("apps.connections.open", APP_TOKEN);
    if (!open?.ok || !open.url) { log("error", `apps.connections.open: ${open?.error || "no url"}`);
      if (open?.error === "invalid_auth") { emitEvent({ type: "status", state: "logged_out" }); return; }
      return reconnect(); }
    url = open.url;
  } catch (e: any) { log("warn", `connect: ${e?.message}`); return reconnect(); }

  ws = new WebSocket(url);
  ws.on("message", (raw: WebSocket.RawData) => {
    let env: any;
    try { env = JSON.parse(raw.toString()); } catch { return; }
    if (env.type === "hello") { emitEvent({ type: "status", state: "open", user: botUserId }); return; }
    if (env.type === "disconnect") { return reconnect(); }   // Slack asks us to reopen
    // ACK every envelope that carries one, immediately, before processing.
    if (env.envelope_id) ws?.send(JSON.stringify({ envelope_id: env.envelope_id }));
    if (env.type === "events_api") handleEvent(env.payload);
  });
  ws.on("close", () => { emitEvent({ type: "status", state: "close" }); if (running) reconnect(); });
  ws.on("error", (e: any) => log("warn", `ws: ${e?.message}`));
}

function reconnect() {
  try { ws?.removeAllListeners(); ws?.close(); } catch { /* ignore */ }
  ws = null;
  if (running) setTimeout(connect, 3000);
}

function handleEvent(payload: any) {
  const e = payload?.event;
  if (!e || e.type !== "message") return;
  if (e.subtype && e.subtype !== "") return;          // skip joins/edits/bot_message/etc.
  if (e.bot_id) return;                               // skip bot posts
  if (botUserId && e.user === botUserId) return;      // skip our own
  if (!e.channel || !e.text) return;
  emitEvent({
    type: "message",
    convExternalId: String(e.channel),
    convTitle: e.user ? String(e.user) : String(e.channel),
    direction: "IN",
    body: String(e.text),
    sender: e.user ? String(e.user) : null,
    ts: e.ts ? Math.round(parseFloat(e.ts) * 1000) : Date.now(),
    providerMsgId: `sl:${e.channel}:${e.ts}`,
    historical: false,
  });
}

/** Map the app's quick emojis to Slack reaction shortcodes (Slack wants a name, not the glyph). */
const SLACK_EMOJI: Record<string, string> = {
  "👍": "thumbsup", "❤️": "heart", "😂": "joy", "😮": "open_mouth", "🙏": "pray", "✅": "white_check_mark",
};
/** React to a message. `channel` = slack channel id, `ts` = message timestamp, emoji = glyph (mapped). */
export async function reactSlack(channel: string, ts: string, emoji: string) {
  const name = SLACK_EMOJI[emoji] || "thumbsup";
  const res = await slackPost("reactions.add", BOT_TOKEN, { channel, timestamp: ts, name });
  if (!res?.ok && res?.error !== "already_reacted") throw new Error(`slack react: ${res?.error || "failed"}`);
}

export async function sendSlack(channelId: string, text: string) {
  const res = await slackPost("chat.postMessage", BOT_TOKEN, { channel: channelId, text });
  if (!res?.ok) throw new Error(`slack send: ${res?.error || "failed"}`);
  return res;
}
