import { Bot, InputFile } from 'grammy';

import type { HookEngine } from '../../hooks/index.js';
import type {
  Attachment,
  InboundMessage,
  OutboundMessage,
  OutboundAttachment,
  ChannelAdapter,
  ChannelStatus,
} from '../types.js';
import { isAllowedChat } from '../../frontends/telegram/handlers.js';
import { parseConfirmationCallback } from '../../frontends/telegram/confirmations.js';
import { splitMessage } from '../utils.js';
import type { PairingManager } from '../pairing.js';

/** Configuration for the Telegram channel adapter. */
export interface TelegramAdapterConfig {
  botToken: string;
  allowedChatIds: number[];
  /** Require bot mention or reply-to-bot to respond in group chats (default: true). */
  requireMention?: boolean;
  hookEngine?: HookEngine;
  /** Optional pairing manager for DM pairing codes. */
  pairingManager?: PairingManager;
}

/**
 * Telegram channel adapter backed by grammy.
 *
 * Handles authentication via allowed-chat-id filtering,
 * confirmation callbacks (when a HookEngine is provided),
 * and message chunking for Telegram's 4096-char limit.
 */
export class TelegramAdapter implements ChannelAdapter {
  readonly name = 'telegram';

  private _status: ChannelStatus = 'disconnected';
  private bot: Bot | null = null;
  private messageHandler?: (msg: InboundMessage) => void;
  private config: TelegramAdapterConfig;
  private botInfo?: { id: number; username?: string };

  get status(): ChannelStatus {
    return this._status;
  }

  constructor(config: TelegramAdapterConfig) {
    this.config = config;
  }

  /** Download a file from Telegram API and convert to base64. */
  private async downloadFileToBase64(fileId: string): Promise<string | null> {
    try {
      const file = await this.bot?.api.getFile(fileId);
      if (!file || !file.file_path) {return null;}

      const token = this.config.botToken;
      const url = `https://api.telegram.org/file/bot${token}/${file.file_path}`;

      const response = await fetch(url);
      if (!response.ok) {return null;}

      const buffer = Buffer.from(await response.arrayBuffer());
      return buffer.toString('base64');
    } catch (error) {
      console.error(`Failed to download file ${fileId}:`, error);
      return null;
    }
  }

  /** Register the inbound message handler. Called by the registry before connect(). */
  onMessage(handler: (msg: InboundMessage) => void): void {
    this.messageHandler = handler;
  }

  /** Create the grammy bot, wire up middleware & handlers, and start long-polling. */
  async connect(): Promise<void> {
    this.bot = new Bot(this.config.botToken);
    this._status = 'connecting';

    // ── Auth middleware — reject messages from unknown chats (with pairing fallback) ──
    this.bot.use(async (ctx, next) => {
      const chatId = ctx.chat?.id;
      if (chatId === undefined) {return;}

      // Allowlist check
      if (isAllowedChat(chatId, this.config.allowedChatIds)) {
        await next();
        return;
      }

      // Pairing fallback — check if sender is already approved or sending a valid code
      const pm = this.config.pairingManager;
      if (pm?.enabled) {
        const senderId = String(chatId);
        if (pm.isApproved('telegram', senderId)) {
          await next();
          return;
        }
        // Check if the message text is a valid pairing code
        const text = ctx.message?.text?.trim();
        if (text && pm.validateCode('telegram', senderId, text)) {
          await ctx.reply('Pairing successful! You can now chat with Flynn.');
          return;
        }
      }

      console.log(`Rejected message from unauthorized chat: ${chatId}`);
    });

    // ── Confirmation callback handler (requires hookEngine) ──
    if (this.config.hookEngine) {
      const hookEngine = this.config.hookEngine;

      this.bot.on('callback_query:data', async (ctx) => {
        const data = ctx.callbackQuery.data;
        const parsed = parseConfirmationCallback(data);

        if (!parsed) {
          await ctx.answerCallbackQuery({ text: 'Invalid action' });
          return;
        }

        const resolved = hookEngine.resolveConfirmation(parsed.id, {
          approved: parsed.approved,
          reason: parsed.approved ? undefined : 'Denied by user',
        });

        if (resolved) {
          await ctx.answerCallbackQuery({
            text: parsed.approved ? '✅ Approved' : '❌ Denied',
          });
          await ctx.editMessageText(
            ctx.callbackQuery.message?.text + `\n\n${parsed.approved ? '✅ Approved' : '❌ Denied'}`,
            { parse_mode: 'Markdown' },
          );
        } else {
          await ctx.answerCallbackQuery({ text: 'Confirmation expired or not found' });
        }
      });
    }

    // ── Command handlers ──

    this.bot.command('start', async (ctx) => {
      await ctx.reply('Flynn is ready. Send me a message!');
    });

    this.bot.command('reset', async (ctx) => {
      // Deliver a special reset message through the channel
      if (this.messageHandler) {
        this.messageHandler({
          id: String(ctx.message?.message_id ?? Date.now()),
          channel: 'telegram',
          senderId: String(ctx.chat.id),
          senderName: ctx.from?.first_name,
          text: '/reset',
          timestamp: Date.now(),
          metadata: { isCommand: true, command: 'reset' },
        });
      }
      await ctx.reply('Conversation reset.');
    });

    this.bot.command('model', async (ctx) => {
      if (!this.messageHandler) {return;}

      // Telegram can deliver group commands in the form: /model@bot_username ...
      // Strip the optional @mention so args parsing is consistent across DMs/groups.
      const args = ctx.message?.text?.replace(/^\/model(?:@\S+)?\s*/i, '').trim() ?? '';

      this.messageHandler({
        id: String(ctx.message?.message_id ?? Date.now()),
        channel: 'telegram',
        senderId: String(ctx.chat.id),
        senderName: ctx.from?.first_name,
        text: `/model ${args}`.trim(),
        timestamp: Date.now(),
        metadata: {
          isCommand: true,
          command: 'model',
          commandArgs: args || undefined,
        },
      });
    });

    this.bot.command('local', async (ctx) => {
      if (!this.messageHandler) {return;}
      this.messageHandler({
        id: String(ctx.message?.message_id ?? Date.now()),
        channel: 'telegram',
        senderId: String(ctx.chat.id),
        senderName: ctx.from?.first_name,
        text: '/model local',
        timestamp: Date.now(),
        metadata: { isCommand: true, command: 'model', commandArgs: 'local' },
      });
    });

    this.bot.command('cloud', async (ctx) => {
      if (!this.messageHandler) {return;}
      this.messageHandler({
        id: String(ctx.message?.message_id ?? Date.now()),
        channel: 'telegram',
        senderId: String(ctx.chat.id),
        senderName: ctx.from?.first_name,
        text: '/model default',
        timestamp: Date.now(),
        metadata: { isCommand: true, command: 'model', commandArgs: 'default' },
      });
    });

    // ── Text message handler ──

    this.bot.on('message:text', async (ctx) => {
      if (!this.messageHandler) {return;}

      // Group chat mention gating
      const isGroup = ctx.chat.type === 'group' || ctx.chat.type === 'supergroup';
      const requireMention = this.config.requireMention ?? true;

      if (isGroup && requireMention && this.botInfo) {
        const rawText = ctx.message.text;
        const username = this.botInfo.username;

        // Check for @bot_username mention
        const isMentioned = username
          ? rawText.includes(`@${username}`)
          : false;

        // Also allow replies to bot messages
        const isReplyToBot = ctx.message.reply_to_message?.from?.id === this.botInfo.id;

        if (!isMentioned && !isReplyToBot) {
          return;
        }
      }

      let text = ctx.message.text;

      // Strip bot mention from text
      if (isGroup && this.botInfo?.username) {
        text = text.replace(new RegExp(`@${this.botInfo.username}\\b`, 'g'), '').trim();
      }

      // Show typing indicator while processing
      await ctx.replyWithChatAction('typing');

      this.messageHandler({
        id: String(ctx.message.message_id),
        channel: 'telegram',
        senderId: String(ctx.chat.id),
        senderName: ctx.from?.first_name,
        text,
        timestamp: Date.now(),
      });
    });

    // ── Photo message handler ──

    this.bot.on('message:photo', async (ctx) => {
      if (!this.messageHandler) {return;}

      const photo = ctx.message.photo;
      if (!photo || photo.length === 0) {return;}

      const largestPhoto = photo[photo.length - 1];

      await ctx.replyWithChatAction('typing');

      const imageData = await this.downloadFileToBase64(largestPhoto.file_id);
      if (!imageData) {
        console.error(`Failed to download photo ${largestPhoto.file_id}`);
        return;
      }

      const caption = ctx.message.caption ?? '';

      this.messageHandler({
        id: String(ctx.message.message_id),
        channel: 'telegram',
        senderId: String(ctx.chat.id),
        senderName: ctx.from?.first_name,
        text: caption,
        attachments: [
          {
            mimeType: 'image/jpeg',
            data: imageData,
            filename: `photo_${largestPhoto.file_unique_id}.jpg`,
            size: largestPhoto.file_size,
          },
        ],
        timestamp: Date.now(),
      });
    });

    // ── Image document handler ──

    this.bot.on('message:document', async (ctx) => {
      if (!this.messageHandler) {return;}

      const document = ctx.message.document;
      if (!document) {return;}

      const mimeType = document.mime_type ?? '';
      if (!mimeType.startsWith('image/')) {return;}

      await ctx.replyWithChatAction('typing');

      const fileData = await this.downloadFileToBase64(document.file_id);
      if (!fileData) {
        console.error(`Failed to download document ${document.file_id}`);
        return;
      }

      const caption = ctx.message.caption ?? '';
      const filename = document.file_name ?? document.file_unique_id;

      this.messageHandler({
        id: String(ctx.message.message_id),
        channel: 'telegram',
        senderId: String(ctx.chat.id),
        senderName: ctx.from?.first_name,
        text: caption,
        attachments: [
          {
            mimeType,
            data: fileData,
            filename,
            size: document.file_size,
          },
        ],
        timestamp: Date.now(),
      });
    });

    // ── Voice message handler ──

    this.bot.on('message:voice', async (ctx) => {
      if (!this.messageHandler) {return;}

      const voice = ctx.message.voice;
      if (!voice) {return;}

      await ctx.replyWithChatAction('typing');

      const fileData = await this.downloadFileToBase64(voice.file_id);
      if (!fileData) {
        console.error(`Failed to download voice message ${voice.file_id}`);
        await ctx.reply('Sorry, I couldn\'t download your voice message. Please try again.', { reply_parameters: { message_id: ctx.message.message_id } });
        return;
      }

      const caption = ctx.message.caption ?? '';
      const mimeType = voice.mime_type ?? 'audio/ogg';

      this.messageHandler({
        id: String(ctx.message.message_id),
        channel: 'telegram',
        senderId: String(ctx.chat.id),
        senderName: ctx.from?.first_name,
        text: caption,
        attachments: [
          {
            mimeType,
            data: fileData,
            filename: `voice_${voice.file_unique_id}.ogg`,
            size: voice.file_size,
          },
        ],
        timestamp: Date.now(),
      });
    });

    // ── Audio message handler ──

    this.bot.on('message:audio', async (ctx) => {
      if (!this.messageHandler) {return;}

      const audio = ctx.message.audio;
      if (!audio) {return;}

      await ctx.replyWithChatAction('typing');

      const fileData = await this.downloadFileToBase64(audio.file_id);
      if (!fileData) {
        console.error(`Failed to download audio message ${audio.file_id}`);
        await ctx.reply('Sorry, I couldn\'t download your audio file. Please try again.', { reply_parameters: { message_id: ctx.message.message_id } });
        return;
      }

      const caption = ctx.message.caption ?? '';
      const mimeType = audio.mime_type ?? 'audio/mpeg';

      this.messageHandler({
        id: String(ctx.message.message_id),
        channel: 'telegram',
        senderId: String(ctx.chat.id),
        senderName: ctx.from?.first_name,
        text: caption,
        attachments: [
          {
            mimeType,
            data: fileData,
            filename: `audio_${audio.file_unique_id}.${mimeType.split('/')[1]}`,
            size: audio.file_size,
          },
        ],
        timestamp: Date.now(),
      });
    });

    // ── Start long polling ──

    this.bot.start({
      onStart: (botInfo) => {
        console.log(`Telegram bot started: @${botInfo.username}`);
        this.botInfo = { id: botInfo.id, username: botInfo.username };
        this._status = 'connected';
      },
    });

    // bot.start() returns immediately for long polling.
    // The onStart callback sets connected above; also set here for safety
    // in case the callback fires before this line is reached.
    this._status = 'connected';
  }

  /** Stop the bot and clean up. */
  async disconnect(): Promise<void> {
    if (this.bot) {
      await this.bot.stop();
      this.bot = null;
    }
    this._status = 'disconnected';
  }

  /** Send an outbound message, automatically chunking if it exceeds Telegram's limit. */
  async send(peerId: string, message: OutboundMessage): Promise<void> {
    if (!this.bot) {throw new Error('Telegram adapter not connected');}

    const chatId = Number(peerId);
    const text = message.text ?? '';

    // Telegram rejects empty text messages.
    // If there is no text, skip straight to attachments.
    if (!text.trim()) {
      if (message.attachments && message.attachments.length > 0) {
        for (const attachment of message.attachments) {
          await this.sendAttachment(chatId, attachment);
        }
      }
      return;
    }

    const sendChunk = async (chunk: string): Promise<void> => {
      // We default to Markdown for nicer formatting, but Telegram's Markdown parsing
      // is strict and can fail on unescaped characters. If Telegram rejects the
      // message, retry once without parse_mode so users still get the content.
      try {
        await this.bot!.api.sendMessage(chatId, chunk, { parse_mode: 'Markdown' });
      } catch (error) {
        const description = error && typeof error === 'object' && 'description' in error
          ? String((error as { description?: unknown }).description)
          : '';

        const isParseError = description.includes("can't parse entities")
          || description.includes('message text is empty');

        if (!isParseError) {
          throw error;
        }

        await this.bot!.api.sendMessage(chatId, chunk);
      }
    };

    // Telegram enforces a 4096-character limit per message
    if (text.length <= 4096) {
      await sendChunk(text);
    } else {
      const chunks = splitMessage(text, 4096);
      for (const chunk of chunks) {
        await sendChunk(chunk);
      }
    }

    // Send outbound attachments after text
    if (message.attachments && message.attachments.length > 0) {
      for (const attachment of message.attachments) {
        await this.sendAttachment(chatId, attachment);
      }
    }
  }

  /** Send a single outbound attachment via the Telegram API. */
  private async sendAttachment(chatId: number, attachment: OutboundAttachment): Promise<void> {
    if (!this.bot) {return;}

    try {
      const file = attachment.data
        ? new InputFile(Buffer.from(attachment.data, 'base64'), attachment.filename)
        : attachment.url ?? '';

      if (attachment.mimeType.startsWith('image/')) {
        await this.bot.api.sendPhoto(chatId, file);
      } else {
        await this.bot.api.sendDocument(chatId, file);
      }
    } catch (error) {
      console.error(
        `Failed to send ${attachment.mimeType} attachment to ${chatId}:`,
        error instanceof Error ? error.message : 'Unknown error',
      );
    }
  }
}
