// Thin Fabric REST client. One auth concept: an agent's Center API key is
// exchanged for a normal user session (POST /auth/agent/login); the returned
// guild access tokens are used to post messages and call channel APIs.
export class FabricClient {
    centerApiBase;
    constructor(centerApiBase) {
        this.centerApiBase = centerApiBase;
    }
    async post(url, body, auth) {
        const res = await fetch(url, {
            method: 'POST',
            headers: {
                'content-type': 'application/json',
                ...(auth ? { authorization: `Bearer ${auth}` } : {}),
            },
            body: JSON.stringify(body),
        });
        if (!res.ok) {
            const text = await res.text().catch(() => '');
            throw new Error(`POST ${url} -> ${res.status} ${text}`);
        }
        return (await res.json());
    }
    // Generic JSON request (GET/PUT/PATCH/DELETE). Empty 2xx body -> null
    // (Fabric returns an empty body when a channel has no canvas).
    async req(method, url, auth, body, extraHeaders) {
        const res = await fetch(url, {
            method,
            headers: {
                ...(body !== undefined ? { 'content-type': 'application/json' } : {}),
                ...(auth ? { authorization: `Bearer ${auth}` } : {}),
                ...(extraHeaders ?? {}),
            },
            body: body !== undefined ? JSON.stringify(body) : undefined,
        });
        if (!res.ok) {
            const text = await res.text().catch(() => '');
            throw new Error(`${method} ${url} -> ${res.status} ${text}`);
        }
        const text = await res.text();
        return (text ? JSON.parse(text) : null);
    }
    // Exchange an agent API key for a Fabric user session (+ guild tokens).
    agentLogin(apiKey) {
        return this.post(`${this.centerApiBase}/auth/agent/login`, { apiKey });
    }
    // Refresh the center access token (guild tokens are re-fetched via /auth/me/guilds).
    async refresh(refreshToken) {
        return this.post(`${this.centerApiBase}/auth/refresh`, { refreshToken });
    }
    async meGuilds(accessToken) {
        const res = await fetch(`${this.centerApiBase}/auth/me/guilds`, {
            headers: { authorization: `Bearer ${accessToken}` },
        });
        if (!res.ok)
            throw new Error(`me/guilds -> ${res.status}`);
        return (await res.json());
    }
    // ---- guild-scoped (use the per-guild access token) ----
    postMessage(guildEndpoint, guildToken, channelId, content, authorUserId) {
        return this.post(`${guildEndpoint}/api/channels/${channelId}/messages`, { content, authorUserId }, guildToken);
    }
    createChannel(guildEndpoint, guildToken, body) {
        return this.post(`${guildEndpoint}/api/channels`, body, guildToken);
    }
    // PATCH /api/channels/:id — backend currently only patches `purpose`.
    // Caller must be a member of the channel (or any user if public).
    setChannelPurpose(guildEndpoint, guildToken, channelId, purpose) {
        return this.req('PATCH', `${guildEndpoint}/api/channels/${channelId}`, guildToken, { purpose });
    }
    closeChannel(guildEndpoint, guildToken, channelId) {
        return this.post(`${guildEndpoint}/api/channels/${channelId}/close`, {}, guildToken);
    }
    joinChannel(guildEndpoint, guildToken, channelId) {
        return this.post(`${guildEndpoint}/api/channels/${channelId}/join`, {}, guildToken);
    }
    leaveChannel(guildEndpoint, guildToken, channelId) {
        return this.post(`${guildEndpoint}/api/channels/${channelId}/leave`, {}, guildToken);
    }
    // Register the OpenClaw slash-command catalog with this guild (idempotent
    // full replace). The frontend GETs it for `/` autocomplete; execution
    // still flows as a normal /<cmd> message into OpenClaw's command system.
    syncCommands(guildEndpoint, guildToken, commands, syncKey) {
        // Guild C-2: the shared key is sourced from the channel config
        // (channels.fabric.commandsSyncKey) and must equal the guild's
        // FABRIC_BACKEND_GUILD_COMMANDS_SYNC_KEY for the catalog write.
        return this.req('PUT', `${guildEndpoint}/api/commands`, guildToken, { commands }, syncKey ? { 'x-commands-sync-key': syncKey } : undefined);
    }
    // [{ userId, bypass }] — bypass is true only for discuss/work bypass-list
    channelMembers(guildEndpoint, guildToken, channelId) {
        return this.req('GET', `${guildEndpoint}/api/channels/${channelId}/members`, guildToken);
    }
    // ---- channel canvas (one pinned doc per channel) ----
    canvasUrl(endpoint, channelId) {
        return `${endpoint}/api/channels/${channelId}/canvas`;
    }
    // null when the channel has no canvas
    getCanvas(endpoint, token, channelId) {
        return this.req('GET', this.canvasUrl(endpoint, channelId), token);
    }
    // share / replace (caller becomes the sharer)
    shareCanvas(endpoint, token, channelId, body) {
        return this.req('PUT', this.canvasUrl(endpoint, channelId), token, body);
    }
    // update in place (original sharer only — else the guild returns 403)
    updateCanvas(endpoint, token, channelId, body) {
        return this.req('PATCH', this.canvasUrl(endpoint, channelId), token, body);
    }
    // remove ("close") the canvas (original sharer only)
    removeCanvas(endpoint, token, channelId) {
        return this.req('DELETE', this.canvasUrl(endpoint, channelId), token);
    }
    // ---- channel discovery + message read (used by the agent-facing
    // fabric-channel-list / fabric-message-history tools) ----
    /**
     * List channels in a guild visible to the calling user. Backend
     * filters to public + channels the user is a member of.
     */
    listChannels(guildEndpoint, guildToken, guildNodeId) {
        return this.req('GET', `${guildEndpoint}/api/channels?guildId=${encodeURIComponent(guildNodeId)}`, guildToken);
    }
    /**
     * Page through a channel's message history by `seq`.
     *
     * Backend defaults: 50 / call, max 200. The `seq` field starts at 1
     * per channel; pass `seqFrom=channel.lastSeq - N + 1` to get the
     * tail. Page metadata in the response describes what to ask next.
     */
    listMessages(guildEndpoint, guildToken, channelId, opts = {}) {
        const qs = new URLSearchParams();
        if (opts.seqFrom !== undefined)
            qs.set('seq_from', String(opts.seqFrom));
        if (opts.seqTo !== undefined)
            qs.set('seq_to', String(opts.seqTo));
        if (opts.limit !== undefined)
            qs.set('limit', String(opts.limit));
        const url = `${guildEndpoint}/api/channels/${channelId}/messages` + (qs.toString() ? `?${qs}` : '');
        return this.req('GET', url, guildToken);
    }
}
