import { Bot, InputFile } from "grammy";
import type { Context } from "grammy";
import { spawn } from "node:child_process";
import ffmpegPath from "ffmpeg-static";
import type { Db } from "@paperclipai/db";
import { chatService } from "./chat.js";
import { agentService } from "./agents.js";
import { companyService } from "./companies.js";
import { puterProxyService } from "./puter-proxy.js";
import { nexusSettingsService } from "./nexus-settings.js";
import { voicePipelineService } from "./voice-pipeline.js";
import { logger } from "../middleware/logger.js";

// In-memory session map: `${chatId}:${agentId}` -> conversationId
const sessionMap = new Map<string, string>();

interface ResolvedAgent {
  companyId: string;
  agentId: string;
  agentName: string;
}

async function resolveDefaultAgent(db: Db): Promise<ResolvedAgent | null> {
  try {
    const companies = await companyService(db).list();
    if (!companies || companies.length === 0) return null;

    const company = companies[0]!;
    const agentsList = await agentService(db).list(company.id);
    if (!agentsList || agentsList.length === 0) return null;

    const agent = agentsList[0]!;
    return {
      companyId: company.id,
      agentId: agent.id,
      agentName: agent.name,
    };
  } catch (err) {
    logger.error({ err }, "telegram: failed to resolve default agent");
    return null;
  }
}

async function getOrCreateConversation(
  chatId: string,
  agentId: string,
  companyId: string,
  db: Db,
): Promise<string> {
  const key = `${chatId}:${agentId}`;
  const existing = sessionMap.get(key);
  if (existing) return existing;

  const chatSvc = chatService(db);
  const conv = await chatSvc.createConversation(companyId, {
    title: `Telegram chat ${chatId}`,
    agentId,
  });

  sessionMap.set(key, conv.id);
  return conv.id;
}

function splitMessage(text: string, limit = 4000): string[] {
  if (text.length <= limit) return [text];
  const parts: string[] = [];
  let remaining = text;
  while (remaining.length > 0) {
    parts.push(remaining.slice(0, limit));
    remaining = remaining.slice(limit);
  }
  return parts;
}

/**
 * Transcode raw PCM s16le (from Piper TTS) to OGG Opus for Telegram voice notes.
 * Piper en_US-lessac-medium outputs 22050Hz.
 */
function transcodeToOggOpus(rawPcmBuffer: Buffer): Promise<Buffer> {
  return new Promise<Buffer>((resolve, reject) => {
    if (!ffmpegPath) {
      reject(new Error("ffmpeg-static binary not found"));
      return;
    }
    const ffmpegBin = ffmpegPath as unknown as string;

    // Input: raw PCM s16le at 22050Hz mono (Piper output rate)
    // Output: OGG Opus at 48000Hz (Telegram requirement)
    const ffmpeg = spawn(ffmpegBin, [
      "-f", "s16le", "-ar", "22050", "-ac", "1", "-i", "pipe:0",
      "-c:a", "libopus", "-ar", "48000", "-f", "ogg", "pipe:1",
    ], { stdio: ["pipe", "pipe", "pipe"] });

    const chunks: Buffer[] = [];

    ffmpeg.stdout.on("data", (chunk: Buffer) => chunks.push(chunk));
    ffmpeg.stderr.on("data", () => { /* discard */ });

    ffmpeg.on("close", (code) => {
      if (code === 0) {
        resolve(Buffer.concat(chunks));
      } else {
        reject(new Error(`ffmpeg exited with code ${code}`));
      }
    });

    ffmpeg.on("error", reject);

    ffmpeg.stdin.write(rawPcmBuffer);
    ffmpeg.stdin.end();
  });
}

/**
 * Core relay: persist user message, collect LLM stream, send reply parts.
 * Both text and voice handlers share this function.
 *
 * @param voiceMode - If true, attempt a TTS voice reply after sending text.
 */
async function relayToAgent(
  ctx: Context,
  chatId: string,
  userText: string,
  db: Db,
  voiceMode = false,
): Promise<void> {
  const resolved = await resolveDefaultAgent(db);
  if (!resolved) {
    await ctx.reply("No agents configured. Please set up an agent first.");
    return;
  }

  const { companyId, agentId, agentName } = resolved;
  const chatSvc = chatService(db);

  const convId = await getOrCreateConversation(chatId, agentId, companyId, db);

  // Persist user message
  await chatSvc.addMessage(convId, { role: "user", content: userText });

  // Build messages array (last 20 messages, reverse for LLM)
  const { items } = await chatSvc.listMessages(convId, { limit: 20 });
  const messages = [...items].reverse().map((m) => ({
    role: m.role as "user" | "assistant" | "system",
    content: m.content,
  }));

  // Collect response from puter proxy
  const puterProxy = puterProxyService(db);
  let fullResponse = "";
  for await (const chunk of puterProxy.chatStream(
    companyId,
    agentId,
    messages,
    undefined,
    undefined,
  )) {
    fullResponse += chunk;
  }

  // Persist assistant message
  await chatSvc.addMessage(convId, {
    role: "assistant",
    content: fullResponse,
    agentId,
  });

  // Prefix with agent name (TGRAM-02) and send reply parts
  const prefixed = `[${agentName}]: ${fullResponse}`;
  const parts = splitMessage(prefixed, 4000);
  for (const part of parts) {
    await ctx.reply(part, { parse_mode: "Markdown" });
  }

  // If voice mode: attempt TTS voice reply as bonus (text already sent)
  if (voiceMode) {
    try {
      const voiceSvc = voicePipelineService();
      const voiceText = voiceSvc.formatForVoice(fullResponse);
      if (voiceText) {
        const pcmBuffer = await voiceSvc.synthesize(voiceText);
        const oggBuffer = await transcodeToOggOpus(pcmBuffer);
        await ctx.replyWithVoice(new InputFile(oggBuffer, "response.ogg"));
      }
    } catch (ttsErr) {
      // TTS is optional — log warning but don't fail (text reply already sent)
      logger.warn({ err: ttsErr }, "telegram: TTS voice reply failed (degrading to text-only)");
    }
  }
}

/**
 * Process a voice message asynchronously after sending the immediate "Transcribing..." reply.
 * OGG is downloaded from Telegram CDN, transcribed via voicePipelineService, then relayed.
 */
async function processVoiceMessage(
  ctx: Context,
  chatId: string,
  token: string,
  db: Db,
): Promise<void> {
  // Download OGG from Telegram file CDN
  const file = await ctx.getFile();
  if (!file.file_path) {
    await ctx.reply("Could not retrieve voice message file.");
    return;
  }

  const downloadUrl = `https://api.telegram.org/file/bot${token}/${file.file_path}`;
  const response = await fetch(downloadUrl);
  if (!response.ok) {
    await ctx.reply("Could not download voice message.");
    return;
  }
  const arrayBuf = await response.arrayBuffer();
  const oggBuffer = Buffer.from(arrayBuf);

  // Transcribe — voicePipelineService handles OGG->WAV16k internally
  const voiceSvc = voicePipelineService();
  const { text } = await voiceSvc.transcribe(oggBuffer, "ogg");

  if (!text || !text.trim()) {
    await ctx.reply("Could not transcribe voice message.");
    return;
  }

  // Confirm transcription to user (truncate for readability)
  await ctx.reply(`Heard: ${text.slice(0, 200)}`);

  // Relay transcribed text to agent (with voice reply enabled)
  await relayToAgent(ctx, chatId, text.trim(), db, true);
}

export function telegramService(db: Db) {
  let bot: Bot | null = null;
  let botToken: string | null = null;

  async function start(token: string): Promise<void> {
    if (bot) {
      await stop();
    }

    botToken = token;
    // Assign bot early so handlers reference the module-level ref
    bot = new Bot(token);

    bot.on("message:text", async (ctx) => {
      const chatId = String(ctx.chat.id);

      try {
        await relayToAgent(ctx, chatId, ctx.message.text, db);
      } catch (err) {
        logger.error({ err, chatId }, "telegram: error handling text message");
        try {
          await ctx.reply("Sorry, something went wrong.");
        } catch {
          // ignore reply errors
        }
      }
    });

    bot.on("message:voice", async (ctx) => {
      const chatId = String(ctx.chat.id);

      // Reply immediately to prevent Telegram from resending the update (Pitfall 1)
      try {
        await ctx.reply("Transcribing...");
      } catch {
        // ignore — continue processing
      }

      // Process async — do NOT await in handler body
      processVoiceMessage(ctx, chatId, botToken!, db).catch(async (err) => {
        logger.error({ err, chatId }, "telegram: error processing voice message");
        try {
          await ctx.reply("Voice transcription failed.");
        } catch {
          // ignore reply errors
        }
      });
    });

    bot.catch((err) => {
      logger.error({ err }, "Telegram bot error");
    });

    // Delete webhook first to ensure long polling works (Pitfall 4)
    await bot.api.deleteWebhook();

    // Do NOT await — bot.start() is a never-resolving promise
    bot.start().catch((err) => {
      logger.error({ err }, "Telegram bot polling error");
      bot = null;
    });

    logger.info("Telegram bot started (long polling)");
  }

  async function stop(): Promise<void> {
    if (bot) {
      try {
        await bot.stop();
      } catch (err) {
        logger.error({ err }, "Error stopping Telegram bot");
      }
      bot = null;
      botToken = null;
      logger.info("Telegram bot stopped");
    }
  }

  function isRunning(): boolean {
    return bot !== null;
  }

  return { start, stop, isRunning };
}

export type TelegramService = ReturnType<typeof telegramService>;

// Re-export nexusSettingsService for convenience in routes
export { nexusSettingsService };
