/**
 * ChatV2Gateway — native WebSocket gateway for the Phase 1 Chat MVP.
 *
 * Listens on `/ws/chat?channelId=X&token=Y&lastSeenSeq=N` and fans out
 * wire-format frames to subscribers per Sam's tech spec §6 / the frontend
 * contract implemented by Max in `packages/chat-ui/src/api/client.ts`.
 *
 * Wire contract this gateway must honor (enforced by co-located tests):
 *  - Client→server: `{type:"ping",ts}` every 25s
 *  - Server→client: `{type:"pong",ts}` echo within 10s
 *  - Server→client: `{type:"message",payload:{channelId,message:MessageDTO}}`
 *  - Server→client: `{type:"presence",payload:{agentSession,status,lastSeenAt?}}`
 *  - Server→client: `{type:"error",code,message}` followed by close() on fatal
 *
 * This module is intentionally thin: it does not persist, does not dispatch
 * to agents, and does not manage presence state. The controller/dispatcher
 * call `broadcast()` after the service layer writes to SQLite, so the
 * gateway's only job is routing frames to the right sockets.
 *
 * Upgrade-handoff pattern copied from `BrowserBridgeService.attach()` —
 * Socket.IO's Engine.IO upgrade handler would otherwise corrupt frames on
 * non-`/socket.io/` paths, so we intercept the `upgrade` event on the
 * HTTP server and handle `/ws/chat` exclusively.
 *
 * @module websocket/chat-v2.gateway
 */
import type { Server as HttpServer } from 'http';
import { WebSocketServer, WebSocket } from 'ws';
import type { ChatV2Service } from '../services/chat-v2/chat-v2.service.js';
import type { ChatAgentPresenceStatus, ChatChannelDTO, ChatMessageDTO } from '../services/chat-v2/types.js';
/** Server→client wire frame. Discriminated by `type`. */
export type ChatWireEvent = {
    type: 'message';
    payload: {
        channelId: string;
        message: ChatMessageDTO;
    };
} | {
    type: 'presence';
    payload: {
        agentSession: string;
        status: ChatAgentPresenceStatus;
        lastSeenAt?: number | null;
    };
} | {
    type: 'pong';
    ts: number;
} | {
    type: 'error';
    code: string;
    message: string;
};
/** Client→server wire frame (Phase 1 only has `ping`). */
export interface ChatClientFrame {
    type: 'ping';
    ts: number;
}
/**
 * Pluggable auth for the WS handshake. Must resolve the `?token=` query
 * parameter into a userId, or throw to reject the connection.
 *
 * In production this verifies the OSS session JWT. In dev (no secret
 * configured) `DevTokenVerifier` allows any token (or none) and returns a
 * static userId — sufficient for localhost tests.
 */
export interface ChatWsTokenVerifier {
    (token: string | null): Promise<{
        userId: string;
    } | null>;
}
/** Constructor options for {@link ChatV2Gateway}. */
export interface ChatV2GatewayOptions {
    service: ChatV2Service;
    verifyToken: ChatWsTokenVerifier;
    /** Mount path on the HTTP server. Defaults to `/ws/chat`. */
    path?: string;
    /** Optional WebSocketServer override for tests. */
    webSocketServerImpl?: typeof WebSocketServer;
    /** Optional clock override (tests). Defaults to `Date.now`. */
    now?: () => number;
}
/** Default WS mount path — matches the frontend `?${qs}` URL in client.ts. */
export declare const CHAT_V2_WS_PATH = "/ws/chat";
/**
 * Always-accept token verifier used when `CREWLY_JWT_SECRET` is not
 * configured. Returns the SAME `userId` that `requireAuth`'s dev
 * fallback uses (`dev-user-001`) so that channels created via the
 * REST API are owned by the same principal the WS client will verify
 * as. Not for production — in production a JWT-verifying `verifyToken`
 * function is passed to `ChatV2Gateway` instead.
 */
export declare const devAnonymousTokenVerifier: ChatWsTokenVerifier;
/**
 * A WebSocket gateway that fans chat events out to subscribers.
 *
 * Lifecycle:
 *  1. `attach(httpServer)` — hooks the upgrade interceptor
 *  2. `broadcast(channelId, event)` — called by controller after persist
 *  3. `close()` — shutdown hook (graceful close all sockets)
 *
 * The gateway owns the WebSocketServer and the subscribers map. It never
 * reaches into the service layer on its own — all domain data arrives via
 * `broadcast()` from callers who have already persisted.
 */
export declare class ChatV2Gateway {
    private readonly service;
    private readonly verifyToken;
    private readonly path;
    private readonly webSocketServerImpl;
    private readonly now;
    private readonly logger;
    private wss;
    private attached;
    /** channelId → set of subscribers listening on that channel. */
    private readonly subscribers;
    /**
     * External broadcast listeners registered via {@link onBroadcast}.
     * Independent from the WebSocket subscriber map so a listener still
     * fires when there are no local WS clients (e.g. when only the Cloud
     * Portal is observing the channel via the relay adapter).
     */
    private readonly externalListeners;
    constructor(options: ChatV2GatewayOptions);
    /**
     * Mount the gateway on an HTTP server.
     *
     * Uses `noServer:true` + an `httpServer.emit` override identical to
     * `BrowserBridgeService.attach()` — the override exclusively handles
     * upgrades whose pathname matches `this.path`, so Engine.IO never sees
     * them and cannot corrupt the frames.
     *
     * @param httpServer - Shared Node HTTP server instance
     */
    attach(httpServer: HttpServer): void;
    /**
     * Broadcast a wire event to every subscriber on `channelId`.
     *
     * Safe to call synchronously from inside a DB transaction — sends are
     * queued on the underlying socket and do not block.
     *
     * @param channelId - Target channel
     * @param event - Wire frame to push
     */
    broadcast(channelId: string, event: ChatWireEvent): void;
    /**
     * Register an external listener that fires on every {@link broadcast} call,
     * even when there are no local WebSocket subscribers.
     *
     * The primary consumer is `ChatV2RelayAdapter`, which forwards events
     * over the Cloud relay so the Crewly Portal (running on a different
     * device) can render the same realtime updates that the local OSS UI
     * receives via WebSocket.
     *
     * Listener errors are caught and logged — a thrown listener never
     * interrupts the broadcast loop or affects other listeners / WS sends.
     *
     * @param listener - Called with `(channelId, event)` per broadcast
     * @returns Unsubscribe function; call once during shutdown
     */
    onBroadcast(listener: (channelId: string, event: ChatWireEvent) => void): () => void;
    /**
     * Count of currently-open subscribers for a channel. Exposed for tests
     * and future observability dashboards.
     */
    subscriberCount(channelId: string): number;
    /** Total open sockets across all channels. Observability helper. */
    totalSubscribers(): number;
    /**
     * Gracefully close all sockets. Called during shutdown.
     */
    close(): Promise<void>;
    /**
     * Handle a freshly-upgraded socket: parse query string, verify token,
     * authorize against the channel, and register the subscriber.
     *
     * On any failure we send a single `{type:"error",...}` frame and close
     * with a 1008 (policy violation) code — matching the frontend's
     * "fatal, stop reconnecting" contract.
     */
    handleConnection(ws: WebSocket, req: {
        url?: string;
        headers: Record<string, string | undefined>;
    }): Promise<void>;
    /**
     * Handle a frame from the client.
     *
     * Phase 1 only understands `{type:"ping",ts}`; any other shape is
     * silently dropped (forward-compat with future client-origin frames).
     */
    private handleMessage;
    private registerSubscriber;
    private unregisterSubscriber;
    /**
     * Send an error frame then close with 1008. Used for all pre-subscribe
     * rejections (auth, malformed URL, missing channelId, not-found).
     */
    private sendErrorAndClose;
}
/**
 * Build a presence wire frame. Placed here so both the gateway's tests and
 * future presence emitters use the same shape.
 *
 * @param agentSession - The agent whose presence changed
 * @param status - New status
 * @param lastSeenAt - Optional last-seen timestamp (ms)
 */
export declare function buildPresenceEvent(agentSession: string, status: ChatAgentPresenceStatus, lastSeenAt?: number | null): Extract<ChatWireEvent, {
    type: 'presence';
}>;
/**
 * Build a message wire frame from a DTO. Used by both the controller (on
 * HTTP persist) and the dispatcher (on agent reply).
 */
export declare function buildMessageEvent(channelId: string, message: ChatMessageDTO): Extract<ChatWireEvent, {
    type: 'message';
}>;
export type { ChatChannelDTO };
//# sourceMappingURL=chat-v2.gateway.d.ts.map