/**
 * Slack Channel Client
 *
 * Slack bot implementation using the Slack Web API.
 * Supports both HTTP mode and Socket Mode.
 */
import WebSocket from 'ws';
import crypto from 'crypto';
import { BaseChannel, getSessionKey, checkDMPairing } from '../index.js';
import { ReconnectionManager } from '../reconnection-manager.js';
const SLACK_API_BASE = 'https://slack.com/api';
const SLACK_SOCKET_URL = 'wss://wss-primary.slack.com/websocket';
/**
 * Slack channel implementation
 */
export class SlackChannel extends BaseChannel {
    ws = null;
    botUserId = null;
    reconnecting = false;
    pingInterval = null;
    userCache = new Map();
    channelCache = new Map();
    reconnectionManager;
    constructor(config) {
        super('slack', config);
        if (!config.token) {
            throw new Error('Slack bot token is required');
        }
        this.reconnectionManager = new ReconnectionManager('slack', {
            maxRetries: 10,
            initialDelayMs: 1000,
            maxDelayMs: 60000,
        });
    }
    get slackConfig() {
        return this.config;
    }
    /**
     * Make API request to Slack
     */
    async apiRequest(method, params) {
        const url = `${SLACK_API_BASE}/${method}`;
        const headers = {
            Authorization: `Bearer ${this.slackConfig.token}`,
            'Content-Type': 'application/json; charset=utf-8',
        };
        const response = await fetch(url, {
            method: 'POST',
            headers,
            body: params ? JSON.stringify(params) : undefined,
        });
        const data = (await response.json());
        if (!data.ok) {
            throw new Error(`Slack API error: ${data.error || 'Unknown error'}`);
        }
        return data;
    }
    /**
     * Connect to Slack
     */
    async connect() {
        try {
            // Test authentication and get bot user ID
            const authTest = await this.apiRequest('auth.test');
            this.botUserId = authTest.user_id;
            this.slackConfig.botUserId = authTest.user_id;
            this.status.connected = true;
            this.status.authenticated = true;
            this.status.info = {
                botId: authTest.user_id,
                botName: authTest.user,
                teamId: authTest.team_id,
                teamName: authTest.team,
            };
            // Connect via Socket Mode if configured
            if (this.slackConfig.socketMode && this.slackConfig.appToken) {
                await this.connectSocketMode();
            }
            this.emit('connected', 'slack');
        }
        catch (error) {
            this.status.error = error instanceof Error ? error.message : String(error);
            this.emit('error', 'slack', error);
            throw error;
        }
    }
    /**
     * Connect via Socket Mode
     */
    async connectSocketMode() {
        return new Promise((resolve, reject) => {
            try {
                // Get WebSocket URL
                this.getSocketUrl()
                    .then((url) => {
                    this.ws = new WebSocket(url);
                    this.ws.on('open', () => {
                        // Start ping interval
                        this.pingInterval = setInterval(() => {
                            if (this.ws?.readyState === WebSocket.OPEN) {
                                this.ws.ping();
                            }
                        }, 30000);
                        resolve();
                    });
                    this.ws.on('message', async (data) => {
                        try {
                            const payload = JSON.parse(data.toString());
                            await this.handleSocketMessage(payload);
                        }
                        catch (error) {
                            this.emit('error', 'slack', error);
                        }
                    });
                    this.ws.on('close', () => {
                        this.handleClose();
                    });
                    this.ws.on('error', (error) => {
                        this.emit('error', 'slack', error);
                        if (!this.status.connected) {
                            reject(error);
                        }
                    });
                })
                    .catch(reject);
            }
            catch (error) {
                reject(error);
            }
        });
    }
    /**
     * Get Socket Mode URL
     */
    async getSocketUrl() {
        const response = await fetch(`${SLACK_API_BASE}/apps.connections.open`, {
            method: 'POST',
            headers: {
                Authorization: `Bearer ${this.slackConfig.appToken}`,
                'Content-Type': 'application/x-www-form-urlencoded',
            },
        });
        const data = (await response.json());
        if (!data.ok || !data.url) {
            throw new Error(`Failed to get Socket Mode URL: ${data.error}`);
        }
        return data.url;
    }
    /**
     * Handle Socket Mode message
     */
    async handleSocketMessage(payload) {
        // Acknowledge the message
        if (payload.envelope_id) {
            this.ws?.send(JSON.stringify({ envelope_id: payload.envelope_id }));
        }
        switch (payload.type) {
            case 'events_api':
                if (payload.payload && 'event' in payload.payload) {
                    await this.handleEvent(payload.payload.event);
                }
                break;
            case 'interactive':
                if (payload.payload && 'type' in payload.payload) {
                    await this.handleInteraction(payload.payload);
                }
                break;
            case 'slash_commands':
                if (payload.payload) {
                    await this.handleSlashCommand(payload.payload);
                }
                break;
            case 'hello':
                // Connection established
                break;
            case 'disconnect':
                this.handleClose();
                break;
        }
    }
    /**
     * Handle event
     */
    async handleEvent(event) {
        // Ignore bot messages
        if (event.bot_id || event.app_id)
            return;
        // Check if user is allowed
        if (event.user && !this.isUserAllowed(event.user))
            return;
        // Check if channel is allowed
        if (event.channel && !this.isChannelAllowed(event.channel))
            return;
        switch (event.type) {
            case 'message':
                if (!event.subtype) {
                    await this.handleMessage(event);
                }
                else if (event.subtype === 'message_changed') {
                    // Could emit 'message-edited' event
                }
                break;
            case 'app_mention':
                await this.handleMessage(event);
                break;
            case 'reaction_added':
                // Could emit reaction event
                break;
        }
    }
    /**
     * Handle message event
     */
    async handleMessage(event) {
        if (!event.text && !event.files?.length)
            return;
        const message = await this.convertEvent(event);
        const parsed = this.parseCommand(message);
        // Attach session key for session isolation
        parsed.sessionKey = getSessionKey(parsed);
        // DM pairing check: gate unapproved DM senders
        const pairingStatus = await checkDMPairing(parsed);
        if (!pairingStatus.approved) {
            const { getDMPairing } = await import('../dm-pairing.js');
            const pairingMessage = getDMPairing().getPairingMessage(pairingStatus);
            if (pairingMessage && event.channel) {
                await this.send({
                    channelId: event.channel,
                    content: pairingMessage,
                });
            }
            return;
        }
        this.emit('message', parsed);
        if (parsed.isCommand) {
            this.emit('command', parsed);
        }
    }
    /**
     * Handle interaction (button click, select, etc.)
     */
    async handleInteraction(payload) {
        if (payload.type === 'block_actions' && payload.actions?.length) {
            const action = payload.actions[0];
            const message = {
                id: `interaction-${Date.now()}`,
                channel: {
                    id: payload.channel?.id ?? 'unknown',
                    type: 'slack',
                    name: payload.channel?.name,
                },
                sender: {
                    id: payload.user.id,
                    username: payload.user.username,
                    displayName: payload.user.name,
                },
                content: action.value ?? action.selected_option?.value ?? action.action_id,
                contentType: 'command',
                timestamp: new Date(),
                isCommand: true,
                commandName: 'action',
                commandArgs: [action.action_id, action.value ?? ''],
                raw: payload,
            };
            // Attach session key for session isolation
            message.sessionKey = getSessionKey(message);
            this.emit('command', message);
        }
    }
    /**
     * Handle slash command
     */
    async handleSlashCommand(command) {
        const message = {
            id: `command-${Date.now()}`,
            channel: {
                id: command.channel_id,
                type: 'slack',
                name: command.channel_name,
            },
            sender: {
                id: command.user_id,
                username: command.user_name,
            },
            content: `${command.command} ${command.text}`.trim(),
            contentType: 'command',
            timestamp: new Date(),
            isCommand: true,
            commandName: command.command.replace(/^\//, ''),
            commandArgs: command.text.split(/\s+/).filter(Boolean),
            raw: command,
        };
        // Attach session key for session isolation
        message.sessionKey = getSessionKey(message);
        this.emit('command', message);
    }
    /**
     * Handle webhook event (for HTTP mode)
     */
    async handleWebhook(body, signature, timestamp) {
        // URL verification challenge
        if (body.type === 'url_verification') {
            return body.challenge;
        }
        // Verify signature if signing secret is configured
        if (this.slackConfig.signingSecret && signature && timestamp) {
            if (!this.verifySignature(JSON.stringify(body), signature, timestamp)) {
                throw new Error('Invalid request signature');
            }
        }
        if (body.type === 'event_callback') {
            await this.handleEvent(body.event);
        }
    }
    /**
     * Verify request signature
     */
    verifySignature(body, signature, timestamp) {
        const sigBasestring = `v0:${timestamp}:${body}`;
        const mySignature = 'v0=' +
            crypto
                .createHmac('sha256', this.slackConfig.signingSecret)
                .update(sigBasestring)
                .digest('hex');
        return crypto.timingSafeEqual(Buffer.from(mySignature), Buffer.from(signature));
    }
    /**
     * Handle connection close
     */
    handleClose() {
        if (this.pingInterval) {
            clearInterval(this.pingInterval);
            this.pingInterval = null;
        }
        this.status.connected = false;
        if (!this.reconnecting && this.slackConfig.socketMode) {
            this.reconnect();
        }
        else {
            this.emit('disconnected', 'slack');
        }
    }
    /**
     * Reconnect to Socket Mode using ReconnectionManager for backoff
     */
    reconnect() {
        if (this.reconnecting)
            return;
        this.reconnectionManager.scheduleReconnect(async () => {
            this.reconnecting = true;
            try {
                // Clear ping interval before reconnecting to prevent duplicates
                if (this.pingInterval) {
                    clearInterval(this.pingInterval);
                    this.pingInterval = null;
                }
                if (this.ws) {
                    this.ws.removeAllListeners();
                    if (this.ws.readyState === WebSocket.OPEN) {
                        this.ws.close();
                    }
                    this.ws = null;
                }
                await this.connect();
                this.reconnectionManager.onConnected();
            }
            finally {
                this.reconnecting = false;
            }
        });
    }
    /**
     * Disconnect from Slack
     */
    async disconnect() {
        this.reconnectionManager.cancel();
        if (this.pingInterval) {
            clearInterval(this.pingInterval);
            this.pingInterval = null;
        }
        if (this.ws) {
            this.ws.close();
            this.ws = null;
        }
        this.status.connected = false;
        this.emit('disconnected', 'slack');
    }
    /**
     * Send a message
     */
    async send(message) {
        try {
            const params = {
                channel: message.channelId,
                text: message.content,
                unfurl_links: !message.disablePreview,
                unfurl_media: !message.disablePreview,
            };
            // Add thread reference
            if (message.threadId) {
                params.thread_ts = message.threadId;
            }
            // Add reply reference (reply in thread)
            if (message.replyTo) {
                params.thread_ts = message.replyTo;
            }
            // Build blocks for rich content
            const blocks = [];
            // Add buttons as action block
            if (message.buttons && message.buttons.length > 0) {
                blocks.push(this.buildActionsBlock(message.buttons));
            }
            // Add image attachments
            if (message.attachments) {
                for (const attachment of message.attachments) {
                    if (attachment.type === 'image' && attachment.url) {
                        blocks.push({
                            type: 'image',
                            image_url: attachment.url,
                            alt_text: attachment.caption || 'Image',
                        });
                    }
                }
            }
            if (blocks.length > 0) {
                params.blocks = blocks;
            }
            const result = await this.apiRequest('chat.postMessage', params);
            return {
                success: true,
                messageId: result.ts,
                timestamp: new Date(),
            };
        }
        catch (error) {
            return {
                success: false,
                error: error instanceof Error ? error.message : String(error),
                timestamp: new Date(),
            };
        }
    }
    /**
     * Convert Slack event to InboundMessage
     */
    async convertEvent(event) {
        const channel = await this.getChannelInfo(event.channel);
        const user = await this.getUserInfo(event.user);
        const attachments = this.extractAttachments(event.files);
        return {
            id: event.ts ?? String(Date.now()),
            channel: {
                id: event.channel,
                type: 'slack',
                name: channel?.name,
                isDM: channel?.is_im,
                isGroup: channel?.is_group || channel?.is_mpim,
            },
            sender: {
                id: event.user,
                username: user?.name ?? user?.profile?.display_name,
                displayName: user?.profile?.real_name ??
                    user?.profile?.display_name ??
                    user?.name,
                avatarUrl: user?.profile?.image_72,
                isBot: user?.is_bot,
                isAdmin: user?.is_admin || user?.is_owner,
            },
            content: event.text ?? '',
            contentType: this.determineContentType(event),
            attachments: attachments.length > 0 ? attachments : undefined,
            threadId: event.thread_ts,
            timestamp: event.ts
                ? new Date(parseFloat(event.ts) * 1000)
                : new Date(),
            raw: event,
        };
    }
    /**
     * Get channel info (cached)
     */
    async getChannelInfo(channelId) {
        if (this.channelCache.has(channelId)) {
            return this.channelCache.get(channelId);
        }
        try {
            const result = await this.apiRequest('conversations.info', { channel: channelId });
            if (this.channelCache.size >= 5000) {
                const oldest = this.channelCache.keys().next().value;
                if (oldest)
                    this.channelCache.delete(oldest);
            }
            this.channelCache.set(channelId, result.channel);
            return result.channel;
        }
        catch {
            return null;
        }
    }
    /**
     * Get user info (cached)
     */
    async getUserInfo(userId) {
        if (this.userCache.has(userId)) {
            return this.userCache.get(userId);
        }
        try {
            const result = await this.apiRequest('users.info', { user: userId });
            if (this.userCache.size >= 5000) {
                const oldest = this.userCache.keys().next().value;
                if (oldest)
                    this.userCache.delete(oldest);
            }
            this.userCache.set(userId, result.user);
            return result.user;
        }
        catch {
            return null;
        }
    }
    /**
     * Extract attachments from files
     */
    extractAttachments(files) {
        if (!files)
            return [];
        return files.map((file) => ({
            type: this.getFileType(file.mimetype),
            url: file.url_private,
            fileName: file.name,
            mimeType: file.mimetype,
            size: file.size,
            width: file.original_w,
            height: file.original_h,
        }));
    }
    /**
     * Get file type from mimetype
     */
    getFileType(mimetype) {
        if (!mimetype)
            return 'file';
        if (mimetype.startsWith('image/'))
            return 'image';
        if (mimetype.startsWith('audio/'))
            return 'audio';
        if (mimetype.startsWith('video/'))
            return 'video';
        return 'file';
    }
    /**
     * Determine content type from event
     */
    determineContentType(event) {
        if (event.files?.length) {
            return this.getFileType(event.files[0].mimetype);
        }
        if (event.text?.startsWith('/'))
            return 'command';
        return 'text';
    }
    /**
     * Build actions block from buttons
     */
    buildActionsBlock(buttons) {
        const elements = buttons.slice(0, 5).map((btn, i) => {
            if (btn.type === 'url' && btn.url) {
                return {
                    type: 'button',
                    action_id: `link_${i}`,
                    text: { type: 'plain_text', text: btn.text },
                    url: btn.url,
                };
            }
            return {
                type: 'button',
                action_id: btn.data ?? `btn_${i}`,
                text: { type: 'plain_text', text: btn.text },
                value: btn.data,
            };
        });
        return {
            type: 'actions',
            elements,
        };
    }
    /**
     * Update a message
     */
    async updateMessage(channelId, messageTs, text, blocks) {
        await this.apiRequest('chat.update', {
            channel: channelId,
            ts: messageTs,
            text,
            blocks,
        });
    }
    /**
     * Delete a message
     */
    async deleteMessage(channelId, messageTs) {
        await this.apiRequest('chat.delete', {
            channel: channelId,
            ts: messageTs,
        });
    }
    /**
     * Add reaction to message
     */
    async addReaction(channelId, messageTs, emoji) {
        await this.apiRequest('reactions.add', {
            channel: channelId,
            timestamp: messageTs,
            name: emoji.replace(/:/g, ''),
        });
    }
    /**
     * Upload file
     */
    async uploadFile(channelId, file, filename, options) {
        const formData = new FormData();
        formData.append('file', new Blob([new Uint8Array(file)]), filename);
        formData.append('channels', channelId);
        if (options?.title)
            formData.append('title', options.title);
        if (options?.comment)
            formData.append('initial_comment', options.comment);
        if (options?.threadTs)
            formData.append('thread_ts', options.threadTs);
        const response = await fetch(`${SLACK_API_BASE}/files.upload`, {
            method: 'POST',
            headers: {
                Authorization: `Bearer ${this.slackConfig.token}`,
            },
            body: formData,
        });
        const result = (await response.json());
        if (!result.ok) {
            throw new Error(`Failed to upload file: ${result.error}`);
        }
        return result.file;
    }
    /**
     * Send ephemeral message (only visible to one user)
     */
    async sendEphemeral(channelId, userId, text, blocks) {
        await this.apiRequest('chat.postEphemeral', {
            channel: channelId,
            user: userId,
            text,
            blocks,
        });
    }
    /**
     * Open a modal
     */
    async openModal(triggerId, view) {
        await this.apiRequest('views.open', {
            trigger_id: triggerId,
            view: {
                type: 'modal',
                title: { type: 'plain_text', text: view.title },
                submit: view.submit ? { type: 'plain_text', text: view.submit } : undefined,
                close: view.close ? { type: 'plain_text', text: view.close } : undefined,
                blocks: view.blocks,
                callback_id: view.callbackId,
                private_metadata: view.privateMetadata,
            },
        });
    }
}
export default SlackChannel;
//# sourceMappingURL=client.js.map