/**
 * Slack Channel Adapter
 * 
 * Uses @slack/bolt for Slack API with Socket Mode.
 */

import type { ChannelAdapter } from './types.js';
import type { InboundAttachment, InboundMessage, InboundReaction, OutboundFile, OutboundMessage } from '../core/types.js';
import { createReadStream } from 'node:fs';
import { basename } from 'node:path';
import { buildAttachmentPath, downloadToFile } from './attachments.js';
import { parseCommand, HELP_TEXT } from '../core/commands.js';
import { markdownToSlackMrkdwn } from './slack-format.js';
import { isGroupAllowed, isGroupUserAllowed, resolveGroupMode, resolveDailyLimits, checkDailyLimit, type GroupMode, type GroupModeConfig } from './group-mode.js';

import { EMOJI_ALIASES } from './shared/emoji.js';
import { createLogger } from '../logger.js';

const log = createLogger('Slack');
// Dynamic import to avoid requiring Slack deps if not used
let App: typeof import('@slack/bolt').App;

export interface SlackConfig {
  botToken: string;       // xoxb-...
  appToken: string;       // xapp-... (for Socket Mode)
  dmPolicy?: 'pairing' | 'allowlist' | 'open';
  allowedUsers?: string[]; // Slack user IDs (e.g., U01234567)
  streaming?: boolean;    // Stream responses via progressive message edits (default: false)
  attachmentsDir?: string;
  attachmentsMaxBytes?: number;
  groups?: Record<string, GroupModeConfig>;  // Per-channel settings
  agentName?: string;       // For scoping daily limit counters in multi-agent mode
}

export class SlackAdapter implements ChannelAdapter {
  readonly id = 'slack' as const;
  readonly name = 'Slack';
  
  private app: InstanceType<typeof App> | null = null;
  private config: SlackConfig;
  private running = false;
  private attachmentsDir?: string;
  private attachmentsMaxBytes?: number;
  
  onMessage?: (msg: InboundMessage) => Promise<void>;
  onCommand?: (command: string, chatId?: string, args?: string) => Promise<string | null>;
  
  constructor(config: SlackConfig) {
    this.config = config;
    this.attachmentsDir = config.attachmentsDir;
    this.attachmentsMaxBytes = config.attachmentsMaxBytes;
  }
  
  async start(): Promise<void> {
    if (this.running) return;
    
    // Dynamic import
    const bolt = await import('@slack/bolt');
    App = bolt.App;
    
    this.app = new App({
      token: this.config.botToken,
      appToken: this.config.appToken,
      socketMode: true,
    });
    
    // Handle messages
    this.app.message(async ({ message, say, client }) => {
      // Type guard for regular messages (allow file_share for voice messages)
      if (message.subtype !== undefined && message.subtype !== 'file_share') return;
      if (!('user' in message)) return;
      
      const userId = message.user;
      let text = message.text || '';
      const channelId = message.channel;
      const threadTs = message.thread_ts || message.ts; // Reply in thread if applicable
      
      // Handle audio file attachments
      const files = (message as any).files as Array<{ mimetype?: string; url_private_download?: string; name?: string }> | undefined;
      const audioFile = files?.find(f => f.mimetype?.startsWith('audio/'));
      if (audioFile?.url_private_download) {
        try {
          const { isTranscriptionConfigured } = await import('../transcription/index.js');
          if (!isTranscriptionConfigured()) {
            await say('Voice messages require a transcription API key. See: https://github.com/letta-ai/lettabot#voice');
          } else {
            // Download file (requires bot token for auth)
            const response = await fetch(audioFile.url_private_download, {
              headers: { 'Authorization': `Bearer ${this.config.botToken}` }
            });
            const buffer = Buffer.from(await response.arrayBuffer());
            
            const { transcribeAudio } = await import('../transcription/index.js');
            const ext = audioFile.mimetype?.split('/')[1] || 'mp3';
            const result = await transcribeAudio(buffer, audioFile.name || `audio.${ext}`);
            
            if (result.success && result.text) {
              log.info(`Transcribed audio: "${result.text.slice(0, 50)}..."`);
              text = (text ? text + '\n' : '') + `[Voice message]: ${result.text}`;
            } else {
              log.error(`Transcription failed: ${result.error}`);
              text = (text ? text + '\n' : '') + `[Voice message - transcription failed: ${result.error}]`;
            }
          }
        } catch (error) {
          log.error('Error transcribing audio:', error);
          text = (text ? text + '\n' : '') + `[Voice message - error: ${error instanceof Error ? error.message : 'unknown error'}]`;
        }
      }
      
      // Check allowed users
      if (this.config.allowedUsers && this.config.allowedUsers.length > 0) {
        if (!this.config.allowedUsers.includes(userId)) {
          await say("Sorry, you're not authorized to use this bot.");
          return;
        }
      }
      
      // Handle slash commands
      const parsed = parseCommand(text);
      if (parsed) {
        if (parsed.command === 'help' || parsed.command === 'start') {
          await say(await markdownToSlackMrkdwn(HELP_TEXT));
        } else if (this.onCommand) {
          const result = await this.onCommand(parsed.command, channelId, parsed.args || undefined);
          if (result) await say(await markdownToSlackMrkdwn(result));
        }
        return; // Don't pass commands to agent
      }
      
      if (this.onMessage) {
        const attachments = await this.collectAttachments(
          (message as { files?: SlackFile[] }).files,
          channelId
        );
        // Determine if this is a group/channel (not a DM)
        // DMs have channel IDs starting with 'D', channels start with 'C'
        const isGroup = !channelId.startsWith('D');
        let mode: GroupMode = 'open';

        // Group gating: config-based allowlist + mode
        if (isGroup) {
          if (!this.isChannelAllowed(channelId)) {
            return; // Channel not in allowlist -- silent drop
          }
          if (!isGroupUserAllowed(this.config.groups, [channelId], userId || '')) {
            return; // User not in group allowedUsers -- silent drop
          }
          mode = this.resolveChannelMode(channelId);
          if (mode === 'disabled') {
            return; // Groups disabled for this channel -- silent drop
          }
          if (mode === 'mention-only') {
            // Non-mention message in channel that requires mentions.
            // The app_mention handler will process actual @mentions.
            return;
          }

          // Daily rate limit check
          const limits = resolveDailyLimits(this.config.groups, [channelId]);
          const counterKey = `${this.config.agentName ?? ''}:slack:${limits.matchedKey ?? channelId}`;
          const limitResult = checkDailyLimit(counterKey, userId || '', limits);
          if (!limitResult.allowed) {
            log.info(`Daily limit reached for ${counterKey} (${limitResult.reason})`);
            return;
          }
        }
        
        await this.onMessage({
          channel: 'slack',
          chatId: channelId,
          userId: userId || '',
          userHandle: userId || '',  // Slack user ID serves as handle
          messageId: message.ts || undefined,
          text: text || '',
          timestamp: new Date(Number(message.ts) * 1000),
          threadId: threadTs,
          isGroup,
          groupName: isGroup ? channelId : undefined,  // Would need conversations.info for name
          wasMentioned: false, // Regular messages; app_mention handles mentions
          isListeningMode: mode === 'listen',
          attachments,
          formatterHints: this.getFormatterHints(),
        });
      }
    });

    // Handle app mentions (@bot)
    this.app.event('app_mention', async ({ event }) => {
      const userId = event.user || '';
      let text = (event.text || '').replace(/<@[A-Z0-9]+>/g, '').trim(); // Remove mention
      const channelId = event.channel;
      const threadTs = event.thread_ts || event.ts; // Reply in thread, or start new thread from the mention

      // Handle audio file attachments
      const files = (event as any).files as Array<{ mimetype?: string; url_private_download?: string; name?: string }> | undefined;
      const audioFile = files?.find(f => f.mimetype?.startsWith('audio/'));
      if (audioFile?.url_private_download) {
        try {
          const { isTranscriptionConfigured } = await import('../transcription/index.js');
          if (!isTranscriptionConfigured()) {
            await this.sendMessage({ chatId: channelId, text: 'Voice messages require a transcription API key. See: https://github.com/letta-ai/lettabot#voice', threadId: threadTs });
            return;
          }
          // Download file (requires bot token for auth)
          const response = await fetch(audioFile.url_private_download, {
            headers: { 'Authorization': `Bearer ${this.config.botToken}` }
          });
          const buffer = Buffer.from(await response.arrayBuffer());

          const { transcribeAudio } = await import('../transcription/index.js');
          const ext = audioFile.mimetype?.split('/')[1] || 'mp3';
          const result = await transcribeAudio(buffer, audioFile.name || `audio.${ext}`);

          if (result.success && result.text) {
            log.info(`Transcribed audio: "${result.text.slice(0, 50)}..."`);
            text = (text ? text + '\n' : '') + `[Voice message]: ${result.text}`;
          } else {
            log.error(`Transcription failed: ${result.error}`);
            text = (text ? text + '\n' : '') + `[Voice message - transcription failed: ${result.error}]`;
          }
        } catch (error) {
          log.error('Error transcribing audio:', error);
          text = (text ? text + '\n' : '') + `[Voice message - error: ${error instanceof Error ? error.message : 'unknown error'}]`;
        }
      }

      if (this.config.allowedUsers && this.config.allowedUsers.length > 0) {
        if (!userId || !this.config.allowedUsers.includes(userId)) {
          // Can't use say() in app_mention event the same way
          return;
        }
      }

      // Group gating: allowlist + mode + user check (mention already satisfied by app_mention)
      if (this.config.groups && !this.isChannelAllowed(channelId)) {
        return; // Channel not in allowlist -- silent drop
      }
      if (this.resolveChannelMode(channelId) === 'disabled') {
        return; // Groups disabled for this channel -- silent drop
      }
      if (!isGroupUserAllowed(this.config.groups, [channelId], userId)) {
        return; // User not in group allowedUsers -- silent drop
      }

      // Handle slash commands (before rate limiting -- commands should always work)
      const parsed = parseCommand(text);
      if (parsed) {
        if (parsed.command === 'help' || parsed.command === 'start') {
          await this.sendMessage({ chatId: channelId, text: HELP_TEXT, threadId: threadTs });
        } else if (this.onCommand) {
          const result = await this.onCommand(parsed.command, channelId, parsed.args || undefined);
          if (result) await this.sendMessage({ chatId: channelId, text: result, threadId: threadTs });
        }
        return; // Don't pass commands to agent
      }

      // Daily rate limit check (after commands so /help, /reset etc. always work)
      const mentionLimits = resolveDailyLimits(this.config.groups, [channelId]);
      const mentionCounterKey = `${this.config.agentName ?? ''}:slack:${mentionLimits.matchedKey ?? channelId}`;
      const mentionLimitResult = checkDailyLimit(mentionCounterKey, userId, mentionLimits);
      if (!mentionLimitResult.allowed) {
        log.info(`Daily limit reached for ${mentionCounterKey} (${mentionLimitResult.reason})`);
        return;
      }
      
      if (this.onMessage) {
        const attachments = await this.collectAttachments(
          (event as { files?: SlackFile[] }).files,
          channelId
        );
        // app_mention is always in a channel (group)
        const isGroup = !channelId.startsWith('D');
        
        await this.onMessage({
          channel: 'slack',
          chatId: channelId,
          userId: userId || '',
          userHandle: userId || '',  // Slack user ID serves as handle
          messageId: event.ts || undefined,
          text: text || '',
          timestamp: new Date(Number(event.ts) * 1000),
          threadId: threadTs,
          isGroup,
          groupName: isGroup ? channelId : undefined,
          wasMentioned: true, // app_mention is always a mention
          attachments,
          formatterHints: this.getFormatterHints(),
        });
      }
    });

    this.app.event('reaction_added', async ({ event }) => {
      await this.handleReactionEvent(event as SlackReactionEvent, 'added');
    });

    this.app.event('reaction_removed', async ({ event }) => {
      await this.handleReactionEvent(event as SlackReactionEvent, 'removed');
    });
    
    log.info('Connecting via Socket Mode...');
    await this.app.start();
    log.info('Bot started in Socket Mode');
    this.running = true;
  }
  
  async stop(): Promise<void> {
    if (!this.running || !this.app) return;
    await this.app.stop();
    this.running = false;
  }
  
  isRunning(): boolean {
    return this.running;
  }
  
  async sendMessage(msg: OutboundMessage): Promise<{ messageId: string }> {
    if (!this.app) throw new Error('Slack not started');

    const formatted = await markdownToSlackMrkdwn(msg.text);
    const result = await this.app.client.chat.postMessage({
      channel: msg.chatId,
      text: formatted,
      thread_ts: msg.threadId,
    });
    
    return { messageId: result.ts || '' };
  }

  async sendFile(file: OutboundFile): Promise<{ messageId: string }> {
    if (!this.app) throw new Error('Slack not started');

    const initialComment = file.caption ? await markdownToSlackMrkdwn(file.caption) : undefined;
    const basePayload = {
      channels: file.chatId,
      file: createReadStream(file.filePath),
      filename: basename(file.filePath),
      initial_comment: initialComment,
    };
    const result = file.threadId
      ? await this.app.client.files.upload({ ...basePayload, thread_ts: file.threadId })
      : await this.app.client.files.upload(basePayload);

    const shares = (result.file as { shares?: Record<string, Record<string, { ts?: string }[]>> } | undefined)?.shares;
    const ts = shares?.public?.[file.chatId]?.[0]?.ts
      || shares?.private?.[file.chatId]?.[0]?.ts
      || '';

    return { messageId: ts };
  }
  
  supportsEditing(): boolean {
    return this.config.streaming ?? false;
  }

  async editMessage(chatId: string, messageId: string, text: string): Promise<void> {
    if (!this.app) throw new Error('Slack not started');

    const formatted = await markdownToSlackMrkdwn(text);
    await this.app.client.chat.update({
      channel: chatId,
      ts: messageId,
      text: formatted,
    });
  }

  async addReaction(chatId: string, messageId: string, emoji: string): Promise<void> {
    if (!this.app) throw new Error('Slack not started');
    const name = resolveSlackEmojiName(emoji);
    if (!name) {
      throw new Error('Unknown emoji alias for Slack');
    }
    await this.app.client.reactions.add({
      channel: chatId,
      name,
      timestamp: messageId,
    });
  }
  
  getDmPolicy(): string {
    return this.config.dmPolicy || 'pairing';
  }

  getFormatterHints() {
    return {
      supportsReactions: true,
      supportsFiles: true,
      formatHint: 'Slack mrkdwn: *bold* _italic_ `code` <URL|text> — NO standard markdown headers',
    };
  }

  /** Check if a channel is allowed by the groups config allowlist */
  private isChannelAllowed(channelId: string): boolean {
    return isGroupAllowed(this.config.groups, [channelId]);
  }

  /** Resolve group mode for a channel (specific > wildcard > open). */
  private resolveChannelMode(channelId: string): GroupMode {
    return resolveGroupMode(this.config.groups, [channelId], 'open');
  }

  async sendTypingIndicator(_chatId: string): Promise<void> {
    // Slack doesn't have a typing indicator API for bots
    // This is a no-op
  }

  private async handleReactionEvent(
    event: SlackReactionEvent,
    action: InboundReaction['action']
  ): Promise<void> {
    const userId = event.user || '';
    if (!userId) return;

    if (this.config.allowedUsers && this.config.allowedUsers.length > 0) {
      if (!this.config.allowedUsers.includes(userId)) {
        return;
      }
    }

    const channelId = event.item?.channel;
    const messageId = event.item?.ts;
    if (!channelId || !messageId) return;

    const emoji = event.reaction ? `:${event.reaction}:` : '';
    if (!emoji) return;

    const isGroup = !channelId.startsWith('D');
    const eventTs = Number(event.event_ts);
    const timestamp = Number.isFinite(eventTs) ? new Date(eventTs * 1000) : new Date();

    await this.onMessage?.({
      channel: 'slack',
      chatId: channelId,
      userId,
      userHandle: userId,
      messageId,
      text: '',
      timestamp,
      isGroup,
      groupName: isGroup ? channelId : undefined,
      reaction: {
        emoji,
        messageId,
        action,
      },
      formatterHints: this.getFormatterHints(),
    });
  }

  private async collectAttachments(
    files: SlackFile[] | undefined,
    channelId: string
  ): Promise<InboundAttachment[]> {
    return collectSlackAttachments(
      this.attachmentsDir,
      this.attachmentsMaxBytes,
      channelId,
      files,
      this.config.botToken
    );
  }
}

type SlackFile = {
  id?: string;
  name?: string;
  mimetype?: string;
  size?: number;
  url_private?: string;
  url_private_download?: string;
};

type SlackReactionEvent = {
  user?: string;
  reaction?: string;
  item?: {
    channel?: string;
    ts?: string;
  };
  event_ts?: string;
};

async function maybeDownloadSlackFile(
  attachmentsDir: string | undefined,
  attachmentsMaxBytes: number | undefined,
  channelId: string,
  file: SlackFile,
  token: string
): Promise<InboundAttachment> {
  const name = file.name || file.id || 'attachment';
  const url = file.url_private_download || file.url_private;
  const attachment: InboundAttachment = {
    id: file.id,
    name,
    mimeType: file.mimetype,
    size: file.size,
    kind: file.mimetype?.startsWith('image/') ? 'image' : 'file',
    url,
  };
  if (!attachmentsDir) {
    return attachment;
  }
  if (attachmentsMaxBytes === 0) {
    return attachment;
  }
  if (attachmentsMaxBytes && file.size && file.size > attachmentsMaxBytes) {
    log.warn(`Attachment ${name} exceeds size limit, skipping download.`);
    return attachment;
  }
  if (!url) {
    return attachment;
  }
  const target = buildAttachmentPath(attachmentsDir, 'slack', channelId, name);
  try {
    await downloadToFile(url, target, {
      headers: { Authorization: `Bearer ${token}` },
    });
    attachment.localPath = target;
    log.info(`Attachment saved to ${target}`);
  } catch (err) {
    log.warn('Failed to download attachment:', err);
  }
  return attachment;
}

async function collectSlackAttachments(
  attachmentsDir: string | undefined,
  attachmentsMaxBytes: number | undefined,
  channelId: string,
  files: SlackFile[] | undefined,
  token: string
): Promise<InboundAttachment[]> {
  if (!files || files.length === 0) return [];
  const attachments: InboundAttachment[] = [];
  for (const file of files) {
    attachments.push(await maybeDownloadSlackFile(attachmentsDir, attachmentsMaxBytes, channelId, file, token));
  }
  return attachments;
}

// Reverse lookup: unicode -> alias name (for Slack API which uses names, not unicode)
const UNICODE_TO_ALIAS = new Map<string, string>(
  Object.entries(EMOJI_ALIASES).map(([name, value]) => [value, name])
);

function resolveSlackEmojiName(input: string): string | null {
  const aliasMatch = input.match(/^:([^:]+):$/);
  if (aliasMatch) {
    return aliasMatch[1];
  }
  if (EMOJI_ALIASES[input]) {
    return input;
  }
  return UNICODE_TO_ALIAS.get(input) || null;
}
