import type { FabricClient } from './fabric-client.js';
import type { IdentityRegistry } from './identity.js';

// OpenClaw tool registration api (loose typing — concrete shape from
// openclaw/plugin-sdk/core at host SDK version).
type ToolApi = {
  registerTool: (def: unknown) => void;
  logger: { info: (m: string) => void; warn: (m: string) => void };
};

type Ctx = { agentId?: string };

const X_BY_KIND: Record<string, string> = {
  chat: 'general',
  work: 'work',
  report: 'report',
  discussion: 'discuss',
};

export function registerFabricTools(
  api: ToolApi,
  client: FabricClient,
  identity: IdentityRegistry,
): void {
  // Resolve the calling agent's Fabric session + a guild's token/endpoint.
  const ctxGuild = async (agentId: string, guildNodeId: string) => {
    const entry = identity.findByAgentId(agentId);
    if (!entry)
      throw new Error(
        `agent ${agentId} not registered — run: AGENT_ID=${agentId} ` +
          `~/.openclaw/bin/fabric-register --api-key <fak_…> (or set ` +
          `channels.fabric.accounts.${agentId}); then restart the gateway`,
      );
    const session = await client.agentLogin(entry.fabricApiKey);
    const guild = session.guilds.find((g) => g.nodeId === guildNodeId);
    const token = session.guildAccessTokens.find((t) => t.guildNodeId === guildNodeId)?.token;
    if (!guild || !token) throw new Error(`agent not a member of guild ${guildNodeId}`);
    return { session, guild, token };
  };

  // NOTE: binding an agent's Fabric API key is intentionally NOT a tool.
  // It's a one-time step done out-of-band via the installed script
  //   ~/.openclaw/bin/fabric-register --api-key <fak_…>   (AGENT_ID or --agent-id)
  // or via static config (channels.fabric.accounts.<agentId>).

  const makeCreate = (kind: 'chat' | 'work' | 'report' | 'discussion') =>
    api.registerTool((ctx: Ctx) => ({
      name: `create-${kind}-channel`,
      description: `Create a Fabric ${kind} channel (x_type=${X_BY_KIND[kind]}).`,
      parameters: {
        type: 'object',
        additionalProperties: false,
        required: ['guildNodeId', 'name'],
        properties: {
          guildNodeId: { type: 'string' },
          name: { type: 'string' },
          isPublic: { type: 'boolean' },
          memberUserIds: { type: 'array', items: { type: 'string' } },
          onDuty: { type: 'string', description: 'required for triage-like flows (unused for these kinds)' },
          listeners: { type: 'array', items: { type: 'string' } },
        },
      },
      execute: async (p: {
        guildNodeId: string;
        name: string;
        isPublic?: boolean;
        memberUserIds?: string[];
      }) => {
        const agentId = ctx.agentId;
        if (!agentId) return { ok: false, error: 'no agent context' };
        const { guild, token } = await ctxGuild(agentId, p.guildNodeId);
        const ch = await client.createChannel(guild.endpoint, token, {
          guildId: p.guildNodeId,
          name: p.name,
          xType: X_BY_KIND[kind],
          isPublic: p.isPublic ?? false,
          memberUserIds: p.memberUserIds ?? [],
        });
        return { ok: true, channelId: ch.id };
      },
    }));

  makeCreate('chat');
  makeCreate('work');
  makeCreate('report');
  makeCreate('discussion');

  // discussion-complete: post a summary then close the channel (Guild
  // /channels/:id/close — history stays readable, new posts -> 409).
  api.registerTool((ctx: Ctx) => ({
    name: 'discussion-complete',
    description: 'Conclude a discussion: post a summary then close the channel.',
    parameters: {
      type: 'object',
      additionalProperties: false,
      required: ['guildNodeId', 'channelId', 'summary'],
      properties: {
        guildNodeId: { type: 'string' },
        channelId: { type: 'string' },
        summary: { type: 'string' },
        callbackChannelId: { type: 'string', description: 'optional channel to also post the summary to' },
      },
    },
    execute: async (p: {
      guildNodeId: string;
      channelId: string;
      summary: string;
      callbackChannelId?: string;
    }) => {
      const agentId = ctx.agentId;
      if (!agentId) return { ok: false, error: 'no agent context' };
      const { session, guild, token } = await ctxGuild(agentId, p.guildNodeId);
      await client.postMessage(guild.endpoint, token, p.channelId, p.summary, session.user.id);
      if (p.callbackChannelId) {
        await client
          .postMessage(guild.endpoint, token, p.callbackChannelId, p.summary, session.user.id)
          .catch(() => undefined);
      }
      await client.closeChannel(guild.endpoint, token, p.channelId);
      return { ok: true, closed: true };
    },
  }));

  // fabric-canvas: share / update / read / close the channel's single
  // pinned canvas document (one tool, four actions). update/close are
  // sharer-only server-side (the guild returns 403 otherwise).
  api.registerTool((ctx: Ctx) => ({
    name: 'fabric-canvas',
    description:
      "Manage a channel's pinned canvas document. action: " +
      "read (current canvas or null) | share (create/replace; you become " +
      'the sharer) | update (edit in place; sharer only) | close (remove; ' +
      'sharer only).',
    parameters: {
      type: 'object',
      additionalProperties: false,
      required: ['action', 'guildNodeId', 'channelId'],
      properties: {
        action: { type: 'string', enum: ['read', 'share', 'update', 'close'] },
        guildNodeId: { type: 'string' },
        channelId: { type: 'string' },
        title: { type: 'string', description: 'share: required; update: optional' },
        format: {
          type: 'string',
          enum: ['md', 'html', 'text'],
          description: 'share: required; update: optional',
        },
        source: {
          type: 'string',
          description: 'document body. share: required; update: optional',
        },
      },
    },
    execute: async (p: {
      action: 'read' | 'share' | 'update' | 'close';
      guildNodeId: string;
      channelId: string;
      title?: string;
      format?: 'md' | 'html' | 'text';
      source?: string;
    }) => {
      const agentId = ctx.agentId;
      if (!agentId) return { ok: false, error: 'no agent context' };
      const { guild, token } = await ctxGuild(agentId, p.guildNodeId);
      const ep = guild.endpoint;
      switch (p.action) {
        case 'read': {
          const canvas = await client.getCanvas(ep, token, p.channelId);
          return { ok: true, canvas };
        }
        case 'share': {
          if (!p.title || !p.format || p.source === undefined) {
            return { ok: false, error: 'share requires title, format, and source' };
          }
          const canvas = await client.shareCanvas(ep, token, p.channelId, {
            title: p.title,
            format: p.format,
            source: p.source,
          });
          return { ok: true, canvas };
        }
        case 'update': {
          const body: Partial<{ title: string; format: 'md' | 'html' | 'text'; source: string }> = {};
          if (p.title !== undefined) body.title = p.title;
          if (p.format !== undefined) body.format = p.format;
          if (p.source !== undefined) body.source = p.source;
          if (Object.keys(body).length === 0) {
            return { ok: false, error: 'update needs at least one of title/format/source' };
          }
          const canvas = await client.updateCanvas(ep, token, p.channelId, body);
          return { ok: true, canvas };
        }
        case 'close': {
          await client.removeCanvas(ep, token, p.channelId);
          return { ok: true, removed: true };
        }
        default:
          return { ok: false, error: `unknown action ${String(p.action)}` };
      }
    },
  }));

  // fabric-channel: channel membership (one tool, three actions).
  api.registerTool((ctx: Ctx) => ({
    name: 'fabric-channel',
    description:
      'Channel membership. action: members (list channel member userIds) | ' +
      'join (this agent joins the channel) | leave (this agent leaves).',
    parameters: {
      type: 'object',
      additionalProperties: false,
      required: ['action', 'guildNodeId', 'channelId'],
      properties: {
        action: { type: 'string', enum: ['members', 'join', 'leave'] },
        guildNodeId: { type: 'string' },
        channelId: { type: 'string' },
      },
    },
    execute: async (p: {
      action: 'members' | 'join' | 'leave';
      guildNodeId: string;
      channelId: string;
    }) => {
      const agentId = ctx.agentId;
      if (!agentId) return { ok: false, error: 'no agent context' };
      const { guild, token } = await ctxGuild(agentId, p.guildNodeId);
      const ep = guild.endpoint;
      switch (p.action) {
        case 'members': {
          const members = await client.channelMembers(ep, token, p.channelId);
          return { ok: true, members };
        }
        case 'join': {
          await client.joinChannel(ep, token, p.channelId);
          return { ok: true, joined: true };
        }
        case 'leave': {
          await client.leaveChannel(ep, token, p.channelId);
          return { ok: true, left: true };
        }
        default:
          return { ok: false, error: `unknown action ${String(p.action)}` };
      }
    },
  }));

  // -----------------------------------------------------------------
  // fabric-send-message: post a message into a specific channel.
  //
  // Unlike a normal channel reply (which goes back to whatever channel
  // woke the agent), this lets the agent proactively initiate text into
  // any channel they are a member of — e.g. ARD broadcasting daily
  // workload to #agents-room, or triage agent following up on an
  // already-routed task by commenting in #updates.
  // -----------------------------------------------------------------
  api.registerTool((ctx: Ctx) => ({
    name: 'fabric-send-message',
    description:
      'Send a text message into a specific Fabric channel. Author is the calling agent. ' +
      'Requires guildNodeId + channelId + content. Returns {ok, messageId, seq}.',
    parameters: {
      type: 'object',
      additionalProperties: false,
      required: ['guildNodeId', 'channelId', 'content'],
      properties: {
        guildNodeId: { type: 'string' },
        channelId: { type: 'string' },
        content: { type: 'string', description: 'Message body (markdown supported by the renderer).' },
      },
    },
    execute: async (p: { guildNodeId: string; channelId: string; content: string }) => {
      const agentId = ctx.agentId;
      if (!agentId) return { ok: false, error: 'no agent context' };
      const { session, guild, token } = await ctxGuild(agentId, p.guildNodeId);
      const res = (await client.postMessage(
        guild.endpoint,
        token,
        p.channelId,
        p.content,
        session.user.id,
      )) as { messageId?: string; seq?: number };
      return { ok: true, messageId: res.messageId, seq: res.seq };
    },
  }));

  // -----------------------------------------------------------------
  // fabric-channel-list: enumerate channels the calling agent can see
  // in a given guild. Backend filters to public channels + channels the
  // agent is a member of. Returns id / name / xType per channel so the
  // agent can pick a channelId for fabric-send-message etc.
  // -----------------------------------------------------------------
  api.registerTool((ctx: Ctx) => ({
    name: 'fabric-channel-list',
    description:
      'List channels visible to the calling agent in a guild. Optional ' +
      'nameFilter does a case-insensitive substring match client-side. ' +
      'Use this to find a channelId before fabric-send-message / fabric-message-history.',
    parameters: {
      type: 'object',
      additionalProperties: false,
      required: ['guildNodeId'],
      properties: {
        guildNodeId: { type: 'string' },
        nameFilter: { type: 'string', description: 'optional substring match on channel name (case-insensitive)' },
        xType: {
          type: 'string',
          enum: ['general', 'work', 'report', 'discuss', 'triage', 'custom', 'dm'],
          description: 'optional filter by x_type',
        },
        includeClosed: { type: 'boolean', description: 'default false — closed channels filtered out' },
      },
    },
    execute: async (p: {
      guildNodeId: string;
      nameFilter?: string;
      xType?: string;
      includeClosed?: boolean;
    }) => {
      const agentId = ctx.agentId;
      if (!agentId) return { ok: false, error: 'no agent context' };
      const { guild, token } = await ctxGuild(agentId, p.guildNodeId);
      const all = await client.listChannels(guild.endpoint, token, p.guildNodeId);
      const needle = (p.nameFilter ?? '').toLowerCase();
      const filtered = all.filter((c) => {
        if (!p.includeClosed && c.closed) return false;
        if (p.xType && c.xType !== p.xType) return false;
        if (needle && !c.name.toLowerCase().includes(needle)) return false;
        return true;
      });
      return {
        ok: true,
        count: filtered.length,
        channels: filtered.map((c) => ({
          id: c.id,
          name: c.name,
          xType: c.xType,
          isPublic: c.isPublic,
          closed: c.closed,
          lastSeq: c.lastSeq,
        })),
      };
    },
  }));

  // -----------------------------------------------------------------
  // fabric-message-history: read a channel's recent message history by
  // `seq`. Tail-by-default: when `seqFrom`/`seqTo` are omitted, returns
  // the last `limit` messages (limit defaults to 20, max 200).
  //
  // Use cases: catch-up on a channel that was muted while the agent was
  // gated; verify a previous message went through; lookup recent
  // duplicates before opening a new task in triage.
  // -----------------------------------------------------------------
  api.registerTool((ctx: Ctx) => ({
    name: 'fabric-message-history',
    description:
      "Read a channel's recent message history. Omit seqFrom/seqTo to " +
      'tail (last `limit` messages, default 20, max 200). Backend ' +
      'requires the calling agent to be a channel participant.',
    parameters: {
      type: 'object',
      additionalProperties: false,
      required: ['guildNodeId', 'channelId'],
      properties: {
        guildNodeId: { type: 'string' },
        channelId: { type: 'string' },
        seqFrom: { type: 'integer', minimum: 1, description: 'inclusive lower bound; default = tail' },
        seqTo: { type: 'integer', minimum: 1, description: 'inclusive upper bound; default = channel head' },
        limit: { type: 'integer', minimum: 1, maximum: 200, description: 'default 20' },
      },
    },
    execute: async (p: {
      guildNodeId: string;
      channelId: string;
      seqFrom?: number;
      seqTo?: number;
      limit?: number;
    }) => {
      const agentId = ctx.agentId;
      if (!agentId) return { ok: false, error: 'no agent context' };
      const { guild, token } = await ctxGuild(agentId, p.guildNodeId);
      const limit = p.limit ?? 20;

      // Tail mode: discover channel head via channel listing, then ask
      // for [head-limit+1, head]. Avoids needing the agent to know seq.
      let seqFrom = p.seqFrom;
      let seqTo = p.seqTo;
      if (seqFrom === undefined && seqTo === undefined) {
        const channels = await client.listChannels(guild.endpoint, token, p.guildNodeId);
        const ch = channels.find((c) => c.id === p.channelId);
        const head = ch?.lastSeq ?? 0;
        seqFrom = Math.max(1, head - limit + 1);
        seqTo = head;
      }

      const res = await client.listMessages(guild.endpoint, token, p.channelId, {
        seqFrom,
        seqTo,
        limit,
      });
      return {
        ok: true,
        page: res.page,
        messages: res.items.map((m) => ({
          messageId: m.messageId,
          seq: m.seq,
          authorUserId: m.authorUserId,
          content: m.content,
          createdAt: m.createdAt,
          isDeleted: m.isDeleted,
        })),
      };
    },
  }));
}
