/**
 * Javis bot entry point.
 *
 * A normal Discord bot that:
 *  - exposes /자비스 (join / leave / ask / stream / stop / status)
 *  - replies to every slash command EPHEMERALLY (only the invoker sees it)
 *  - joins the caller's voice channel for live voice conversation (brain in bridge/)
 *  - broadcasts the VNC screen via a pluggable backend (selfbot / novnc / screenshot)
 */
import {
  Client,
  GatewayIntentBits,
  MessageFlags,
  type ChatInputCommandInteraction,
  type GuildMember,
  type TextBasedChannel,
} from "discord.js";
import { AttachmentBuilder } from "discord.js";
import { config } from "./config.ts";
import { ask, health } from "./bridge.ts";
import { joinChannel, leaveGuild, getSession } from "./voice.ts";
import { type StreamContext } from "./stream/index.ts";
import { getStreamer, peekStreamer, maybeCoupleBroadcast, stopBroadcast } from "./broadcast.ts";

const client = new Client({
  intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates],
});

const eph = { flags: MessageFlags.Ephemeral } as const;

client.once("clientReady", () => {
  console.log(`✓ 로그인: ${client.user?.tag} | stream backend: ${config.streamBackend}`);
});

client.on("interactionCreate", async (interaction) => {
  if (!interaction.isChatInputCommand()) return;
  if (interaction.commandName !== "자비스") return;
  const i = interaction as ChatInputCommandInteraction;
  const sub = i.options.getSubcommand();

  try {
    switch (sub) {
      case "join":
        return void (await handleJoin(i));
      case "leave":
        return void (await handleLeave(i));
      case "ask":
        return void (await handleAsk(i));
      case "stream":
        return void (await handleStream(i));
      case "stop":
        return void (await handleStop(i));
      case "status":
        return void (await handleStatus(i));
    }
  } catch (err) {
    console.error(`[/자비스 ${sub}]`, err);
    const msg = `오류: ${(err as Error).message}`;
    if (i.deferred || i.replied) await i.editReply(msg);
    else await i.reply({ content: msg, ...eph });
  }
});

async function handleJoin(i: ChatInputCommandInteraction) {
  const member = i.member as GuildMember;
  const channel = member?.voice?.channel;
  if (!channel) {
    return i.reply({ content: "먼저 음성 채널에 들어간 뒤 다시 호출해주세요.", ...eph });
  }
  await i.deferReply(eph);
  const session = await joinChannel(channel);
  session.onTurn = ({ transcript, reply }) =>
    console.log(`🗣️  ${transcript}\n🤖 ${reply}`);

  // Screen-share mode (STREAM_BROWSER=true): the broadcast is coupled to the
  // voice session. Auto-start it on join, report its live state to the brain
  // each turn (search routes Chrome while live / Gemini when off), and let the
  // brain toggle it via voice ("방송 켜줘 / 꺼줘"). In voice-only mode
  // (STREAM_BROWSER=false) none of this runs and the broadcast stays off.
  await maybeCoupleBroadcast(session, { guildId: i.guildId!, voiceChannelId: channel.id });

  await i.editReply(`🎙️ '${channel.name}' 채널에 접속했습니다. 말씀하세요.`);
}

async function handleLeave(i: ChatInputCommandInteraction) {
  const left = leaveGuild(i.guildId!);
  // Leaving voice also ends the broadcast — don't leave a stream live with no
  // session driving it.
  await stopBroadcast(i.guildId!);
  await i.reply({ content: left ? "음성 채널에서 나갔습니다." : "접속 중인 세션이 없습니다.", ...eph });
}

async function handleAsk(i: ChatInputCommandInteraction) {
  const q = i.options.getString("질문", true);
  await i.deferReply(eph);
  const res = await ask(q);
  const reply = res.reply || res.error || "(응답 없음)";
  await i.editReply(reply.slice(0, 1900));
}

async function handleStream(i: ChatInputCommandInteraction) {
  const member = i.member as GuildMember;
  await i.deferReply(eph);
  const streamer = await getStreamer(i.guildId!);
  const ctx: StreamContext = {
    guildId: i.guildId!,
    voiceChannelId: member?.voice?.channelId ?? "",
    postImage: async (png, name) => {
      const ch = i.channel as TextBasedChannel | null;
      if (ch && "send" in ch) {
        await (ch as any).send({ files: [new AttachmentBuilder(png, { name })] });
      }
    },
  };
  if (!config.screenBrowser) {
    return i.editReply(
      "화면 공유(브라우저) 모드가 꺼져 있습니다 (STREAM_BROWSER=false). 음성 + API/MCP 모드로만 동작합니다.",
    );
  }
  if (config.streamBackend === "selfbot" && !ctx.voiceChannelId) {
    return i.editReply("셀프봇 송출은 음성 채널 안에서 호출해야 합니다. 음성 채널에 들어간 뒤 다시 시도하세요.");
  }
  const msg = await streamer.start(ctx);
  await i.editReply(msg);
}

async function handleStop(i: ChatInputCommandInteraction) {
  const streamer = peekStreamer(i.guildId!);
  if (!streamer) return i.reply({ content: "송출 중이 아닙니다.", ...eph });
  await streamer.stop();
  await i.reply({ content: "송출을 중단했습니다.", ...eph });
}

async function handleStatus(i: ChatInputCommandInteraction) {
  await i.deferReply(eph);
  let brain = "unreachable";
  try {
    const h = await health();
    brain = h.brain_ready ? "ready" : `not-ready${h.brain_error ? " (" + h.brain_error + ")" : ""}`;
  } catch {
    /* keep unreachable */
  }
  const session = getSession(i.guildId!);
  const streamer = peekStreamer(i.guildId!);
  await i.editReply(
    [
      `브릿지 두뇌: ${brain}`,
      `음성 세션: ${session ? "접속 중" : "없음"}`,
      `송출 백엔드: ${config.streamBackend} (${streamer?.isActive() ? "활성" : "대기"})`,
    ].join("\n"),
  );
}

// Block until the brain bridge reports the TTS voice (MeloTTS worker) is warm.
// The bot, bridge and melo worker all boot together; the voice model takes tens
// of seconds to load. If we log in and auto-join the voice channel before
// MeloTTS is ready, the first reply synthesises to nothing and is silently
// dropped. Gating login on TTS readiness guarantees the first spoken turn has
// audio. We deliberately do NOT wait on brain_ready: the reply engine / Whisper
// load lazily on the first turn (so brain_ready stays false on an idle boot) —
// a slow first turn is fine, a silent one is the bug. After `maxMs` we proceed
// anyway so a TTS load failure degrades to text-only rather than taking the
// whole bot offline.
async function waitForBridgeReady(maxMs = 180_000): Promise<void> {
  const start = Date.now();
  let announced = false;
  while (Date.now() - start < maxMs) {
    try {
      const h = await health();
      if (h.tts_ready) {
        console.log("✓ 음성(MeloTTS) 준비 완료 — 로그인 진행");
        return;
      }
      if (!announced) {
        console.log("⏳ MeloTTS 준비 대기 중 (음성 모델 로딩)...");
        announced = true;
      }
    } catch {
      /* bridge not listening yet — keep polling */
    }
    await new Promise((r) => setTimeout(r, 2000));
  }
  console.warn("⚠️ MeloTTS 준비 시간 초과 — 음성(TTS) 없이 진행합니다.");
}

// Mode select: a USER account is the only kind Discord lets Go Live, so when
// there's no normal-bot token but a selfbot token is present, run as a userbot
// (voice + broadcast on one user account). Otherwise run the legacy normal bot.
(async () => {
  await waitForBridgeReady();
  if (!config.botToken && config.selfbotToken) {
    const { runUserbot } = await import("./userbot.ts");
    await runUserbot();
  } else if (config.botToken) {
    await client.login(config.botToken);
  } else {
    throw new Error(
      "DISCORD_BOT_TOKEN(일반 봇) 또는 DISCORD_SELFBOT_TOKEN(유저봇) 중 하나는 .env에 필요합니다.",
    );
  }
})().catch((e) => {
  console.error("[bot] fatal:", e);
  process.exit(1);
});
