/**
 * @license
 * Copyright 2026 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import crypto from 'node:crypto';
import { logger } from '../../logger.js';
const MAX_BUFFER_SIZE = 1000;
const DEFAULT_CLEARCUT_ENDPOINT = 'https://play.googleapis.com/log?format=json_proto';
const DEFAULT_FLUSH_INTERVAL_MS = 15 * 60 * 1000;
const LOG_SOURCE = 2839;
const CLIENT_TYPE = 47;
const MIN_RATE_LIMIT_WAIT_MS = 30_000;
const REQUEST_TIMEOUT_MS = 30_000;
const SHUTDOWN_TIMEOUT_MS = 5_000;
const SESSION_ROTATION_INTERVAL_MS = 24 * 60 * 60 * 1000;
export class ClearcutSender {
    #appVersion;
    #osType;
    #clearcutEndpoint;
    #flushIntervalMs;
    #includePidHeader;
    #sessionId;
    #sessionCreated;
    #buffer = [];
    #flushTimer = null;
    #isFlushing = false;
    #timerStarted = false;
    constructor(config) {
        this.#appVersion = config.appVersion;
        this.#osType = config.osType;
        this.#clearcutEndpoint =
            config.clearcutEndpoint ?? DEFAULT_CLEARCUT_ENDPOINT;
        this.#flushIntervalMs =
            config.forceFlushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;
        this.#includePidHeader = config.includePidHeader ?? false;
        this.#sessionId = crypto.randomUUID();
        this.#sessionCreated = Date.now();
    }
    enqueueEvent(event) {
        if (Date.now() - this.#sessionCreated > SESSION_ROTATION_INTERVAL_MS) {
            this.#sessionId = crypto.randomUUID();
            this.#sessionCreated = Date.now();
        }
        const eventToSend = {
            ...event,
            session_id: this.#sessionId,
            app_version: this.#appVersion,
            os_type: this.#osType,
        };
        logger?.('Enqueing telemetry event', JSON.stringify(eventToSend, null, 2));
        this.#addToBuffer(eventToSend);
        if (!this.#timerStarted) {
            this.#timerStarted = true;
            this.#scheduleFlush(this.#flushIntervalMs);
        }
    }
    async sendShutdownEvent() {
        if (this.#flushTimer) {
            clearTimeout(this.#flushTimer);
            this.#flushTimer = null;
        }
        const shutdownEvent = {
            server_shutdown: {},
        };
        this.enqueueEvent(shutdownEvent);
        try {
            await Promise.race([
                this.#finalFlush(),
                new Promise(resolve => setTimeout(resolve, SHUTDOWN_TIMEOUT_MS)),
            ]);
            logger?.('Final flush completed');
        }
        catch (error) {
            logger?.('Final flush failed:', error);
        }
    }
    async #flush() {
        if (this.#isFlushing) {
            return;
        }
        if (this.#buffer.length === 0) {
            this.#scheduleFlush(this.#flushIntervalMs);
            return;
        }
        this.#isFlushing = true;
        let nextDelayMs = this.#flushIntervalMs;
        // Optimistically remove events from buffer before sending.
        // This prevents race conditions where a simultaneous #finalFlush would include these same events.
        const eventsToSend = [...this.#buffer];
        this.#buffer = [];
        try {
            const result = await this.#sendBatch(eventsToSend);
            if (result.success) {
                if (result.nextRequestWaitMs !== undefined) {
                    nextDelayMs = Math.max(result.nextRequestWaitMs, MIN_RATE_LIMIT_WAIT_MS);
                }
            }
            else if (result.isPermanentError) {
                logger?.('Permanent error, dropped batch of', eventsToSend.length, 'events');
            }
            else {
                // Transient error: Requeue events at the front of the buffer
                // to maintain order and retry them later.
                this.#buffer = [...eventsToSend, ...this.#buffer];
            }
        }
        catch (error) {
            // Safety catch for unexpected errors, requeue events
            this.#buffer = [...eventsToSend, ...this.#buffer];
            logger?.('Flush failed unexpectedly:', error);
        }
        finally {
            this.#isFlushing = false;
            this.#scheduleFlush(nextDelayMs);
        }
    }
    #addToBuffer(event) {
        if (this.#buffer.length >= MAX_BUFFER_SIZE) {
            this.#buffer.shift();
            logger?.('Telemetry buffer overflow: dropped oldest event');
        }
        this.#buffer.push({
            event,
            timestamp: Date.now(),
        });
    }
    #scheduleFlush(delayMs) {
        logger?.(`Scheduling flush in ${delayMs}`);
        if (this.#flushTimer) {
            clearTimeout(this.#flushTimer);
        }
        this.#flushTimer = setTimeout(() => {
            this.#flush().catch(err => {
                logger?.('Flush error:', err);
            });
        }, delayMs);
    }
    async #sendBatch(events) {
        logger?.(`Sending batch of ${events.length}`);
        const requestBody = {
            log_source: LOG_SOURCE,
            request_time_ms: Date.now().toString(),
            client_info: {
                client_type: CLIENT_TYPE,
            },
            log_event: events.map(({ event, timestamp }) => ({
                event_time_ms: timestamp.toString(),
                source_extension_json: JSON.stringify(event),
            })),
        };
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
        try {
            const response = await fetch(this.#clearcutEndpoint, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    // Used in E2E tests to confirm that the watchdog process is killed
                    ...(this.#includePidHeader
                        ? { 'X-Watchdog-Pid': process.pid.toString() }
                        : {}),
                },
                body: JSON.stringify(requestBody),
                signal: controller.signal,
            });
            clearTimeout(timeoutId);
            if (response.ok) {
                const data = (await response.json());
                return {
                    success: true,
                    nextRequestWaitMs: data.next_request_wait_millis,
                };
            }
            const status = response.status;
            if (status >= 500 || status === 429) {
                return { success: false };
            }
            logger?.('Telemetry permanent error:', status);
            return { success: false, isPermanentError: true };
        }
        catch {
            clearTimeout(timeoutId);
            return { success: false };
        }
    }
    async #finalFlush() {
        if (this.#buffer.length === 0) {
            return;
        }
        const eventsToSend = [...this.#buffer];
        await this.#sendBatch(eventsToSend);
    }
    stopForTesting() {
        if (this.#flushTimer) {
            clearTimeout(this.#flushTimer);
            this.#flushTimer = null;
        }
        this.#timerStarted = false;
    }
    get bufferSizeForTesting() {
        return this.#buffer.length;
    }
}
//# sourceMappingURL=ClearcutSender.js.map