import { createHash, createHmac, randomUUID, timingSafeEqual } from 'node:crypto';
import { ChannelDeliveryError } from '@mosaicstack/types';
import type {
  ChannelAdapterHealthDto,
  ChannelAuthorizedPrincipalDto,
  ChannelConversationRouteDto,
  ChannelEgressDto,
  ChannelEgressPort,
  ChannelIngressDto,
  ChannelIngressPort,
  ChannelMessageDto,
  OfficialChannelAdapter,
} from '@mosaicstack/types';
import {
  ChannelType,
  Client,
  GatewayIntentBits,
  ThreadAutoArchiveDuration,
  type Message as DiscordMessage,
} from 'discord.js';
import { io, type Socket } from 'socket.io-client';

export interface DiscordPluginDependencies {
  ingressPort?: ChannelIngressPort;
  client?: Client;
  socketFactory?: (url: string, options: Parameters<typeof io>[1]) => Socket;
}

export interface DiscordPluginConfig {
  token: string;
  gatewayUrl: string;
  /** Maximum authorized turns per Discord user/channel per minute. */
  messageRateLimitPerMinute?: number;
  /** Maximum mention-triggered thread routes per Discord user/channel per minute. */
  threadRateLimitPerMinute?: number;
  /** Shared service credential injected by the approved secret mechanism. */
  serviceToken: string;
  /** Which guild to bind to (single-guild only for v0.1.0). */
  guildId?: string;
  allowedGuildIds: readonly string[];
  allowedChannelIds: readonly string[];
  allowedUserIds: readonly string[];
  /** Provisioned interaction bindings; instance identity is configuration, never code. */
  interactionBindings?: readonly DiscordInteractionBinding[];
}

export type DiscordInteractionOperation = 'bind' | 'attach' | 'send' | 'approve' | 'stop';
export type DiscordInteractionRole = 'viewer' | 'operator' | 'admin';

/** A provisioned Discord-to-Mosaic identity pairing. */
export interface DiscordInteractionUserBinding {
  role: DiscordInteractionRole;
  /** Required for privileged approval and stop operations. */
  mosaicUserId?: string;
}

/** Legacy role-only pairings remain valid for non-privileged Discord ingress. */
export type DiscordInteractionPairing = DiscordInteractionRole | DiscordInteractionUserBinding;

export interface DiscordInteractionBinding {
  /** Stable logical agent identity used in channel conversation routes. */
  instanceId: string;
  /** Trusted gateway database agent-config ID selected for this binding. */
  agentConfigId: string;
  guildId: string;
  channelId: string;
  /** Pairing roster keyed by Discord user ID. */
  pairedUsers: Readonly<Record<string, DiscordInteractionPairing>>;
}

const DEFAULT_MESSAGE_RATE_LIMIT_PER_MINUTE = 30;
const DEFAULT_THREAD_RATE_LIMIT_PER_MINUTE = 5;
const RATE_LIMIT_WINDOW_MS = 60_000;
const DELIVERY_MAX_ATTEMPTS = 3;
const DELIVERY_RETRY_BASE_MS = 50;
const MAX_CONVERSATION_ROUTES = 1_000;

const operationRoles: Readonly<
  Record<DiscordInteractionOperation, readonly DiscordInteractionRole[]>
> = {
  bind: ['admin'],
  attach: ['operator', 'admin'],
  send: ['operator', 'admin'],
  approve: ['admin'],
  stop: ['admin'],
};

/** Resolves a configuration-owned binding and applies pairing/RBAC before ingress. */
export function resolveDiscordInteractionBinding(
  bindings: readonly DiscordInteractionBinding[],
  guildId: string,
  channelId: string,
  userId: string,
  operation: DiscordInteractionOperation,
): DiscordInteractionBinding | null {
  const binding = bindings.find(
    (candidate) => candidate.guildId === guildId && candidate.channelId === channelId,
  );
  if (!binding) return null;
  const pairing = binding.pairedUsers[userId];
  const role = typeof pairing === 'string' ? pairing : pairing?.role;
  return role && operationRoles[operation].includes(role) ? binding : null;
}

/** Resolves the provisioned Mosaic identity for an already-authorized Discord user. */
export function resolveDiscordInteractionActorId(
  binding: DiscordInteractionBinding,
  discordUserId: string,
): string | null {
  const pairing = binding.pairedUsers[discordUserId];
  if (typeof pairing === 'string') return null;
  const mosaicUserId = pairing?.mosaicUserId?.trim();
  return mosaicUserId || null;
}

/** Parses provisioned binding roster JSON and rejects malformed or empty data. */
export function parseDiscordInteractionBindings(
  value: string | undefined,
): DiscordInteractionBinding[] {
  if (!value) throw new Error('DISCORD_INTERACTION_BINDINGS is required when Discord is enabled');
  const parsed: unknown = JSON.parse(value);
  if (!Array.isArray(parsed) || parsed.length === 0) {
    throw new Error('DISCORD_INTERACTION_BINDINGS must be a non-empty JSON array');
  }
  return parsed.map((binding: unknown): DiscordInteractionBinding => {
    if (typeof binding !== 'object' || binding === null)
      throw new Error('Invalid Discord interaction binding');
    const candidate = binding as Partial<DiscordInteractionBinding>;
    if (
      !candidate.instanceId ||
      !candidate.agentConfigId ||
      !candidate.guildId ||
      !candidate.channelId ||
      !candidate.pairedUsers ||
      typeof candidate.pairedUsers !== 'object'
    ) {
      throw new Error('Invalid Discord interaction binding');
    }
    const pairedUsers = Object.fromEntries(
      Object.entries(candidate.pairedUsers).map(([discordUserId, pairing]: [string, unknown]) => {
        if (!discordUserId.trim()) {
          throw new Error('Invalid Discord interaction user binding');
        }
        if (typeof pairing === 'string') {
          if (!['viewer', 'operator', 'admin'].includes(pairing)) {
            throw new Error('Invalid Discord interaction user binding');
          }
          return [discordUserId, pairing];
        }
        if (typeof pairing !== 'object' || pairing === null) {
          throw new Error('Invalid Discord interaction user binding');
        }
        const userBinding = pairing as Partial<DiscordInteractionUserBinding>;
        if (
          !userBinding.role ||
          !['viewer', 'operator', 'admin'].includes(userBinding.role) ||
          (userBinding.mosaicUserId !== undefined && !userBinding.mosaicUserId.trim())
        ) {
          throw new Error('Invalid Discord interaction user binding');
        }
        return [
          discordUserId,
          {
            role: userBinding.role,
            ...(userBinding.mosaicUserId ? { mosaicUserId: userBinding.mosaicUserId } : {}),
          },
        ];
      }),
    ) as Record<string, DiscordInteractionPairing>;
    return {
      instanceId: candidate.instanceId,
      agentConfigId: candidate.agentConfigId,
      guildId: candidate.guildId,
      channelId: candidate.channelId,
      pairedUsers,
    };
  });
}

export interface DiscordIngressPayload {
  correlationId: string;
  messageId: string;
  guildId: string;
  channelId: string;
  userId: string;
  conversationId: string;
  content: string;
  threadId?: string;
  attachments?: readonly DiscordAttachment[];
}

export interface DiscordAttachment {
  id: string;
  name: string;
  url: string;
  contentType: string | null;
  sizeBytes?: number;
}

export interface DiscordIngressEnvelope {
  payload: DiscordIngressPayload;
  signature: string;
}

export interface DiscordIngressAllowlists {
  guildIds: readonly string[];
  channelIds: readonly string[];
  userIds: readonly string[];
}

function signedPayload(payload: DiscordIngressPayload): string {
  return [
    payload.correlationId,
    payload.messageId,
    payload.guildId,
    payload.channelId,
    payload.userId,
    payload.conversationId,
    payload.content,
    payload.threadId ?? '',
    JSON.stringify(payload.attachments ?? []),
  ].join('\n');
}

function signPayload(payload: DiscordIngressPayload, serviceToken: string): string {
  return createHmac('sha256', serviceToken).update(signedPayload(payload)).digest('hex');
}

function isSignatureValid(actual: string, expected: string): boolean {
  const actualBuffer = Buffer.from(actual, 'hex');
  const expectedBuffer = Buffer.from(expected, 'hex');
  return (
    actualBuffer.length === expectedBuffer.length && timingSafeEqual(actualBuffer, expectedBuffer)
  );
}

function includesId(allowedIds: readonly string[], id: string): boolean {
  return allowedIds.includes(id);
}

/** Creates the signed, auditable envelope accepted by the gateway Discord service boundary. */
export function createDiscordIngressEnvelope(
  payload: DiscordIngressPayload,
  serviceToken: string,
): DiscordIngressEnvelope {
  return { payload, signature: signPayload(payload, serviceToken) };
}

/**
 * Verifies service-origin integrity and applies default-deny Discord identity allowlists.
 * Returns null instead of a partially trusted payload on every failure path.
 */
export function verifyDiscordIngressEnvelope(
  envelope: DiscordIngressEnvelope,
  serviceToken: string,
  allowlists?: DiscordIngressAllowlists,
): DiscordIngressPayload | null {
  const expectedSignature = signPayload(envelope.payload, serviceToken);
  if (!isSignatureValid(envelope.signature, expectedSignature)) return null;

  if (
    allowlists &&
    (!includesId(allowlists.guildIds, envelope.payload.guildId) ||
      !includesId(allowlists.channelIds, envelope.payload.channelId) ||
      !includesId(allowlists.userIds, envelope.payload.userId))
  ) {
    return null;
  }

  return envelope.payload;
}

export class DiscordPlugin implements OfficialChannelAdapter, ChannelEgressPort {
  readonly name = 'discord';

  private client: Client;
  private socket: Socket | null = null;
  /** Bounded last-authorized routes for response-target egress validation. */
  private conversationRoutes = new Map<string, ChannelConversationRouteDto>();
  /** Track in-flight responses to avoid duplicate streaming. */
  private pendingResponses = new Map<string, string>();
  private readonly messageRateWindows = new Map<string, number[]>();
  private readonly threadRateWindows = new Map<string, number[]>();

  constructor(
    private readonly config: DiscordPluginConfig,
    private readonly dependencies: DiscordPluginDependencies = {},
  ) {
    this.client =
      dependencies.client ??
      new Client({
        intents: [
          GatewayIntentBits.Guilds,
          GatewayIntentBits.GuildMessages,
          GatewayIntentBits.MessageContent,
          GatewayIntentBits.DirectMessages,
        ],
      });
  }

  async start(): Promise<void> {
    const socketFactory = this.dependencies.socketFactory ?? io;
    this.socket = socketFactory(`${this.config.gatewayUrl}/chat`, {
      auth: { discordServiceToken: this.config.serviceToken },
      transports: ['websocket'],
    });

    this.socket.on('connect', () => {
      console.log('[discord] Connected to gateway');
    });

    this.socket.on('disconnect', (reason: string) => {
      console.error(`[discord] Disconnected from gateway: ${reason}`);
      this.pendingResponses.clear();
    });

    this.socket.on('connect_error', (err: Error) => {
      console.error(`[discord] Gateway connection error: ${err.message}`);
    });

    this.socket.on('agent:text', (data: { conversationId: string; text: string }) => {
      const pending = this.pendingResponses.get(data.conversationId);
      if (pending !== undefined) {
        this.pendingResponses.set(data.conversationId, pending + data.text);
      }
    });

    this.socket.on('agent:end', (data: { conversationId: string }) => {
      const text = this.pendingResponses.get(data.conversationId);
      this.pendingResponses.delete(data.conversationId);
      if (text) {
        this.sendAgentResponse(data.conversationId, text).catch((err: unknown) => {
          console.error(`[discord] Error sending response for ${data.conversationId}:`, err);
        });
      } else {
        this.conversationRoutes.delete(data.conversationId);
      }
    });

    this.socket.on('agent:start', (data: { conversationId: string }) => {
      this.pendingResponses.set(data.conversationId, '');
    });

    this.socket.on('error', (data: { conversationId?: unknown }) => {
      if (typeof data.conversationId !== 'string') return;
      this.pendingResponses.delete(data.conversationId);
      this.conversationRoutes.delete(data.conversationId);
    });

    this.client.on('messageCreate', (message: DiscordMessage) => {
      void Promise.resolve(this.handleDiscordMessage(message)).catch((error: unknown): void => {
        const errorName = error instanceof Error ? error.name : 'UnknownError';
        console.error(
          `[discord] Message routing failed. channel=${message.channelId} message=${message.id} error=${errorName}`,
        );
      });
    });

    this.client.on('ready', () => {
      console.log(`[discord] Bot logged in as ${this.client.user?.tag}`);
    });

    try {
      await this.client.login(this.config.token);
    } catch (error: unknown) {
      this.socket.disconnect();
      this.socket = null;
      await this.client.destroy();
      throw error;
    }
  }

  async stop(): Promise<void> {
    this.socket?.disconnect();
    await this.client.destroy();
  }

  async health(): Promise<ChannelAdapterHealthDto> {
    const discordReady = this.client.isReady();
    const gatewayConnected = this.socket?.connected === true;
    if (discordReady && gatewayConnected) return { status: 'connected' };
    if (discordReady || gatewayConnected) return { status: 'degraded' };
    return { status: 'disconnected' };
  }

  async createProjectChannel(project: {
    id: string;
    name: string;
    description?: string;
  }): Promise<{ channelId: string } | null> {
    if (!this.config.guildId) return null;

    const guild = this.client.guilds.cache.get(this.config.guildId);
    if (!guild) return null;

    const channelName = `mosaic-${project.name
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, '-')
      .replace(/^-|-$/g, '')}`;

    const channel = await guild.channels.create({
      name: channelName,
      type: ChannelType.GuildText,
      topic: project.description ?? `Mosaic project: ${project.name}`,
    });

    // A project channel has no logical-agent conversation until a configured
    // binding authorizes a message. Do not seed a legacy channel-only key.
    return { channelId: channel.id };
  }

  private handleDiscordMessage(message: DiscordMessage): void | Promise<void> {
    if (message.author.bot || !this.client.user || !message.guildId) return;

    const authorizationChannelId = this.authorizationChannelId(message);
    if (!this.isAllowedMessage(message, authorizationChannelId)) return;

    const isMention = message.mentions.has(this.client.user);
    const content = isMention
      ? message.content.replace(new RegExp(`<@!?${this.client.user.id}>`, 'g'), '').trim()
      : message.content.trim();
    if (!content && message.attachments.size === 0) return;
    const operation = this.interactionOperation(content);
    const binding = resolveDiscordInteractionBinding(
      this.config.interactionBindings ?? [],
      message.guildId,
      authorizationChannelId,
      message.author.id,
      operation,
    );
    // Pairing and operation-specific role checks happen before thread creation
    // or any gateway dispatch, including privileged control events.
    if (!binding) return;

    const rateKey = `${message.guildId}:${authorizationChannelId}:${message.author.id}`;
    if (
      !this.consumeRateLimit(
        this.messageRateWindows,
        rateKey,
        this.config.messageRateLimitPerMinute ?? DEFAULT_MESSAGE_RATE_LIMIT_PER_MINUTE,
      )
    ) {
      console.error(
        `[discord] Message rate limit reached. guild=${message.guildId} channel=${authorizationChannelId} user=${message.author.id}`,
      );
      return;
    }

    const createThread = isMention && operation === 'send';
    if (
      createThread &&
      !this.consumeRateLimit(
        this.threadRateWindows,
        rateKey,
        this.config.threadRateLimitPerMinute ?? DEFAULT_THREAD_RATE_LIMIT_PER_MINUTE,
      )
    ) {
      console.error(
        `[discord] Thread rate limit reached. guild=${message.guildId} channel=${authorizationChannelId} user=${message.author.id}`,
      );
      return;
    }

    if (!this.dependencies.ingressPort && !this.socket?.connected) {
      console.error(
        `[discord] Cannot forward message: not connected to gateway. channel=${message.channelId} message=${message.id}`,
      );
      return;
    }

    // Approval/stop commands act on the current durable session. They remain
    // at the current channel/thread target rather than creating a new topic.
    const route = this.resolveConversationRoute(
      message,
      binding,
      authorizationChannelId,
      createThread,
    );
    if (route instanceof Promise) {
      return route.then((resolved: ChannelConversationRouteDto): void | Promise<void> =>
        this.dispatchDiscordIngress(message, content, operation, binding, resolved),
      );
    }
    return this.dispatchDiscordIngress(message, content, operation, binding, route);
  }

  private dispatchDiscordIngress(
    message: DiscordMessage,
    content: string,
    operation: 'send' | 'approve' | 'stop',
    binding: DiscordInteractionBinding,
    route: ChannelConversationRouteDto,
  ): void | Promise<void> {
    const guildId = message.guildId;
    if (!guildId) return;
    if (operation === 'send') this.rememberConversationRoute(route);
    const correlationId = randomUUID();
    const ingress: ChannelIngressDto = {
      correlationId,
      nativeMessageId: message.id,
      operation:
        operation === 'approve'
          ? 'approval.create'
          : operation === 'stop'
            ? 'session.stop'
            : 'message.send',
      principal: this.authorizedPrincipal(binding, message.author.id),
      message: this.channelMessage(message, content, route),
      route,
    };
    if (this.dependencies.ingressPort) {
      return this.dependencies.ingressPort.receive(ingress).catch((error: unknown) => {
        if (operation === 'send') this.conversationRoutes.delete(route.conversationId);
        throw error;
      });
    }
    const socket = this.socket;
    if (!socket?.connected) {
      console.error(
        `[discord] Cannot dispatch routed message: gateway disconnected. channel=${message.channelId} message=${message.id}`,
      );
      return;
    }
    this.emitDiscordIngress(socket, ingress);
  }

  private authorizedPrincipal(
    binding: DiscordInteractionBinding,
    channelUserId: string,
  ): ChannelAuthorizedPrincipalDto {
    const pairing = binding.pairedUsers[channelUserId];
    if (!pairing) throw new Error('Authorized Discord pairing is unavailable');
    if (typeof pairing === 'string') return { channelUserId, role: pairing };
    return {
      channelUserId,
      role: pairing.role,
      ...(pairing.mosaicUserId ? { mosaicUserId: pairing.mosaicUserId } : {}),
    };
  }

  private channelMessage(
    message: DiscordMessage,
    content: string,
    route: ChannelConversationRouteDto,
  ): ChannelMessageDto {
    const attachments = Array.from(message.attachments.values()).map((attachment) => ({
      id: attachment.id,
      name: attachment.name,
      url: attachment.url,
      mimeType: attachment.contentType,
      sizeBytes: attachment.size,
    }));
    const firstContentType = attachments[0]?.mimeType;
    return {
      id: randomUUID(),
      channelName: this.name,
      channelId: route.responseTarget.channelId,
      senderId: message.author.id,
      senderKind: 'user',
      content,
      contentKind:
        content.length > 0 ? 'markdown' : firstContentType?.startsWith('image/') ? 'image' : 'file',
      timestamp:
        message.createdAt instanceof Date
          ? message.createdAt.toISOString()
          : new Date().toISOString(),
      ...(route.responseTarget.threadId ? { threadId: route.responseTarget.threadId } : {}),
      ...(attachments.length > 0 ? { attachments } : {}),
      metadata: {
        channelMessageId: message.id,
        guildId: message.guildId ?? '',
      },
    };
  }

  private emitDiscordIngress(socket: Socket, ingress: ChannelIngressDto): void {
    const envelope = createDiscordIngressEnvelope(
      {
        correlationId: ingress.correlationId,
        messageId: ingress.nativeMessageId,
        guildId: String(ingress.message.metadata['guildId'] ?? ''),
        channelId: ingress.route.authorizationChannelId,
        userId: ingress.principal.channelUserId,
        conversationId: ingress.route.conversationId,
        content: ingress.message.content,
        ...(ingress.route.responseTarget.threadId
          ? { threadId: ingress.route.responseTarget.threadId }
          : {}),
        attachments: ingress.message.attachments?.map((attachment) => ({
          id: attachment.id,
          name: attachment.name,
          url: attachment.url,
          contentType: attachment.mimeType,
          ...(attachment.sizeBytes !== undefined ? { sizeBytes: attachment.sizeBytes } : {}),
        })),
      },
      this.config.serviceToken,
    );
    socket.emit(
      ingress.operation === 'approval.create'
        ? 'discord:approve'
        : ingress.operation === 'session.stop'
          ? 'discord:stop'
          : 'message',
      envelope,
    );
  }

  private authorizationChannelId(message: DiscordMessage): string {
    // A normal guild channel can itself have a category parent. Only Discord
    // threads inherit authorization from a configured parent text channel.
    const channel = message.channel as DiscordMessage['channel'] & {
      isThread?: () => boolean;
      parentId?: string | null;
    };
    const isThread =
      typeof channel.isThread === 'function'
        ? channel.isThread()
        : channel.parentId !== undefined && channel.parentId !== null;
    return isThread && channel.parentId ? channel.parentId : message.channelId;
  }

  private isAllowedMessage(message: DiscordMessage, authorizationChannelId: string): boolean {
    const guildId = message.guildId;
    return (
      guildId !== null &&
      includesId(this.config.allowedGuildIds, guildId) &&
      includesId(this.config.allowedChannelIds, authorizationChannelId) &&
      includesId(this.config.allowedUserIds, message.author.id)
    );
  }

  private resolveConversationRoute(
    message: DiscordMessage,
    binding: DiscordInteractionBinding,
    authorizationChannelId: string,
    createThread: boolean,
  ): ChannelConversationRouteDto | Promise<ChannelConversationRouteDto> {
    if (authorizationChannelId !== message.channelId) {
      return this.createConversationRoute(
        binding,
        authorizationChannelId,
        message.channelId,
        message.channelId,
      );
    }
    if (!createThread) {
      return this.createConversationRoute(binding, authorizationChannelId, message.channelId);
    }

    if (message.hasThread) {
      const cachedThread = message.thread;
      if (cachedThread) {
        return this.createConversationRoute(
          binding,
          authorizationChannelId,
          cachedThread.id,
          cachedThread.id,
        );
      }
      if (!('threads' in message.channel)) {
        return Promise.reject(new Error('Existing Discord thread manager is unavailable'));
      }
      return message.channel.threads
        .fetch(message.id)
        .then((thread): ChannelConversationRouteDto => {
          if (!thread) throw new Error('Existing Discord thread is unavailable');
          return this.createConversationRoute(
            binding,
            authorizationChannelId,
            thread.id,
            thread.id,
          );
        });
    }
    return message
      .startThread({
        name: `Mosaic conversation ${message.id.slice(-8)}`,
        autoArchiveDuration: ThreadAutoArchiveDuration.OneHour,
        reason: 'Authorized Mosaic mention',
      })
      .then(
        (thread): ChannelConversationRouteDto =>
          this.createConversationRoute(binding, authorizationChannelId, thread.id, thread.id),
      );
  }

  private createConversationRoute(
    binding: DiscordInteractionBinding,
    authorizationChannelId: string,
    responseChannelId: string,
    threadId?: string,
  ): ChannelConversationRouteDto {
    // Recompute from configuration on every turn so a stale in-memory map can
    // never carry a channel-only or differently bound agent identity forward.
    const conversationId = `${binding.instanceId}:discord:${responseChannelId}`;
    return {
      bindingId: `${binding.guildId}:${binding.channelId}:${binding.instanceId}`,
      logicalAgentId: binding.instanceId,
      conversationId,
      channelName: this.name,
      authorizationChannelId,
      responseTarget: {
        channelId: responseChannelId,
        ...(threadId ? { threadId } : {}),
      },
    };
  }

  private consumeRateLimit(
    windows: Map<string, number[]>,
    key: string,
    limit: number,
    now = Date.now(),
  ): boolean {
    const active = (windows.get(key) ?? []).filter(
      (timestamp: number): boolean => now - timestamp < RATE_LIMIT_WINDOW_MS,
    );
    if (active.length >= Math.max(1, limit)) {
      windows.set(key, active);
      return false;
    }
    active.push(now);
    windows.set(key, active);
    return true;
  }

  private interactionOperation(content: string): 'send' | 'approve' | 'stop' {
    if (/^\/approve$/i.test(content)) return 'approve';
    if (/^\/stop\s+\S+/i.test(content)) return 'stop';
    return 'send';
  }

  async send(egress: ChannelEgressDto): Promise<void> {
    if (!this.isConfiguredRoute(egress.route) || !this.isMessageAlignedWithRoute(egress)) {
      throw new ChannelDeliveryError(
        'invalid_route',
        `Discord egress route is not authorized for conversation ${egress.route.conversationId}`,
      );
    }
    const channelId = egress.route.responseTarget.channelId;
    const channel = this.client.channels.cache.get(channelId);
    if (!channel || !('send' in channel)) {
      throw new ChannelDeliveryError(
        'destination_unavailable',
        `Discord destination is unavailable for conversation ${egress.route.conversationId}`,
      );
    }

    const chunks = this.chunkText(egress.message.content, 1900);
    for (const [chunkIndex, chunk] of chunks.entries()) {
      await this.sendChunkWithRetry(
        channel as {
          send(options: { content: string; nonce: string; enforceNonce: true }): Promise<unknown>;
        },
        chunk,
        egress.correlationId,
        chunkIndex,
        egress.route.conversationId,
      );
    }
  }

  private async sendChunkWithRetry(
    channel: {
      send(options: { content: string; nonce: string; enforceNonce: true }): Promise<unknown>;
    },
    chunk: string,
    correlationId: string,
    chunkIndex: number,
    conversationId: string,
  ): Promise<void> {
    const nonce = createHash('sha256')
      .update(`${correlationId}:${chunkIndex}`)
      .digest('hex')
      .slice(0, 25);
    let lastError: unknown;
    for (let attempt = 1; attempt <= DELIVERY_MAX_ATTEMPTS; attempt += 1) {
      try {
        await channel.send({ content: chunk, nonce, enforceNonce: true });
        return;
      } catch (error: unknown) {
        lastError = error;
        const retryable = this.isTransientDeliveryError(error);
        if (!retryable) {
          throw new ChannelDeliveryError(
            'delivery_failed',
            `Discord delivery failed for conversation ${conversationId}`,
            false,
            { cause: error },
          );
        }
        if (attempt < DELIVERY_MAX_ATTEMPTS) {
          await new Promise<void>((resolve): void => {
            setTimeout(resolve, DELIVERY_RETRY_BASE_MS * 2 ** (attempt - 1));
          });
        }
      }
    }
    throw new ChannelDeliveryError(
      'delivery_failed',
      `Discord delivery failed for conversation ${conversationId}`,
      true,
      { cause: lastError },
    );
  }

  private isTransientDeliveryError(error: unknown): boolean {
    if (typeof error !== 'object' || error === null) return false;
    const candidate = error as { status?: unknown; code?: unknown };
    if (
      typeof candidate.status === 'number' &&
      (candidate.status === 429 || candidate.status >= 500)
    ) {
      return true;
    }
    return (
      typeof candidate.code === 'string' &&
      ['ECONNRESET', 'ETIMEDOUT', 'EAI_AGAIN', 'UND_ERR_CONNECT_TIMEOUT'].includes(candidate.code)
    );
  }

  private async sendAgentResponse(conversationId: string, text: string): Promise<void> {
    const route = this.conversationRoutes.get(conversationId);
    if (!route) {
      throw new ChannelDeliveryError(
        'invalid_route',
        `Discord response route is unavailable for conversation ${conversationId}`,
      );
    }
    try {
      await this.send({
        correlationId: randomUUID(),
        route,
        message: {
          id: randomUUID(),
          channelName: this.name,
          channelId: route.responseTarget.channelId,
          senderId: route.logicalAgentId,
          senderKind: 'agent',
          content: text,
          contentKind: 'markdown',
          timestamp: new Date().toISOString(),
          ...(route.responseTarget.threadId ? { threadId: route.responseTarget.threadId } : {}),
          metadata: {},
        },
      });
    } finally {
      this.conversationRoutes.delete(conversationId);
    }
  }

  /** Compatibility wrapper while Socket.IO agent events carry only conversation ID. */
  private async sendToDiscord(conversationId: string, text: string): Promise<void> {
    await this.sendAgentResponse(conversationId, text);
  }

  private rememberConversationRoute(route: ChannelConversationRouteDto): void {
    if (
      !this.conversationRoutes.has(route.conversationId) &&
      this.conversationRoutes.size >= MAX_CONVERSATION_ROUTES
    ) {
      throw new ChannelDeliveryError(
        'delivery_failed',
        'Discord has reached its active response-route limit',
        true,
      );
    }
    this.conversationRoutes.set(route.conversationId, route);
  }

  private isConfiguredRoute(route: ChannelConversationRouteDto): boolean {
    if (
      route.channelName !== this.name ||
      route.conversationId !==
        `${route.logicalAgentId}:${this.name}:${route.responseTarget.channelId}`
    ) {
      return false;
    }
    const bindingMatches = (this.config.interactionBindings ?? []).some(
      (binding): boolean =>
        route.bindingId === `${binding.guildId}:${binding.channelId}:${binding.instanceId}` &&
        route.logicalAgentId === binding.instanceId &&
        route.authorizationChannelId === binding.channelId,
    );
    if (!bindingMatches) return false;
    if (
      route.responseTarget.channelId === route.authorizationChannelId &&
      route.responseTarget.threadId === undefined
    ) {
      return true;
    }
    const observed = this.conversationRoutes.get(route.conversationId);
    return (
      observed?.bindingId === route.bindingId &&
      observed.logicalAgentId === route.logicalAgentId &&
      observed.authorizationChannelId === route.authorizationChannelId &&
      observed.responseTarget.channelId === route.responseTarget.channelId &&
      observed.responseTarget.threadId === route.responseTarget.threadId
    );
  }

  private isMessageAlignedWithRoute(egress: ChannelEgressDto): boolean {
    return (
      egress.message.channelName === this.name &&
      egress.message.channelId === egress.route.responseTarget.channelId &&
      egress.message.threadId === egress.route.responseTarget.threadId
    );
  }

  private chunkText(text: string, maxLength: number): string[] {
    if (text.length <= maxLength) return [text];

    const chunks: string[] = [];
    let remaining = text;
    while (remaining.length > 0) {
      if (remaining.length <= maxLength) {
        chunks.push(remaining);
        break;
      }
      let breakPoint = remaining.lastIndexOf('\n', maxLength);
      if (breakPoint <= 0) breakPoint = maxLength;
      chunks.push(remaining.slice(0, breakPoint));
      remaining = remaining.slice(breakPoint).trimStart();
    }
    return chunks;
  }
}
