/**
 * WebSocket client for outgoing peer-to-peer connections between orchestrators.
 *
 * Follows PlatformClient patterns closely:
 * - State machine: disconnected -> connecting -> handshaking -> authenticating -> connected
 * - Exponential backoff with jitter for reconnection
 * - Periodic heartbeat (30s default)
 * - Intentional disconnect flag to prevent reconnection
 *
 * Authentication uses ECDH key exchange followed by join token (first connect)
 * or HMAC credential proof (reconnection).
 */
import { type PeerHeartbeat, type PeerToPeerMessage, type JobReroute, type JobProgress, type PeerJobCancel, type PeerLogChunk, type PeerCacheUploadRequest, type PeerCacheUploadResponse, type PeerConfigReload, type PeerConfigReloadResponse, type PeerLeaving, type PeerAgentTokenRevoke, type RaftVoteRequest, type RaftVoteResponse, type RaftAppendEntries } from '@kici-dev/engine';
import type { PeerRegistry } from './peer-registry.js';
type PeerConnectionState = 'disconnected' | 'connecting' | 'handshaking' | 'authenticating' | 'connected';
export interface PeerClientOptions {
    /** WebSocket URL of the remote peer orchestrator. */
    url: string;
    /** Join token for first-time cluster join (optional if credential file exists). */
    joinToken?: string;
    /** Path to the credential file for reconnection. */
    credentialFile: string;
    /** This orchestrator's instance ID. */
    instanceId: string;
    /** Peer registry to update on heartbeats from remote peer. */
    peerRegistry: PeerRegistry;
    /** Callback to get this orchestrator's local agent inventory for heartbeats. */
    getLocalInventory: () => Omit<PeerHeartbeat, 'type'>;
    /** Heartbeat interval in ms. Default: 30000 (30s). */
    heartbeatIntervalMs?: number;
    /** Maximum reconnect delay in ms. Default: 60000 (60s). */
    maxReconnectDelayMs?: number;
    /** Callback when a job reroute request is received from peer. */
    onJobReroute: (msg: JobReroute) => Promise<void>;
    /** Callback when a job progress update is received from peer. */
    onJobProgress: (msg: JobProgress) => void;
    /** This orchestrator's role. Default: 'coordinator'. */
    role?: 'coordinator' | 'worker';
    /** Callback when a job cancel request is received from peer. */
    onJobCancel: (msg: PeerJobCancel) => void;
    /** Callback when a log chunk is received from a peer (coordinator side). */
    onPeerLogChunk?: (chunk: PeerLogChunk) => void;
    /** Callback when a cache upload request is received from a peer (coordinator side). */
    onPeerCacheUploadRequest?: (req: PeerCacheUploadRequest) => Promise<PeerCacheUploadResponse>;
    /** Callback for Raft vote requests. */
    onRaftVoteRequest?: (msg: RaftVoteRequest) => RaftVoteResponse;
    /** Callback for Raft vote responses (forwarded to Raft module). */
    onRaftVoteResponse?: (msg: RaftVoteResponse) => void;
    /** Callback for Raft append entries (leader heartbeat). */
    onRaftAppendEntries?: (msg: RaftAppendEntries) => void;
    /** Callback when a peer.leaving announcement is received. */
    onPeerLeaving?: (msg: PeerLeaving) => void;
    /**
     * Callback when a peer.agent-token.revoke announcement is received.
     * The local handler should call agentRegistry.disconnectByTokenId(tokenId)
     * to close every in-flight WS authenticated by the now-revoked token.
     */
    onAgentTokenRevoke?: (msg: PeerAgentTokenRevoke) => void;
    /**
     * Callback when a config reload request is received from the peer.
     * Should execute a local config reload and return the result fields,
     * which are sent back via peer.config.reload.response.
     *
     * If undefined, incoming reload requests are answered with success=false.
     */
    onPeerConfigReload?: (msg: PeerConfigReload) => Promise<{
        success: boolean;
        version?: number;
        errors?: string[];
        restartRequired?: string[];
        fieldsChanged?: string[];
    }>;
    /**
     * Callback invoked once the remote peer accepts our auth handshake,
     * carrying the remote peer's instanceId. Used by callers that initially
     * register this client in `sub.peerClients` keyed by a placeholder (URL
     * or stale id) to re-key the map by the canonical instanceId so later
     * Platform-mediated discovery dedupes against the same client.
     */
    onAuthenticated?: (targetInstanceId: string) => void;
}
export declare class PeerClient {
    private ws;
    private _state;
    private heartbeatTimer;
    private reconnectTimer;
    private reconnectAttempts;
    private intentionalDisconnect;
    private _targetInstanceId;
    private sessionKey;
    private readonly ackWaiters;
    private readonly cacheWaiters;
    private readonly configReloadWaiters;
    private readonly url;
    private readonly joinToken?;
    private readonly credentialFile;
    private readonly instanceId;
    private readonly role;
    private readonly peerRegistry;
    private readonly getLocalInventory;
    private readonly heartbeatIntervalMs;
    private readonly maxReconnectDelayMs;
    private readonly onJobReroute;
    private readonly onJobProgress;
    private readonly onJobCancel;
    private readonly onPeerLogChunk?;
    private readonly onPeerCacheUploadRequest?;
    private readonly onRaftVoteRequest?;
    private readonly onRaftVoteResponse?;
    private readonly onRaftAppendEntries?;
    private readonly onPeerLeaving?;
    private readonly onAgentTokenRevoke?;
    private readonly onPeerConfigReload?;
    private readonly onAuthenticated?;
    constructor(options: PeerClientOptions);
    /** Current connection state. */
    get state(): PeerConnectionState;
    /** The remote peer's instanceId (set after auth handshake). */
    get targetInstanceId(): string | null;
    /**
     * Initiate connection to the peer orchestrator.
     */
    connect(): void;
    /**
     * Gracefully disconnect. Does not trigger reconnection.
     */
    disconnect(): void;
    /**
     * Send a peer protocol message. Returns false if not connected.
     * Messages are encrypted with the session key.
     */
    send(msg: PeerToPeerMessage): boolean;
    /**
     * Send a message and wait for an ACK response.
     * Used for job.reroute which expects job.reroute.ack.
     *
     * @returns true if accepted, false if rejected or timeout
     */
    sendAndWaitAck(msg: JobReroute, timeoutMs?: number): Promise<boolean>;
    /**
     * Send a log chunk to the coordinator (worker -> coordinator relay).
     * Fire-and-forget: no ACK expected.
     */
    sendLogChunk(chunk: PeerLogChunk): boolean;
    /**
     * Send a peer.config.reload request to the connected peer and wait for the
     * matching peer.config.reload.response.
     *
     * @returns The response, or null if not connected. Resolves with success=false
     *   if the peer doesn't reply within the timeout.
     */
    sendConfigReloadAndWait(msg: PeerConfigReload, timeoutMs?: number): Promise<PeerConfigReloadResponse | null>;
    /**
     * Send a cache upload request to the coordinator and wait for a response
     * with a pre-signed URL.
     *
     * @returns The response with the uploadUrl, or rejects on timeout/disconnect.
     */
    sendCacheUploadRequest(req: PeerCacheUploadRequest, timeoutMs?: number): Promise<PeerCacheUploadResponse>;
    /**
     * Calculate the reconnection delay with exponential backoff and jitter.
     */
    getReconnectDelay(): number;
    private doConnect;
    private handleMessage;
    /**
     * Determine auth method and send encrypted auth request.
     *
     * Credentials are **identity-scoped**, not URL-scoped. A single orchestrator
     * runs N peer-clients (one per remote peer) and they all share the same
     * on-disk credential file. The credential represents "this orchestrator's
     * cluster membership credential", and the server (peer-handler) verifies it
     * by `instanceId` alone — not by the requester URL. So any peer-client
     * connecting to any peer can use the same credential, as long as the
     * `instanceId` on disk matches ours.
     *
     * (4-coordinator mesh) bug history: the previous gate was
     * `cred.coordinatorUrl === this.coordinatorUrl`, which broke the moment a
     * second peer-client on the same orchestrator tried to authenticate to a
     * different peer URL — it would see the first peer-client's credential,
     * reject it (URL mismatch), fall through to the join token, and get
     * permanently rejected because the token had already been consumed by the
     * first peer-client. The Option B fix (commit 79f93da4) keyed the match on
     * `instanceId`; this follow-up (Plan 04 Task 1 cleanup) drops the
     * `coordinatorUrl` field from the credential file schema entirely because
     * it was dead weight once the match switched to identity scope.
     */
    private sendAuthRequest;
    private routeMessage;
    private startHeartbeat;
    private stopHeartbeat;
    private scheduleReconnect;
    private cancelReconnect;
    private clearAckWaiters;
    private clearCacheWaiters;
    private clearConfigReloadWaiters;
}
export {};
//# sourceMappingURL=peer-client.d.ts.map