/**
 * Slack channel adapter.
 *
 * Implements the ChannelAdapter interface using @slack/bolt with Socket Mode.
 * Thread-aware: each channel+thread gets its own session via channelId:threadTs peer IDs.
 * Messages are chunked at 4000 chars for readability.
 */

import { App } from '@slack/bolt';
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 Slack channel adapter. */
export interface SlackAdapterConfig {
  botToken: string;
  appToken: string;
  signingSecret: string;
  /** Channel IDs to respond in. Empty = all channels. */
  allowedChannelIds?: string[];
  /** Require bot mention to respond (default: false). */
  requireMention?: boolean;
  /** Optional pairing manager for DM pairing codes. */
  pairingManager?: PairingManager;
}

/** Minimal shape of a Slack message event from Bolt. */
interface SlackMessageEvent {
  ts?: string;
  thread_ts?: string;
  channel?: string;
  user?: string;
  text?: string;
  bot_id?: string;
  subtype?: string;
  files?: Array<{
    id?: string;
    mimetype?: string;
    name?: string;
    size?: number;
    url_private?: string;
    url_private_download?: string;
  }>;
}

/**
 * Slack channel adapter backed by @slack/bolt.
 *
 * Handles channel filtering, thread-aware peer IDs,
 * and message chunking for readability (4000 chars).
 */
export class SlackAdapter implements ChannelAdapter {
  readonly name = 'slack';

  private _status: ChannelStatus = 'disconnected';
  private app: App | null = null;
  private messageHandler?: (msg: InboundMessage) => void;
  private config: SlackAdapterConfig;
  private userNameCache: Map<string, string> = new Map();
  private botUserId?: string;

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

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

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

  /** Create the Bolt app with Socket Mode, wire up event handlers, and start. */
  async connect(): Promise<void> {
    this._status = 'connecting';

    try {
      this.app = new App({
        token: this.config.botToken,
        appToken: this.config.appToken,
        signingSecret: this.config.signingSecret,
        socketMode: true,
      });

      // Register message event handler
      this.app.message(async ({ message }) => {
        await this.handleMessage(message as unknown as SlackMessageEvent);
      });

      await this.app.start();

      // Resolve bot user ID for mention detection
      try {
        const authResult = await this.app.client.auth.test();
        this.botUserId = authResult.user_id as string | undefined;
      } catch {
        console.warn('Slack: could not resolve bot user ID for mention detection');
      }

      this._status = 'connected';
      console.log('Slack bot connected via Socket Mode');
    } catch (error) {
      this._status = 'error';
      throw error;
    }
  }

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

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

    // Parse peerId: "channelId:threadTs"
    const colonIndex = peerId.indexOf(':');
    if (colonIndex === -1) {throw new Error(`Invalid peer ID format: ${peerId}`);}

    const channel = peerId.slice(0, colonIndex);
    const threadTs = peerId.slice(colonIndex + 1);
    if (!channel || !threadTs) {throw new Error(`Invalid peer ID format: ${peerId}`);}

    const text = message.text;

    if (text.length <= 4000) {
      await this.app.client.chat.postMessage({
        channel,
        text,
        thread_ts: threadTs,
      });
    } else {
      const chunks = splitMessage(text, 4000);
      for (const chunk of chunks) {
        await this.app.client.chat.postMessage({
          channel,
          text: chunk,
          thread_ts: threadTs,
        });
      }
    }

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

  /** Upload and send a single outbound attachment via Slack's files.uploadV2 API. */
  private async sendAttachment(
    channel: string,
    threadTs: string,
    attachment: OutboundAttachment,
  ): Promise<void> {
    if (!this.app) {return;}

    try {
      if (attachment.data) {
        await this.app.client.files.uploadV2({
          channel_id: channel,
          thread_ts: threadTs,
          file: Buffer.from(attachment.data, 'base64'),
          filename: attachment.filename ?? 'attachment',
        });
      } else if (attachment.url) {
        // For URL-based attachments, share as a text message with the URL
        await this.app.client.chat.postMessage({
          channel,
          text: attachment.url,
          thread_ts: threadTs,
        });
      }
    } catch (error) {
      console.error(
        `Slack: failed to send ${attachment.mimeType} attachment:`,
        error instanceof Error ? error.message : 'Unknown error',
      );
    }
  }

  /** Resolve a Slack user ID to a display name, with caching. */
  private async resolveUserName(userId: string): Promise<string> {
    const cached = this.userNameCache.get(userId);
    if (cached) {return cached;}

    try {
      const result = await this.app!.client.users.info({ user: userId });
      const name = result.user?.real_name || result.user?.name || userId;
      this.userNameCache.set(userId, name);
      return name;
    } catch {
      return userId;
    }
  }

  /**
   * Download media files from a Slack message and convert to base64 Attachments.
   * Non-media files are skipped. Download errors are logged but don't crash the handler.
   */
  private async extractMediaAttachments(
    files?: SlackMessageEvent['files'],
  ): Promise<Attachment[]> {
    if (!files || files.length === 0) {return [];}

    const attachments: Attachment[] = [];

    for (const file of files) {
      // Only process image and audio files
      if (!file.mimetype?.startsWith('image/') && !file.mimetype?.startsWith('audio/')) {continue;}

      const downloadUrl = file.url_private_download || file.url_private;
      if (!downloadUrl) {continue;}

      try {
        const response = await fetch(downloadUrl, {
          headers: { Authorization: `Bearer ${this.config.botToken}` },
        });

        if (!response.ok) {
          console.warn(
            `Slack: failed to download file ${file.name ?? file.id ?? 'unknown'}: HTTP ${response.status}`,
          );
          continue;
        }

        const arrayBuffer = await response.arrayBuffer();
        const base64 = Buffer.from(arrayBuffer).toString('base64');

        attachments.push({
          mimeType: file.mimetype,
          data: base64,
          filename: file.name,
          size: file.size,
        });
      } catch (error) {
        console.warn(
          `Slack: error downloading file ${file.name ?? file.id ?? 'unknown'}:`,
          error instanceof Error ? error.message : 'Unknown error',
        );
      }
    }

    return attachments;
  }

  /** Internal: process an inbound Slack message event. */
  private async handleMessage(message: SlackMessageEvent): Promise<void> {
    if (!this.messageHandler) {return;}

    // Ignore bot messages
    if (message.bot_id || message.subtype === 'bot_message') {return;}

    const channelId = message.channel;
    if (!channelId) {return;}

    // Check allowed channel IDs
    if (
      this.config.allowedChannelIds &&
      this.config.allowedChannelIds.length > 0 &&
      !this.config.allowedChannelIds.includes(channelId)
    ) {
      // Pairing fallback — check if the Slack user is approved or sending a valid code
      const pm = this.config.pairingManager;
      const userId = message.user;
      if (pm?.enabled && userId) {
        if (pm.isApproved('slack', userId)) {
          // Approved — fall through to normal message handling
        } else {
          const text = (message.text ?? '').trim();
          if (text && pm.validateCode('slack', userId, text)) {
            // Code validated — send confirmation via Slack
            if (this.app) {
              const threadTs = message.thread_ts ?? message.ts ?? '';
              try {
                await this.app.client.chat.postMessage({
                  channel: channelId,
                  text: 'Pairing successful! You can now chat with Flynn.',
                  thread_ts: threadTs || undefined,
                });
              } catch { /* ignore send errors */ }
            }
          }
          return;
        }
      } else {
        return;
      }
    }

    // Mention requirement
    const requireMention = this.config.requireMention ?? false;
    if (requireMention && this.botUserId) {
      const mentionPattern = `<@${this.botUserId}>`;
      if (!(message.text ?? '').includes(mentionPattern)) {
        return;
      }
    }

    // Note: Slack doesn't expose a typing indicator API for bots

    // Build peer ID: channelId:threadTs (thread-aware)
    const threadTs = message.thread_ts ?? message.ts ?? '';
    const peerId = `${channelId}:${threadTs}`;

    // Strip bot mentions: <@U\w+> pattern
    let text = (message.text ?? '').replace(/<@U\w+>/g, '').trim();

    // Resolve display name from Slack user ID
    const senderName = message.user
      ? await this.resolveUserName(message.user)
      : undefined;

    // Extract media attachments from Slack file uploads
    const attachments = await this.extractMediaAttachments(message.files);

    // Detect reset command
    if (text === '!reset' || text === 'reset') {
      this.messageHandler({
        id: message.ts ?? '',
        channel: 'slack',
        senderId: peerId,
        senderName,
        text: '!reset',
        timestamp: Date.now(),
        metadata: { isCommand: true, command: 'reset' },
        ...(attachments.length > 0 && { attachments }),
      });
      return;
    }

    // Regular message
    this.messageHandler({
      id: message.ts ?? '',
      channel: 'slack',
      senderId: peerId,
      senderName,
      text,
      timestamp: Date.now(),
      ...(attachments.length > 0 && { attachments }),
    });
  }
}
