/** WhatsApp remote-control controller that translates chat commands into runtime session actions. */
import { appendFile, mkdir } from 'node:fs/promises';
import { dirname } from 'node:path';
import { APPROVAL } from '../../app/approvals/types.js';
import { formatBrokerDesktopList, formatRemoteAttachResult, formatRemoteSessionList, parseRemoteChatCommand } from '../chat-commands.js';
import { WhatsAppBindingStore } from '../whatsapp-binding-store.js';
import { extractAssistantTextFromEvent, formatOutgoingText } from './formatting.js';
import { createDefaultWhatsAppTransport } from './transport.js';
export class LinkedWhatsAppRemoteControl {
    authDir;
    configFilePath;
    bindingStore;
    runtime;
    transportFactory;
    log;
    eventLogFilePath;
    channelLabel;
    onStateChange;
    transport = null;
    pairedChatId = null;
    unsubscribeRuntime = null;
    unsubscribeMessages = null;
    lastQrText;
    pendingAssistantReply;
    constructor(options) {
        this.authDir = options.authDir;
        this.configFilePath = options.configFilePath;
        this.bindingStore = new WhatsAppBindingStore(options.bindingFilePath);
        this.runtime = options.runtime;
        this.transportFactory = options.transportFactory ?? createDefaultWhatsAppTransport;
        this.log = options.log ?? ((message) => console.warn(message));
        this.eventLogFilePath = options.logFilePath;
        this.channelLabel = options.channelLabel ?? 'WhatsApp';
        this.onStateChange = options.onStateChange;
    }
    async start() {
        const bindingState = await this.bindingStore.load();
        this.pairedChatId = bindingState.pairedChatId;
        this.lastQrText = undefined;
        this.pendingAssistantReply = undefined;
        this.transport = await this.transportFactory({
            authDir: this.authDir,
            configFilePath: this.configFilePath,
            logFilePath: this.eventLogFilePath,
            onQr: async (qr) => {
                const qrText = await this.renderQrToAscii(qr);
                this.lastQrText = qrText;
                this.emitState({ type: 'qr_rendered', qrText });
            },
            onInfo: (message) => {
                this.log(message);
                if (message.includes('connected.')) {
                    this.emitState({ type: 'healthy' });
                }
                void this.writeEventLog({
                    type: 'transport_info',
                    text: message
                });
            }
        });
        await this.transport.connect();
        if (this.pairedChatId && !this.transport.isSelfChat(this.pairedChatId)) {
            await this.clearInvalidPairedChat('startup restore');
        }
        this.emitState({ type: 'paired_chat_updated', chatId: this.pairedChatId });
        await this.writeEventLog({
            type: 'transport_started',
            pairedChatId: this.pairedChatId
        });
        this.unsubscribeRuntime = this.runtime.subscribe((event) => {
            void this.handleAgentLoopEvent(event).catch(async (error) => {
                await this.reportIssue('runtime event handling failed', error, {
                    type: 'runtime_event_error'
                });
            });
        });
        this.unsubscribeMessages = this.transport.onMessage((message) => {
            void this.handleInboundMessage(message).catch(async (error) => {
                await this.reportIssue('message handling failed', error, {
                    type: 'inbound_message_error',
                    chatId: message.chatId
                });
            });
        });
        if (!this.pairedChatId) {
            this.log(`${this.channelLabel} is linked. Open WhatsApp Message yourself and send a message there. The first self-chat message becomes the controller for this session.`);
        }
        else {
            this.log(`${this.channelLabel} remote control paired chat restored: ${this.pairedChatId}`);
        }
        return {
            pairedChatId: this.pairedChatId,
            qrText: this.lastQrText
        };
    }
    async close() {
        this.unsubscribeRuntime?.();
        this.unsubscribeRuntime = null;
        this.unsubscribeMessages?.();
        this.unsubscribeMessages = null;
        if (this.transport) {
            await this.transport.close();
            this.transport = null;
        }
    }
    getPairedChatId() {
        return this.pairedChatId;
    }
    getLastQrText() {
        return this.lastQrText;
    }
    emitState(event) {
        this.onStateChange?.(event);
    }
    buildPhoneCommandHelp(connected) {
        return [
            connected
                ? `This ${this.channelLabel} chat is connected to Shorty.`
                : `This ${this.channelLabel} chat is not connected yet.`,
            connected
                ? 'Send prompts normally to control the active ShortcutXL desktop.'
                : 'Open Message yourself in WhatsApp and send any normal message there.',
            connected
                ? 'Commands: /sessions, /attach <session>, /interrupt, /help'
                : 'Commands after connecting: /sessions, /attach <session>, /interrupt, /help',
            'If I ask for approval, reply with y or n.'
        ].join('\n');
    }
    async sendOutgoingText(chatId, text, kind = 'system') {
        if (!this.transport) {
            throw new Error('WhatsApp transport is not connected');
        }
        if (!this.transport.isSelfChat(chatId)) {
            throw new Error(`Refusing to deliver ${kind} message to non-self WhatsApp chat ${chatId}.`);
        }
        if (this.pairedChatId && chatId !== this.pairedChatId) {
            throw new Error(`Refusing to deliver ${kind} message to WhatsApp chat ${chatId} because only the paired self-chat ${this.pairedChatId} may receive outbound phone-control messages.`);
        }
        try {
            await this.transport.sendText(chatId, formatOutgoingText(text, kind));
            this.emitState({ type: 'healthy' });
        }
        catch (error) {
            const detail = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
            const message = `Failed to deliver ${kind} message to WhatsApp chat ${chatId}: ${detail}`;
            this.emitState({ type: 'issue', message });
            await this.writeEventLog({
                type: 'outbound_delivery_failed',
                chatId,
                text: message
            });
            throw new Error(message);
        }
    }
    async handleInboundMessage(message) {
        if (!this.transport) {
            return;
        }
        if (this.pairedChatId && !this.transport.isSelfChat(this.pairedChatId)) {
            await this.clearInvalidPairedChat('inbound routing');
        }
        await this.writeEventLog({
            type: 'inbound_message',
            chatId: message.chatId,
            text: message.text,
            isSelfChat: message.isSelfChat === true
        });
        if (message.isSelfChat !== true) {
            await this.writeEventLog({
                type: 'inbound_message_rejected_non_self_chat',
                chatId: message.chatId,
                text: 'Ignored inbound message from a non-self WhatsApp chat.'
            });
            return;
        }
        const command = parseRemoteChatCommand(message.text);
        if (command.kind === 'help') {
            const helpText = this.buildPhoneCommandHelp(!!this.pairedChatId);
            await this.sendOutgoingText(message.chatId, helpText);
            await this.writeEventLog({
                type: 'outbound_message',
                chatId: message.chatId,
                text: helpText
            });
            return;
        }
        if (!this.pairedChatId) {
            await this.bindingStore.save(message.chatId);
            this.pairedChatId = message.chatId;
            this.emitState({ type: 'paired_chat_updated', chatId: this.pairedChatId });
            this.log(`${this.channelLabel} self-chat connected: ${message.chatId}`);
            await this.writeEventLog({
                type: 'chat_connected',
                chatId: message.chatId,
                text: message.text
            });
        }
        if (message.chatId !== this.pairedChatId) {
            await this.writeEventLog({
                type: 'inbound_message_ignored_unpaired_self_chat',
                chatId: message.chatId,
                text: 'Ignored inbound message from a self-chat that is not the paired controller chat.'
            });
            return;
        }
        switch (command.kind) {
            case 'approve-default':
            case 'deny-default': {
                const snapshot = await this.runtime.handleCommand({ type: 'get_snapshot' });
                if (snapshot.kind !== 'snapshot' || snapshot.snapshot.pendingApprovals.length === 0) {
                    await this.sendOutgoingText(message.chatId, 'No approval is pending.');
                    await this.writeEventLog({
                        type: 'outbound_message',
                        chatId: message.chatId,
                        text: 'No approval is pending.'
                    });
                    return;
                }
                const nextApproval = snapshot.snapshot.pendingApprovals[0];
                await this.runtime.handleCommand({
                    type: 'approval_response',
                    approvalId: nextApproval.id,
                    choice: command.kind === 'approve-default' ? APPROVAL.ACCEPT : APPROVAL.REJECT
                });
                return;
            }
            case 'sessions': {
                const result = await this.runtime.handleCommand({ type: 'list_sessions' });
                const text = result.kind === 'sessions'
                    ? formatRemoteSessionList(result.sessions)
                    : formatRemoteAttachResult(result);
                await this.sendOutgoingText(message.chatId, text);
                await this.writeEventLog({
                    type: 'outbound_message',
                    chatId: message.chatId,
                    text
                });
                return;
            }
            case 'desktops': {
                const result = await this.runtime.handleCommand({ type: 'list_desktops' });
                const text = result.kind === 'desktops'
                    ? formatBrokerDesktopList(result.desktops)
                    : formatRemoteAttachResult(result);
                await this.sendOutgoingText(message.chatId, text);
                await this.writeEventLog({
                    type: 'outbound_message',
                    chatId: message.chatId,
                    text
                });
                return;
            }
            case 'desktop': {
                const result = await this.runtime.handleCommand({
                    type: 'select_desktop',
                    desktopId: command.desktopId ?? ''
                });
                const text = formatRemoteAttachResult(result);
                await this.sendOutgoingText(message.chatId, text);
                await this.writeEventLog({
                    type: 'outbound_message',
                    chatId: message.chatId,
                    text
                });
                return;
            }
            case 'attach': {
                const result = await this.runtime.handleCommand({
                    type: 'attach',
                    requestedSessionId: command.sessionId
                });
                const text = formatRemoteAttachResult(result);
                await this.sendOutgoingText(message.chatId, text);
                await this.writeEventLog({
                    type: 'outbound_message',
                    chatId: message.chatId,
                    text
                });
                return;
            }
            case 'new': {
                await this.runtime.handleCommand({ type: 'new_session' });
                const text = 'Started a new session on this desktop.';
                await this.sendOutgoingText(message.chatId, text);
                await this.writeEventLog({
                    type: 'outbound_message',
                    chatId: message.chatId,
                    text
                });
                return;
            }
            case 'interrupt': {
                await this.runtime.handleCommand({ type: 'abort' });
                const text = 'Interrupted the current run.';
                await this.sendOutgoingText(message.chatId, text);
                await this.writeEventLog({
                    type: 'outbound_message',
                    chatId: message.chatId,
                    text
                });
                return;
            }
            case 'approve': {
                const result = await this.runtime.handleCommand({
                    type: 'approval_response',
                    approvalId: command.approvalId,
                    choice: APPROVAL.ACCEPT
                });
                const text = result.kind === 'approval_missing'
                    ? `Approval ${command.approvalId} is no longer pending.`
                    : `Approved ${command.approvalId}.`;
                await this.sendOutgoingText(message.chatId, text);
                await this.writeEventLog({
                    type: 'outbound_message',
                    chatId: message.chatId,
                    text
                });
                return;
            }
            case 'deny': {
                const result = await this.runtime.handleCommand({
                    type: 'approval_response',
                    approvalId: command.approvalId,
                    choice: APPROVAL.REJECT
                });
                const text = result.kind === 'approval_missing'
                    ? `Approval ${command.approvalId} is no longer pending.`
                    : `Denied ${command.approvalId}.`;
                await this.sendOutgoingText(message.chatId, text);
                await this.writeEventLog({
                    type: 'outbound_message',
                    chatId: message.chatId,
                    text
                });
                return;
            }
            case 'prompt': {
                const attachResult = await this.runtime.handleCommand({ type: 'attach' });
                if (attachResult.kind !== 'attached') {
                    const text = formatRemoteAttachResult(attachResult);
                    await this.sendOutgoingText(message.chatId, text);
                    await this.writeEventLog({
                        type: 'outbound_message',
                        chatId: message.chatId,
                        text
                    });
                    return;
                }
                const promptResult = await this.runtime.handleCommand({
                    type: 'prompt',
                    message: command.message
                });
                if (promptResult.kind !== 'ok') {
                    const text = formatRemoteAttachResult(promptResult);
                    await this.sendOutgoingText(message.chatId, text);
                    await this.writeEventLog({
                        type: 'outbound_message',
                        chatId: message.chatId,
                        text
                    });
                }
                return;
            }
        }
    }
    async handleAgentLoopEvent(event) {
        if (!this.transport) {
            return;
        }
        if (this.pairedChatId && !this.transport.isSelfChat(this.pairedChatId)) {
            await this.clearInvalidPairedChat('outbound delivery');
        }
        if (!this.pairedChatId) {
            return;
        }
        if (event.type === 'approval_pending') {
            const approval = event.approval;
            const details = approval.request.payload &&
                typeof approval.request.payload === 'object' &&
                'summary' in approval.request.payload
                ? String(approval.request.payload.summary ?? '')
                : '';
            await this.sendOutgoingText(this.pairedChatId, [`${approval.request.title || approval.id}`, details, 'Reply with y or n.']
                .filter(Boolean)
                .join('\n'), 'approval');
            await this.writeEventLog({
                type: 'approval_prompt',
                chatId: this.pairedChatId,
                approvalId: approval.id,
                text: approval.request.title || approval.id
            });
            return;
        }
        if (event.type === 'approval_resolved') {
            return;
        }
        if (event.type !== 'session_event') {
            return;
        }
        if (event.event.type === 'message_end' && event.event.message.role === 'assistant') {
            this.pendingAssistantReply = extractAssistantTextFromEvent(event);
            return;
        }
        if (event.event.type === 'agent_end') {
            const assistantReply = this.pendingAssistantReply;
            this.pendingAssistantReply = undefined;
            if (!assistantReply) {
                return;
            }
            await this.sendOutgoingText(this.pairedChatId, assistantReply, 'assistant');
            await this.writeEventLog({
                type: 'outbound_message',
                chatId: this.pairedChatId,
                text: assistantReply
            });
            return;
        }
    }
    async reportIssue(context, error, logEntry) {
        const detail = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
        const message = `WhatsApp remote control ${context}: ${detail}`;
        this.log(message);
        this.emitState({ type: 'issue', message });
        await this.writeEventLog({
            ...logEntry,
            text: message
        });
    }
    async renderQrToAscii(qr) {
        const qrcode = await import('qrcode-terminal');
        return await new Promise((resolve) => {
            qrcode.default.generate(qr, { small: true }, (output) => {
                resolve(output);
            });
        });
    }
    async clearInvalidPairedChat(reason) {
        if (!this.pairedChatId) {
            return;
        }
        const staleChatId = this.pairedChatId;
        this.pairedChatId = null;
        await this.bindingStore.clear();
        this.emitState({ type: 'paired_chat_updated', chatId: null });
        const message = `Discarded stale WhatsApp paired chat ${staleChatId} during ${reason} because it is not your self-chat.`;
        this.log(message);
        this.emitState({ type: 'issue', message });
        await this.writeEventLog({
            type: 'stale_paired_chat_cleared',
            chatId: staleChatId,
            text: message
        });
    }
    async writeEventLog(entry) {
        if (!this.eventLogFilePath) {
            return;
        }
        await mkdir(dirname(this.eventLogFilePath), { recursive: true });
        const normalized = {
            ...entry,
            textPreview: typeof entry.text === 'string' ? entry.text.slice(0, 160) : undefined,
            timestamp: new Date().toISOString()
        };
        await appendFile(this.eventLogFilePath, `${JSON.stringify(normalized)}\n`, 'utf8');
    }
}
//# sourceMappingURL=controller.js.map