/**
 * Discord channel adapter.
 *
 * Implements the ChannelAdapter interface using discord.js v14.
 * Supports guild channels (with optional mention requirement) and DMs.
 * Messages are chunked at Discord's 2000-char limit.
 */

import { Client, GatewayIntentBits, Events, AttachmentBuilder } from 'discord.js';
import type { Message as DiscordMessage } from 'discord.js';

import type {
  Attachment,
  InboundMessage,
  OutboundMessage,
  OutboundAttachment,
  ChannelAdapter,
  ChannelStatus,
} from '../types.js';
import { splitMessage } from '../utils.js';
import type { PairingManager } from '../pairing.js';

/** Configuration for the Discord channel adapter. */
export interface DiscordAdapterConfig {
  botToken: string;
  /** Guild IDs to respond in. Empty = all guilds. */
  allowedGuildIds?: string[];
  /** Channel IDs to respond in. Empty = all channels. */
  allowedChannelIds?: string[];
  /** Whether to require mention to respond in guild channels (default: true). DMs always respond. */
  requireMention?: boolean;
  /** Optional pairing manager for DM pairing codes. */
  pairingManager?: PairingManager;
}

/**
 * Discord channel adapter backed by discord.js.
 *
 * Handles guild/channel filtering, optional mention requirement,
 * DM support, and message chunking for Discord's 2000-char limit.
 */
export class DiscordAdapter implements ChannelAdapter {
  readonly name = 'discord';

  private _status: ChannelStatus = 'disconnected';
  private client: Client | null = null;
  private messageHandler?: (msg: InboundMessage) => void;
  private config: DiscordAdapterConfig;

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

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

  /** Infer MIME type from URL if contentType is not provided. */
  private _inferMimeTypeFromUrl(url: string): string | null {
    const ext = url.split('.').pop()?.toLowerCase();
    const mimeTypes: Record<string, string> = {
      png: 'image/png',
      jpg: 'image/jpeg',
      jpeg: 'image/jpeg',
      gif: 'image/gif',
      webp: 'image/webp',
      svg: 'image/svg+xml',
    };
    return mimeTypes[ext || ''] || null;
  }

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

  /** Create the discord.js client, wire up event handlers, and log in. */
  async connect(): Promise<void> {
    this._status = 'connecting';

    this.client = new Client({
      intents: [
        GatewayIntentBits.Guilds,
        GatewayIntentBits.GuildMessages,
        GatewayIntentBits.MessageContent,
        GatewayIntentBits.DirectMessages,
      ],
    });

    // ── Ready handler — resolve connect() when the bot is online ──
    const readyPromise = new Promise<void>((resolve) => {
      this.client!.on(Events.ClientReady, () => {
        console.log(`Discord bot ready as ${this.client!.user?.tag}`);
        this._status = 'connected';
        resolve();
      });
    });

    // ── Message handler — route inbound messages ──
    this.client.on(Events.MessageCreate, (message: DiscordMessage) => {
      this.handleMessage(message);
    });

    // Log in and wait for the ready event
    await this.client.login(this.config.botToken);
    await readyPromise;
  }

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

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

    const channel = await this.client.channels.fetch(peerId);
    if (!channel || !('send' in channel)) {
      throw new Error(`Channel ${peerId} not found or is not a text channel`);
    }

    const text = message.text;
    const sendable = channel as { send: (content: string | Record<string, unknown>) => Promise<unknown> };

    if (text.length <= 2000) {
      await sendable.send(text);
    } else {
      const chunks = splitMessage(text, 2000);
      for (const chunk of chunks) {
        await sendable.send(chunk);
      }
    }

    // Send outbound attachments after text
    if (message.attachments && message.attachments.length > 0) {
      const files = message.attachments
        .filter((a) => a.data || a.url)
        .map((a) => this.buildDiscordAttachment(a));

      if (files.length > 0) {
        await sendable.send({ files });
      }
    }
  }

  /** Build a discord.js AttachmentBuilder from an OutboundAttachment. */
  private buildDiscordAttachment(attachment: OutboundAttachment): AttachmentBuilder {
    if (attachment.data) {
      return new AttachmentBuilder(Buffer.from(attachment.data, 'base64'), {
        name: attachment.filename ?? 'attachment',
      });
    }
    // URL-based attachment
    return new AttachmentBuilder(attachment.url!, {
      name: attachment.filename ?? 'attachment',
    });
  }

  /** Internal: process an inbound Discord message. */
  private handleMessage(message: DiscordMessage): void {
    if (!this.messageHandler) {return;}

    // Ignore bot messages
    if (message.author.bot) {return;}

    const isDM = !message.guild;

    // ── Guild/channel filtering ──
    if (!isDM) {
      // Check allowed guild IDs
      if (
        this.config.allowedGuildIds &&
        this.config.allowedGuildIds.length > 0 &&
        !this.config.allowedGuildIds.includes(message.guild!.id)
      ) {
        return;
      }

      // Check allowed channel IDs
      if (
        this.config.allowedChannelIds &&
        this.config.allowedChannelIds.length > 0 &&
        !this.config.allowedChannelIds.includes(message.channelId)
      ) {
        return;
      }

      // ── Mention requirement in guild channels ──
      const requireMention = this.config.requireMention ?? true;
      if (requireMention && this.client?.user) {
        if (!message.mentions.has(this.client.user)) {
          return;
        }
      }
    } else {
      // DM pairing check — if pairing is enabled, require approval
      const pm = this.config.pairingManager;
      if (pm?.enabled && !pm.isApproved('discord', message.channelId)) {
        const text = message.content.trim();
        if (text && pm.validateCode('discord', message.channelId, text)) {
          try {
            if ('send' in message.channel) {
              (message.channel as any).send('Pairing successful! You can now chat with Flynn.');
            }
          } catch { /* ignore send errors */ }
        }
        return;
      }
    }

    // Send typing indicator (lasts 10 seconds, no need for interval)
    try {
      if ('sendTyping' in message.channel) {
        (message.channel as any).sendTyping();
      }
    } catch { /* ignore typing errors */ }

    // Strip bot mention from the message text
    const text = message.content.replace(/<@!?\d+>/g, '').trim();

    // ── Extract media attachments ──
    const attachments: Attachment[] = [];
    if (message.attachments && message.attachments.size > 0) {
      for (const attachment of message.attachments.values()) {
        const mimeType = attachment.contentType || this._inferMimeTypeFromUrl(attachment.url);
        if (mimeType && (mimeType.startsWith('image/') || mimeType.startsWith('audio/'))) {
          attachments.push({
            mimeType,
            url: attachment.url,
            filename: attachment.name,
            size: attachment.size,
          });
        }
      }
    }

    // ── Reset command ──
    if (text === '!reset' || text === 'reset') {
      this.messageHandler({
        id: message.id,
        channel: 'discord',
        senderId: message.channelId,
        senderName: message.author.username,
        text: '!reset',
        timestamp: Date.now(),
        metadata: { isCommand: true, command: 'reset' },
      });
      return;
    }

    // ── Regular message ──
    this.messageHandler({
      id: message.id,
      channel: 'discord',
      senderId: message.channelId,
      senderName: message.author.username,
      text,
      attachments: attachments.length > 0 ? attachments : undefined,
      timestamp: Date.now(),
    });
  }
}
