/**
 * Telegram Bridge — Multi-Tenant Module
 *
 * Manages a single @claudemesh_bot instance with long-polling and a pool of
 * WebSocket connections (one per unique mesh). Multiple Telegram chats can
 * share the same mesh connection; push messages fan out to all chats.
 *
 * This file is self-contained. The broker's index.ts imports bootTelegramBridge
 * and connectChat, passing DB accessor callbacks so we never import db.ts.
 */

import { Bot, InputFile } from "grammy";
import WebSocket from "ws";
import sodium from "libsodium-wrappers";
import { validateTelegramConnectToken } from "./telegram-token";
import { log } from "./logger";

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

export interface BridgeRow {
  chatId: number;
  meshId: string;
  meshSlug?: string;
  /** memberId can be null until the bridge claims a mesh.member row. */
  memberId: string | null;
  pubkey: string;
  secretKey: string;
  displayName: string | null;
  chatType: string | null;
  chatTitle: string | null;
}

interface MeshCredentials {
  meshId: string;
  memberId: string;
  pubkey: string;
  secretKey: string;
  displayName: string;
  brokerUrl: string;
}

interface PeerInfo {
  displayName: string;
  pubkey: string;
  status: string;
  summary?: string;
  cwd?: string;
  groups?: string[];
  avatar?: string;
}

// ---------------------------------------------------------------------------
// Crypto helpers (mirrors apps/telegram/src/index.ts)
// ---------------------------------------------------------------------------

let sodiumReady = false;

async function ensureSodium() {
  if (!sodiumReady) {
    await sodium.ready;
    sodiumReady = true;
  }
  return sodium;
}

async function generateSessionKeypair() {
  const s = await ensureSodium();
  const kp = s.crypto_sign_keypair();
  return {
    publicKey: s.to_hex(kp.publicKey),
    secretKey: s.to_hex(kp.privateKey),
  };
}

async function signHello(
  meshId: string,
  memberId: string,
  pubkey: string,
  secretKeyHex: string,
) {
  const s = await ensureSodium();
  const timestamp = Date.now();
  const canonical = `${meshId}|${memberId}|${pubkey}|${timestamp}`;
  const sig = s.crypto_sign_detached(
    s.from_string(canonical),
    s.from_hex(secretKeyHex),
  );
  return { timestamp, signature: s.to_hex(sig) };
}

async function decryptDirect(
  nonce: string,
  ciphertext: string,
  senderPubkeyHex: string,
  recipientSecretKeyHex: string,
): Promise<string | null> {
  const s = await ensureSodium();
  try {
    const senderPub = s.crypto_sign_ed25519_pk_to_curve25519(
      s.from_hex(senderPubkeyHex),
    );
    const recipientSec = s.crypto_sign_ed25519_sk_to_curve25519(
      s.from_hex(recipientSecretKeyHex),
    );
    const nonceBytes = s.from_base64(nonce, s.base64_variants.ORIGINAL);
    const ciphertextBytes = s.from_base64(
      ciphertext,
      s.base64_variants.ORIGINAL,
    );
    const plain = s.crypto_box_open_easy(
      ciphertextBytes,
      nonceBytes,
      senderPub,
      recipientSec,
    );
    return s.to_string(plain);
  } catch {
    return null;
  }
}

async function encryptDirect(
  message: string,
  recipientPubkeyHex: string,
  senderSecretKeyHex: string,
): Promise<{ nonce: string; ciphertext: string }> {
  const s = await ensureSodium();
  const recipientPub = s.crypto_sign_ed25519_pk_to_curve25519(
    s.from_hex(recipientPubkeyHex),
  );
  const senderSec = s.crypto_sign_ed25519_sk_to_curve25519(
    s.from_hex(senderSecretKeyHex),
  );
  const nonceBytes = s.randombytes_buf(s.crypto_box_NONCEBYTES);
  const ciphertextBytes = s.crypto_box_easy(
    s.from_string(message),
    nonceBytes,
    recipientPub,
    senderSec,
  );
  return {
    nonce: s.to_base64(nonceBytes, s.base64_variants.ORIGINAL),
    ciphertext: s.to_base64(ciphertextBytes, s.base64_variants.ORIGINAL),
  };
}

// ---------------------------------------------------------------------------
// MeshConnection — one WS per unique mesh, shared across chats
// ---------------------------------------------------------------------------

class MeshConnection {
  private ws: WebSocket | null = null;
  private creds: MeshCredentials;
  private sessionPubkey: string | null = null;
  private sessionSecretKey: string | null = null;
  private connected = false;
  private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
  private reconnectAttempt = 0;
  private resolvers = new Map<
    string,
    { resolve: (v: any) => void; timer: ReturnType<typeof setTimeout> }
  >();
  /** pubkey/sessionPubkey → { name, avatar } */
  private peerInfo = new Map<string, { name: string; avatar?: string }>();
  private onPush: (meshId: string, from: string, text: string, priority: string) => void;
  private peerRefreshInterval: ReturnType<typeof setInterval> | null = null;

  constructor(
    creds: MeshCredentials,
    onPush: (meshId: string, from: string, text: string, priority: string) => void,
  ) {
    this.creds = creds;
    this.onPush = onPush;
  }

  async connect(): Promise<void> {
    const sessionKP = await generateSessionKeypair();
    this.sessionPubkey = sessionKP.publicKey;
    this.sessionSecretKey = sessionKP.secretKey;
    await this._connect();
    // Refresh peer name cache every 30 s
    this.peerRefreshInterval = setInterval(
      () => this.listPeers().catch(() => {}),
      30_000,
    );
  }

  private _connect(): Promise<void> {
    return new Promise((resolve, reject) => {
      const ws = new WebSocket(this.creds.brokerUrl);
      this.ws = ws;

      ws.on("open", async () => {
        try {
          const { timestamp, signature } = await signHello(
            this.creds.meshId,
            this.creds.memberId,
            this.creds.pubkey,
            this.creds.secretKey,
          );
          ws.send(
            JSON.stringify({
              type: "hello",
              meshId: this.creds.meshId,
              memberId: this.creds.memberId,
              pubkey: this.creds.pubkey,
              sessionPubkey: this.sessionPubkey,
              displayName: this.creds.displayName,
              sessionId: `tg-bridge-${this.creds.meshId.slice(0, 8)}-${Date.now()}`,
              pid: process.pid,
              cwd: process.cwd(),
              hostname: require("os").hostname(),
              peerType: "bridge",
              channel: "telegram",
              timestamp,
              signature,
            }),
          );
        } catch (e) {
          reject(e);
        }
      });

      const helloTimeout = setTimeout(() => {
        ws.close();
        reject(new Error("hello_ack timeout"));
      }, 10_000);

      ws.on("message", async (raw) => {
        try {
          const msg = JSON.parse(raw.toString()) as Record<string, any>;

          if (msg.type === "hello_ack") {
            clearTimeout(helloTimeout);
            this.connected = true;
            this.reconnectAttempt = 0;
            console.log(
              `[tg-bridge] WS connected to mesh ${this.creds.meshId.slice(0, 8)} as ${this.creds.displayName}`,
            );
            resolve();
            return;
          }

          // Push messages from peers → forward to Telegram
          if (msg.type === "push") {
            let text: string | null = null;
            const senderPubkey =
              msg.senderPubkey ?? msg.senderSessionPubkey;

            if (msg.subtype === "system") {
              const event = msg.event ?? "";
              const data = msg.eventData ?? {};
              if (event === "peer_joined")
                text = `[joined] ${data.displayName ?? "peer"}`;
              else if (event === "peer_left")
                text = `[left] ${data.displayName ?? "peer"}`;
              else if (event === "peer_returned")
                text = `[returned] ${data.name ?? "peer"}`;
              else text = msg.plaintext ?? `[${event}]`;
            } else if (senderPubkey && msg.nonce && msg.ciphertext) {
              // Try session key, then member key
              text =
                (await decryptDirect(
                  msg.nonce,
                  msg.ciphertext,
                  senderPubkey,
                  this.sessionSecretKey!,
                )) ??
                (await decryptDirect(
                  msg.nonce,
                  msg.ciphertext,
                  senderPubkey,
                  this.creds.secretKey,
                ));
              if (!text) text = "[could not decrypt]";
            } else if (msg.plaintext) {
              text = msg.plaintext;
            } else if (msg.ciphertext && !msg.nonce) {
              try {
                text = Buffer.from(msg.ciphertext, "base64").toString("utf-8");
              } catch {
                text = "[decode error]";
              }
            }

            if (text) {
              const info = senderPubkey
                ? this.peerInfo.get(senderPubkey)
                : null;
              const fromName =
                info?.name ?? senderPubkey?.slice(0, 12) ?? "system";
              const avatar = info?.avatar ?? "🤖";
              this.onPush(
                this.creds.meshId,
                `${avatar} ${fromName}`,
                text,
                msg.priority ?? "next",
              );
            }
          }

          // Resolve pending request/response pairs
          const reqId = msg._reqId;
          if (reqId && this.resolvers.has(reqId)) {
            const r = this.resolvers.get(reqId)!;
            clearTimeout(r.timer);
            this.resolvers.delete(reqId);
            r.resolve(msg);
          }
        } catch {
          /* ignore parse errors */
        }
      });

      ws.on("close", () => {
        this.connected = false;
        this.ws = null;
        if (this.reconnectTimer) return;
        const MAX_RECONNECT_ATTEMPTS = 20;
        if (this.reconnectAttempt >= MAX_RECONNECT_ATTEMPTS) {
          console.error(
            `[tg-bridge] mesh ${this.creds.meshId.slice(0, 8)} giving up after ${MAX_RECONNECT_ATTEMPTS} attempts`,
          );
          meshConnections.delete(this.creds.meshId);
          return;
        }
        const delays = [1000, 2000, 4000, 8000, 16000, 30000];
        const delay =
          delays[Math.min(this.reconnectAttempt, delays.length - 1)]!;
        this.reconnectAttempt++;
        console.log(
          `[tg-bridge] mesh ${this.creds.meshId.slice(0, 8)} reconnecting in ${delay}ms (attempt ${this.reconnectAttempt}/${MAX_RECONNECT_ATTEMPTS})`,
        );
        this.reconnectTimer = setTimeout(() => {
          this.reconnectTimer = null;
          this._connect().catch((e) =>
            console.error("[tg-bridge] reconnect failed:", e),
          );
        }, delay);
      });

      ws.on("error", (err) => {
        console.error(
          `[tg-bridge] WS error mesh ${this.creds.meshId.slice(0, 8)}:`,
          err.message,
        );
      });
    });
  }

  // -- Request / Response helpers --

  private makeReqId(): string {
    return Math.random().toString(36).slice(2) + Date.now().toString(36);
  }

  private request(
    msg: Record<string, unknown>,
    timeout = 10_000,
  ): Promise<any> {
    return new Promise((resolve) => {
      const reqId = this.makeReqId();
      const timer = setTimeout(() => {
        this.resolvers.delete(reqId);
        resolve(null);
      }, timeout);
      this.resolvers.set(reqId, { resolve, timer });
      this.ws?.send(JSON.stringify({ ...msg, _reqId: reqId }));
    });
  }

  // -- Public API --

  async sendMessage(
    to: string,
    message: string,
    priority = "next",
  ): Promise<boolean> {
    if (!this.ws || !this.connected) return false;

    const isDirect = /^[0-9a-f]{64}$/.test(to);
    let nonce = "";
    let ciphertext = "";

    if (isDirect) {
      const enc = await encryptDirect(
        message,
        to,
        this.sessionSecretKey!,
      );
      nonce = enc.nonce;
      ciphertext = enc.ciphertext;
    } else {
      ciphertext = Buffer.from(message, "utf-8").toString("base64");
    }

    this.ws.send(
      JSON.stringify({
        type: "send",
        id: this.makeReqId(),
        targetSpec: to,
        priority,
        nonce,
        ciphertext,
      }),
    );
    return true;
  }

  async listPeers(): Promise<PeerInfo[]> {
    const resp = await this.request({ type: "list_peers" });
    if (!resp?.peers) return [];
    return resp.peers.map((p: any) => {
      const name = p.displayName ?? p.pubkey?.slice(0, 12) ?? "?";
      const avatar = p.profile?.avatar;
      const info = { name, avatar };
      if (p.pubkey) this.peerInfo.set(p.pubkey, info);
      if (p.sessionPubkey) this.peerInfo.set(p.sessionPubkey, info);
      return {
        displayName: name,
        pubkey: p.pubkey ?? "",
        status: p.status ?? "unknown",
        summary: p.summary,
        cwd: p.cwd,
        groups: p.groups?.map((g: any) => g.name) ?? [],
        avatar,
      };
    });
  }

  async findPeersByName(name: string): Promise<PeerInfo[]> {
    const peers = await this.listPeers();
    return peers.filter(
      (p) => p.displayName.toLowerCase() === name.toLowerCase(),
    );
  }

  async setSummary(summary: string): Promise<void> {
    this.ws?.send(JSON.stringify({ type: "set_summary", summary }));
  }

  async uploadFile(
    data: Buffer,
    fileName: string,
    tags?: string[],
  ): Promise<string | null> {
    const brokerHttp = this.creds.brokerUrl
      .replace("wss://", "https://")
      .replace("ws://", "http://")
      .replace("/ws", "");
    try {
      const res = await fetch(`${brokerHttp}/upload`, {
        method: "POST",
        headers: {
          "Content-Type": "application/octet-stream",
          "X-Mesh-Id": this.creds.meshId,
          "X-Member-Id": this.creds.memberId,
          "X-File-Name": fileName,
          "X-Tags": JSON.stringify(tags ?? ["telegram"]),
          "X-Persistent": "true",
        },
        body: data,
        signal: AbortSignal.timeout(30_000),
      });
      const body = (await res.json()) as {
        ok?: boolean;
        fileId?: string;
        error?: string;
      };
      if (!res.ok || !body.fileId) return null;
      return body.fileId;
    } catch (e) {
      console.error("[tg-bridge] upload failed:", e);
      return null;
    }
  }

  async getFileUrl(
    fileId: string,
  ): Promise<{ url: string; name: string } | null> {
    const resp = await this.request({ type: "get_file", fileId });
    if (!resp?.url) return null;
    return { url: resp.url, name: resp.name ?? "file" };
  }

  isConnected(): boolean {
    return this.connected;
  }

  getMeshId(): string {
    return this.creds.meshId;
  }

  close(): void {
    if (this.peerRefreshInterval) clearInterval(this.peerRefreshInterval);
    if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
    this.ws?.close();
  }
}

// ---------------------------------------------------------------------------
// Routing maps
// ---------------------------------------------------------------------------

/** chatId → meshIds this chat is connected to */
const chatMeshes = new Map<number, string[]>();

/** meshId → chatIds that should receive push messages */
const meshChats = new Map<string, Set<number>>();

/** meshId → shared WS connection */
const meshConnections = new Map<string, MeshConnection>();

/** meshId → slug (human-readable name) */
const meshSlugs = new Map<string, string>();

// Pending DM picker state: chatId → { message, matches, meshId }
const pendingDMs = new Map<
  number,
  { message: string; matches: PeerInfo[]; meshId: string }
>();

// Pending file upload picker state: chatId → { fileId, fileName, meshId, caption }
const pendingFiles = new Map<
  number,
  { fileId: string; fileName: string; meshId: string; caption: string }
>();

// Pending email verification state: chatId → { email, code, expiresAt, attempts }
const pendingVerifications = new Map<
  number,
  { email: string; code: string; expiresAt: number; attempts: number }
>();

// Conversation state: chatId → which input the bot is waiting for
const conversationState = new Map<number, "awaiting_email" | "awaiting_code">();

/** Pending AI actions awaiting user confirmation */
const pendingAiActions = new Map<string, {
  chatId: number;
  meshIds: string[];
  toolCall: { name: string; input: Record<string, unknown> };
  expiresAt: number;
}>();

/** Chat → mesh slugs mapping for AI context */
const chatMeshSlugs = new Map<number, string[]>();

// Clean expired AI actions every 5 min
setInterval(() => {
  const now = Date.now();
  for (const [k, v] of pendingAiActions) {
    if (now > v.expiresAt) pendingAiActions.delete(k);
  }
}, 5 * 60 * 1000);

/** Invite URL regex: https://claudemesh.com/join/<token> */
const INVITE_URL_RE =
  /https?:\/\/(?:www\.)?claudemesh\.com\/join\/([A-Za-z0-9_\-\.]+)/;

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/** Add a chat ↔ mesh link to the in-memory maps. */
function linkChatMesh(chatId: number, meshId: string): void {
  const meshes = chatMeshes.get(chatId) ?? [];
  if (!meshes.includes(meshId)) {
    meshes.push(meshId);
    chatMeshes.set(chatId, meshes);
  }
  const chats = meshChats.get(meshId) ?? new Set();
  chats.add(chatId);
  meshChats.set(meshId, chats);
}

/** Remove a chat ↔ mesh link from in-memory maps. */
function unlinkChatMesh(chatId: number, meshId: string): void {
  const meshes = chatMeshes.get(chatId);
  if (meshes) {
    const idx = meshes.indexOf(meshId);
    if (idx !== -1) meshes.splice(idx, 1);
    if (meshes.length === 0) chatMeshes.delete(chatId);
  }
  const chats = meshChats.get(meshId);
  if (chats) {
    chats.delete(chatId);
    if (chats.size === 0) meshChats.delete(meshId);
  }
}

/**
 * Resolve which MeshConnection a chat command should target.
 * If the chat is connected to exactly one mesh, return it.
 * If connected to multiple and a prefix is given (e.g. "dev-team"),
 * match by meshId prefix. Otherwise return null (caller should prompt).
 */
function resolveMesh(
  chatId: number,
  meshPrefix?: string,
): MeshConnection | null {
  const meshIds = chatMeshes.get(chatId);
  if (!meshIds || meshIds.length === 0) return null;

  if (meshIds.length === 1) {
    return meshConnections.get(meshIds[0]!) ?? null;
  }

  if (meshPrefix) {
    const lower = meshPrefix.toLowerCase();
    const match = meshIds.find((id) => id.toLowerCase().startsWith(lower));
    if (match) return meshConnections.get(match) ?? null;
    // Also try partial match anywhere in the id
    const partial = meshIds.find((id) => id.toLowerCase().includes(lower));
    if (partial) return meshConnections.get(partial) ?? null;
  }

  return null;
}

/**
 * Parse an optional mesh prefix from command text.
 * Format: "meshSlug:rest" or "meshSlug rest" (for /peers etc.)
 * Returns [meshPrefix | undefined, remainingText].
 */
function parseMeshPrefix(
  chatId: number,
  text: string,
): [string | undefined, string] {
  const meshIds = chatMeshes.get(chatId);
  if (!meshIds || meshIds.length <= 1) return [undefined, text];

  // Try "slug:rest" format
  const colonIdx = text.indexOf(":");
  if (colonIdx > 0 && colonIdx < 40) {
    return [text.slice(0, colonIdx), text.slice(colonIdx + 1).trimStart()];
  }

  // Try "slug rest" — only if first word matches a known meshId
  const spaceIdx = text.indexOf(" ");
  const firstWord = spaceIdx === -1 ? text : text.slice(0, spaceIdx);
  const lower = firstWord.toLowerCase();
  const isSlug = meshIds.some(
    (id) =>
      id.toLowerCase().startsWith(lower) || id.toLowerCase().includes(lower),
  );
  if (isSlug) {
    return [
      firstWord,
      spaceIdx === -1 ? "" : text.slice(spaceIdx + 1).trimStart(),
    ];
  }

  return [undefined, text];
}

// ---------------------------------------------------------------------------
// Push handler — fan out mesh push to Telegram chats
// ---------------------------------------------------------------------------

function createPushHandler(bot: Bot) {
  return (
    meshId: string,
    from: string,
    text: string,
    _priority: string,
  ) => {
    const chatIds = meshChats.get(meshId);
    if (!chatIds || chatIds.size === 0) return;

    const meshLabel = meshSlugs.get(meshId) ?? meshId.slice(0, 12);
    const formatted = `💬 [${meshLabel}] ${from}\n${text}`;

    for (const chatId of chatIds) {
      bot.api
        .sendMessage(chatId, formatted)
        .catch((e: unknown) => {
          console.error(`[tg-bridge] send to chat ${chatId} failed:`, e instanceof Error ? e.message : String(e));
        });
    }
  };
}

/** Escape Markdown v1 special chars for Telegram. */
function escapeMarkdown(s: string): string {
  return s.replace(/([_*\[\]()~`>#+\-=|{}.!\\])/g, "\\$1");
}

/** Strip markdown formatting from AI text responses for plain Telegram display. */
function stripMarkdown(s: string): string {
  return s
    .replace(/\*\*(.*?)\*\*/g, "$1")    // **bold** → bold
    .replace(/\*(.*?)\*/g, "$1")         // *italic* → italic
    .replace(/__(.*?)__/g, "$1")         // __underline__ → underline
    .replace(/~~(.*?)~~/g, "$1")         // ~~strike~~ → strike
    .replace(/`(.*?)`/g, "$1")           // `code` → code
    .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1"); // [text](url) → text
}

// ---------------------------------------------------------------------------
// Bot command handlers
// ---------------------------------------------------------------------------

// ---------------------------------------------------------------------------
// Email verification helpers
// ---------------------------------------------------------------------------

function generateCode(): string {
  return Math.floor(100000 + Math.random() * 900000).toString();
}

async function startEmailVerification(
  ctx: any,
  chatId: number,
  email: string,
  lookupMeshesByEmail: (email: string) => Promise<UserMeshInfo[]>,
  sendVerificationEmail: (email: string, code: string) => Promise<boolean>,
): Promise<void> {
  // Check if email exists in our system
  const meshes = await lookupMeshesByEmail(email);
  if (meshes.length === 0) {
    conversationState.delete(chatId);
    pendingVerifications.delete(chatId);
    await ctx.reply(
      "❌ No claudemesh account found for that email.\n\n" +
        "Sign up at claudemesh.com first, or use a connect link:\n" +
        "`claudemesh connect telegram`",
      { parse_mode: "Markdown" },
    );
    return;
  }

  const code = generateCode();
  const sent = await sendVerificationEmail(email, code);
  if (!sent) {
    conversationState.delete(chatId);
    await ctx.reply("❌ Failed to send verification email. Try again later.");
    return;
  }

  pendingVerifications.set(chatId, {
    email,
    code,
    expiresAt: Date.now() + 10 * 60_000, // 10 min
    attempts: 0,
  });
  conversationState.set(chatId, "awaiting_code");

  const masked = email.replace(/(.{2})(.*)(@.*)/, "$1***$3");
  await ctx.reply(`📬 Verification code sent to ${masked}\n\nEnter the 6-digit code:`);
}

/** Result from looking up a user's meshes by email */
export interface UserMeshInfo {
  userId: string;
  meshId: string;
  meshSlug: string;
  memberId: string;
  pubkey: string;
  secretKey: string;
}

function setupBotCommands(
  bot: Bot,
  botToken: string,
  brokerUrl: string,
  saveBridge: (
    row: Omit<BridgeRow, "chatId"> & { chatId: number },
  ) => Promise<void>,
  deactivateBridge: (chatId: number, meshId: string) => Promise<void>,
  pushHandler: (
    meshId: string,
    from: string,
    text: string,
    priority: string,
  ) => void,
  lookupMeshesByEmail: (email: string) => Promise<UserMeshInfo[]>,
  sendVerificationEmail: (email: string, code: string) => Promise<boolean>,
): void {
  // --- /start <token> ---
  bot.command("start", async (ctx) => {
    const token = ctx.match?.trim();
    if (!token) {
      await ctx.reply(
        "🔗 *Claudemesh Telegram Bridge*\n\n" +
          "Use a connect link from the dashboard or CLI to get started.\n" +
          "Or type /connect to link via email.\n\n" +
          "Commands: /help",
        { parse_mode: "Markdown" },
      );
      return;
    }

    // Validate JWT signature, expiry, and claims
    const encKey = process.env.BROKER_ENCRYPTION_KEY;
    if (!encKey) {
      await ctx.reply("❌ Broker not configured for token validation.");
      return;
    }

    const payload = validateTelegramConnectToken(token, encKey);
    if (!payload) {
      await ctx.reply("❌ Invalid, expired, or tampered token. Request a new link.");
      return;
    }

    const { meshId, memberId, pubkey, secretKey, meshSlug } = payload;

    const chatId = ctx.chat.id;
    const chatType = ctx.chat.type;
    const chatTitle =
      ctx.chat.type === "private"
        ? (ctx.from?.first_name ?? "Private")
        : ("title" in ctx.chat ? ctx.chat.title : null) ?? "Group";
    const displayName = `tg:${chatTitle}`;

    // Check if already connected
    const existing = chatMeshes.get(chatId);
    if (existing?.includes(meshId)) {
      await ctx.reply(`Already connected to mesh \`${meshSlug ?? meshId.slice(0, 8)}\`.`, {
        parse_mode: "Markdown",
      });
      return;
    }

    try {
      // Persist bridge row
      await saveBridge({
        chatId,
        meshId,
        memberId,
        pubkey,
        secretKey,
        displayName,
        chatType,
        chatTitle,
      });

      // Connect or reuse WS
      await ensureMeshConnection(
        { meshId, memberId, pubkey, secretKey, displayName, brokerUrl },
        pushHandler,
      );

      linkChatMesh(chatId, meshId);
      if (meshSlug) meshSlugs.set(meshId, meshSlug);

      await ctx.reply(
        `✅ Connected to mesh *${escapeMarkdown(meshSlug ?? meshId.slice(0, 8))}*\\!`,
        { parse_mode: "MarkdownV2" },
      );
    } catch (e) {
      console.error("[tg-bridge] /start connect failed:", e);
      await ctx.reply("❌ Connection failed. Try again or request a new token.");
    }
  });

  // --- /connect (email verification flow) ---
  bot.command("connect", async (ctx) => {
    const chatId = ctx.chat.id;

    // If they passed an email inline: /connect user@example.com
    const emailArg = ctx.match?.trim();
    if (emailArg && emailArg.includes("@")) {
      conversationState.set(chatId, "awaiting_code");
      await startEmailVerification(ctx, chatId, emailArg, lookupMeshesByEmail, sendVerificationEmail);
      return;
    }

    conversationState.set(chatId, "awaiting_email");
    await ctx.reply(
      "📧 *Connect via email*\n\nEnter the email address you used to sign up on claudemesh.com:",
      { parse_mode: "Markdown" },
    );
  });

  // --- /disconnect ---
  bot.command("disconnect", async (ctx) => {
    const chatId = ctx.chat.id;
    const meshIds = chatMeshes.get(chatId);
    if (!meshIds || meshIds.length === 0) {
      await ctx.reply("Not connected to any mesh.");
      return;
    }

    const [meshPrefix, _] = parseMeshPrefix(chatId, ctx.match ?? "");
    let targetMeshId: string | undefined;

    if (meshIds.length === 1) {
      targetMeshId = meshIds[0]!;
    } else if (meshPrefix) {
      const lower = meshPrefix.toLowerCase();
      targetMeshId = meshIds.find(
        (id) =>
          id.toLowerCase().startsWith(lower) ||
          id.toLowerCase().includes(lower),
      );
    }

    if (!targetMeshId && meshIds.length > 1) {
      const list = meshIds.map((id) => `• \`${id.slice(0, 12)}\``).join("\n");
      await ctx.reply(
        `Connected to multiple meshes. Specify which:\n${list}\n\n/disconnect <mesh-slug>`,
        { parse_mode: "Markdown" },
      );
      return;
    }

    if (!targetMeshId) {
      await ctx.reply("Mesh not found.");
      return;
    }

    try {
      await deactivateBridge(chatId, targetMeshId);
      unlinkChatMesh(chatId, targetMeshId);

      // If no more chats reference this mesh, close the WS
      const remaining = meshChats.get(targetMeshId);
      if (!remaining || remaining.size === 0) {
        const conn = meshConnections.get(targetMeshId);
        if (conn) {
          conn.close();
          meshConnections.delete(targetMeshId);
        }
      }

      await ctx.reply(`✅ Disconnected from mesh \`${targetMeshId.slice(0, 12)}\`.`, {
        parse_mode: "Markdown",
      });
    } catch (e) {
      console.error("[tg-bridge] /disconnect failed:", e);
      await ctx.reply("❌ Disconnect failed.");
    }
  });

  // --- /meshes ---
  bot.command("meshes", async (ctx) => {
    const meshIds = chatMeshes.get(ctx.chat.id);
    if (!meshIds || meshIds.length === 0) {
      await ctx.reply("Not connected to any mesh. Use a connect link to join.");
      return;
    }
    const lines = meshIds.map((id) => {
      const conn = meshConnections.get(id);
      const status = conn?.isConnected() ? "🟢" : "🔴";
      return `${status} \`${meshSlugs.get(id) ?? id.slice(0, 12)}\``;
    });
    await ctx.reply(`*Connected meshes:*\n${lines.join("\n")}`, {
      parse_mode: "Markdown",
    });
  });

  // --- /peers [mesh-slug] ---
  bot.command("peers", async (ctx) => {
    const chatId = ctx.chat.id;
    const [meshPrefix] = parseMeshPrefix(chatId, ctx.match ?? "");
    const conn = resolveMesh(chatId, meshPrefix);

    if (!conn) {
      const meshIds = chatMeshes.get(chatId);
      if (!meshIds || meshIds.length === 0) {
        await ctx.reply("Not connected to any mesh.");
      } else {
        await ctx.reply(
          "Connected to multiple meshes. Specify which: /peers <mesh-slug>",
        );
      }
      return;
    }

    const peers = await conn.listPeers();
    if (peers.length === 0) {
      await ctx.reply("No peers online.");
      return;
    }
    const lines = peers.map((p) => {
      const icon =
        p.status === "idle" ? "🟢" : p.status === "working" ? "🟡" : "🔴";
      const summary = p.summary ? ` — _${escapeMarkdown(p.summary)}_` : "";
      return `${icon} *${escapeMarkdown(p.displayName)}*${summary}`;
    });
    await ctx.reply(lines.join("\n"), { parse_mode: "Markdown" });
  });

  // --- /dm [mesh:]<name> <message> ---
  bot.command("dm", async (ctx) => {
    const chatId = ctx.chat.id;
    const rawText = ctx.match ?? "";
    if (!rawText.trim()) {
      await ctx.reply("Usage: /dm <peer-name> <message>");
      return;
    }

    const [meshPrefix, text] = parseMeshPrefix(chatId, rawText);
    const conn = resolveMesh(chatId, meshPrefix);
    if (!conn) {
      const meshIds = chatMeshes.get(chatId);
      if (!meshIds || meshIds.length === 0) {
        await ctx.reply("Not connected to any mesh.");
      } else {
        await ctx.reply(
          "Connected to multiple meshes. Prefix with mesh slug: /dm mesh-slug:Mou hello",
        );
      }
      return;
    }

    const spaceIdx = text.indexOf(" ");
    if (spaceIdx === -1) {
      await ctx.reply("Usage: /dm <peer-name> <message>");
      return;
    }

    const target = text.slice(0, spaceIdx);
    const message = text.slice(spaceIdx + 1);
    const matches = await conn.findPeersByName(target);

    if (matches.length === 0) {
      await ctx.reply(`❌ No peer named "${target}" found.`);
      return;
    }

    if (matches.length === 1) {
      const ok = await conn.sendMessage(
        matches[0]!.pubkey,
        `[via Telegram] ${message}`,
        "now",
      );
      await ctx.reply(
        ok
          ? `✅ → ${matches[0]!.avatar ?? "🤖"} ${matches[0]!.displayName}`
          : "❌ Not connected",
      );
      return;
    }

    // Multiple matches — show inline keyboard picker
    pendingDMs.set(chatId, {
      message,
      matches,
      meshId: conn.getMeshId(),
    });
    const buttons = matches.map((p, i) => {
      const dir = p.cwd?.split("/").pop() ?? "?";
      const avatar = p.avatar ?? "🤖";
      return [
        { text: `${avatar} ${p.displayName} (${dir})`, callback_data: `dm:${i}` },
      ];
    });
    buttons.push([{ text: "📨 Send to ALL", callback_data: "dm:all" }]);
    await ctx.reply(`Multiple "${target}" peers online. Pick one or all:`, {
      reply_markup: { inline_keyboard: buttons },
    });
  });

  // --- /broadcast [mesh:] <message> ---
  bot.command("broadcast", async (ctx) => {
    const chatId = ctx.chat.id;
    const [meshPrefix, message] = parseMeshPrefix(chatId, ctx.match ?? "");
    if (!message.trim()) {
      await ctx.reply("Usage: /broadcast <message>");
      return;
    }
    const conn = resolveMesh(chatId, meshPrefix);
    if (!conn) {
      await ctx.reply("Not connected or specify mesh: /broadcast mesh-slug:hello");
      return;
    }
    const ok = await conn.sendMessage("*", `[via Telegram] ${message}`, "now");
    await ctx.reply(ok ? "✅ Broadcast sent" : "❌ Not connected");
  });

  // --- /group [mesh:]@name <message> ---
  bot.command("group", async (ctx) => {
    const chatId = ctx.chat.id;
    const [meshPrefix, text] = parseMeshPrefix(chatId, ctx.match ?? "");
    if (!text.trim()) {
      await ctx.reply("Usage: /group @group-name <message>");
      return;
    }
    const spaceIdx = text.indexOf(" ");
    if (spaceIdx === -1) {
      await ctx.reply("Usage: /group @group-name <message>");
      return;
    }
    const target = text.slice(0, spaceIdx);
    const message = text.slice(spaceIdx + 1);
    const conn = resolveMesh(chatId, meshPrefix);
    if (!conn) {
      await ctx.reply("Not connected or specify mesh.");
      return;
    }
    const ok = await conn.sendMessage(target, `[via Telegram] ${message}`, "now");
    await ctx.reply(ok ? `✅ Sent to ${target}` : "❌ Not connected");
  });

  // --- /file <id> ---
  bot.command("file", async (ctx) => {
    const chatId = ctx.chat.id;
    const fileId = ctx.match?.trim();
    if (!fileId) {
      await ctx.reply("Usage: /file <file-id>");
      return;
    }

    // Try all connected meshes for this chat
    const meshIds = chatMeshes.get(chatId) ?? [];
    for (const meshId of meshIds) {
      const conn = meshConnections.get(meshId);
      if (!conn?.isConnected()) continue;
      const file = await conn.getFileUrl(fileId);
      if (!file) continue;
      try {
        const resp = await fetch(file.url, {
          signal: AbortSignal.timeout(30_000),
        });
        if (!resp.ok) continue;
        const buf = Buffer.from(await resp.arrayBuffer());
        await ctx.replyWithDocument(new InputFile(buf, file.name));
        return;
      } catch {
        continue;
      }
    }
    await ctx.reply(`❌ File \`${fileId}\` not found in any connected mesh.`, {
      parse_mode: "Markdown",
    });
  });

  // --- /status ---
  bot.command("status", async (ctx) => {
    const chatId = ctx.chat.id;
    const meshIds = chatMeshes.get(chatId);
    if (!meshIds || meshIds.length === 0) {
      await ctx.reply("Not connected to any mesh.");
      return;
    }
    const lines = meshIds.map((id) => {
      const conn = meshConnections.get(id);
      const icon = conn?.isConnected() ? "🟢" : "🔴";
      return `${icon} \`${meshSlugs.get(id) ?? id.slice(0, 12)}\``;
    });
    await ctx.reply(
      `*Claudemesh Telegram Bridge*\n${lines.join("\n")}`,
      { parse_mode: "Markdown" },
    );
  });

  // --- /help ---
  bot.command("help", async (ctx) => {
    await ctx.reply(
      "🔗 *Claudemesh Telegram Bridge*\n\n" +
        "*Commands:*\n" +
        "• /start <token> — Connect via deep link\n" +
        "• /connect — Link via email (coming soon)\n" +
        "• /disconnect — Disconnect from mesh\n" +
        "• /meshes — List connected meshes\n" +
        "• /peers — List online peers\n" +
        "• /dm <name> <msg> — DM a peer\n" +
        "• /broadcast <msg> — Message all peers\n" +
        "• /group @name <msg> — Message a group\n" +
        "• /file <id> — Download a mesh file\n" +
        "• /status — Connection status\n\n" +
        "_Multi-mesh: prefix commands with mesh slug_\n" +
        "`/peers dev-team` or `/dm dev-team:Mou hello`",
      { parse_mode: "Markdown" },
    );
  });

  // --- Callback query handler (DM picker + file picker) ---
  bot.on("callback_query:data", async (ctx) => {
    const data = ctx.callbackQuery.data;
    const chatId = ctx.chat?.id;
    if (!chatId) { await ctx.answerCallbackQuery(); return; }

    // --- AI action confirmation ---
    if (data.startsWith("ai_")) {
      const [action, actionId] = data.split(":");
      if (!actionId) { await ctx.answerCallbackQuery(); return; }

      const pending = pendingAiActions.get(actionId);
      if (!pending || pending.chatId !== chatId) {
        await ctx.answerCallbackQuery({ text: "Expired. Send your message again." });
        return;
      }

      if (action === "ai_cancel") {
        pendingAiActions.delete(actionId);
        await ctx.answerCallbackQuery({ text: "Cancelled" });
        await ctx.editMessageText("❌ Cancelled.");
        return;
      }

      if (action === "ai_edit") {
        pendingAiActions.delete(actionId);
        await ctx.answerCallbackQuery({ text: "Type your edited message" });
        await ctx.editMessageText("✏️ Type your message again with corrections.");
        return;
      }

      if (action === "ai_confirm") {
        pendingAiActions.delete(actionId);
        await ctx.answerCallbackQuery({ text: "Executing..." });

        try {
          const { formatResult, recordToolResult } = await import("./telegram-ai");
          const result = await executeAiToolCall(pending.toolCall, pending.meshIds);
          const resultText = formatResult(pending.toolCall.name, result);
          recordToolResult(chatId, pending.toolCall.name, resultText.replace(/<[^>]+>/g, "").slice(0, 200));
          await ctx.editMessageText(resultText, { parse_mode: "HTML" });
        } catch (err) {
          await ctx.editMessageText(`❌ Failed: ${err instanceof Error ? err.message : String(err)}`);
        }
        return;
      }
    }

    // --- File recipient picker ---
    if (data.startsWith("file:")) {
      const pending = pendingFiles.get(chatId);
      if (!pending) {
        await ctx.answerCallbackQuery({ text: "Session expired. Send the file again." });
        return;
      }

      const conn = meshConnections.get(pending.meshId);
      if (!conn?.isConnected()) {
        pendingFiles.delete(chatId);
        await ctx.answerCallbackQuery({ text: "Not connected." });
        return;
      }

      const target = data.slice(5); // after "file:"
      const emoji = pending.fileName.endsWith(".jpg") ? "📷" : "📎";
      const captionSuffix = pending.caption ? ` — "${pending.caption}"` : "";
      const fileMsg = `[via Telegram] ${emoji} ${pending.fileName}${captionSuffix} (file: ${pending.fileId})`;

      if (target === "none") {
        // Keep in mesh only — no message sent
        pendingFiles.delete(chatId);
        await ctx.answerCallbackQuery({ text: "Kept private" });
        await ctx.editMessageText(`🔒 File stored in mesh. ID: \`${pending.fileId}\``, { parse_mode: "Markdown" });
        return;
      }

      if (target === "*") {
        // Broadcast to everyone
        await conn.sendMessage("*", fileMsg, "next");
        pendingFiles.delete(chatId);
        await ctx.answerCallbackQuery({ text: "Sent to everyone" });
        await ctx.editMessageText(`📢 ${emoji} Shared with everyone: \`${pending.fileName}\``, { parse_mode: "Markdown" });
        return;
      }

      // Send to specific peer (target is pubkey prefix)
      const peers = await conn.listPeers();
      const peer = peers.find(p => p.pubkey.startsWith(target));
      if (!peer) {
        await ctx.answerCallbackQuery({ text: "Peer not found" });
        return;
      }

      await conn.sendMessage(peer.pubkey, fileMsg, "now");
      pendingFiles.delete(chatId);
      await ctx.answerCallbackQuery({ text: `Sent to ${peer.displayName}` });
      await ctx.editMessageText(
        `${emoji} Sent to ${peer.avatar ?? "🤖"} *${escapeMarkdown(peer.displayName)}*: \`${escapeMarkdown(pending.fileName)}\``,
        { parse_mode: "Markdown" },
      );
      return;
    }

    // --- DM peer picker ---
    if (!data.startsWith("dm:")) {
      await ctx.answerCallbackQuery();
      return;
    }

    const pending = pendingDMs.get(chatId);
    if (!pending) {
      await ctx.answerCallbackQuery({ text: "Session expired. Send /dm again." });
      return;
    }

    const conn = meshConnections.get(pending.meshId);
    if (!conn?.isConnected()) {
      pendingDMs.delete(chatId);
      await ctx.answerCallbackQuery({ text: "Not connected." });
      return;
    }

    if (data === "dm:all") {
      let sent = 0;
      for (const p of pending.matches) {
        const ok = await conn.sendMessage(
          p.pubkey,
          `[via Telegram] ${pending.message}`,
          "now",
        );
        if (ok) sent++;
      }
      pendingDMs.delete(chatId);
      await ctx.answerCallbackQuery({ text: `Sent to ${sent} peers` });
      await ctx.editMessageText(
        `✅ Sent to all ${sent} ${pending.matches[0]?.displayName ?? "?"} peers`,
      );
      return;
    }

    const idx = parseInt(data.slice(3));
    const peer = pending.matches[idx];
    if (!peer) {
      await ctx.answerCallbackQuery({ text: "Invalid selection" });
      return;
    }

    const ok = await conn.sendMessage(
      peer.pubkey,
      `[via Telegram] ${pending.message}`,
      "now",
    );
    pendingDMs.delete(chatId);
    const dir = peer.cwd?.split("/").pop() ?? "?";
    await ctx.answerCallbackQuery({ text: ok ? "Sent!" : "Failed" });
    await ctx.editMessageText(
      ok
        ? `✅ → ${peer.avatar ?? "🤖"} ${peer.displayName} (${dir})`
        : "❌ Not connected",
    );
  });

  // --- Photo/Document upload → upload to mesh, then show recipient picker ---

  async function handleFileUpload(
    ctx: any,
    tgFileId: string,
    fileName: string,
    isPhoto: boolean,
  ): Promise<void> {
    const chatId = ctx.chat.id;
    const meshIds = chatMeshes.get(chatId);
    if (!meshIds || meshIds.length === 0) return;

    const caption = ctx.message?.caption ?? "";
    const emoji = isPhoto ? "📷" : "📎";

    try {
      const file = await ctx.api.getFile(tgFileId);
      const url = `https://api.telegram.org/file/bot${botToken}/${file.file_path}`;
      const resp = await fetch(url, { signal: AbortSignal.timeout(30_000) });
      const buf = Buffer.from(await resp.arrayBuffer());

      // Upload to first connected mesh
      const meshId = meshIds[0]!;
      const conn = meshConnections.get(meshId);
      if (!conn?.isConnected()) {
        await ctx.reply("❌ Not connected to mesh.");
        return;
      }

      const meshFileId = await conn.uploadFile(buf, fileName, [
        "telegram",
        isPhoto ? "photo" : "document",
      ]);
      if (!meshFileId) {
        await ctx.reply("❌ Upload failed.");
        return;
      }

      // Store pending file and show recipient picker
      pendingFiles.set(chatId, { fileId: meshFileId, fileName, meshId, caption });

      const peers = await conn.listPeers();
      // Filter out the bridge itself
      const targets = peers.filter(p => !p.displayName.startsWith("tg:"));

      if (targets.length === 0) {
        // No peers online — broadcast anyway
        await conn.sendMessage("*", `[via Telegram] ${emoji} ${fileName}${caption ? ` — "${caption}"` : ""} (file: ${meshFileId})`, "next");
        pendingFiles.delete(chatId);
        await ctx.reply(`${emoji} Uploaded and broadcast (no peers online).`);
        return;
      }

      // Build inline keyboard: top peers + Everyone + Keep private
      const buttons: { text: string; callback_data: string }[][] = [];
      const shown = targets.slice(0, 6); // Cap at 6 to avoid huge keyboard
      for (const p of shown) {
        buttons.push([{
          text: `${p.avatar ?? "🤖"} ${p.displayName}`,
          callback_data: `file:${p.pubkey.slice(0, 16)}`,
        }]);
      }
      buttons.push([{ text: "📢 Everyone", callback_data: "file:*" }]);
      buttons.push([{ text: "🔒 Keep in mesh only", callback_data: "file:none" }]);

      await ctx.reply(`${emoji} *Uploaded:* \`${escapeMarkdown(fileName)}\`\nSend to:`, {
        parse_mode: "Markdown",
        reply_markup: { inline_keyboard: buttons },
      });
    } catch (e) {
      await ctx.reply(`❌ ${e instanceof Error ? e.message : String(e)}`);
    }
  }

  bot.on("message:photo", async (ctx) => {
    const photo = ctx.message.photo.at(-1);
    if (!photo) return;
    await handleFileUpload(ctx, photo.file_id, `telegram-photo-${Date.now()}.jpg`, true);
  });

  bot.on("message:document", async (ctx) => {
    const doc = ctx.message.document;
    if (!doc) return;
    await handleFileUpload(ctx, doc.file_id, doc.file_name ?? `telegram-file-${Date.now()}`, false);
  });

  // --- Default text handler: conversation state, invite URLs, @mentions, broadcast ---
  bot.on("message:text", async (ctx) => {
    const chatId = ctx.chat.id;
    const text = ctx.message.text;
    if (text.startsWith("/")) return; // Skip unknown commands

    // --- Email verification conversation state ---
    const state = conversationState.get(chatId);

    if (state === "awaiting_email") {
      const email = text.trim().toLowerCase();
      if (!email.includes("@") || !email.includes(".")) {
        await ctx.reply("That doesn't look like an email. Try again:");
        return;
      }
      await startEmailVerification(ctx, chatId, email, lookupMeshesByEmail, sendVerificationEmail);
      return;
    }

    if (state === "awaiting_code") {
      const pending = pendingVerifications.get(chatId);
      if (!pending) {
        conversationState.delete(chatId);
        await ctx.reply("Session expired. Type /connect to start again.");
        return;
      }

      // Check expiry
      if (Date.now() > pending.expiresAt) {
        pendingVerifications.delete(chatId);
        conversationState.delete(chatId);
        await ctx.reply("⏰ Code expired. Type /connect to get a new one.");
        return;
      }

      const inputCode = text.trim().replace(/\s/g, "");

      // Check attempts
      pending.attempts++;
      if (pending.attempts > 5) {
        pendingVerifications.delete(chatId);
        conversationState.delete(chatId);
        await ctx.reply("❌ Too many attempts. Type /connect to start again.");
        return;
      }

      if (inputCode !== pending.code) {
        await ctx.reply(`❌ Wrong code. ${5 - pending.attempts} attempts left.`);
        return;
      }

      // Code correct — connect to all meshes for this email
      pendingVerifications.delete(chatId);
      conversationState.delete(chatId);

      const meshes = await lookupMeshesByEmail(pending.email);
      if (meshes.length === 0) {
        await ctx.reply("❌ No meshes found. The account may have been removed.");
        return;
      }

      const chatType = ctx.chat.type;
      const chatTitle =
        ctx.chat.type === "private"
          ? (ctx.from?.first_name ?? "Private")
          : ("title" in ctx.chat ? ctx.chat.title : null) ?? "Group";
      const displayName = `tg:${chatTitle}`;

      let connected = 0;
      for (const m of meshes) {
        try {
          // Skip if already connected
          const existing = chatMeshes.get(chatId);
          if (existing?.includes(m.meshId)) { connected++; continue; }

          await saveBridge({
            chatId,
            meshId: m.meshId,
            memberId: m.memberId,
            pubkey: m.pubkey,
            secretKey: m.secretKey,
            displayName,
            chatType,
            chatTitle,
          });
          await ensureMeshConnection(
            { meshId: m.meshId, memberId: m.memberId, pubkey: m.pubkey, secretKey: m.secretKey, displayName, brokerUrl },
            pushHandler,
          );
          linkChatMesh(chatId, m.meshId);
          connected++;
        } catch (e) {
          console.error(`[tg-bridge] /connect failed for mesh ${m.meshId.slice(0, 8)}:`, e);
        }
      }

      if (connected === 0) {
        await ctx.reply("❌ Connection failed for all meshes.");
      } else if (meshes.length === 1) {
        await ctx.reply(
          `✅ Connected to mesh *${escapeMarkdown(meshes[0]!.meshSlug)}*\\!`,
          { parse_mode: "MarkdownV2" },
        );
      } else {
        const names = meshes.map(m => m.meshSlug).join(", ");
        await ctx.reply(`✅ Connected to ${connected} mesh(es): ${names}`);
      }
      return;
    }

    // --- Invite URL detection ---
    const inviteMatch = text.match(INVITE_URL_RE);
    if (inviteMatch) {
      const inviteToken = inviteMatch[1]!;
      await ctx.reply(
        `🔗 Detected invite link.\n\nTo connect, use the deep link from the dashboard or CLI.\nInvite token: \`${inviteToken}\``,
        { parse_mode: "Markdown" },
      );
      return;
    }

    const meshIds = chatMeshes.get(chatId);
    if (!meshIds || meshIds.length === 0) {
      // Not connected — ignore non-command messages
      return;
    }

    // --- @Mention pattern: "@PeerName message" ---
    const mentionMatch = text.match(/^@(\S+)\s+([\s\S]+)$/);
    if (mentionMatch) {
      const target = mentionMatch[1]!;
      const message = mentionMatch[2]!;

      // For multi-mesh, try all connections
      for (const meshId of meshIds) {
        const conn = meshConnections.get(meshId);
        if (!conn?.isConnected()) continue;
        const matches = await conn.findPeersByName(target);
        if (matches.length === 0) continue;

        if (matches.length === 1) {
          const ok = await conn.sendMessage(
            matches[0]!.pubkey,
            `[via Telegram] ${message}`,
            "now",
          );
          await ctx.reply(
            ok
              ? `✅ → ${matches[0]!.avatar ?? "🤖"} ${matches[0]!.displayName}`
              : "❌ Not connected",
          );
          return;
        }

        // Multiple matches — picker
        pendingDMs.set(chatId, { message, matches, meshId });
        const buttons = matches.map((p, i) => {
          const dir = p.cwd?.split("/").pop() ?? "?";
          return [
            {
              text: `${p.avatar ?? "🤖"} ${p.displayName} (${dir})`,
              callback_data: `dm:${i}`,
            },
          ];
        });
        buttons.push([
          { text: "📨 Send to ALL", callback_data: "dm:all" },
        ]);
        await ctx.reply(
          `Multiple "${target}" peers. Pick one or all:`,
          { reply_markup: { inline_keyboard: buttons } },
        );
        return;
      }

      await ctx.reply(`❌ No peer named "${target}" in any connected mesh.`);
      return;
    }

    // --- No mention → process through Claude AI ---
    try {
      const { processMessage, formatConfirmation, formatResult, CONFIRM_ACTIONS } = await import("./telegram-ai");

      // Gather context for the AI
      const firstMeshId = meshIds[0]!;
      const firstConn = meshConnections.get(firstMeshId);
      const allMeshSlugs = meshIds.map(id => meshSlugs.get(id) ?? id.slice(0, 12));
      let recentPeers: string[] = [];
      if (firstConn?.isConnected()) {
        try {
          const peers = await firstConn.listPeers();
          recentPeers = peers.map(p => p.displayName);
        } catch {}
      }

      const result = await processMessage(chatId, text, {
        meshSlug: allMeshSlugs[0],
        meshSlugs: allMeshSlugs,
        userName: ctx.from?.first_name,
        recentPeers,
      });

      if (result.type === "error") {
        await ctx.reply(stripMarkdown(result.text ?? "Something went wrong."));
        return;
      }

      if (result.type === "text") {
        await ctx.reply(stripMarkdown(result.text ?? ""));
        return;
      }

      if (result.type === "tool_call" && result.toolCall) {
        if (result.requiresConfirmation) {
          // Store pending action and show confirmation buttons
          const actionId = `ai_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
          pendingAiActions.set(actionId, {
            chatId,
            meshIds,
            toolCall: result.toolCall,
            expiresAt: Date.now() + 5 * 60 * 1000,
          });

          const confirmText = formatConfirmation(result.toolCall);
          await ctx.reply(confirmText, {
            parse_mode: "HTML",
            reply_markup: {
              inline_keyboard: [
                [
                  { text: "✅ Confirm", callback_data: `ai_confirm:${actionId}` },
                  { text: "✏️ Edit", callback_data: `ai_edit:${actionId}` },
                  { text: "❌ Cancel", callback_data: `ai_cancel:${actionId}` },
                ],
              ],
            },
          });
        } else {
          // Read-only action — execute immediately
          const execResult = await executeAiToolCall(result.toolCall, meshIds);
          const resultText = formatResult(result.toolCall.name, execResult);
          // Record in conversation history
          const { recordToolResult } = await import("./telegram-ai");
          recordToolResult(chatId, result.toolCall.name, resultText.replace(/<[^>]+>/g, "").slice(0, 200));
          await ctx.reply(resultText, {
            parse_mode: "HTML",
          });
        }
      }
    } catch (err) {
      log.error("telegram-ai-handler", { error: err instanceof Error ? err.message : String(err) });
      // Fallback: broadcast the text directly
      let sent = 0;
      for (const meshId of meshIds) {
        const conn = meshConnections.get(meshId);
        if (!conn?.isConnected()) continue;
        const ok = await conn.sendMessage("*", `[via Telegram] ${text}`, "next");
        if (ok) sent++;
      }
      if (sent === 0) await ctx.reply("❌ Not connected.");
    }
  });
}

// ---------------------------------------------------------------------------
// AI tool call executor
// ---------------------------------------------------------------------------

async function executeAiToolCall(
  toolCall: { name: string; input: Record<string, unknown> },
  meshIds: string[],
): Promise<unknown> {
  const firstMeshId = meshIds[0];
  if (!firstMeshId) throw new Error("No mesh connected");

  const conn = meshConnections.get(firstMeshId);
  if (!conn?.isConnected()) throw new Error("Not connected to mesh");

  switch (toolCall.name) {
    case "send_message": {
      const to = String(toolCall.input.to ?? "*");
      const message = String(toolCall.input.message ?? "");
      const priority = String(toolCall.input.priority ?? "next");

      // Resolve peer name → pubkey
      let targetSpec = to;
      if (!to.startsWith("@") && to !== "*" && !/^[0-9a-f]{64}$/.test(to)) {
        const peers = await conn.listPeers();
        const match = peers.find(p => p.displayName.toLowerCase() === to.toLowerCase());
        if (!match) {
          const partials = peers.filter(p => p.displayName.toLowerCase().includes(to.toLowerCase()));
          if (partials.length === 1) targetSpec = partials[0]!.pubkey;
          else throw new Error(`Peer "${to}" not found`);
        } else {
          targetSpec = match.pubkey;
        }
      }

      const ok = await conn.sendMessage(targetSpec, `[via Telegram] ${message}`, priority as "now" | "next" | "low");
      if (!ok) throw new Error("Send failed");
      return { ok: true };
    }

    case "list_peers":
      return conn.listPeers();

    case "list_services": {
      // Query deployed services from the broker DB
      try {
        const { listDbMeshServices } = await import("./broker");
        const allServices: Array<{ name: string; type: string; tools: number; status: string }> = [];
        for (const meshId of meshIds) {
          const services = await listDbMeshServices(meshId);
          for (const s of services) {
            const sx = s as Record<string, unknown>;
            allServices.push({
              name: String(sx.name ?? ""),
              type: String(sx.type ?? "mcp"),
              tools: Number(sx.tool_count ?? 0),
              status: String(sx.status ?? "running"),
            });
          }
        }
        return allServices;
      } catch {
        return [];
      }
    }

    case "list_meshes": {
      const results: Array<{ slug: string; peers: number }> = [];
      for (const meshId of meshIds) {
        const conn = meshConnections.get(meshId);
        const slug = meshSlugs.get(meshId) ?? meshId.slice(0, 12);
        let peerCount = 0;
        if (conn?.isConnected()) {
          try {
            const peers = await conn.listPeers();
            peerCount = peers.length;
          } catch {}
        }
        results.push({ slug, peers: peerCount });
      }
      return results;
    }

    case "list_commands":
      return null; // The formatter handles this — no execution needed

    case "remember":
    case "recall":
    case "get_state":
    case "set_state":
      throw new Error(`${toolCall.name} not yet available via Telegram. Use the CLI.`);

    default:
      throw new Error(`Unknown tool: ${toolCall.name}`);
  }
}

// ---------------------------------------------------------------------------
// Ensure a mesh WS connection exists (create or reuse)
// ---------------------------------------------------------------------------

async function ensureMeshConnection(
  creds: MeshCredentials,
  pushHandler: (
    meshId: string,
    from: string,
    text: string,
    priority: string,
  ) => void,
): Promise<MeshConnection> {
  const existing = meshConnections.get(creds.meshId);
  if (existing?.isConnected()) return existing;

  // Close stale connection if any
  if (existing) {
    existing.close();
    meshConnections.delete(creds.meshId);
  }

  const conn = new MeshConnection(creds, pushHandler);
  meshConnections.set(creds.meshId, conn);
  await conn.connect();
  await conn.setSummary(
    "Telegram bridge — relays messages between Telegram chats and mesh peers",
  );
  return conn;
}

// ---------------------------------------------------------------------------
// Boot — called by broker on startup
// ---------------------------------------------------------------------------

export async function bootTelegramBridge(
  loadActiveBridges: () => Promise<BridgeRow[]>,
  saveBridge: (
    row: Omit<BridgeRow, "chatId"> & { chatId: number },
  ) => Promise<void>,
  deactivateBridge: (chatId: number, meshId: string) => Promise<void>,
  botToken: string,
  brokerUrl: string,
  lookupMeshesByEmail?: (email: string) => Promise<UserMeshInfo[]>,
  sendVerificationEmail?: (email: string, code: string) => Promise<boolean>,
): Promise<void> {
  await ensureSodium();

  const bot = new Bot(botToken);
  const pushHandler = createPushHandler(bot);

  // Load all active bridges from DB
  const rows = await loadActiveBridges();
  console.log(`[tg-bridge] loaded ${rows.length} active bridge(s) from DB`);

  // Group by meshId to connect WS pool
  const byMesh = new Map<string, BridgeRow[]>();
  for (const row of rows) {
    const arr = byMesh.get(row.meshId) ?? [];
    arr.push(row);
    byMesh.set(row.meshId, arr);
  }

  // Connect one WS per unique mesh
  for (const [meshId, meshRows] of byMesh) {
    const first = meshRows[0]!;
    try {
      // memberId/displayName come back from DB nullable; bridge only
      // works once both are populated, so skip rows missing either.
      if (!first.memberId || !first.displayName) continue;
      await ensureMeshConnection(
        {
          meshId,
          memberId: first.memberId,
          pubkey: first.pubkey,
          secretKey: first.secretKey,
          displayName: first.displayName,
          brokerUrl,
        },
        pushHandler,
      );
      console.log(
        `[tg-bridge] connected WS for mesh ${meshId.slice(0, 8)} (${meshRows.length} chat(s))`,
      );
    } catch (e) {
      console.error(
        `[tg-bridge] failed to connect mesh ${meshId.slice(0, 8)}:`,
        e,
      );
    }

    // Populate routing maps and slug cache for all chats in this mesh
    for (const row of meshRows) {
      linkChatMesh(row.chatId, meshId);
      if (row.meshSlug) meshSlugs.set(meshId, row.meshSlug);
    }
  }

  // Grammy global error handler — prevents unhandled rejections from crashing broker
  bot.catch((err) => {
    console.error("[tg-bridge] Grammy error:", err.message ?? err);
  });

  // Expire stale pendingDMs entries every 5 minutes (prevent memory leak)
  setInterval(() => {
    // pendingDMs/pendingFiles have no timestamp, so we cap size — clear all if > 1000
    if (pendingDMs.size > 1000) {
      console.warn(`[tg-bridge] clearing ${pendingDMs.size} stale pendingDMs`);
      pendingDMs.clear();
    }
    if (pendingFiles.size > 1000) {
      console.warn(`[tg-bridge] clearing ${pendingFiles.size} stale pendingFiles`);
      pendingFiles.clear();
    }
  }, 5 * 60_000).unref();

  // Default stubs if email callbacks not provided
  const emailLookup = lookupMeshesByEmail ?? (async () => []);
  const emailSend = sendVerificationEmail ?? (async () => false);

  // Wire up bot commands
  setupBotCommands(
    bot,
    botToken,
    brokerUrl,
    saveBridge,
    deactivateBridge,
    pushHandler,
    emailLookup,
    emailSend,
  );

  // Start Grammy long-polling (fire-and-forget, must not crash broker)
  console.log("[tg-bridge] starting bot...");
  bot.start({
    onStart: () =>
      console.log(
        `[tg-bridge] bot running — ${meshConnections.size} mesh(es), ${chatMeshes.size} chat(s)`,
      ),
  }).catch((err: unknown) => {
    console.error("[tg-bridge] bot.start() error:", err instanceof Error ? err.message : String(err));
  });
}

// ---------------------------------------------------------------------------
// Connect a new chat at runtime (called from broker HTTP endpoints)
// ---------------------------------------------------------------------------

export async function connectChat(
  chatId: number,
  chatType: string,
  chatTitle: string | null,
  meshCreds: MeshCredentials,
  pushHandler?: (
    meshId: string,
    from: string,
    text: string,
    priority: string,
  ) => void,
): Promise<void> {
  // Default push handler is a no-op if bot isn't running yet
  // (the real one is wired during bootTelegramBridge)
  const handler = pushHandler ?? (() => {});

  await ensureMeshConnection(meshCreds, handler);
  linkChatMesh(chatId, meshCreds.meshId);

  console.log(
    `[tg-bridge] chat ${chatId} (${chatType}) connected to mesh ${meshCreds.meshId.slice(0, 8)}`,
  );
}
