/**
 * Matrix Channel Adapter
 *
 * Matrix integration via the matrix-js-sdk.
 * Supports text, media messaging, room events, and end-to-end encryption (if olm is available).
 *
 * Requires: npm install matrix-js-sdk (optional dependency, loaded dynamically)
 *
 * Reference: https://spec.matrix.org/latest/client-server-api/
 */
import { BaseChannel, getSessionKey, checkDMPairing } from '../index.js';
import { ReconnectionManager } from '../reconnection-manager.js';
import { logger } from '../../utils/logger.js';
// ============================================================================
// Channel Implementation
// ============================================================================
/**
 * Matrix channel using matrix-js-sdk
 */
export class MatrixChannel extends BaseChannel {
    client = null;
    roomCache = new Map();
    syncing = false;
    reconnectionManager;
    constructor(config) {
        super('matrix', config);
        if (!config.homeserverUrl) {
            throw new Error('Matrix homeserver URL is required');
        }
        if (!config.userId) {
            throw new Error('Matrix user ID is required');
        }
        if (!config.accessToken) {
            throw new Error('Matrix access token is required');
        }
        // Apply defaults
        if (config.autoJoin === undefined) {
            this.config.autoJoin = true;
        }
        this.reconnectionManager = new ReconnectionManager('matrix', {
            maxRetries: 10,
            initialDelayMs: 2000,
            maxDelayMs: 60000,
        });
    }
    get matrixConfig() {
        return this.config;
    }
    // ==========================================================================
    // Lifecycle
    // ==========================================================================
    /**
     * Connect to Matrix using matrix-js-sdk
     */
    async connect() {
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        let sdk;
        try {
            // @ts-expect-error -- optional dependency, loaded dynamically
            sdk = await import('matrix-js-sdk');
        }
        catch {
            throw new Error('Matrix channel requires matrix-js-sdk. Install it with: npm install matrix-js-sdk');
        }
        try {
            const createClient = sdk.createClient;
            const clientOpts = {
                baseUrl: this.matrixConfig.homeserverUrl,
                accessToken: this.matrixConfig.accessToken,
                userId: this.matrixConfig.userId,
                deviceId: this.matrixConfig.deviceId,
            };
            // Use a local store if path is configured
            if (this.matrixConfig.storePath) {
                try {
                    const MemoryStore = sdk.MemoryStore;
                    if (MemoryStore) {
                        clientOpts.store = new MemoryStore();
                    }
                }
                catch {
                    // Fallback: no persistent store
                }
            }
            const client = createClient(clientOpts);
            this.client = client;
            // Register event handlers before starting sync
            this.registerEventHandlers(client);
            // Start the sync loop
            await client.startClient({ initialSyncLimit: 10 });
            this.syncing = true;
            // Wait for initial sync
            await new Promise((resolve, reject) => {
                const timeout = setTimeout(() => {
                    reject(new Error('Matrix initial sync timed out after 30 seconds'));
                }, 30000);
                const onSync = (state) => {
                    if (state === 'PREPARED' || state === 'SYNCING') {
                        clearTimeout(timeout);
                        client.removeListener('sync', onSync);
                        resolve();
                    }
                    else if (state === 'ERROR') {
                        clearTimeout(timeout);
                        client.removeListener('sync', onSync);
                        reject(new Error('Matrix sync failed'));
                    }
                };
                client.on('sync', onSync);
            });
            // Join initial rooms if configured
            if (this.matrixConfig.initialRooms) {
                for (const roomId of this.matrixConfig.initialRooms) {
                    try {
                        await client.joinRoom(roomId);
                        logger.debug('Matrix: joined room', { roomId });
                    }
                    catch (err) {
                        logger.debug('Matrix: failed to join room', {
                            roomId,
                            error: err instanceof Error ? err.message : String(err),
                        });
                    }
                }
            }
            // Get user profile for status info
            let displayName = this.matrixConfig.userId;
            try {
                const profile = await client.getProfileInfo(this.matrixConfig.userId);
                displayName = profile.displayname ?? displayName;
            }
            catch {
                // Non-critical
            }
            this.status.connected = true;
            this.status.authenticated = true;
            this.status.info = {
                userId: this.matrixConfig.userId,
                homeserver: this.matrixConfig.homeserverUrl,
                displayName,
                deviceId: this.matrixConfig.deviceId,
            };
            logger.debug('Matrix connected', {
                userId: this.matrixConfig.userId,
                homeserver: this.matrixConfig.homeserverUrl,
            });
            this.emit('connected', 'matrix');
        }
        catch (error) {
            this.status.error = error instanceof Error ? error.message : String(error);
            this.emit('error', 'matrix', error instanceof Error ? error : new Error(String(error)));
            throw error;
        }
    }
    /**
     * Disconnect from Matrix
     */
    async disconnect() {
        this.reconnectionManager.cancel();
        if (this.client) {
            try {
                const client = this.client;
                client.stopClient();
            }
            catch {
                // Ignore errors during disconnect
            }
            this.client = null;
        }
        this.syncing = false;
        this.roomCache.clear();
        this.status.connected = false;
        this.status.authenticated = false;
        this.emit('disconnected', 'matrix');
    }
    /**
     * Send a message to a Matrix room
     */
    async send(message) {
        if (!this.client || !this.status.connected) {
            return { success: false, error: 'Matrix not connected', timestamp: new Date() };
        }
        const client = this.client;
        try {
            const roomId = message.channelId;
            // Handle media attachments
            if (message.attachments && message.attachments.length > 0) {
                return this.sendMedia(roomId, message.attachments[0], message.content);
            }
            // Determine content format
            const content = {
                body: message.content,
                msgtype: 'm.text',
            };
            // Add formatted body for markdown/html
            if (message.parseMode === 'html') {
                content.format = 'org.matrix.custom.html';
                content.formatted_body = message.content;
            }
            else if (message.parseMode !== 'plain') {
                // Default to markdown - Matrix clients typically render markdown
                content.format = 'org.matrix.custom.html';
                content.formatted_body = message.content; // Could convert MD->HTML here
            }
            // Reply threading
            if (message.replyTo) {
                content['m.relates_to'] = {
                    'm.in_reply_to': { event_id: message.replyTo },
                };
            }
            // Thread support (MSC3440)
            if (message.threadId) {
                content['m.relates_to'] = {
                    ...content['m.relates_to'],
                    rel_type: 'm.thread',
                    event_id: message.threadId,
                };
            }
            const result = await client.sendMessage(roomId, content);
            return {
                success: true,
                messageId: result.event_id,
                timestamp: new Date(),
            };
        }
        catch (error) {
            logger.debug('Matrix send error', {
                error: error instanceof Error ? error.message : String(error),
            });
            return {
                success: false,
                error: error instanceof Error ? error.message : String(error),
                timestamp: new Date(),
            };
        }
    }
    // ==========================================================================
    // Media Sending
    // ==========================================================================
    /**
     * Send a media attachment to a room
     */
    async sendMedia(roomId, attachment, caption) {
        const client = this.client;
        try {
            let mxcUrl;
            // If we have a data URI or buffer, upload it
            if (attachment.data) {
                const buffer = Buffer.from(attachment.data, 'base64');
                const uploadResult = await client.uploadContent(buffer, {
                    name: attachment.fileName ?? 'attachment',
                    type: attachment.mimeType ?? 'application/octet-stream',
                });
                mxcUrl = uploadResult.content_uri;
            }
            else if (attachment.url) {
                // For remote URLs, we'd need to download and re-upload to Matrix
                // For now, send as a text message with the URL
                const result = await client.sendMessage(roomId, {
                    msgtype: 'm.text',
                    body: `${caption ? caption + '\n' : ''}${attachment.url}`,
                });
                return { success: true, messageId: result.event_id, timestamp: new Date() };
            }
            if (!mxcUrl) {
                return { success: false, error: 'No attachment data to upload', timestamp: new Date() };
            }
            const msgtype = this.contentTypeToMsgtype(attachment.type);
            const content = {
                msgtype,
                body: caption ?? attachment.fileName ?? 'attachment',
                url: mxcUrl,
                info: {
                    mimetype: attachment.mimeType,
                    size: attachment.size,
                    w: attachment.width,
                    h: attachment.height,
                    duration: attachment.duration ? attachment.duration * 1000 : undefined,
                },
                filename: attachment.fileName,
            };
            const result = await client.sendMessage(roomId, content);
            return { success: true, messageId: result.event_id, timestamp: new Date() };
        }
        catch (error) {
            return {
                success: false,
                error: error instanceof Error ? error.message : String(error),
                timestamp: new Date(),
            };
        }
    }
    // ==========================================================================
    // Event Handling
    // ==========================================================================
    /**
     * Register event handlers on the Matrix client
     */
    registerEventHandlers(client) {
        const c = client;
        // Room timeline events (messages)
        c.on('Room.timeline', (event, room, toStartOfTimeline) => {
            if (toStartOfTimeline)
                return; // Ignore backfill
            this.handleTimelineEvent(event, room);
        });
        // Room invites (auto-join)
        c.on('RoomMember.membership', (event, member) => {
            this.handleMembershipChange(c, event, member);
        });
        // Typing notifications
        c.on('RoomMember.typing', (event, member) => {
            this.handleTypingEvent(event, member);
        });
        // Sync state changes with reconnection support
        c.on('sync', (state, prevState) => {
            if (state === 'PREPARED' || state === 'SYNCING') {
                this.reconnectionManager.onConnected();
            }
            else if (state === 'ERROR') {
                logger.debug('Matrix: sync error, scheduling reconnect');
                this.status.connected = false;
                this.emit('error', 'matrix', new Error('Matrix sync error'));
                this.reconnectionManager.scheduleReconnect(async () => {
                    if (this.client) {
                        try {
                            const cli = this.client;
                            cli.stopClient();
                        }
                        catch {
                            // Ignore
                        }
                    }
                    await this.connect();
                    this.reconnectionManager.onConnected();
                });
            }
            else if (state === 'RECONNECTING') {
                logger.debug('Matrix: reconnecting');
            }
        });
    }
    /**
     * Handle a room timeline event
     */
    async handleTimelineEvent(event, room) {
        // Only process m.room.message events
        if (event.type !== 'm.room.message')
            return;
        // Ignore our own messages
        if (event.sender === this.matrixConfig.userId)
            return;
        // Ignore redacted/edited messages (for now)
        const content = event.content;
        if (!content || content.msgtype === undefined)
            return;
        // Check user allowlist
        if (!this.isUserAllowed(event.sender))
            return;
        // Check room allowlist
        if (!this.isChannelAllowed(event.room_id))
            return;
        // Determine room info
        const roomInfo = this.getRoomInfo(event.room_id, room);
        // Build InboundMessage
        const text = content.body ?? '';
        const attachments = this.extractAttachments(content);
        const inbound = {
            id: event.event_id,
            channel: {
                id: event.room_id,
                type: 'matrix',
                name: roomInfo.name ?? event.room_id,
                isDM: roomInfo.isDirect,
                isGroup: !roomInfo.isDirect,
                participantCount: roomInfo.memberCount,
                description: roomInfo.topic,
            },
            sender: {
                id: event.sender,
                username: event.sender,
                displayName: this.getDisplayName(event.sender, room),
            },
            content: text,
            contentType: this.msgtypeToContentType(content.msgtype),
            attachments: attachments.length > 0 ? attachments : undefined,
            replyTo: content['m.relates_to']?.['m.in_reply_to']?.event_id,
            threadId: content['m.relates_to']?.rel_type === 'm.thread'
                ? content['m.relates_to']?.event_id
                : undefined,
            timestamp: new Date(event.origin_server_ts),
            raw: event,
        };
        const parsed = this.parseCommand(inbound);
        parsed.sessionKey = getSessionKey(parsed);
        // DM pairing check
        const pairingStatus = await checkDMPairing(parsed);
        if (!pairingStatus.approved) {
            const { getDMPairing } = await import('../dm-pairing.js');
            const pairingMessage = getDMPairing().getPairingMessage(pairingStatus);
            if (pairingMessage) {
                await this.send({ channelId: event.room_id, content: pairingMessage });
            }
            return;
        }
        this.status.lastActivity = new Date();
        this.emit('message', parsed);
        if (parsed.isCommand) {
            this.emit('command', parsed);
        }
    }
    /**
     * Handle membership changes (auto-join on invite)
     */
    async handleMembershipChange(client, event, member) {
        const m = member;
        if (m.userId === client.getUserId() &&
            m.membership === 'invite' &&
            this.matrixConfig.autoJoin) {
            try {
                const roomId = m.roomId ?? event.room_id;
                await client.joinRoom(roomId);
                logger.debug('Matrix: auto-joined room on invite', { roomId });
            }
            catch (err) {
                logger.debug('Matrix: failed to auto-join room', {
                    error: err instanceof Error ? err.message : String(err),
                });
            }
        }
    }
    /**
     * Handle typing notifications
     */
    handleTypingEvent(event, member) {
        const m = member;
        if (m.typing && m.userId && m.userId !== this.matrixConfig.userId) {
            this.emit('typing', {
                id: m.roomId ?? event.room_id,
                type: 'matrix',
            }, {
                id: m.userId,
            });
        }
    }
    // ==========================================================================
    // Room Management
    // ==========================================================================
    /**
     * Join a Matrix room by ID or alias
     */
    async joinRoom(roomIdOrAlias) {
        if (!this.client)
            throw new Error('Matrix not connected');
        const client = this.client;
        const result = await client.joinRoom(roomIdOrAlias);
        return result.roomId;
    }
    /**
     * Leave a Matrix room
     */
    async leaveRoom(roomId) {
        if (!this.client)
            throw new Error('Matrix not connected');
        const client = this.client;
        await client.leave(roomId);
        this.roomCache.delete(roomId);
    }
    /**
     * Get joined rooms
     */
    async getJoinedRooms() {
        if (!this.client)
            return [];
        const client = this.client;
        try {
            const result = await client.getJoinedRooms();
            return result.joined_rooms;
        }
        catch {
            return [];
        }
    }
    /**
     * Send a typing indicator
     */
    async sendTyping(roomId, typing = true, timeoutMs = 10000) {
        if (!this.client)
            return;
        const client = this.client;
        try {
            await client.sendTyping(roomId, typing, timeoutMs);
        }
        catch {
            // Non-critical
        }
    }
    /**
     * Send a read receipt for an event
     */
    async sendReadReceipt(roomId, eventId) {
        if (!this.client)
            return;
        const client = this.client;
        try {
            await client.sendReadReceipt({ roomId, getId: () => eventId });
        }
        catch {
            // Non-critical
        }
    }
    /**
     * React to a message with an emoji
     */
    async react(roomId, eventId, emoji) {
        if (!this.client)
            return;
        const client = this.client;
        try {
            await client.sendEvent(roomId, 'm.reaction', {
                'm.relates_to': {
                    rel_type: 'm.annotation',
                    event_id: eventId,
                    key: emoji,
                },
            });
        }
        catch (err) {
            logger.debug('Matrix: react error', {
                error: err instanceof Error ? err.message : String(err),
            });
        }
    }
    /**
     * Redact (delete) a message
     */
    async redactMessage(roomId, eventId, reason) {
        if (!this.client)
            throw new Error('Matrix not connected');
        const client = this.client;
        await client.redactEvent(roomId, eventId, undefined, reason ? { reason } : undefined);
    }
    // ==========================================================================
    // Helpers
    // ==========================================================================
    /**
     * Get room info from the SDK room object or cache
     */
    getRoomInfo(roomId, room) {
        if (this.roomCache.has(roomId)) {
            return this.roomCache.get(roomId);
        }
        const r = room;
        const info = {
            roomId,
            name: r?.name ?? roomId,
            memberCount: r?.getJoinedMemberCount?.() ?? undefined,
            isDirect: !!r?.getDMInviter?.(),
        };
        // Get topic
        try {
            const topicEvents = r?.currentState?.getStateEvents?.('m.room.topic');
            if (topicEvents && topicEvents.length > 0) {
                info.topic = topicEvents[0].getContent().topic;
            }
        }
        catch {
            // Non-critical
        }
        // Cache with size limit
        if (this.roomCache.size >= 1000) {
            const oldest = this.roomCache.keys().next().value;
            if (oldest)
                this.roomCache.delete(oldest);
        }
        this.roomCache.set(roomId, info);
        return info;
    }
    /**
     * Get display name for a user from the room member list
     */
    getDisplayName(userId, room) {
        const r = room;
        try {
            const member = r?.getMember?.(userId);
            return member?.name ?? member?.rawDisplayName ?? userId;
        }
        catch {
            return userId;
        }
    }
    /**
     * Extract attachments from Matrix event content
     */
    extractAttachments(content) {
        const attachments = [];
        if (!content.msgtype)
            return attachments;
        const mediaTypes = ['m.image', 'm.audio', 'm.video', 'm.file'];
        if (!mediaTypes.includes(content.msgtype))
            return attachments;
        attachments.push({
            type: this.msgtypeToContentType(content.msgtype),
            url: content.url, // mxc:// URL
            fileName: content.filename ?? content.body,
            mimeType: content.info?.mimetype,
            size: content.info?.size,
            width: content.info?.w,
            height: content.info?.h,
            duration: content.info?.duration ? Math.floor(content.info.duration / 1000) : undefined,
        });
        return attachments;
    }
    /**
     * Convert Matrix msgtype to ContentType
     */
    msgtypeToContentType(msgtype) {
        switch (msgtype) {
            case 'm.image': return 'image';
            case 'm.audio': return 'audio';
            case 'm.video': return 'video';
            case 'm.file': return 'file';
            case 'm.location': return 'location';
            default: return 'text';
        }
    }
    /**
     * Convert ContentType to Matrix msgtype
     */
    contentTypeToMsgtype(type) {
        switch (type) {
            case 'image': return 'm.image';
            case 'audio':
            case 'voice': return 'm.audio';
            case 'video': return 'm.video';
            case 'file': return 'm.file';
            case 'location': return 'm.location';
            default: return 'm.file';
        }
    }
}
export default MatrixChannel;
//# sourceMappingURL=index.js.map