import { RelayCast } from '@relaycast/sdk';
import { relaycastTelemetryOptions } from '../relaycast-telemetry.js';
import { normalizeAgent, normalizeAgentPresence, normalizeAgentRegistration, normalizeChannel, normalizeChannelMember, normalizeChannelName, normalizeChannelReadStatus, normalizeGroupDirectConversation, normalizeInbox, normalizeMessage, normalizeMessagingEvent, normalizeReaction, normalizeReadReceipt, normalizeSearchResult, normalizeThread, } from './normalize.js';
/**
 * Translate a relay capability registration into a relaycast `actions.register`
 * request. Relaycast 2.x replaced the `commands` registry with `actions`:
 * `command` → `name`, `parameters` → `inputSchema`.
 */
function toRegisterActionRequest(input) {
    // `inputSchema` (a converted JSON Schema) takes precedence over the legacy
    // `parameters` field when both are present.
    const inputSchema = input.inputSchema ?? input.parameters;
    return {
        name: input.command,
        description: input.description,
        handlerAgent: input.handlerAgent,
        ...(inputSchema === undefined ? {} : { inputSchema }),
        ...(input.outputSchema === undefined ? {} : { outputSchema: input.outputSchema }),
        ...(input.availableTo === undefined ? {} : { availableTo: input.availableTo }),
    };
}
/**
 * Translate a relaycast `ActionDefinition` back into a relay `RelayCapability`,
 * preserving the relay-facing `command`/`parameters` vocabulary.
 */
function toRelayCapability(raw) {
    const action = (raw ?? {});
    const command = (action.name ?? action.command);
    return {
        ...action,
        command,
        description: action.description,
        handlerAgent: action.handlerAgent,
        parameters: action.inputSchema ?? action.parameters,
    };
}
/** Translate a relay completion result into the relaycast `CompleteInvocationRequest` shape. */
function toCompleteInvocationRequest(data) {
    return {
        ...(data.output === undefined ? {} : { output: data.output }),
        ...(data.error === undefined ? {} : { error: data.error }),
        ...(data.durationMs === undefined ? {} : { durationMs: data.durationMs }),
    };
}
function asRecord(value) {
    return value !== null && typeof value === 'object' && !Array.isArray(value)
        ? value
        : {};
}
function readStr(record, ...keys) {
    for (const key of keys) {
        const value = record[key];
        if (typeof value === 'string')
            return value;
    }
    return undefined;
}
function readRecord(record, ...keys) {
    for (const key of keys) {
        const value = record[key];
        if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
            return value;
        }
    }
    return undefined;
}
/** Normalize a relaycast invoke ack (camelized) into the relay `RelayActionInvocationAck`. */
function normalizeActionInvocationAck(raw) {
    const record = asRecord(raw);
    return {
        invocationId: readStr(record, 'invocationId', 'invocation_id') ?? '',
        actionName: readStr(record, 'actionName', 'action_name') ?? '',
        ...(readStr(record, 'handlerAgentId', 'handler_agent_id')
            ? { handlerAgentId: readStr(record, 'handlerAgentId', 'handler_agent_id') }
            : {}),
        ...(readRecord(record, 'input') ? { input: readRecord(record, 'input') } : {}),
        ...(readStr(record, 'status') ? { status: readStr(record, 'status') } : {}),
        ...(readStr(record, 'createdAt', 'created_at')
            ? { createdAt: readStr(record, 'createdAt', 'created_at') }
            : {}),
    };
}
/** Normalize a relaycast invocation record (camelized) into `RelayActionInvocation`. */
function normalizeActionInvocation(raw) {
    const record = asRecord(raw);
    return {
        invocationId: readStr(record, 'invocationId', 'invocation_id') ?? '',
        actionName: readStr(record, 'actionName', 'action_name') ?? '',
        callerId: (readStr(record, 'callerId', 'caller_id') ?? null),
        callerName: (readStr(record, 'callerName', 'caller_name') ?? null),
        input: readRecord(record, 'input') ?? {},
        output: readRecord(record, 'output') ?? null,
        status: readStr(record, 'status') ?? 'invoked',
        error: (readStr(record, 'error') ?? null),
        durationMs: typeof record.durationMs === 'number'
            ? record.durationMs
            : typeof record.duration_ms === 'number'
                ? record.duration_ms
                : null,
        ...(readStr(record, 'createdAt', 'created_at')
            ? { createdAt: readStr(record, 'createdAt', 'created_at') }
            : {}),
        completedAt: (readStr(record, 'completedAt', 'completed_at') ?? null),
    };
}
/** Normalize a relaycast inbound webhook (snake_case) into `RelayInboundWebhook`. */
function normalizeInboundWebhook(raw) {
    const record = asRecord(raw);
    return {
        webhookId: readStr(record, 'webhookId', 'webhook_id', 'id') ?? '',
        url: readStr(record, 'url') ?? '',
        token: readStr(record, 'token') ?? '',
        channel: readStr(record, 'channel') ?? '',
        ...(readStr(record, 'name') ? { name: readStr(record, 'name') } : {}),
        ...(readStr(record, 'createdAt', 'created_at')
            ? { createdAt: readStr(record, 'createdAt', 'created_at') }
            : {}),
    };
}
/** Normalize a relaycast event subscription into `RelayWebhookSubscription`. */
function normalizeWebhookSubscription(raw) {
    const record = asRecord(raw);
    const events = Array.isArray(record.events)
        ? record.events.filter((event) => typeof event === 'string')
        : undefined;
    return {
        id: readStr(record, 'id') ?? '',
        ...(readStr(record, 'url') ? { url: readStr(record, 'url') } : {}),
        ...(events ? { events } : {}),
        ...(readStr(record, 'createdAt', 'created_at')
            ? { createdAt: readStr(record, 'createdAt', 'created_at') }
            : {}),
    };
}
function definedOptions(options) {
    return Object.fromEntries(Object.entries(options).filter(([, value]) => value !== undefined));
}
function toMessageListOptions(options) {
    if (!options)
        return undefined;
    return definedOptions({
        limit: options.limit,
        before: options.before,
        after: options.after,
    });
}
function serializeAttachmentInputs(input) {
    if (!input)
        return undefined;
    return input.map((attachment) => typeof attachment === 'string' ? attachment : JSON.stringify(attachment));
}
function createRelaycastClient(options) {
    if (options.relaycast)
        return options.relaycast;
    const workspaceKey = options.workspaceKey ?? options.apiKey;
    if (!workspaceKey) {
        throw new Error('RelaycastMessagingClient requires workspaceKey when relaycast is not provided.');
    }
    return new RelayCast(definedOptions({
        apiKey: workspaceKey,
        baseUrl: options.baseUrl,
        retryPolicy: options.retryPolicy,
        ...relaycastTelemetryOptions({
            harness: options.harness,
            agentRelayDistinctId: options.agentRelayDistinctId,
        }),
    }));
}
export class RelaycastMessagingClient {
    capabilities = {
        serverDeliveryState: false,
        durableDelivery: false,
        durableAck: false,
        durableFail: false,
        durableDefer: false,
    };
    relaycast;
    agentClient;
    eventHandlers = new Map();
    eventUnsubscribe;
    constructor(options) {
        this.relaycast = createRelaycastClient(options);
        this.agentClient =
            options.agentClient ??
                (options.agentToken ? this.relaycast.as?.(options.agentToken, options.agentClientOptions) : undefined);
    }
    agents = {
        list: async (options) => {
            const agents = await this.relaycast.agents.list(definedOptions({ status: options?.status }));
            return agents.map(normalizeAgent);
        },
        get: async (name) => normalizeAgent(await this.relaycast.agents.get(name)),
        register: async (input) => normalizeAgentRegistration(await this.relaycast.agents.register(input)),
        me: async () => normalizeAgent(await this.requireAgentClient('agents.me').me()),
        update: async (name, input) => normalizeAgent(await this.relaycast.agents.update(name, input)),
        delete: async (name) => {
            await this.relaycast.agents.delete(name);
        },
        presence: async () => {
            const presence = await this.relaycast.agents.presence();
            return presence.map(normalizeAgentPresence);
        },
    };
    channels = {
        list: async (options) => {
            const channels = await this.relaycast.channels.list(definedOptions({ includeArchived: options?.includeArchived }));
            return channels.map(normalizeChannel);
        },
        get: async (name) => normalizeChannel(await this.relaycast.channels.get(normalizeChannelName(name))),
        create: async (input) => normalizeChannel(await this.requireAgentClient('channels.create').channels.create(input)),
        update: async (name, input) => normalizeChannel(await this.requireAgentClient('channels.update').channels.update(normalizeChannelName(name), input)),
        archive: async (name) => {
            await this.requireAgentClient('channels.archive').channels.archive(normalizeChannelName(name));
        },
        join: async (name) => {
            await this.requireAgentClient('channels.join').channels.join(normalizeChannelName(name));
        },
        leave: async (name) => {
            await this.requireAgentClient('channels.leave').channels.leave(normalizeChannelName(name));
        },
        invite: async (channel, agent) => {
            await this.requireAgentClient('channels.invite').channels.invite(normalizeChannelName(channel), agent);
        },
        members: async (name) => {
            const members = await this.requireAgentClient('channels.members').channels.members(normalizeChannelName(name));
            return members.map(normalizeChannelMember);
        },
        mute: async (name) => {
            await this.requireAgentClient('channels.mute').channels.mute(normalizeChannelName(name));
        },
        unmute: async (name) => {
            await this.requireAgentClient('channels.unmute').channels.unmute(normalizeChannelName(name));
        },
    };
    messages = {
        send: async (input) => {
            const message = await this.requireAgentClient('messages.send').send(input.channel, input.text, definedOptions({
                attachments: serializeAttachmentInputs(input.attachments),
                blocks: input.blocks,
                mode: input.mode,
                idempotencyKey: input.idempotencyKey,
            }));
            return normalizeMessage(message, { kind: 'channel', channelName: normalizeChannelName(input.channel) });
        },
        list: async (channel, options) => {
            const channelName = normalizeChannelName(channel);
            const messages = await this.relaycast.messages.list(channelName, toMessageListOptions(options));
            return messages.map((message) => normalizeMessage(message, { kind: 'channel', channelName }));
        },
        get: async (id) => normalizeMessage(await this.relaycast.messages.get(id)),
        reply: async (input) => {
            const message = await this.requireAgentClient('messages.reply').reply(input.messageId, input.text, definedOptions({ blocks: input.blocks, idempotencyKey: input.idempotencyKey }));
            return normalizeMessage(message, {
                kind: 'thread_reply',
                parentId: input.messageId,
                threadId: input.messageId,
            });
        },
        direct: async (input) => {
            const response = await this.requireAgentClient('messages.direct').dm(input.to, input.text, definedOptions({
                attachments: serializeAttachmentInputs(input.attachments),
                mode: input.mode,
                idempotencyKey: input.idempotencyKey,
            }));
            return this.normalizeDirectResponse(response, 'dm');
        },
        groupDirect: async (input) => {
            const agent = this.requireAgentClient('messages.groupDirect');
            const conversationId = input.conversationId ??
                normalizeGroupDirectConversation(await agent.dms.createGroup({ participants: input.participants ?? [], ...(input.name ? { name: input.name } : {}) }, definedOptions({ idempotencyKey: input.idempotencyKey }))).id;
            if (!conversationId) {
                throw new Error('messages.groupDirect requires conversationId or participants that create a conversation.');
            }
            const response = await agent.dms.sendMessage(conversationId, input.text, definedOptions({
                attachments: serializeAttachmentInputs(input.attachments),
                mode: input.mode,
                idempotencyKey: input.idempotencyKey,
            }));
            return this.normalizeDirectResponse(response, 'group_dm', conversationId);
        },
        createGroupDirect: async (input) => normalizeGroupDirectConversation(await this.requireAgentClient('messages.createGroupDirect').dms.createGroup({ participants: input.participants, ...(input.name ? { name: input.name } : {}) }, definedOptions({ idempotencyKey: input.idempotencyKey }))),
        listDirect: async (input) => {
            const options = toMessageListOptions(input);
            const list = this.agentClient
                ? await this.agentClient.dms.messages(input.conversationId, options)
                : await this.requireWorkspaceDmMessages()(input.conversationId, options);
            return list.map((message) => normalizeMessage(message, { kind: 'dm', conversationId: input.conversationId }));
        },
        markRead: async (messageId) => normalizeReadReceipt(await this.requireAgentClient('messages.markRead').markRead(messageId)),
        readers: async (messageId) => {
            const readers = await this.requireAgentClient('messages.readers').readers(messageId);
            return readers.map((reader) => normalizeReadReceipt({ ...(reader ?? {}), messageId }));
        },
        readStatus: async (channel) => {
            const statuses = await this.requireAgentClient('messages.readStatus').readStatus(normalizeChannelName(channel));
            return statuses.map(normalizeChannelReadStatus);
        },
        reactions: async (messageId) => {
            const reactions = this.agentClient
                ? await this.agentClient.reactions(messageId)
                : await this.relaycast.messages.reactions(messageId);
            return reactions.map(normalizeReaction);
        },
        react: async (messageId, emoji) => normalizeReaction(await this.requireAgentClient('messages.react').react(messageId, emoji)),
        unreact: async (messageId, emoji) => {
            await this.requireAgentClient('messages.unreact').unreact(messageId, emoji);
        },
        search: async (query, options) => {
            const results = await this.requireAgentClient('messages.search').search(query, definedOptions({
                channel: options?.channel ? normalizeChannelName(options.channel) : undefined,
                from: options?.from,
                limit: options?.limit,
                before: options?.before,
                after: options?.after,
            }));
            return results.map(normalizeSearchResult);
        },
    };
    threads = {
        get: async (messageId, options) => normalizeThread(await this.relaycast.messages.thread(messageId, toMessageListOptions(options))),
        reply: async (input) => this.messages.reply(input),
    };
    inbox = {
        get: async (options) => normalizeInbox(await this.requireAgentClient('inbox.get').inbox(definedOptions({ limit: options?.limit }))),
        list: async (_input) => ({ items: [] }),
        subscribe: (_input) => this.emptyInboxSubscription(),
        ack: async (input) => this.unsupportedInboxDelivery('ack', input.inboxItemId),
        fail: async (input) => this.unsupportedInboxDelivery('fail', input.inboxItemId, input.error),
        defer: async (input) => this.unsupportedInboxDelivery('defer', input.inboxItemId, input.reason, input.availableAt),
        markRead: async (input) => this.unsupportedInboxDelivery('ack', input.inboxItemId, 'Inbox read state updates require server delivery state.'),
    };
    events = {
        connect: () => {
            if (this.eventUnsubscribe)
                return;
            const forward = (event) => this.emitEvent(normalizeMessagingEvent(event));
            // Agent-scoped clients stream through their own connection; a workspace-key
            // client streams all workspace-visible events through the workspace stream.
            if (this.agentClient) {
                this.agentClient.connect();
                this.eventUnsubscribe = this.agentClient.on.any(forward);
                return;
            }
            if (typeof this.relaycast.connect === 'function' && this.relaycast.on) {
                this.relaycast.connect();
                this.eventUnsubscribe = this.relaycast.on.any(forward);
                return;
            }
            // No agent client and no workspace stream available: preserve the
            // explicit "needs an agent token" error.
            this.requireAgentClient('events.connect');
        },
        disconnect: async () => {
            this.eventUnsubscribe?.();
            this.eventUnsubscribe = undefined;
            if (this.agentClient) {
                await this.agentClient.disconnect();
            }
            else if (typeof this.relaycast.disconnect === 'function') {
                this.relaycast.disconnect();
            }
        },
        subscribe: (channels) => {
            this.requireAgentClient('events.subscribe').subscribe(channels.map(normalizeChannelName));
        },
        unsubscribe: (channels) => {
            this.requireAgentClient('events.unsubscribe').unsubscribe(channels.map(normalizeChannelName));
        },
        on: (event, handler) => this.addEventListener(event, handler),
    };
    deliveries = {
        ack: async (messageId) => ({
            supported: false,
            action: 'ack',
            messageId,
            reason: 'Durable acknowledgements are not supported by the Relaycast messaging backend yet.',
        }),
        fail: async (messageId, reason) => ({
            supported: false,
            action: 'fail',
            messageId,
            ...(reason
                ? { reason }
                : { reason: 'Durable failure reporting is not supported by the Relaycast messaging backend yet.' }),
        }),
        defer: async (messageId, deferUntil) => ({
            supported: false,
            action: 'defer',
            messageId,
            ...(deferUntil ? { deferUntil } : {}),
            reason: 'Durable deferral is not supported by the Relaycast messaging backend yet.',
        }),
    };
    integrations = {
        webhooks: {
            create: async (input) => (await this.requireWebhooks().create(input)),
            list: async () => (await this.requireWebhooks().list()),
            delete: async (id) => {
                await this.requireWebhooks().delete(id);
            },
            trigger: async (id, payload) => this.requireWebhooks().trigger(id, payload ?? {}),
        },
        subscriptions: {
            create: async (input) => (await this.requireSubscriptions().create(input)),
            list: async () => (await this.requireSubscriptions().list()),
            get: async (id) => (await this.requireSubscriptions().get(id)),
            delete: async (id) => {
                await this.requireSubscriptions().delete(id);
            },
        },
    };
    webhooks = {
        createInbound: async (input) => normalizeInboundWebhook(await this.requireWebhooks().createInbound({
            channel: normalizeChannelName(input.channel),
            ...(input.name === undefined ? {} : { name: input.name }),
        })),
        subscribe: async (input) => normalizeWebhookSubscription(await this.requireSubscriptions().create(definedOptions({
            url: input.url,
            events: input.events,
            secret: input.secret,
            headers: input.headers,
        }))),
        list: async () => (await this.requireWebhooks().list()).map(normalizeInboundWebhook),
        delete: async (webhookId) => {
            await this.requireWebhooks().delete(webhookId);
        },
        subscriptions: async () => (await this.requireSubscriptions().list()).map(normalizeWebhookSubscription),
        unsubscribe: async (id) => {
            await this.requireSubscriptions().delete(id);
        },
    };
    commands = {
        register: async (input) => toRelayCapability(await this.requireActions().register(toRegisterActionRequest(input))),
        list: async () => (await this.requireActions().list()).map(toRelayCapability),
        delete: async (command) => {
            await this.requireActions().delete(command);
        },
        available: () => Boolean(this.relaycast.actions),
        agentScoped: () => Boolean(this.agentClient?.actions),
        invoke: async (name, input) => normalizeActionInvocationAck(await this.requireAgentActions('commands.invoke').invoke(name, input)),
        getInvocation: async (name, invocationId) => normalizeActionInvocation(await this.requireAgentActions('commands.getInvocation').getInvocation(name, invocationId)),
        completeInvocation: async (name, invocationId, data) => normalizeActionInvocation(await this.requireAgentActions('commands.completeInvocation').completeInvocation(name, invocationId, toCompleteInvocationRequest(data))),
    };
    workspace = {
        info: async () => {
            if (!this.relaycast.workspace) {
                throw new Error('RelaycastMessagingClient.workspace.info requires the relaycast workspace API.');
            }
            return (await this.relaycast.workspace.info());
        },
    };
    requireWebhooks() {
        if (!this.relaycast.webhooks) {
            throw new Error('RelaycastMessagingClient.integrations.webhooks requires the relaycast webhooks API.');
        }
        return this.relaycast.webhooks;
    }
    requireSubscriptions() {
        if (!this.relaycast.subscriptions) {
            throw new Error('RelaycastMessagingClient.integrations.subscriptions requires the relaycast subscriptions API.');
        }
        return this.relaycast.subscriptions;
    }
    requireActions() {
        if (!this.relaycast.actions) {
            throw new Error('RelaycastMessagingClient.commands requires the relaycast actions API.');
        }
        return this.relaycast.actions;
    }
    requireAgentActions(operation) {
        const actions = this.agentClient?.actions;
        if (!actions) {
            throw new Error(`RelaycastMessagingClient.${operation} requires an agent-scoped client with the actions API.`);
        }
        return actions;
    }
    requireAgentClient(operation) {
        if (!this.agentClient) {
            throw new Error(`RelaycastMessagingClient.${operation} requires agentToken or agentClient.`);
        }
        return this.agentClient;
    }
    requireWorkspaceDmMessages() {
        if (!this.relaycast.dmMessages) {
            throw new Error('RelaycastMessagingClient.messages.listDirect requires agentClient or relaycast.dmMessages.');
        }
        return this.relaycast.dmMessages;
    }
    async *emptyInboxSubscription() {
        return;
    }
    unsupportedInboxDelivery(action, messageId, reason, deferUntil) {
        return {
            supported: false,
            action,
            messageId,
            ...(reason
                ? { reason }
                : {
                    reason: 'Relaycast messaging does not expose durable inbox delivery state in this SDK surface yet.',
                }),
            ...(deferUntil ? { deferUntil } : {}),
        };
    }
    normalizeDirectResponse(input, kind, conversationId) {
        const record = input !== null && typeof input === 'object' && !Array.isArray(input)
            ? input
            : {};
        const resolvedConversationId = conversationId ??
            (typeof record.conversationId === 'string'
                ? record.conversationId
                : typeof record.conversation_id === 'string'
                    ? record.conversation_id
                    : undefined);
        const createdAt = typeof record.createdAt === 'string'
            ? record.createdAt
            : typeof record.created_at === 'string'
                ? record.created_at
                : undefined;
        return normalizeMessage(record.message, {
            kind,
            conversationId: resolvedConversationId,
            createdAt,
        });
    }
    addEventListener(event, handler) {
        let handlers = this.eventHandlers.get(event);
        if (!handlers) {
            handlers = new Set();
            this.eventHandlers.set(event, handlers);
        }
        const typedHandler = handler;
        handlers.add(typedHandler);
        return () => {
            const current = this.eventHandlers.get(event);
            if (current !== handlers)
                return;
            current.delete(typedHandler);
            if (current.size === 0) {
                this.eventHandlers.delete(event);
            }
        };
    }
    emitEvent(event) {
        const handlers = [
            ...(this.eventHandlers.get(event.type) ?? []),
            ...(this.eventHandlers.get('any') ?? []),
        ];
        for (const handler of handlers) {
            try {
                const result = handler(event);
                if (result && typeof result.catch === 'function') {
                    void result.catch((error) => {
                        console.error(`[agent-relay] messaging listener for "${event.type}" threw:`, error);
                    });
                }
            }
            catch (error) {
                console.error(`[agent-relay] messaging listener for "${event.type}" threw:`, error);
            }
        }
    }
}
//# sourceMappingURL=relaycast.js.map