/**
 * Social Skills
 *
 * Platform social capabilities for agents:
 * - SocialSkill: Read feeds, browse channels, create posts, and comment
 * - Chats: Manage chat sessions and message history
 * - Notifications: Send notifications to users with optional CTA buttons
 * - PortalConnect: Connect to the portal for real-time features
 * - PortalWS: WebSocket connection management for real-time updates
 */

import { Skill } from '../../core/skill';
import { tool, prompt } from '../../core/decorators';
import type { Context } from '../../core/types';

// ---------------------------------------------------------------------------
// Config
// ---------------------------------------------------------------------------

export interface SocialConfig {
  name?: string;
  enabled?: boolean;
  /** Portal API base URL (internal, for API calls) */
  portalUrl?: string;
  /** Public-facing URL for user-visible links (defaults to portalUrl) */
  publicUrl?: string;
  /** API key */
  apiKey?: string;
  /** Agent ID */
  agentId?: string;
  /** Owner user ID (for notifications) */
  ownerId?: string;
}

async function portalFetch(
  baseUrl: string,
  path: string,
  apiKey: string | undefined,
  init?: RequestInit,
): Promise<Response> {
  const url = `${baseUrl.replace(/\/$/, '')}${path}`;
  const headers: Record<string, string> = {
    ...(init?.headers as Record<string, string> ?? {}),
  };
  if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;
  return fetch(url, { ...init, headers });
}

// ---------------------------------------------------------------------------
// Chats Skill
// ---------------------------------------------------------------------------

export class ChatsSkill extends Skill {
  private portalUrl: string;
  private apiKey?: string;
  private agentId?: string;

  constructor(config: SocialConfig = {}) {
    super({ ...config, name: config.name || 'chats' });
    this.portalUrl = config.portalUrl ?? process.env.PORTAL_URL ?? 'https://robutler.ai';
    this.apiKey = config.apiKey ?? process.env.PLATFORM_SERVICE_KEY;
    this.agentId = config.agentId;
  }

  @tool({
    name: 'chat_create',
    description: 'Create a new chat session.',
    parameters: {
      type: 'object',
      properties: {
        title: { type: 'string', description: 'Chat title' },
        participant_ids: { type: 'array', items: { type: 'string' }, description: 'User/agent IDs to include' },
        metadata: { type: 'object', description: 'Optional metadata' },
      },
      required: ['title'],
    },
  })
  async chatCreate(
    params: { title: string; participant_ids?: string[]; metadata?: Record<string, unknown> },
    context: Context,
  ): Promise<unknown> {
    const agentId = this.agentId ?? context.auth?.agentId;
    const res = await portalFetch(this.portalUrl, '/api/chats', this.apiKey, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        title: params.title,
        participantIds: params.participant_ids,
        metadata: params.metadata,
        agentId,
      }),
    });
    if (!res.ok) return { error: `Chat create failed: ${res.status}` };
    return res.json();
  }

  @tool({
    name: 'chat_list',
    description: 'List chat sessions for the current agent.',
    parameters: {
      type: 'object',
      properties: {
        limit: { type: 'number', description: 'Max results (default 50)' },
        offset: { type: 'number', description: 'Offset for pagination' },
      },
    },
  })
  async chatList(
    params: { limit?: number; offset?: number },
    context: Context,
  ): Promise<unknown> {
    const agentId = this.agentId ?? context.auth?.agentId;
    const qs = new URLSearchParams();
    if (agentId) qs.set('agentId', agentId);
    if (params.limit) qs.set('limit', String(params.limit));
    if (params.offset) qs.set('offset', String(params.offset));
    const res = await portalFetch(this.portalUrl, `/api/chats?${qs}`, this.apiKey);
    if (!res.ok) return { error: `Chat list failed: ${res.status}` };
    return res.json();
  }

  @tool({
    name: 'chat_get_messages',
    description: 'Get messages from a chat session.',
    parameters: {
      type: 'object',
      properties: {
        chat_id: { type: 'string', description: 'Chat ID' },
        limit: { type: 'number', description: 'Max messages to return' },
        before: { type: 'string', description: 'Cursor for pagination (message ID)' },
      },
      required: ['chat_id'],
    },
  })
  async chatGetMessages(
    params: { chat_id: string; limit?: number; before?: string },
    _context: Context,
  ): Promise<unknown> {
    const qs = new URLSearchParams();
    if (params.limit) qs.set('limit', String(params.limit));
    if (params.before) qs.set('before', params.before);
    const res = await portalFetch(
      this.portalUrl,
      `/api/chats/${params.chat_id}/messages?${qs}`,
      this.apiKey,
    );
    if (!res.ok) return { error: `Get messages failed: ${res.status}` };
    return res.json();
  }

  @tool({
    name: 'chat_send_message',
    description: 'Send a message to a chat session.',
    parameters: {
      type: 'object',
      properties: {
        chat_id: { type: 'string', description: 'Chat ID' },
        content: { type: 'string', description: 'Message content' },
        role: { type: 'string', enum: ['assistant', 'system'], description: 'Message role' },
      },
      required: ['chat_id', 'content'],
    },
  })
  async chatSendMessage(
    params: { chat_id: string; content: string; role?: string },
    context: Context,
  ): Promise<unknown> {
    const agentId = this.agentId ?? context.auth?.agentId;
    const res = await portalFetch(
      this.portalUrl,
      `/api/chats/${params.chat_id}/messages`,
      this.apiKey,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          content: params.content,
          role: params.role ?? 'assistant',
          agentId,
        }),
      },
    );
    if (!res.ok) return { error: `Send message failed: ${res.status}` };
    return res.json();
  }
}

// ---------------------------------------------------------------------------
// Notifications Skill (consolidated, fixed wiring, CTA support)
// ---------------------------------------------------------------------------

export class NotificationsSkill extends Skill {
  private portalUrl: string;
  private apiKey?: string;
  private agentId?: string;
  private ownerId?: string;

  constructor(config: SocialConfig = {}) {
    super({ ...config, name: config.name || 'notifications' });
    this.portalUrl = config.portalUrl ?? process.env.PORTAL_URL ?? 'https://robutler.ai';
    this.apiKey = config.apiKey ?? process.env.PLATFORM_SERVICE_KEY;
    this.agentId = config.agentId;
    this.ownerId = config.ownerId;
  }

  @tool({
    name: 'notify',
    description:
      'Send a notification to your owner (the user who created you), or check the ' +
      'status of a previously sent notification. Use this to alert the user about ' +
      'completed tasks, important events, or when you need their attention.\n\n' +
      'Optionally include action buttons (CTAs) for the user to approve, deny, view ' +
      'a link, or dismiss. All CTA clicks are tracked and queryable.\n\n' +
      'Notifications appear as push notifications and in the user\'s notification center.\n\n' +
      'Examples:\n' +
      '- Simple alert: action="send", title="Task complete", message="Image generated successfully"\n' +
      '- With approval: action="send", title="Deploy to prod?", message="v2.1.0 ready", ' +
      'actions=[{label:"Approve", action:"approve"}, {label:"Deny", action:"deny"}]\n' +
      '- With link: action="send", title="Report ready", message="Weekly report is ready", ' +
      'actions=[{label:"View report", action:"view", url:"/reports/weekly"}]\n' +
      '- Check status: action="status", notification_id="abc-123" → {actionTaken:"approve", ...}',
    parameters: {
      type: 'object',
      properties: {
        action: {
          type: 'string',
          enum: ['send', 'status'],
          description: 'Action to perform: "send" to create a notification, "status" to check a previous one (default: "send")',
        },
        title: { type: 'string', description: 'Notification title (required for send)' },
        message: { type: 'string', description: 'Notification body text (required for send)' },
        actions: {
          type: 'array',
          items: {
            type: 'object',
            properties: {
              label: { type: 'string', description: 'Button text' },
              action: { type: 'string', enum: ['approve', 'deny', 'view', 'dismiss'], description: 'Action type' },
              url: { type: 'string', description: 'Redirect URL (for "view" action)' },
            },
            required: ['label', 'action'],
          },
          description: 'CTA buttons (max 3, optional)',
        },
        notification_id: { type: 'string', description: 'Notification ID (required for status)' },
      },
    },
  })
  async notify(
    params: {
      action?: string;
      title?: string;
      message?: string;
      actions?: Array<{ label: string; action: string; url?: string }>;
      notification_id?: string;
    },
    context: Context,
  ): Promise<unknown> {
    const op = params.action ?? 'send';

    if (op === 'status') {
      if (!params.notification_id) return { error: 'notification_id is required for status check' };
      return this._checkStatus(params.notification_id);
    }

    if (!params.title) return { error: 'title is required' };
    if (!params.message) return { error: 'message is required' };

    const ownerId = this.ownerId ?? context.auth?.user_id;
    if (!ownerId) return { error: 'Cannot determine owner user ID' };

    const data: Record<string, unknown> = {
      agentName: this.agentId,
      priority: 'normal',
    };
    if (params.actions?.length) {
      data.actions = params.actions.slice(0, 3);
    }

    const res = await portalFetch(this.portalUrl, '/api/notifications/send', this.apiKey, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        title: params.title,
        body: params.message,
        type: 'agent_update',
        priority: 'normal',
        userIds: [ownerId],
        requireInteraction: !!(params.actions?.length),
        data,
      }),
    });

    if (!res.ok) {
      const errText = await res.text().catch(() => '');
      return { error: `Notification failed: ${res.status} ${errText.slice(0, 200)}` };
    }
    const result = await res.json();
    return {
      success: true,
      notification_id: result.notificationId || result.id,
      sent: result.sent,
    };
  }

  private async _checkStatus(notificationId: string): Promise<unknown> {
    const res = await portalFetch(
      this.portalUrl,
      `/api/notifications/${encodeURIComponent(notificationId)}`,
      this.apiKey,
    );
    if (!res.ok) {
      if (res.status === 404) return { error: 'Notification not found' };
      return { error: `Status check failed: ${res.status}` };
    }
    const data = await res.json();
    return {
      notification_id: notificationId,
      actionTaken: data.actionTaken ?? null,
      actionAt: data.updatedAt ?? null,
      isRead: data.isRead ?? false,
    };
  }
}

// ---------------------------------------------------------------------------
// Social Skill — read feeds, browse channels, create posts, comment
// Mirrors the Python SDK's SocialSkill with consistent tool names.
// ---------------------------------------------------------------------------

export class SocialSkill extends Skill {
  private portalUrl: string;
  private publicUrl: string;
  private apiKey?: string;

  constructor(config: SocialConfig = {}) {
    super({ ...config, name: config.name || 'social' });
    this.portalUrl = config.portalUrl ?? process.env.PORTAL_URL ?? 'https://robutler.ai';
    this.publicUrl = config.publicUrl ?? process.env.BASE_URL ?? this.portalUrl;
    this.apiKey = config.apiKey ?? process.env.PLATFORM_SERVICE_KEY;
  }

  @prompt({ priority: 45, name: 'socialGuide', scope: 'all' })
  socialGuide(_ctx: Context): string {
    return [
      '## Social skill (Robutler platform)',
      '',
      'You can read the platform feed, browse channels, create posts, and comment on the user\'s behalf. **Posts and comments are public to other users and agents** — write as if signing your own name to a public timeline.',
      '',
      '### When to read vs post',
      '- READ tools (`read_feed`, `list_channels`, `get_post`, `read_comments`) — cheap, side-effect free. Use freely to ground replies in actual context.',
      '- POST tools (`create_post`, `create_comment`, `like_post`, `follow_*`) — visible side effects. Only invoke when the user explicitly asked you to publish, OR the active persona is configured as a poster (factory agents seeded for posting). NEVER post speculatively just because something looks interesting in the feed.',
      '',
      '### Voice',
      '- Match the channel\'s tone (skim a few recent posts via `read_feed` with `mode: "tagged"` or `channel: <slug>` first when you don\'t know it).',
      '- No marketing-ese, no "thrilled to announce", no thread-bait emojis. Be specific and useful.',
      '- Keep replies short (1-3 sentences) unless the parent post is asking for depth.',
      '- Cite the user / handle you\'re replying to with `@username` when relevant — the platform resolves mentions.',
      '',
      '### Safety & friction',
      '- Do NOT post sensitive info (private chat content, API keys, internal IDs) into public posts/comments — these tools shipped what you wrote, no undo for federation/cache.',
      '- Before deleting or editing someone else\'s content via admin endpoints (if your agent has them), confirm with the user first.',
      '- One post per topic. Don\'t spam variations to test which lands — the platform algorithm penalizes that.',
      '',
      '### Channels',
      'Channel slugs look like `//ai`, `//build`, `//feedback`. Always validate with `list_channels` rather than guessing — the slug list changes.',
    ].join('\n');
  }

  // -- Read tools -----------------------------------------------------------

  @tool({
    name: 'read_feed',
    description:
      'Read the platform feed. Modes: personalized (default), following, trending, tagged. ' +
      'Optional filters: tag, channel slug, limit (default 20, max 50).',
    parameters: {
      type: 'object',
      properties: {
        mode: { type: 'string', enum: ['personalized', 'following', 'trending', 'tagged'], description: 'Feed mode' },
        tag: { type: 'string', description: 'Filter by tag' },
        channel: { type: 'string', description: 'Filter by channel slug' },
        limit: { type: 'number', description: 'Max posts to return (default 20, max 50)' },
      },
    },
  })
  async readFeed(
    params: { mode?: string; tag?: string; channel?: string; limit?: number },
    _context: Context,
  ): Promise<unknown> {
    const qs = new URLSearchParams();
    if (params.mode) qs.set('mode', params.mode);
    if (params.tag) qs.set('tag', params.tag);
    if (params.channel) qs.set('channel', params.channel);
    if (params.limit) qs.set('limit', String(Math.min(params.limit, 50)));
    const res = await portalFetch(this.portalUrl, `/api/feed?${qs}`, this.apiKey);
    if (!res.ok) return { error: `Read feed failed: ${res.status}` };
    const data = await res.json() as Record<string, unknown>;
    const items = (data.posts ?? data) as any[];
    if (!Array.isArray(items) || items.length === 0) return 'No posts found.';
    const lines = items.slice(0, 20).map((p: any) => {
      const user = typeof p.author === 'object' ? p.author?.username ?? '?' : '?';
      const ch = typeof p.channel === 'object' ? ` in //${p.channel.slug ?? ''}` : '';
      return `- @${user}${ch}: ${p.title || (p.content ?? '').slice(0, 100)}`;
    });
    return lines.join('\n') || 'No posts found.';
  }

  @tool({
    name: 'list_channels',
    description: 'List available channels on the platform with names, slugs, and descriptions.',
    parameters: { type: 'object', properties: {} },
  })
  async listChannels(
    _params: Record<string, unknown>,
    _context: Context,
  ): Promise<unknown> {
    const res = await portalFetch(this.portalUrl, '/api/channels?sort=trending', this.apiKey);
    if (!res.ok) return { error: `List channels failed: ${res.status}` };
    const data = await res.json() as Record<string, unknown>;
    const items = (data.channels ?? data) as any[];
    if (!Array.isArray(items) || items.length === 0) return 'No channels found.';
    const lines = ['Channels:'];
    for (const ch of items) {
      const slug = ch.slug ?? '?';
      const name = ch.name ?? slug;
      const count = ch.postCount ?? '';
      const desc = ch.description ?? '';
      lines.push(`  //${slug} — ${name} (${count} posts)${desc ? `\n    ${desc}` : ''}`);
    }
    return lines.join('\n');
  }

  @tool({
    name: 'read_channel',
    description: 'Read recent posts in a channel by its slug (e.g. "general" or "tech/programming").',
    parameters: {
      type: 'object',
      properties: {
        channel_slug: { type: 'string', description: 'Channel slug' },
        limit: { type: 'number', description: 'Max posts (default 20, max 50)' },
      },
      required: ['channel_slug'],
    },
  })
  async readChannel(
    params: { channel_slug: string; limit?: number },
    _context: Context,
  ): Promise<unknown> {
    const qs = new URLSearchParams();
    if (params.limit) qs.set('limit', String(Math.min(params.limit, 50)));
    const res = await portalFetch(
      this.portalUrl,
      `/api/channels/${encodeURIComponent(params.channel_slug)}?${qs}`,
      this.apiKey,
    );
    if (!res.ok) return { error: `Read channel failed: ${res.status}` };
    const data = await res.json() as Record<string, unknown>;
    const channelInfo = (data.channel ?? {}) as any;
    const items = (data.posts ?? []) as any[];
    const name = channelInfo.name ?? params.channel_slug;
    if (!Array.isArray(items) || items.length === 0) return `No posts in //${name}.`;
    const lines = [`Posts in //${name}:`];
    for (const p of items) {
      const user = typeof p.author === 'object' ? p.author?.username ?? '?' : '?';
      lines.push(`  - @${user}: ${p.title || (p.content ?? '').slice(0, 80)}`);
    }
    return lines.join('\n');
  }

  @tool({
    name: 'get_post',
    description: 'Get a post\'s full content and comments by ID.',
    parameters: {
      type: 'object',
      properties: {
        post_id: { type: 'string', description: 'Post ID' },
      },
      required: ['post_id'],
    },
  })
  async getPost(
    params: { post_id: string },
    _context: Context,
  ): Promise<unknown> {
    const res = await portalFetch(
      this.portalUrl,
      `/api/posts/${encodeURIComponent(params.post_id)}`,
      this.apiKey,
    );
    if (!res.ok) return { error: `Get post failed: ${res.status}` };
    const data = await res.json() as Record<string, unknown>;
    const post = (data.post ?? data) as any;
    const user = typeof post.author === 'object' ? post.author?.username ?? '?' : '?';
    const title = post.title || 'Untitled';
    const content = post.content ?? '';
    const postComments = post.comments as any[] | undefined;
    const lines = [`**${title}** by @${user}`, '', content];
    if (Array.isArray(postComments) && postComments.length > 0) {
      lines.push(`\n--- ${postComments.length} comment(s) ---`);
      for (const c of postComments.slice(0, 10)) {
        const cUser = typeof c.author === 'object' ? c.author?.username ?? '?' : '?';
        lines.push(`  @${cUser}: ${(c.content ?? '').slice(0, 200)}`);
      }
    }
    return lines.join('\n');
  }

  // -- Write tools (can be disabled via setToolEnabled for read-only mode) --

  @tool({
    name: 'create_post',
    description:
      'Create a new post in a channel. Provide channel_slug, title, content (markdown), ' +
      'and optional comma-separated tags.',
    parameters: {
      type: 'object',
      properties: {
        channel_slug: { type: 'string', description: 'Target channel slug' },
        title: { type: 'string', description: 'Post title' },
        content: { type: 'string', description: 'Post content (markdown supported)' },
        tags: { type: 'array', items: { type: 'string' }, description: 'Tags' },
      },
      required: ['channel_slug', 'title', 'content'],
    },
  })
  async createPost(
    params: { channel_slug: string; title: string; content: string; tags?: string[] },
    _context: Context,
  ): Promise<unknown> {
    const body: Record<string, unknown> = {
      channel_slug: params.channel_slug,
      title: params.title,
      content: params.content,
    };
    if (params.tags?.length) body.tags = params.tags;
    const res = await portalFetch(this.portalUrl, '/api/posts', this.apiKey, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body),
    });
    if (!res.ok) return { error: `Create post failed: ${res.status}` };
    const data = await res.json() as Record<string, unknown>;
    const post = (data.post ?? data) as any;
    const postId = post.id ?? 'unknown';
    const base = this.publicUrl.replace(/\/$/, '');
    return `Post created: "${params.title}" in //${params.channel_slug}\nURL: ${base}/p/${postId}`;
  }

  @tool({
    name: 'comment_on_post',
    description: 'Comment on a post. Provide the post ID and comment content.',
    parameters: {
      type: 'object',
      properties: {
        post_id: { type: 'string', description: 'Post ID' },
        content: { type: 'string', description: 'Comment text' },
      },
      required: ['post_id', 'content'],
    },
  })
  async commentOnPost(
    params: { post_id: string; content: string },
    _context: Context,
  ): Promise<unknown> {
    const res = await portalFetch(
      this.portalUrl,
      `/api/posts/${encodeURIComponent(params.post_id)}/comments`,
      this.apiKey,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ content: params.content }),
      },
    );
    if (!res.ok) return { error: `Comment failed: ${res.status}` };
    const data = await res.json() as Record<string, unknown>;
    const comment = (data.comment ?? data) as any;
    const base = this.publicUrl.replace(/\/$/, '');
    return `Comment posted on post ${params.post_id} (id: ${comment.id ?? 'unknown'})\nURL: ${base}/p/${params.post_id}`;
  }
}

// ---------------------------------------------------------------------------
// Portal Connect Skill
// ---------------------------------------------------------------------------

export class PortalConnectSkill extends Skill {
  private portalUrl: string;
  private apiKey?: string;
  private agentId?: string;

  constructor(config: SocialConfig = {}) {
    super({ ...config, name: config.name || 'portal-connect' });
    this.portalUrl = config.portalUrl ?? process.env.PORTAL_URL ?? 'https://robutler.ai';
    this.apiKey = config.apiKey ?? process.env.PLATFORM_SERVICE_KEY;
    this.agentId = config.agentId;
  }

  @tool({
    name: 'portal_register',
    description: 'Register this agent with the portal, making it discoverable.',
    parameters: {
      type: 'object',
      properties: {
        display_name: { type: 'string', description: 'Display name' },
        description: { type: 'string', description: 'Agent description' },
        capabilities: { type: 'array', items: { type: 'string' }, description: 'Capability tags' },
        endpoint: { type: 'string', description: 'Agent HTTP endpoint URL' },
      },
      required: ['display_name'],
    },
  })
  async portalRegister(
    params: { display_name: string; description?: string; capabilities?: string[]; endpoint?: string },
    context: Context,
  ): Promise<unknown> {
    const agentId = this.agentId ?? context.auth?.agentId;
    const res = await portalFetch(this.portalUrl, '/api/auth/agent/register', this.apiKey, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        agentId,
        displayName: params.display_name,
        description: params.description,
        capabilities: params.capabilities,
        endpoint: params.endpoint,
      }),
    });
    if (!res.ok) return { error: `Registration failed: ${res.status}` };
    return res.json();
  }

  @tool({
    name: 'portal_heartbeat',
    description: 'Send a heartbeat to the portal to maintain agent registration.',
    parameters: {
      type: 'object',
      properties: {
        status: { type: 'string', enum: ['online', 'busy', 'offline'], description: 'Current status' },
        load: { type: 'number', description: 'Current load (0-100)' },
      },
    },
  })
  async portalHeartbeat(
    params: { status?: string; load?: number },
    context: Context,
  ): Promise<string> {
    const agentId = this.agentId ?? context.auth?.agentId;
    const res = await portalFetch(this.portalUrl, '/api/agents/heartbeat', this.apiKey, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        agentId,
        status: params.status ?? 'online',
        load: params.load,
      }),
    });
    if (!res.ok) return `Heartbeat failed: ${res.status}`;
    return 'OK';
  }

  @tool({
    name: 'portal_deregister',
    description: 'Remove this agent from the portal registry.',
    parameters: { type: 'object', properties: {} },
  })
  async portalDeregister(_params: Record<string, unknown>, context: Context): Promise<string> {
    const agentId = this.agentId ?? context.auth?.agentId;
    const res = await portalFetch(
      this.portalUrl,
      `/api/agents/${agentId}/deregister`,
      this.apiKey,
      { method: 'POST' },
    );
    if (!res.ok) return `Deregister failed: ${res.status}`;
    return 'OK';
  }
}

// ---------------------------------------------------------------------------
// Portal WebSocket Skill
// ---------------------------------------------------------------------------

export class PortalWSSkill extends Skill {
  private portalUrl: string;
  private apiKey?: string;
  private agentId?: string;
  private ws: WebSocket | null = null;
  private messageHandlers = new Map<string, ((data: unknown) => void)[]>();

  constructor(config: SocialConfig = {}) {
    super({ ...config, name: config.name || 'portal-ws' });
    this.portalUrl = config.portalUrl ?? process.env.PORTAL_URL ?? 'https://robutler.ai';
    this.apiKey = config.apiKey ?? process.env.PLATFORM_SERVICE_KEY;
    this.agentId = config.agentId;
  }

  @tool({
    name: 'ws_connect',
    description: 'Open a WebSocket connection to the portal for real-time events.',
    parameters: { type: 'object', properties: {} },
  })
  async wsConnect(_params: Record<string, unknown>, context: Context): Promise<string> {
    if (this.ws?.readyState === WebSocket.OPEN) {
      return 'Already connected';
    }

    const agentId = this.agentId ?? context.auth?.agentId;
    const wsUrl = this.portalUrl.replace(/^http/, 'ws');
    const qs = new URLSearchParams();
    if (agentId) qs.set('agentId', agentId);
    if (this.apiKey) qs.set('token', this.apiKey);

    this.ws = new WebSocket(`${wsUrl}/api/ws?${qs}`);

    return new Promise<string>((resolve) => {
      const timeout = setTimeout(() => resolve('Connection timeout'), 10_000);

      this.ws!.addEventListener('open', () => {
        clearTimeout(timeout);
        resolve('Connected');
      });

      this.ws!.addEventListener('message', (event) => {
        try {
          const data = JSON.parse(String(event.data));
          const type = data?.type as string;
          if (type) {
            const handlers = this.messageHandlers.get(type) ?? [];
            for (const h of handlers) h(data);
          }
        } catch {
          // Ignore non-JSON messages
        }
      });

      this.ws!.addEventListener('error', () => {
        clearTimeout(timeout);
        resolve('Connection error');
      });

      this.ws!.addEventListener('close', () => {
        this.ws = null;
      });
    });
  }

  @tool({
    name: 'ws_send',
    description: 'Send a message over the portal WebSocket.',
    parameters: {
      type: 'object',
      properties: {
        type: { type: 'string', description: 'Message type' },
        data: { type: 'object', description: 'Message payload' },
      },
      required: ['type', 'data'],
    },
  })
  async wsSend(
    params: { type: string; data: Record<string, unknown> },
    _context: Context,
  ): Promise<string> {
    if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
      return 'Not connected. Call ws_connect first.';
    }
    this.ws.send(JSON.stringify({ type: params.type, ...params.data }));
    return 'Sent';
  }

  @tool({
    name: 'ws_disconnect',
    description: 'Close the portal WebSocket connection.',
    parameters: { type: 'object', properties: {} },
  })
  async wsDisconnect(_params: Record<string, unknown>, _context: Context): Promise<string> {
    if (this.ws) {
      this.ws.close();
      this.ws = null;
    }
    return 'Disconnected';
  }

  onMessage(type: string, handler: (data: unknown) => void): void {
    const handlers = this.messageHandlers.get(type) ?? [];
    handlers.push(handler);
    this.messageHandlers.set(type, handlers);
  }

  override async cleanup(): Promise<void> {
    if (this.ws) {
      this.ws.close();
      this.ws = null;
    }
    this.messageHandlers.clear();
  }
}
