import { ChannelType, Client, GatewayIntentBits, type Message as DiscordMessage } from 'discord.js';
import { io, type Socket } from 'socket.io-client';

export interface DiscordPluginConfig {
  token: string;
  gatewayUrl: string;
  /** Which guild to bind to (single-guild only for v0.1.0) */
  guildId?: string;
}

export class DiscordPlugin {
  private client: Client;
  private socket: Socket | null = null;
  private config: DiscordPluginConfig;
  /** Map Discord channel ID → Mosaic conversation ID */
  private channelConversations = new Map<string, string>();
  /** Track in-flight responses to avoid duplicate streaming */
  private pendingResponses = new Map<string, string>();

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

  async start(): Promise<void> {
    // Connect to gateway WebSocket
    this.socket = io(`${this.config.gatewayUrl}/chat`, {
      transports: ['websocket'],
    });

    this.socket.on('connect', () => {
      console.log('[discord] Connected to gateway');
    });

    this.socket.on('disconnect', (reason: string) => {
      console.error(`[discord] Disconnected from gateway: ${reason}`);
      this.pendingResponses.clear();
    });

    this.socket.on('connect_error', (err: Error) => {
      console.error(`[discord] Gateway connection error: ${err.message}`);
    });

    // Handle streaming text from gateway
    this.socket.on('agent:text', (data: { conversationId: string; text: string }) => {
      const pending = this.pendingResponses.get(data.conversationId);
      if (pending !== undefined) {
        this.pendingResponses.set(data.conversationId, pending + data.text);
      }
    });

    // When agent finishes, send the accumulated response
    this.socket.on('agent:end', (data: { conversationId: string }) => {
      const text = this.pendingResponses.get(data.conversationId);
      if (text) {
        this.pendingResponses.delete(data.conversationId);
        this.sendToDiscord(data.conversationId, text).catch((err) => {
          console.error(`[discord] Error sending response for ${data.conversationId}:`, err);
        });
      }
    });

    this.socket.on('agent:start', (data: { conversationId: string }) => {
      this.pendingResponses.set(data.conversationId, '');
    });

    // Set up Discord message handler
    this.client.on('messageCreate', (message) => this.handleDiscordMessage(message));

    this.client.on('ready', () => {
      console.log(`[discord] Bot logged in as ${this.client.user?.tag}`);
    });

    await this.client.login(this.config.token);
  }

  async stop(): Promise<void> {
    this.socket?.disconnect();
    await this.client.destroy();
  }

  async createProjectChannel(project: {
    id: string;
    name: string;
    description?: string;
  }): Promise<{ channelId: string } | null> {
    if (!this.config.guildId) return null;

    const guild = this.client.guilds.cache.get(this.config.guildId);
    if (!guild) return null;

    // Slugify project name for channel: lowercase, replace spaces/special chars with hyphens
    const channelName = `mosaic-${project.name
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, '-')
      .replace(/^-|-$/g, '')}`;

    const channel = await guild.channels.create({
      name: channelName,
      type: ChannelType.GuildText,
      topic: project.description ?? `Mosaic project: ${project.name}`,
    });

    // Register the channel mapping so messages route correctly
    this.channelConversations.set(channel.id, `discord-${channel.id}`);

    return { channelId: channel.id };
  }

  private handleDiscordMessage(message: DiscordMessage): void {
    // Ignore bot messages
    if (message.author.bot) return;

    // Not ready yet
    if (!this.client.user) return;

    // Check guild binding
    if (this.config.guildId && message.guildId !== this.config.guildId) return;

    // Respond to DMs always, or mentions in channels
    const isDM = !message.guildId;
    const isMention = message.mentions.has(this.client.user);

    if (!isDM && !isMention) return;

    // Strip bot mention from message content
    const content = message.content
      .replace(new RegExp(`<@!?${this.client.user.id}>`, 'g'), '')
      .trim();

    if (!content) return;

    // Get or create conversation for this Discord channel
    const channelId = message.channelId;
    let conversationId = this.channelConversations.get(channelId);
    if (!conversationId) {
      conversationId = `discord-${channelId}`;
      this.channelConversations.set(channelId, conversationId);
    }

    // Send to gateway
    if (!this.socket?.connected) {
      console.error(
        `[discord] Cannot forward message: not connected to gateway. channel=${channelId}`,
      );
      return;
    }
    this.socket.emit('message', {
      conversationId,
      content,
    });
  }

  private async sendToDiscord(conversationId: string, text: string): Promise<void> {
    // Find the Discord channel for this conversation
    const channelId = Array.from(this.channelConversations.entries()).find(
      ([, convId]) => convId === conversationId,
    )?.[0];

    if (!channelId) {
      console.error(`[discord] No channel found for conversation ${conversationId}`);
      return;
    }

    const channel = this.client.channels.cache.get(channelId);
    if (!channel || !('send' in channel)) {
      console.error(
        `[discord] Channel ${channelId} not sendable for conversation ${conversationId}`,
      );
      return;
    }

    // Chunk responses for Discord's 2000-char limit
    const chunks = this.chunkText(text, 1900);
    for (const chunk of chunks) {
      try {
        await (channel as { send: (content: string) => Promise<unknown> }).send(chunk);
      } catch (err) {
        console.error(`[discord] Failed to send message to channel ${channelId}:`, err);
      }
    }
  }

  private chunkText(text: string, maxLength: number): string[] {
    if (text.length <= maxLength) return [text];

    const chunks: string[] = [];
    let remaining = text;

    while (remaining.length > 0) {
      if (remaining.length <= maxLength) {
        chunks.push(remaining);
        break;
      }

      // Try to break at a newline
      let breakPoint = remaining.lastIndexOf('\n', maxLength);
      if (breakPoint <= 0) breakPoint = maxLength;

      chunks.push(remaining.slice(0, breakPoint));
      remaining = remaining.slice(breakPoint).trimStart();
    }

    return chunks;
  }
}
