// Fabric channel plugin object (third-party `createChatChannelPlugin` path).
//
// COMPAT NOTE (openclaw v2026.5.7 plugin SDK):
//   We depend on these generic SDK shapes — re-verify on openclaw upgrade:
//   - createChannelPluginBase requires `capabilities`
//   - ChannelSetupAdapter: only `applyAccountConfig` is required
//   - outbound attachedResults.sendText returns Omit<OutboundDeliveryResult,
//     "channel"> (so `messageId: string` is required)
//   Casts at the createChatChannelPlugin boundary are intentional and
//   localized; keep them here so upgrades touch one file.
import { createChatChannelPlugin, createChannelPluginBase, buildChannelOutboundSessionRoute, } from 'openclaw/plugin-sdk/core';
import { FabricClient } from './fabric-client.js';
import { listFabricAccountIds, resolveFabricAccount, resolveDefaultFabricAccountId, } from './accounts.js';
// ---- target grammar: fabric:<channelId> ----
export function stripFabricTargetPrefix(raw) {
    let s = (raw ?? '').trim();
    if (!s)
        return undefined;
    if (s.toLowerCase().startsWith('fabric:'))
        s = s.slice('fabric:'.length).trim();
    if (s.toLowerCase().startsWith('channel:'))
        s = s.slice('channel:'.length).trim();
    return s || undefined;
}
export function normalizeFabricTarget(raw) {
    const id = stripFabricTargetPrefix(raw);
    return id ? `fabric:${id}`.toLowerCase() : undefined;
}
export function looksLikeFabricTargetId(raw) {
    const t = (raw ?? '').trim();
    if (!t)
        return false;
    if (/^fabric:/i.test(t))
        return true;
    return /^[a-z0-9-]{8,}$/i.test(t);
}
export function resolveFabricOutboundSessionRoute(params) {
    const id = stripFabricTargetPrefix(params.target);
    if (!id)
        return null;
    return buildChannelOutboundSessionRoute({
        cfg: params.cfg,
        agentId: params.agentId,
        channel: 'fabric',
        accountId: params.accountId,
        peer: { kind: 'group', id },
        chatType: 'group',
        from: `fabric:channel:${id}`,
        to: `fabric:${id}`,
    });
}
// Posts an agent's reply to Fabric. `to` is the Fabric channelId; `accountId`
// is the agentId (= Fabric identity). One auth concept: account apiKey ->
// agent/login -> guild token -> POST message.
async function sendToFabric(cfg, accountId, to, text) {
    const channelId = stripFabricTargetPrefix(to) ?? to;
    const acc = resolveFabricAccount(cfg, accountId);
    if (!acc.fabricApiKey)
        throw new Error(`fabric account ${acc.accountId} has no fabricApiKey`);
    const client = new FabricClient(acc.centerApiBase);
    const session = await client.agentLogin(acc.fabricApiKey);
    // find which guild owns this channel by probing each guild's channel list
    for (const g of session.guilds) {
        const gt = session.guildAccessTokens.find((t) => t.guildNodeId === g.nodeId)?.token;
        if (!gt)
            continue;
        const res = await fetch(`${g.endpoint}/api/channels?guildId=${encodeURIComponent(g.nodeId)}`, {
            headers: { authorization: `Bearer ${gt}` },
        });
        const channels = res.ok ? (await res.json()) : [];
        if (channels.some((c) => c.id === channelId)) {
            await client.postMessage(g.endpoint, gt, channelId, text, session.user.id);
            return { messageId: `${channelId}:${Date.now()}` };
        }
    }
    // fallback: first guild
    const g = session.guilds[0];
    const gt = session.guildAccessTokens.find((t) => t.guildNodeId === g?.nodeId)?.token;
    if (g && gt) {
        await client.postMessage(g.endpoint, gt, channelId, text, session.user.id);
        return { messageId: `${channelId}:${Date.now()}` };
    }
    throw new Error('fabric: no guild available to deliver');
}
export const fabricChannelPlugin = createChatChannelPlugin({
    base: {
        ...createChannelPluginBase({
            id: 'fabric',
            meta: { id: 'fabric', label: 'Fabric', blurb: 'Connect OpenClaw agents to a Fabric guild.' },
            capabilities: {
                chatTypes: ['channel', 'group', 'direct'],
                reactions: false,
                threads: false,
                media: false,
                nativeCommands: false,
                // Fabric has no message-length limit and we never want a reply split
                // into multiple messages -> no block streaming.
                blockStreaming: false,
            },
            reload: { configPrefixes: ['channels.fabric'] },
            config: {
                listAccountIds: (cfg) => listFabricAccountIds(cfg),
                resolveAccount: (cfg, accountId) => resolveFabricAccount(cfg, accountId),
                defaultAccountId: (cfg) => resolveDefaultFabricAccountId(cfg),
                isConfigured: (account) => Boolean(account.fabricApiKey),
            },
            // Minimal setup adapter: Fabric is configured directly under
            // channels.fabric.* (no interactive wizard). applyAccountConfig is the
            // only required member.
            setup: {
                applyAccountConfig: ({ cfg }) => cfg,
            },
        }),
        messaging: {
            normalizeTarget: normalizeFabricTarget,
            resolveSessionTarget: ({ id }) => normalizeFabricTarget(`fabric:${id}`),
            resolveOutboundSessionRoute: (params) => resolveFabricOutboundSessionRoute(params),
            targetResolver: { looksLikeId: looksLikeFabricTargetId, hint: '<channelId|fabric:ID>' },
        },
    },
    security: {
        dm: {
            channelKey: 'fabric',
            resolvePolicy: (a) => a.dmPolicy,
            resolveAllowFrom: (a) => a.allowFrom,
            defaultPolicy: 'allowlist',
        },
    },
    threading: { topLevelReplyToMode: 'channel' },
    outbound: {
        base: {
            deliveryMode: 'direct',
            // Fabric has no length limit: never chunk — always one message.
            chunker: (text) => [text],
            textChunkLimit: Number.MAX_SAFE_INTEGER,
        },
        attachedResults: {
            channel: 'fabric',
            sendText: async (ctx) => {
                // openclaw passes config under cfg or config depending on path
                const cfg = (ctx.cfg ?? ctx.config ?? {});
                try {
                    const r = await sendToFabric(cfg, ctx.accountId ?? null, ctx.to, ctx.text);
                    console.log(`[fabric] outbound.sendText -> ${ctx.to} ok`);
                    return r;
                }
                catch (e) {
                    console.log(`[fabric] outbound.sendText FAILED to=${ctx.to}: ${String(e)}`);
                    throw e;
                }
            },
        },
    },
});
