/**
 * Cloud Sync Service
 *
 * Singleton service that replaces the WebSocket Relay pairing model with a
 * simpler heartbeat + polling architecture. All devices under the same Cloud
 * account are automatically visible and can exchange messages.
 *
 * Responsibilities:
 * - Heartbeat: periodically uploads device status to Cloud
 * - Device poll: periodically fetches the device list for the account
 * - Message poll: periodically fetches pending messages for this device
 * - Send: posts messages to specific devices via Cloud
 *
 * @see docs/cloud-sync-design.md
 * @module services/cloud/cloud-sync.service
 */
import { EventEmitter } from 'events';
import type { CloudSyncConfig, CloudSyncState, SyncDevice, MessageType } from './cloud-sync.types.js';
/**
 * CloudSyncService singleton.
 *
 * Manages heartbeat, device discovery, and message polling for the
 * Cloud Sync system. Extends EventEmitter to notify consumers of
 * device and message changes.
 *
 * Events:
 * - `devices_updated` — Fired when the device list changes. Payload: SyncDevice[]
 * - `message` — Fired for each incoming message. Payload: IncomingMessage
 * - `device_online` — Fired when a device comes online. Payload: SyncDevice
 * - `device_offline` — Fired when a device goes offline. Payload: SyncDevice
 *
 * @example
 * ```typescript
 * const sync = CloudSyncService.getInstance();
 * sync.start({ cloudUrl, token, deviceId, deviceName });
 * sync.on('message', (msg) => console.log('Received:', msg));
 * sync.on('devices_updated', (devices) => console.log('Devices:', devices));
 * ```
 */
export declare class CloudSyncService extends EventEmitter {
    private static instance;
    private readonly logger;
    /** Current service state */
    private state;
    /** Configuration (set on start) */
    private config;
    /** Cached device list from last poll */
    private devices;
    /** Cached Crewly version */
    private version;
    /** Heartbeat timer handle */
    private heartbeatTimer;
    /** Device poll timer handle */
    private devicePollTimer;
    /** Message poll timer handle (self-rescheduling long-poll loop). */
    private messagePollTimer;
    /** Guards against overlapping self-scheduled message-poll cycles. */
    private messagePollRunning;
    /** Queue re-register timer handle (lets relay evict stale Portal pairs). */
    private registerTimer;
    /** Consecutive heartbeat failure count */
    private heartbeatFailures;
    /** Consecutive device poll failure count */
    private devicePollFailures;
    /** Consecutive message poll failure count */
    private messagePollFailures;
    /** Error recovery timer handle */
    private errorRecoveryTimer;
    /** Consecutive error recovery attempts (reset on success or stop) */
    private errorRecoveryAttempts;
    /** Our assigned queue ID from Cloud relay queue registration (for message polling) */
    private queueId;
    /**
     * Recently processed message IDs to prevent re-delivery when ackMessages fails.
     * Keeps the last 500 IDs; pruned on overflow.
     */
    private processedMessageIds;
    /** Maximum size of the processedMessageIds dedup set */
    private static readonly MAX_DEDUP_IDS;
    private constructor();
    /**
     * Get the singleton instance.
     *
     * @returns CloudSyncService instance
     */
    static getInstance(): CloudSyncService;
    /**
     * Reset the singleton (for testing).
     */
    static resetInstance(): void;
    /**
     * Start the Cloud Sync service.
     *
     * Begins heartbeat, device polling, and message polling timers.
     * Performs an immediate heartbeat and device poll on start.
     *
     * @param config - Cloud connection configuration
     */
    start(config: CloudSyncConfig): void;
    /**
     * Stop the Cloud Sync service.
     *
     * Clears all timers and resets state. Safe to call multiple times.
     */
    stop(): void;
    /**
     * Check if the sync service has been started.
     *
     * @returns True if the service is currently syncing
     */
    isStarted(): boolean;
    /**
     * Get the current service state.
     *
     * @returns Current CloudSyncState
     */
    getState(): CloudSyncState;
    /**
     * Get all cached devices for this Cloud account.
     *
     * @returns Array of SyncDevice objects (may include offline devices)
     */
    getDevices(): SyncDevice[];
    /**
     * Update the authentication token used for Cloud API requests.
     *
     * Called by CloudClientService after a successful token refresh
     * so that heartbeat, device poll, and message poll use the new token.
     *
     * @param newToken - New JWT access token
     */
    updateToken(newToken: string): void;
    /**
     * Get only online devices (excluding this device).
     *
     * @returns Array of online SyncDevice objects
     */
    getOnlineDevices(): SyncDevice[];
    /**
     * Send a message to another device via the Cloud message queue.
     *
     * @param toDeviceId - Target device identifier
     * @param type - Message type
     * @param payload - Message payload (must be JSON-serializable)
     * @throws Error when not started or API call fails
     *
     * @example
     * ```typescript
     * await sync.sendMessage('dev-456', 'command', {
     *   action: 'delegate-task',
     *   content: 'Implement feature X'
     * });
     * ```
     */
    sendMessage(toDeviceId: string, type: MessageType, payload: unknown): Promise<void>;
    /**
     * Send a message directly to a peer queueId, bypassing the device cache.
     *
     * Used by `ChatV2RelayAdapter` for routing `chat_response` back to a
     * Portal browser whose deviceId may not yet have propagated to the
     * 30s-interval device poll. The production relay's `/queue/send`
     * endpoint accepts a raw `peerQueueId`, so when the target identifier
     * IS already a queueId (which Portal's wrapper.fromDeviceName field
     * carries — see `chat-v2.relay-adapter.service.ts:handleRequest`),
     * we can skip the cache lookup entirely and avoid the race.
     *
     * Falls back to `sendMessage(...)` semantics on the wire (same JSON
     * wrapper) so the receiver's CloudSyncService normalizes it identically.
     *
     * @param peerQueueId - The relay queueId to route to (no cache lookup)
     * @param type - MessageType for the wrapper
     * @param payload - JSON-serializable inner data
     * @throws Error when not started or the HTTP send fails
     */
    sendToPeerQueueId(peerQueueId: string, type: MessageType, payload: unknown): Promise<void>;
    /**
     * Register with the Cloud relay message queue.
     *
     * Calls POST /api/v1/relay/queue/register with a deterministic pairing code
     * derived from the JWT user ID. This allows both devices of the same user
     * to auto-discover each other's queueId for message routing.
     *
     * The assigned queueId is used for message polling. The peerQueueId
     * (other device's queue) is used for sending.
     */
    private registerQueue;
    /**
     * Derive a deterministic pairing code from the JWT token's user ID.
     * Both devices of the same user will produce the same code.
     */
    private derivePairingCode;
    /**
     * Get the queue ID assigned by Cloud registration.
     * Returns null if not yet registered.
     */
    getQueueId(): string | null;
    /**
     * Send a heartbeat to the Cloud server with current device state.
     * Called periodically by the heartbeat timer.
     *
     * Posts to the Cloud Relay handshake endpoint (/api/v1/relay/handshake)
     * which registers/updates the device with enriched metadata.
     */
    sendHeartbeat(): Promise<void>;
    /**
     * Poll the Cloud server for the current device list.
     * Updates the cached device list and emits events on changes.
     *
     * The Cloud server returns devices in relay format:
     * `{ sessionId, role, state, pairedWith, registeredAt, lastHeartbeatAt, name }`
     * which we normalize to the SyncDevice shape used by the frontend.
     */
    pollDevices(): Promise<void>;
    /**
     * Poll the Cloud server for pending messages addressed to this device.
     * Emits 'message' event for each received message, then acknowledges.
     *
     * The Cloud Sync message poll endpoint uses `deviceId` query param.
     * Cloud returns messages as: `{ id, from, fromDeviceName, type, payload, encrypted, sentAt }`.
     */
    pollMessages(): Promise<number>;
    /**
     * One iteration of the self-rescheduling message-poll loop. Runs a
     * (possibly long-held) {@link pollMessages}, then schedules the next cycle
     * with a gap chosen from the outcome:
     *
     *  - **error** (`count < 0`) → back off to {@link CLOUD_SYNC_CONSTANTS.MESSAGE_POLL_INTERVAL_MS}.
     *  - **long-poll disabled** → fixed interval (legacy cadence).
     *  - **message received** (`count > 0`) → re-open right away to drain more.
     *  - **held then empty** (elapsed ≥ half the wait) → short gap; this is the
     *    normal continuous long-poll.
     *  - **empty too fast** (elapsed < half the wait) → the relay likely ignores
     *    `wait=`; degrade to the fixed interval so we don't hot-loop against an
     *    un-upgraded relay.
     *
     * Re-entrancy is guarded so a delayed timer can never overlap an in-flight
     * cycle. No-op once stopped.
     */
    private runMessagePollCycle;
    /**
     * Schedule the next message-poll cycle. Stores the (unref'd) timer handle so
     * `stop()` can cancel a pending cycle. No-op once stopped.
     *
     * @param delayMs - Delay before the next {@link runMessagePollCycle}
     */
    private scheduleNextMessagePoll;
    /**
     * Acknowledge processed messages so Cloud can remove them from the queue.
     * Uses `queueId` in body to identify the target message queue (fallback
     * when JWT has no deviceId claim).
     *
     * @param messageIds - Array of message IDs to acknowledge
     */
    private ackMessages;
    /**
     * Check if any failure counter has exceeded the threshold.
     * If so, transition to error state and schedule a recovery attempt.
     */
    private checkErrorThreshold;
    /**
     * Schedule periodic recovery attempts when in error state.
     *
     * Tries a single heartbeat every ERROR_RECOVERY_INTERVAL_MS (60s by default).
     * If the heartbeat succeeds, resets all failure counters and resumes normal
     * polling. After MAX_ERROR_RECOVERY_ATTEMPTS consecutive auth failures (401/403),
     * transitions to 'auth_expired' terminal state and emits 'auth_expired'.
     */
    private scheduleErrorRecovery;
    /**
     * Transition to the terminal auth_expired state.
     * Clears all timers and emits 'auth_expired' for frontend notification.
     */
    private enterAuthExpiredState;
    /** Guard to prevent concurrent token refresh attempts */
    private tokenRefreshInProgress;
    /**
     * Check if an HTTP status code indicates an authentication failure (401/403).
     * If so, attempt to refresh the token via CloudClientService.
     * On success, updates this service's config with the new token.
     *
     * @param status - HTTP status code from a Cloud API response
     * @returns true if the token was refreshed successfully
     */
    private handleAuthError;
    /**
     * Build authorization headers for Cloud API requests.
     *
     * @returns Headers with Bearer token and Content-Type
     */
    private authHeaders;
}
//# sourceMappingURL=cloud-sync.service.d.ts.map