/**
 * Cloud Client Service
 *
 * Singleton service responsible for all interactions with CrewlyAI Cloud.
 * Handles authentication, premium template fetching, and subscription
 * status synchronization.
 *
 * Premium templates are loaded into memory only — never written to disk
 * to prevent IP leakage.
 *
 * @module services/cloud/cloud-client.service
 */
import * as os from 'os';
import * as path from 'path';
import { readFile, writeFile, mkdir, unlink } from 'fs/promises';
import { LoggerService } from '../core/logger.service.js';
import { CLOUD_CONSTANTS, AUTH_CONSTANTS, } from '../../constants.js';
/**
 * Decode the `exp` (expiry, seconds since epoch) claim from a JWT without
 * verifying its signature. Returns null when the token is malformed or has
 * no `exp`. Used to schedule proactive relay-token refresh before expiry so
 * a continuously-open relay socket never carries an expired token.
 *
 * @param token - JWT string (may be null)
 * @returns The `exp` claim in seconds, or null when unavailable
 */
export function decodeJwtExp(token) {
    if (!token)
        return null;
    try {
        const parts = token.split('.');
        if (parts.length !== 3)
            return null;
        const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
        return typeof payload.exp === 'number' ? payload.exp : null;
    }
    catch {
        return null;
    }
}
// ---------------------------------------------------------------------------
// Service
// ---------------------------------------------------------------------------
/**
 * CloudClientService singleton.
 *
 * Manages the lifecycle of the connection between a local Crewly instance
 * and CrewlyAI Cloud. All cloud API calls are made via native fetch with
 * bearer-token authentication.
 */
export class CloudClientService {
    static instance = null;
    logger;
    /** Cloud API base URL (e.g. "https://api.crewlyai.com") */
    cloudUrl = null;
    /** Bearer token obtained during connect() */
    token = null;
    /** Refresh token for auto-renewing expired access tokens */
    refreshToken = null;
    /** Current connection status */
    connectionStatus = CLOUD_CONSTANTS.CONNECTION_STATUS.DISCONNECTED;
    /** Subscription tier reported by cloud */
    tier = CLOUD_CONSTANTS.TIERS.FREE;
    /** Timestamp of the most recent successful cloud API call */
    lastSyncAt = null;
    /** Timer for proactive token refresh (fires 5 min before expiry) */
    refreshTimer = null;
    /** Guard to prevent concurrent refresh attempts */
    refreshInProgress = false;
    /** Callbacks invoked when the access token is refreshed */
    tokenRefreshCallbacks = [];
    /**
     * Relay-signed access JWT used by BrowserProxyService to register with the
     * Cloud Relay as role 'agent'. First-class, stored credential — distinct
     * from the access `token` and decoupled from socket lifetime. NEVER the
     * Google/Supabase access token (RELAY-TOKEN-TYPE invariant).
     */
    relayToken = null;
    /** Parsed `exp` (seconds) of {@link relayToken}, for proactive refresh. */
    relayTokenExp = null;
    /** Timer for proactive relay-token refresh (fires SKEW before exp). */
    relayRefreshTimer = null;
    /** Callbacks invoked when the relay token is refreshed (distinct channel). */
    relayTokenRefreshCallbacks = [];
    constructor() {
        this.logger = LoggerService.getInstance().createComponentLogger('CloudClientService');
    }
    /**
     * Get the singleton instance.
     *
     * @returns CloudClientService instance
     */
    static getInstance() {
        if (!CloudClientService.instance) {
            CloudClientService.instance = new CloudClientService();
        }
        return CloudClientService.instance;
    }
    /**
     * Reset the singleton (for testing).
     */
    static resetInstance() {
        CloudClientService.instance = null;
    }
    // -------------------------------------------------------------------------
    // Public API
    // -------------------------------------------------------------------------
    /**
     * Connect to CrewlyAI Cloud by verifying the provided token.
     *
     * Calls the cloud auth endpoint to validate credentials and retrieve
     * the subscription tier. On success the service transitions to
     * "connected" status.
     *
     * @param cloudUrl - Base URL of the CrewlyAI Cloud API
     * @param token - Authentication token (API key or JWT)
     * @returns Object with connection result
     * @throws Error when the auth request fails or token is invalid
     *
     * @example
     * ```ts
     * const client = CloudClientService.getInstance();
     * await client.connect('https://api.crewlyai.com', 'sk-abc123');
     * ```
     */
    async connect(cloudUrl, token, refreshToken) {
        this.logger.info('Connecting to CrewlyAI Cloud', { cloudUrl });
        const url = `${cloudUrl}${CLOUD_CONSTANTS.ENDPOINTS.AUTH_TOKEN}`;
        const response = await fetch(url, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                Authorization: `Bearer ${token}`,
            },
            signal: AbortSignal.timeout(CLOUD_CONSTANTS.TIMEOUTS.CONNECT),
        });
        if (!response.ok) {
            this.connectionStatus = CLOUD_CONSTANTS.CONNECTION_STATUS.ERROR;
            const errorText = await response.text().catch(() => 'Unknown error');
            this.logger.error('Cloud connection failed', { status: response.status, errorText });
            throw new Error(`Cloud authentication failed: ${response.status} ${errorText}`);
        }
        // crewly-auth /api/cloud/validate returns { success, data: { plan, relayToken, ... } }
        // Also accept legacy { tier } format for forward compatibility
        const data = (await response.json());
        this.logger.debug('Cloud validate response', { hasData: !!data.data, hasPlan: !!data.data?.plan, hasRelayToken: !!data.data?.relayToken, dataKeys: data.data ? Object.keys(data.data) : [] });
        const resolvedTier = data.data?.plan || data.tier;
        this.cloudUrl = cloudUrl;
        this.token = token;
        this.refreshToken = refreshToken || this.refreshToken;
        this.tier = resolvedTier || CLOUD_CONSTANTS.TIERS.FREE;
        this.connectionStatus = CLOUD_CONSTANTS.CONNECTION_STATUS.CONNECTED;
        this.lastSyncAt = new Date().toISOString();
        // Use relay token from Cloud validate response for BrowserProxyService.
        // Store it as a first-class credential (not a transient callback arg) so
        // it can be persisted, refreshed proactively, and re-fed on restart.
        if (data.data?.relayToken) {
            this.logger.info('Relay token received from Cloud');
            this.storeRelayToken(data.data.relayToken);
        }
        this.logger.info('Connected to CrewlyAI Cloud', { tier: this.tier, hasRelayToken: !!data.data?.relayToken });
        // Schedule proactive token refresh
        this.scheduleTokenRefresh(token);
        // Persist credentials for auto-reconnect on restart
        this.persistConfig().catch((err) => {
            this.logger.warn('Failed to persist cloud config (non-fatal)', {
                error: err instanceof Error ? err.message : String(err),
            });
        });
        return { success: true, tier: this.tier };
    }
    /**
     * Connect using a locally verified JWT (no remote API call needed).
     *
     * Used when the JWT was issued by this same instance or the JWT secret
     * is shared between OSS and Cloud. Avoids the round-trip to the cloud
     * auth endpoint.
     *
     * @param cloudUrl - Base URL of the CrewlyAI Cloud API
     * @param token - JWT access token (already verified locally)
     * @param tier - Subscription tier extracted from the JWT payload
     * @param refreshToken - Optional refresh token for auto-renewal
     */
    connectLocal(cloudUrl, token, tier, refreshToken) {
        this.cloudUrl = cloudUrl;
        this.token = token;
        this.refreshToken = refreshToken || this.refreshToken;
        this.tier = tier || CLOUD_CONSTANTS.TIERS.FREE;
        this.connectionStatus = CLOUD_CONSTANTS.CONNECTION_STATUS.CONNECTED;
        this.lastSyncAt = new Date().toISOString();
        this.logger.info('Connected to CrewlyAI Cloud (local verification)', { tier: this.tier });
        // Schedule proactive token refresh
        this.scheduleTokenRefresh(token);
        // Persist credentials for auto-reconnect on restart
        this.persistConfig().catch((err) => {
            this.logger.warn('Failed to persist cloud config (non-fatal)', {
                error: err instanceof Error ? err.message : String(err),
            });
        });
        // Fetch relay token asynchronously (non-blocking)
        // connectLocal skips Cloud API, but relay token requires Cloud-signed JWT
        this.fetchRelayTokenFromValidate().catch((err) => {
            this.logger.debug('Relay token fetch after connectLocal failed (non-fatal)', {
                error: err instanceof Error ? err.message : String(err),
            });
        });
    }
    /**
     * Disconnect from CrewlyAI Cloud.
     *
     * Clears stored credentials and resets the connection state.
     */
    disconnect() {
        this.logger.info('Disconnecting from CrewlyAI Cloud');
        this.cloudUrl = null;
        this.token = null;
        this.refreshToken = null;
        this.connectionStatus = CLOUD_CONSTANTS.CONNECTION_STATUS.DISCONNECTED;
        this.tier = CLOUD_CONSTANTS.TIERS.FREE;
        this.lastSyncAt = null;
        this.relayToken = null;
        this.relayTokenExp = null;
        if (this.refreshTimer) {
            clearTimeout(this.refreshTimer);
            this.refreshTimer = null;
        }
        if (this.relayRefreshTimer) {
            clearTimeout(this.relayRefreshTimer);
            this.relayRefreshTimer = null;
        }
        // Remove persisted config
        this.removePersistedConfig().catch((err) => {
            this.logger.warn('Failed to remove persisted cloud config (non-fatal)', {
                error: err instanceof Error ? err.message : String(err),
            });
        });
    }
    // -------------------------------------------------------------------------
    // Config Persistence
    // -------------------------------------------------------------------------
    /**
     * Path to the persisted cloud config file.
     */
    static getConfigPath() {
        return path.join(os.homedir(), '.crewly', 'cloud', 'config.json');
    }
    /**
     * Load persisted cloud config from disk.
     * Returns null if file doesn't exist or is invalid.
     */
    async loadPersistedConfig() {
        try {
            const data = await readFile(CloudClientService.getConfigPath(), 'utf-8');
            const config = JSON.parse(data);
            if (config.cloudUrl && config.token && config.tier) {
                // Restore the persisted relay token immediately so BrowserProxy can
                // re-register on restart without waiting for a fresh validate
                // round-trip. fetchRelayTokenFromValidate (kicked off by connectLocal)
                // will refresh it shortly after. Do not broadcast here — connect/
                // connectLocal own the wiring of subscribers; this just primes state.
                if (config.relayToken) {
                    this.relayToken = config.relayToken;
                    this.relayTokenExp = decodeJwtExp(config.relayToken);
                    this.scheduleRelayTokenRefresh();
                }
                return config;
            }
            return null;
        }
        catch {
            return null;
        }
    }
    /**
     * Persist current cloud connection config to disk for auto-reconnect.
     */
    async persistConfig() {
        if (!this.cloudUrl || !this.token)
            return;
        const config = {
            cloudUrl: this.cloudUrl,
            token: this.token,
            tier: this.tier,
            connectedAt: new Date().toISOString(),
            ...(this.refreshToken && { refreshToken: this.refreshToken }),
            ...(this.relayToken && { relayToken: this.relayToken }),
        };
        const configPath = CloudClientService.getConfigPath();
        await mkdir(path.dirname(configPath), { recursive: true });
        await writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
        this.logger.debug('Persisted cloud config to disk');
    }
    /**
     * Remove persisted cloud config from disk (on disconnect).
     */
    async removePersistedConfig() {
        try {
            await unlink(CloudClientService.getConfigPath());
            this.logger.debug('Removed persisted cloud config');
        }
        catch {
            // File may not exist — not an error
        }
    }
    // -------------------------------------------------------------------------
    // Template Fetching
    // -------------------------------------------------------------------------
    /**
     * Fetch the list of premium templates available on CrewlyAI Cloud.
     *
     * Requires an active cloud connection.
     *
     * @returns Array of template summaries
     * @throws Error when not connected or fetch fails
     *
     * @example
     * ```ts
     * const templates = await client.getTemplates();
     * console.log(templates.map(t => t.name));
     * ```
     */
    async getTemplates() {
        // If token is already known to be expired, return empty without hitting the API
        if (this.isTokenExpired())
            return [];
        this.ensureConnected();
        const url = `${this.cloudUrl}${CLOUD_CONSTANTS.ENDPOINTS.TEMPLATES}`;
        const response = await fetch(url, {
            method: 'GET',
            headers: this.authHeaders(),
            signal: AbortSignal.timeout(CLOUD_CONSTANTS.TIMEOUTS.FETCH_TEMPLATES),
        });
        if (!response.ok) {
            // 401/403 means token expired or revoked — return empty list + update status
            if (this.isAuthError(response.status)) {
                this.handleAuthFailure('getTemplates', response.status);
                return [];
            }
            this.logger.error('Failed to fetch templates', { status: response.status });
            throw new Error(`Failed to fetch templates: ${response.status}`);
        }
        const data = (await response.json());
        this.lastSyncAt = new Date().toISOString();
        return data.templates || [];
    }
    /**
     * Fetch full detail for a single premium template.
     *
     * The returned data is held in memory only and must never be
     * persisted to disk.
     *
     * @param id - Template identifier
     * @returns Template detail object
     * @throws Error when not connected, template not found, or fetch fails
     *
     * @example
     * ```ts
     * const detail = await client.getTemplateDetail('tpl-tiktok-ops');
     * ```
     */
    async getTemplateDetail(id) {
        // If token is already known to be expired, throw user-friendly error
        if (this.isTokenExpired()) {
            throw new Error('Cloud token expired. Please reconnect to CrewlyAI Cloud.');
        }
        this.ensureConnected();
        const endpoint = CLOUD_CONSTANTS.ENDPOINTS.TEMPLATE_DETAIL.replace(':id', id);
        const url = `${this.cloudUrl}${endpoint}`;
        const response = await fetch(url, {
            method: 'GET',
            headers: this.authHeaders(),
            signal: AbortSignal.timeout(CLOUD_CONSTANTS.TIMEOUTS.FETCH_TEMPLATE_DETAIL),
        });
        if (!response.ok) {
            // 401/403 means token expired or revoked
            if (this.isAuthError(response.status)) {
                this.handleAuthFailure('getTemplateDetail', response.status);
                throw new Error('Cloud token expired. Please reconnect to CrewlyAI Cloud.');
            }
            if (response.status === 404) {
                throw new Error(`Template not found: ${id}`);
            }
            this.logger.error('Failed to fetch template detail', { id, status: response.status });
            throw new Error(`Failed to fetch template detail: ${response.status}`);
        }
        const data = (await response.json());
        this.lastSyncAt = new Date().toISOString();
        return data;
    }
    /**
     * Get the stored cloud API base URL (set during connect).
     *
     * @returns Cloud URL or null if not connected
     */
    getCloudUrl() {
        return this.cloudUrl;
    }
    /**
     * Get the current cloud connection status and subscription tier.
     *
     * @returns Current status snapshot
     */
    getStatus() {
        return {
            connectionStatus: this.connectionStatus,
            cloudUrl: this.cloudUrl,
            tier: this.tier,
            lastSyncAt: this.lastSyncAt,
        };
    }
    /**
     * Check whether the client is currently connected to cloud.
     *
     * @returns true if connected
     */
    isConnected() {
        return this.connectionStatus === CLOUD_CONSTANTS.CONNECTION_STATUS.CONNECTED;
    }
    /**
     * Check whether the cloud token has expired (401/403 from cloud API).
     *
     * @returns true if the token has expired
     */
    isTokenExpired() {
        return this.connectionStatus === CLOUD_CONSTANTS.CONNECTION_STATUS.TOKEN_EXPIRED;
    }
    /**
     * Get the current subscription tier.
     *
     * @returns Current tier value
     */
    getTier() {
        return this.tier;
    }
    /**
     * Fetch the list of devices registered to this user from Cloud.
     *
     * Proxies GET /api/v1/relay/devices on crewlyai.com and returns
     * the device list for the authenticated user.
     *
     * @returns Array of cloud relay devices
     * @throws Error when not connected or fetch fails
     */
    async fetchCloudDevices() {
        // If token is already known to be expired, return empty without hitting the API
        if (this.isTokenExpired())
            return [];
        this.ensureConnected();
        const url = `${this.cloudUrl}/api/v1/relay/devices`;
        const response = await fetch(url, {
            method: 'GET',
            headers: this.authHeaders(),
            signal: AbortSignal.timeout(CLOUD_CONSTANTS.TIMEOUTS.FETCH_TEMPLATES),
        });
        if (!response.ok) {
            // 401/403 means token expired or revoked — return empty list + update status
            if (this.isAuthError(response.status)) {
                this.handleAuthFailure('fetchCloudDevices', response.status);
                return [];
            }
            // 404 means the cloud devices endpoint is not yet available — return empty list gracefully
            if (response.status === 404) {
                this.logger.warn('Cloud devices endpoint not available (404), returning empty list');
                return [];
            }
            this.logger.error('Failed to fetch cloud devices', { status: response.status });
            throw new Error(`Failed to fetch cloud devices: ${response.status}`);
        }
        const data = (await response.json());
        if (!data.success) {
            throw new Error('Cloud devices API returned unsuccessful response');
        }
        this.lastSyncAt = new Date().toISOString();
        return data.devices ?? [];
    }
    // -------------------------------------------------------------------------
    // Token Auto-Refresh
    // -------------------------------------------------------------------------
    /**
     * Set the refresh token for auto-renewal.
     *
     * Called by the connect controller after OAuth login provides a refresh token.
     *
     * @param refreshToken - Refresh token string
     */
    setRefreshToken(refreshToken) {
        this.refreshToken = refreshToken;
        // Re-persist config with the new refresh token
        this.persistConfig().catch(() => { });
        this.logger.debug('Refresh token stored');
    }
    /**
     * Get the current access token (for CloudSyncService token updates).
     *
     * @returns Current access token or null
     */
    getToken() {
        return this.token;
    }
    /**
     * Register a callback to be invoked whenever the access token is refreshed.
     * Used by BrowserProxyService to update its relay auth token in real-time.
     *
     * @param callback - Function receiving the new access token string
     */
    onTokenRefresh(callback) {
        this.tokenRefreshCallbacks.push(callback);
    }
    /**
     * Get the current relay-signed token (for BrowserProxyService registration).
     *
     * This is DISTINCT from {@link getToken} (the access/HTTP token). The
     * BrowserProxy must register with the relay token, never the access token —
     * see the RELAY-TOKEN-TYPE invariant. Returns null until a relay token has
     * been minted by the Cloud validate/refresh endpoint.
     *
     * @returns Current relay token or null
     */
    getRelayToken() {
        return this.relayToken;
    }
    /**
     * Register a callback invoked whenever the relay token is refreshed.
     *
     * Distinct subscription channel from {@link onTokenRefresh}: subscribers
     * here (BrowserProxyService) receive the relay-signed token, never the
     * access token. Decoupling the channels prevents the proxy from ever being
     * fed the wrong token type on refresh.
     *
     * @param callback - Function receiving the new relay token string
     */
    onRelayTokenRefresh(callback) {
        this.relayTokenRefreshCallbacks.push(callback);
    }
    /**
     * Store a freshly-minted relay token as a first-class credential, parse its
     * expiry, schedule proactive refresh, persist it, and notify subscribers.
     *
     * Centralizes relay-token handling so every code path that obtains one
     * (connect, refresh, validate) treats it identically. Persistence is
     * fire-and-forget; a persist failure must not break the refresh chain.
     *
     * @param relayToken - The relay-signed access JWT from Cloud
     */
    storeRelayToken(relayToken) {
        this.relayToken = relayToken;
        this.relayTokenExp = decodeJwtExp(relayToken);
        // Schedule proactive refresh before this relay token's real exp so a
        // continuously-open relay socket never carries an expired token.
        this.scheduleRelayTokenRefresh();
        // Persist alongside the access token for restart resilience.
        this.persistConfig().catch(() => { });
        // Notify subscribers (BrowserProxyService) on the relay-token channel.
        for (const callback of this.relayTokenRefreshCallbacks) {
            try {
                callback(relayToken);
            }
            catch {
                // Non-fatal — one bad callback must not break the refresh chain.
            }
        }
    }
    /**
     * Attempt to refresh the access token using the stored refresh token.
     *
     * Issues a new access token locally (since both access and refresh tokens
     * are signed with the same HMAC secret on this OSS instance).
     *
     * @returns true if refresh succeeded, false if no refresh token or refresh failed
     */
    async tryRefreshToken() {
        if (this.refreshInProgress) {
            this.logger.debug('Token refresh already in progress, skipping');
            return false;
        }
        if (!this.refreshToken) {
            this.logger.debug('No refresh token available for auto-refresh');
            return false;
        }
        this.refreshInProgress = true;
        try {
            // Try Cloud-side refresh first (gets a token signed with the Cloud's secret,
            // which is accepted by all Cloud services without needing CREWLY_JWT_SECRET locally).
            const cloudUrl = this.cloudUrl || CLOUD_CONSTANTS.DEFAULT_CLOUD_URL;
            const cloudRefreshUrl = `${cloudUrl}/api/cloud/refresh`;
            let newAccessToken = null;
            let relayToken = null;
            try {
                const response = await fetch(cloudRefreshUrl, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ refreshToken: this.refreshToken }),
                    signal: AbortSignal.timeout(10_000),
                });
                if (response.ok) {
                    const data = await response.json();
                    if (data.success && data.accessToken) {
                        newAccessToken = data.accessToken;
                        relayToken = data.relayToken ?? null;
                        this.logger.info('Token refreshed via Cloud auth service');
                    }
                }
            }
            catch {
                this.logger.debug('Cloud refresh unavailable, falling back to local refresh');
            }
            // Fallback: local refresh (requires CREWLY_JWT_SECRET to match Cloud)
            // NOTE: locally-signed tokens may not be accepted by Cloud relay if the
            // JWT secret doesn't match. If Cloud refresh failed, warn the user.
            if (!newAccessToken) {
                this.logger.warn('Cloud token refresh failed — locally-signed token may not work with Cloud relay. Re-login with `crewly cloud login` to get a Cloud-signed refresh token.');
                const { verifyJwt, signJwt } = await import('../../controllers/cloud/cloud-google-auth.controller.js');
                const payload = verifyJwt(this.refreshToken);
                if (!payload || payload.type !== 'refresh') {
                    this.logger.warn('Refresh token verification failed — cannot auto-refresh. Please re-login with `crewly cloud login`.');
                    return false;
                }
                let deviceId;
                try {
                    const { DeviceIdentityService } = await import('./device-identity.service.js');
                    const identity = await DeviceIdentityService.getInstance().getOrCreateIdentity();
                    deviceId = identity.deviceId;
                }
                catch {
                    deviceId = payload.deviceId;
                }
                const now = Math.floor(Date.now() / 1000);
                newAccessToken = signJwt({
                    sub: payload.sub,
                    email: payload.email || '',
                    name: payload.name || '',
                    plan: payload.plan || 'free',
                    ...(deviceId && { deviceId }),
                    iat: now,
                    exp: now + AUTH_CONSTANTS.JWT.ACCESS_TOKEN_EXPIRY_S,
                    iss: AUTH_CONSTANTS.JWT.ISSUER,
                    type: 'access',
                });
                this.logger.info('Token refreshed locally (fallback)');
            }
            // Update service state
            this.token = newAccessToken;
            this.connectionStatus = CLOUD_CONSTANTS.CONNECTION_STATUS.CONNECTED;
            this.lastSyncAt = new Date().toISOString();
            // Schedule next refresh
            this.scheduleTokenRefresh(newAccessToken);
            // Persist new token
            await this.persistConfig();
            // Update CloudSyncService with the new token (if running)
            try {
                const { CloudSyncService } = await import('./cloud-sync.service.js');
                const syncService = CloudSyncService.getInstance();
                if (syncService.isStarted()) {
                    syncService.updateToken(newAccessToken);
                }
            }
            catch {
                // CloudSyncService may not be available — non-fatal
            }
            // Notify access-token refresh subscribers with the ACCESS token only.
            // The relay token has its own channel (storeRelayToken below) — never
            // substitute the access token for the relay token (RELAY-TOKEN-TYPE
            // invariant). Subscribers on this channel that need the relay token
            // should subscribe via onRelayTokenRefresh instead.
            for (const callback of this.tokenRefreshCallbacks) {
                try {
                    callback(newAccessToken);
                }
                catch {
                    // Non-fatal — don't let one bad callback break the refresh chain
                }
            }
            // Store the relay token as a first-class credential when the refresh
            // returned one. If none was returned, keep the existing stored relay
            // token rather than clobbering it with the access token — a continuing
            // socket keeps working on its still-valid relay token until its own
            // proactive refresh fires.
            if (relayToken) {
                this.storeRelayToken(relayToken);
            }
            this.logger.info('Access token auto-refreshed successfully', { hasRelayToken: !!relayToken });
            return true;
        }
        catch (error) {
            this.logger.warn('Token auto-refresh failed', {
                error: error instanceof Error ? error.message : String(error),
            });
            return false;
        }
        finally {
            this.refreshInProgress = false;
        }
    }
    /**
     * Schedule a proactive token refresh 5 minutes before expiry.
     *
     * Parses the JWT `exp` claim and sets a timer. If the token has
     * less than 5 minutes remaining, refreshes immediately.
     *
     * @param token - JWT access token to extract expiry from
     */
    scheduleTokenRefresh(token) {
        if (this.refreshTimer) {
            clearTimeout(this.refreshTimer);
            this.refreshTimer = null;
        }
        if (!this.refreshToken)
            return;
        try {
            // Decode JWT payload to get exp (without verifying — just need the timestamp)
            const parts = token.split('.');
            if (parts.length !== 3)
                return;
            const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
            const exp = payload.exp;
            if (!exp)
                return;
            const now = Math.floor(Date.now() / 1000);
            // Refresh 5 minutes (300s) before expiry
            const refreshAt = exp - 300;
            const delayMs = Math.max(0, (refreshAt - now) * 1000);
            if (delayMs <= 0) {
                // Token already near expiry — refresh immediately
                this.logger.info('Access token near expiry, refreshing immediately');
                this.tryRefreshToken().catch(() => { });
                return;
            }
            this.refreshTimer = setTimeout(() => {
                this.logger.info('Proactive token refresh triggered');
                this.tryRefreshToken().catch(() => { });
            }, delayMs);
            // Unref so the timer doesn't keep the process alive
            if (this.refreshTimer.unref)
                this.refreshTimer.unref();
            const minutesUntilRefresh = Math.round(delayMs / 60_000);
            this.logger.debug('Token refresh scheduled', { minutesUntilRefresh });
        }
        catch {
            this.logger.debug('Could not schedule token refresh (non-fatal)');
        }
    }
    /**
     * Schedule a proactive relay-token refresh `RELAY_REFRESH_SKEW_S` seconds
     * before the relay token's real `exp`.
     *
     * The relay token (~60min TTL) is decoupled from the access token, so it
     * needs its own refresh schedule. When the timer fires it calls
     * {@link fetchRelayTokenFromValidate}, which re-mints a relay token and
     * stores it (re-arming this timer). This keeps a continuously-open relay
     * socket from ever carrying an expired token — the DURABLE-CREDENTIAL
     * invariant. Driven by the JWT `exp` claim, not a wall-clock heuristic.
     */
    scheduleRelayTokenRefresh() {
        if (this.relayRefreshTimer) {
            clearTimeout(this.relayRefreshTimer);
            this.relayRefreshTimer = null;
        }
        if (!this.relayTokenExp)
            return;
        const now = Math.floor(Date.now() / 1000);
        // Refresh 5 minutes (300s) before expiry — same skew as the access token.
        const refreshAt = this.relayTokenExp - 300;
        const delayMs = Math.max(0, (refreshAt - now) * 1000);
        if (delayMs <= 0) {
            // Already near expiry — refresh immediately.
            this.logger.info('Relay token near expiry, refreshing immediately');
            this.fetchRelayTokenFromValidate().catch(() => { });
            return;
        }
        this.relayRefreshTimer = setTimeout(() => {
            this.logger.info('Proactive relay-token refresh triggered');
            this.fetchRelayTokenFromValidate().catch(() => { });
        }, delayMs);
        // Unref so the timer doesn't keep the process alive.
        if (this.relayRefreshTimer.unref)
            this.relayRefreshTimer.unref();
        const minutesUntilRefresh = Math.round(delayMs / 60_000);
        this.logger.debug('Relay token refresh scheduled', { minutesUntilRefresh });
    }
    // -------------------------------------------------------------------------
    // Private helpers
    // -------------------------------------------------------------------------
    /**
     * Fetch relay token by calling Cloud validate endpoint.
     * Used after connectLocal() to get a Cloud-signed relay JWT
     * without blocking the connection flow.
     *
     * @returns Promise that resolves when relay token is fetched (or skipped)
     */
    async fetchRelayTokenFromValidate() {
        if (!this.token || !this.cloudUrl)
            return;
        const url = `${this.cloudUrl}${CLOUD_CONSTANTS.ENDPOINTS.AUTH_TOKEN}`;
        const response = await fetch(url, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                Authorization: `Bearer ${this.token}`,
            },
            signal: AbortSignal.timeout(10_000),
        });
        if (!response.ok) {
            this.logger.debug('Relay token validate call failed', { status: response.status });
            return;
        }
        const data = await response.json();
        if (data.data?.relayToken) {
            this.logger.info('Relay token received from Cloud validate (after connectLocal)');
            this.storeRelayToken(data.data.relayToken);
        }
    }
    /**
     * Throw if the client is not in a connected state.
     *
     * @throws Error when not connected
     */
    ensureConnected() {
        if (!this.isConnected() || !this.cloudUrl || !this.token) {
            throw new Error('Not connected to CrewlyAI Cloud. Call connect() first.');
        }
    }
    /**
     * Build the standard authorization headers for cloud API requests.
     *
     * @returns Headers object with Authorization and Content-Type
     */
    authHeaders() {
        return {
            Authorization: `Bearer ${this.token}`,
            'Content-Type': 'application/json',
        };
    }
    /**
     * Check if an HTTP status code indicates an authentication/authorization failure.
     *
     * @param status - HTTP status code
     * @returns true if the status is 401 or 403
     */
    isAuthError(status) {
        return status === 401 || status === 403;
    }
    /**
     * Handle a 401/403 response from the cloud API by transitioning
     * the connection status to TOKEN_EXPIRED. This signals the frontend
     * to show a reconnect prompt instead of a raw error.
     *
     * @param context - Description of the failed operation (for logging)
     * @param status - HTTP status code from the cloud API
     */
    handleAuthFailure(context, status) {
        this.logger.warn(`Cloud token expired or revoked during ${context}`, { status });
        this.connectionStatus = CLOUD_CONSTANTS.CONNECTION_STATUS.TOKEN_EXPIRED;
        // Attempt auto-refresh in background if refresh token is available
        if (this.refreshToken) {
            this.tryRefreshToken().then((refreshed) => {
                if (refreshed) {
                    this.logger.info(`Token auto-refreshed after ${context} auth failure`);
                }
            }).catch(() => { });
        }
    }
}
//# sourceMappingURL=cloud-client.service.js.map