/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { createHash } from 'node:crypto';
import * as os from 'node:os';
import si from 'systeminformation';
import { HttpsProxyAgent } from 'https-proxy-agent';
import type {
  StartSessionEvent,
  UserPromptEvent,
  ToolCallEvent,
  ApiRequestEvent,
  ApiResponseEvent,
  ApiErrorEvent,
  LoopDetectedEvent,
  NextSpeakerCheckEvent,
  SlashCommandEvent,
  RewindEvent,
  MalformedJsonResponseEvent,
  IdeConnectionEvent,
  ConversationFinishedEvent,
  ChatCompressionEvent,
  FileOperationEvent,
  InvalidChunkEvent,
  ContentRetryEvent,
  ContentRetryFailureEvent,
  ExtensionInstallEvent,
  ToolOutputTruncatedEvent,
  ExtensionUninstallEvent,
  ModelRoutingEvent,
  ExtensionEnableEvent,
  ModelSlashCommandEvent,
  ExtensionDisableEvent,
  EditStrategyEvent,
  EditCorrectionEvent,
  AgentStartEvent,
  AgentFinishEvent,
  RecoveryAttemptEvent,
  WebFetchFallbackAttemptEvent,
  ExtensionUpdateEvent,
  LlmLoopCheckEvent,
  HookCallEvent,
  ApprovalModeSwitchEvent,
  ApprovalModeDurationEvent,
  PlanExecutionEvent,
  ToolOutputMaskingEvent,
} from '../types.js';
import { EventMetadataKey } from './event-metadata-key.js';
import type { Config } from '../../config/config.js';
import { InstallationManager } from '../../utils/installationManager.js';
import { UserAccountManager } from '../../utils/userAccountManager.js';
import {
  safeJsonStringify,
  safeJsonStringifyBooleanValuesOnly,
} from '../../utils/safeJsonStringify.js';
import { FixedDeque } from 'mnemonist';
import { GIT_COMMIT_INFO, CLI_VERSION } from '../../generated/git-commit.js';
import {
  IDE_DEFINITIONS,
  detectIdeFromEnv,
  isCloudShell,
} from '../../ide/detect-ide.js';
import { debugLogger } from '../../utils/debugLogger.js';
import { getErrorMessage } from '../../utils/errors.js';

export enum EventNames {
  START_SESSION = 'start_session',
  NEW_PROMPT = 'new_prompt',
  TOOL_CALL = 'tool_call',
  FILE_OPERATION = 'file_operation',
  API_REQUEST = 'api_request',
  API_RESPONSE = 'api_response',
  API_ERROR = 'api_error',
  END_SESSION = 'end_session',
  FLASH_FALLBACK = 'flash_fallback',
  RIPGREP_FALLBACK = 'ripgrep_fallback',
  LOOP_DETECTED = 'loop_detected',
  LOOP_DETECTION_DISABLED = 'loop_detection_disabled',
  NEXT_SPEAKER_CHECK = 'next_speaker_check',
  SLASH_COMMAND = 'slash_command',
  REWIND = 'rewind',
  MALFORMED_JSON_RESPONSE = 'malformed_json_response',
  IDE_CONNECTION = 'ide_connection',
  KITTY_SEQUENCE_OVERFLOW = 'kitty_sequence_overflow',
  CHAT_COMPRESSION = 'chat_compression',
  CONVERSATION_FINISHED = 'conversation_finished',
  INVALID_CHUNK = 'invalid_chunk',
  CONTENT_RETRY = 'content_retry',
  CONTENT_RETRY_FAILURE = 'content_retry_failure',
  EXTENSION_ENABLE = 'extension_enable',
  EXTENSION_DISABLE = 'extension_disable',
  EXTENSION_INSTALL = 'extension_install',
  EXTENSION_UNINSTALL = 'extension_uninstall',
  EXTENSION_UPDATE = 'extension_update',
  TOOL_OUTPUT_TRUNCATED = 'tool_output_truncated',
  MODEL_ROUTING = 'model_routing',
  MODEL_SLASH_COMMAND = 'model_slash_command',
  EDIT_STRATEGY = 'edit_strategy',
  EDIT_CORRECTION = 'edit_correction',
  AGENT_START = 'agent_start',
  AGENT_FINISH = 'agent_finish',
  RECOVERY_ATTEMPT = 'recovery_attempt',
  WEB_FETCH_FALLBACK_ATTEMPT = 'web_fetch_fallback_attempt',
  LLM_LOOP_CHECK = 'llm_loop_check',
  HOOK_CALL = 'hook_call',
  APPROVAL_MODE_SWITCH = 'approval_mode_switch',
  APPROVAL_MODE_DURATION = 'approval_mode_duration',
  PLAN_EXECUTION = 'plan_execution',
  TOOL_OUTPUT_MASKING = 'tool_output_masking',
}

export interface LogResponse {
  nextRequestWaitMs?: number;
}

export interface LogEventEntry {
  event_time_ms: number;
  source_extension_json: string;
  exp?: {
    gws_experiment: number[];
  };
}

export interface EventValue {
  gemini_cli_key: EventMetadataKey;
  value: string;
}

export interface LogEvent {
  console_type: 'GEMINI_CLI';
  application: number;
  event_name: string;
  event_metadata: EventValue[][];
  client_email?: string;
  client_install_id?: string;
}

export interface LogRequest {
  log_source_name: 'CONCORD';
  request_time_ms: number;
  log_event: LogEventEntry[][];
}

/**
 * Determine the surface that the user is currently using.  Surface is effectively the
 * distribution channel in which the user is using Gemini CLI.  Gemini CLI comes bundled
 * w/ Firebase Studio and Cloud Shell.  Users that manually download themselves will
 * likely be "SURFACE_NOT_SET".
 *
 * This is computed based upon a series of environment variables these distribution
 * methods might have in their runtimes.
 */
function determineSurface(): string {
  if (process.env['SURFACE']) {
    return process.env['SURFACE'];
  } else if (isCloudShell()) {
    return IDE_DEFINITIONS.cloudshell.name;
  } else if (process.env['GITHUB_SHA']) {
    return 'GitHub';
  } else if (process.env['TERM_PROGRAM'] === 'vscode') {
    return detectIdeFromEnv().name || IDE_DEFINITIONS.vscode.name;
  } else {
    return 'SURFACE_NOT_SET';
  }
}

/**
 * Determines the GitHub Actions workflow name if the CLI is running in a GitHub Actions environment.
 */
function determineGHWorkflowName(): string | undefined {
  return process.env['GH_WORKFLOW_NAME'];
}

/**
 * Determines the GitHub repository name if the CLI is running in a GitHub Actions environment.
 */
function determineGHRepositoryName(): string | undefined {
  return process.env['GITHUB_REPOSITORY'];
}

/**
 * Clearcut URL to send logging events to.
 */
const CLEARCUT_URL = 'https://play.googleapis.com/log?format=json&hasfast=true';

/**
 * Interval in which buffered events are sent to clearcut.
 */
const FLUSH_INTERVAL_MS = 1000 * 60;

/**
 * Maximum amount of events to keep in memory. Events added after this amount
 * are dropped until the next flush to clearcut, which happens periodically as
 * defined by {@link FLUSH_INTERVAL_MS}.
 */
const MAX_EVENTS = 1000;

/**
 * Maximum events to retry after a failed clearcut flush
 */
const MAX_RETRY_EVENTS = 100;

const NO_GPU = 'NA';

let cachedGpuInfo: string | undefined;

async function refreshGpuInfo(): Promise<void> {
  try {
    const graphics = await si.graphics();
    if (graphics.controllers && graphics.controllers.length > 0) {
      cachedGpuInfo = graphics.controllers.map((c) => c.model).join(', ');
    } else {
      cachedGpuInfo = NO_GPU;
    }
  } catch (error) {
    cachedGpuInfo = 'FAILED';
    debugLogger.error(
      'Failed to get GPU information for telemetry',
      getErrorMessage(error),
    );
  }
}

async function getGpuInfo(): Promise<string> {
  if (!cachedGpuInfo) {
    await refreshGpuInfo();
  }

  return cachedGpuInfo ?? NO_GPU;
}

// Singleton class for batch posting log events to Clearcut. When a new event comes in, the elapsed time
// is checked and events are flushed to Clearcut if at least a minute has passed since the last flush.
export class ClearcutLogger {
  private static instance: ClearcutLogger;
  private config?: Config;
  private sessionData: EventValue[] = [];
  private promptId: string = '';
  private readonly installationManager: InstallationManager;
  private readonly userAccountManager: UserAccountManager;
  private readonly hashedGHRepositoryName?: string;

  /**
   * Queue of pending events that need to be flushed to the server.  New events
   * are added to this queue and then flushed on demand (via `flushToClearcut`)
   */
  private readonly events: FixedDeque<LogEventEntry[]>;

  /**
   * The last time that the events were successfully flushed to the server.
   */
  private lastFlushTime: number = Date.now();

  /**
   * the value is true when there is a pending flush happening. This prevents
   * concurrent flush operations.
   */
  private flushing: boolean = false;

  /**
   * This value is true when a flush was requested during an ongoing flush.
   */
  private pendingFlush: boolean = false;

  private constructor(config: Config) {
    this.config = config;
    this.events = new FixedDeque<LogEventEntry[]>(Array, MAX_EVENTS);
    this.promptId = config?.getSessionId() ?? '';
    this.installationManager = new InstallationManager();
    this.userAccountManager = new UserAccountManager();

    const ghRepositoryName = determineGHRepositoryName();
    if (ghRepositoryName) {
      this.hashedGHRepositoryName = createHash('sha256')
        .update(ghRepositoryName)
        .digest('hex');
    }
  }

  static getInstance(config?: Config): ClearcutLogger | undefined {
    if (config === undefined || !config?.getUsageStatisticsEnabled())
      return undefined;
    if (!ClearcutLogger.instance) {
      ClearcutLogger.instance = new ClearcutLogger(config);
    }
    return ClearcutLogger.instance;
  }

  /** For testing purposes only. */
  static clearInstance(): void {
    // @ts-expect-error - ClearcutLogger is a singleton, but we need to clear it for tests.
    ClearcutLogger.instance = undefined;
  }

  enqueueHelper(event: LogEvent, experimentIds?: number[]): void {
    // Manually handle overflow for FixedDeque, which throws when full.
    const wasAtCapacity = this.events.size >= MAX_EVENTS;

    if (wasAtCapacity) {
      this.events.shift(); // Evict oldest element to make space.
    }

    const logEventEntry: LogEventEntry = {
      event_time_ms: Date.now(),
      source_extension_json: safeJsonStringify(event),
    };

    if (experimentIds !== undefined) {
      logEventEntry.exp = {
        gws_experiment: experimentIds,
      };
    }

    this.events.push([logEventEntry]);

    if (wasAtCapacity && this.config?.getDebugMode()) {
      debugLogger.debug(
        `ClearcutLogger: Dropped old event to prevent memory leak (queue size: ${this.events.size})`,
      );
    }
  }

  enqueueLogEvent(event: LogEvent): void {
    try {
      this.enqueueHelper(event);
    } catch (error) {
      if (this.config?.getDebugMode()) {
        debugLogger.warn('ClearcutLogger: Failed to enqueue log event.', error);
      }
    }
  }

  async enqueueLogEventAfterExperimentsLoadAsync(
    event: LogEvent,
  ): Promise<void> {
    try {
      // eslint-disable-next-line @typescript-eslint/no-floating-promises
      this.config?.getExperimentsAsync().then((experiments) => {
        if (experiments) {
          const exp_id_data: EventValue[] = [
            {
              gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXPERIMENT_IDS,
              value: experiments.experimentIds.toString() ?? 'NA',
            },
          ];
          event.event_metadata = [[...event.event_metadata[0], ...exp_id_data]];
        }

        this.enqueueHelper(event, experiments?.experimentIds);
      });
    } catch (error) {
      debugLogger.warn('ClearcutLogger: Failed to enqueue log event.', error);
    }
  }

  createBasicLogEvent(
    eventName: EventNames,
    data: EventValue[] = [],
  ): LogEvent {
    const email = this.userAccountManager.getCachedGoogleAccount();
    const surface = determineSurface();
    const ghWorkflowName = determineGHWorkflowName();
    const baseMetadata: EventValue[] = [
      ...data,
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_SURFACE,
        value: surface,
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_VERSION,
        value: CLI_VERSION,
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_GIT_COMMIT_HASH,
        value: GIT_COMMIT_INFO,
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_OS,
        value: process.platform,
      },
    ];

    if (ghWorkflowName) {
      baseMetadata.push({
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_GH_WORKFLOW_NAME,
        value: ghWorkflowName,
      });
    }

    if (this.hashedGHRepositoryName) {
      baseMetadata.push({
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_GH_REPOSITORY_NAME_HASH,
        value: this.hashedGHRepositoryName,
      });
    }

    const logEvent: LogEvent = {
      console_type: 'GEMINI_CLI',
      application: 102, // GEMINI_CLI
      event_name: eventName as string,
      event_metadata: [baseMetadata],
    };

    // Should log either email or install ID, not both. See go/cloudmill-1p-oss-instrumentation#define-sessionable-id
    if (email) {
      logEvent.client_email = email;
    } else {
      logEvent.client_install_id = this.installationManager.getInstallationId();
    }

    return logEvent;
  }

  createLogEvent(eventName: EventNames, data: EventValue[] = []): LogEvent {
    if (eventName !== EventNames.START_SESSION) {
      data.push(...this.sessionData);
    }
    const totalAccounts = this.userAccountManager.getLifetimeGoogleAccounts();

    data = this.addDefaultFields(data, totalAccounts);

    return this.createBasicLogEvent(eventName, data);
  }

  flushIfNeeded(): void {
    if (Date.now() - this.lastFlushTime < FLUSH_INTERVAL_MS) {
      return;
    }

    this.flushToClearcut().catch((error) => {
      debugLogger.debug('Error flushing to Clearcut:', error);
    });
  }

  async flushToClearcut(): Promise<LogResponse> {
    if (this.flushing) {
      if (this.config?.getDebugMode()) {
        debugLogger.debug(
          'ClearcutLogger: Flush already in progress, marking pending flush.',
        );
      }
      this.pendingFlush = true;
      return Promise.resolve({});
    }
    this.flushing = true;

    if (this.config?.getDebugMode()) {
      debugLogger.log('Flushing log events to Clearcut.');
    }
    // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
    const eventsToSend = this.events.toArray() as LogEventEntry[][];
    this.events.clear();

    const request: LogRequest[] = [
      {
        log_source_name: 'CONCORD',
        request_time_ms: Date.now(),
        log_event: eventsToSend,
      },
    ];

    let result: LogResponse = {};

    try {
      const response = await fetch(CLEARCUT_URL, {
        method: 'POST',
        body: safeJsonStringify(request),
        headers: {
          'Content-Type': 'application/json',
        },
      });

      const responseBody = await response.text();

      if (response.status >= 200 && response.status < 300) {
        this.lastFlushTime = Date.now();
        const nextRequestWaitMs = Number(JSON.parse(responseBody)[0]);
        result = {
          ...result,
          nextRequestWaitMs,
        };
      } else {
        if (this.config?.getDebugMode()) {
          debugLogger.warn(
            `Error flushing log events: HTTP ${response.status}: ${response.statusText}`,
          );
        }

        // Re-queue failed events for retry
        this.requeueFailedEvents(eventsToSend);
      }
    } catch (e: unknown) {
      if (this.config?.getDebugMode()) {
        // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
        debugLogger.warn('Error flushing log events:', e as Error);
      }

      // Re-queue failed events for retry
      this.requeueFailedEvents(eventsToSend);
    }

    this.flushing = false;

    // If a flush was requested while we were flushing, flush again
    if (this.pendingFlush) {
      this.pendingFlush = false;
      // Fire and forget the pending flush
      this.flushToClearcut().catch((error) => {
        if (this.config?.getDebugMode()) {
          debugLogger.debug('Error in pending flush to Clearcut:', error);
        }
      });
    }

    return result;
  }

  async logStartSessionEvent(event: StartSessionEvent): Promise<void> {
    const data: EventValue[] = [
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_START_SESSION_MODEL,
        value: event.model,
      },
      {
        gemini_cli_key:
          EventMetadataKey.GEMINI_CLI_START_SESSION_EMBEDDING_MODEL,
        value: event.embedding_model,
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_START_SESSION_SANDBOX,
        value: event.sandbox_enabled.toString(),
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_START_SESSION_CORE_TOOLS,
        value: event.core_tools_enabled,
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_START_SESSION_APPROVAL_MODE,
        value: event.approval_mode,
      },
      {
        gemini_cli_key:
          EventMetadataKey.GEMINI_CLI_START_SESSION_API_KEY_ENABLED,
        value: event.api_key_enabled.toString(),
      },
      {
        gemini_cli_key:
          EventMetadataKey.GEMINI_CLI_START_SESSION_VERTEX_API_ENABLED,
        value: event.vertex_ai_enabled.toString(),
      },
      {
        gemini_cli_key:
          EventMetadataKey.GEMINI_CLI_START_SESSION_DEBUG_MODE_ENABLED,
        value: event.debug_enabled.toString(),
      },
      {
        gemini_cli_key:
          EventMetadataKey.GEMINI_CLI_START_SESSION_VERTEX_API_ENABLED,
        value: event.vertex_ai_enabled.toString(),
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_START_SESSION_MCP_SERVERS,
        value: event.mcp_servers,
      },
      {
        gemini_cli_key:
          EventMetadataKey.GEMINI_CLI_START_SESSION_VERTEX_API_ENABLED,
        value: event.vertex_ai_enabled.toString(),
      },
      {
        gemini_cli_key:
          EventMetadataKey.GEMINI_CLI_START_SESSION_TELEMETRY_ENABLED,
        value: event.telemetry_enabled.toString(),
      },
      {
        gemini_cli_key:
          EventMetadataKey.GEMINI_CLI_START_SESSION_TELEMETRY_LOG_USER_PROMPTS_ENABLED,
        value: event.telemetry_log_user_prompts_enabled.toString(),
      },
      {
        gemini_cli_key:
          EventMetadataKey.GEMINI_CLI_START_SESSION_MCP_SERVERS_COUNT,
        value: event.mcp_servers_count
          ? event.mcp_servers_count.toString()
          : '',
      },
      {
        gemini_cli_key:
          EventMetadataKey.GEMINI_CLI_START_SESSION_MCP_TOOLS_COUNT,
        value: event.mcp_tools_count?.toString() ?? '',
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_START_SESSION_MCP_TOOLS,
        value: event.mcp_tools ? event.mcp_tools : '',
      },
      {
        gemini_cli_key:
          EventMetadataKey.GEMINI_CLI_START_SESSION_EXTENSIONS_COUNT,
        value: event.extensions_count.toString(),
      },
      // We deliberately do not log the names of extensions here, to be safe.
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_START_SESSION_EXTENSION_IDS,
        value: event.extension_ids.toString(),
      },
    ];

    // Add hardware information only to the start session event
    const cpus = os.cpus();
    data.push(
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_CPU_INFO,
        value: cpus[0].model,
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_CPU_CORES,
        value: cpus.length.toString(),
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_RAM_TOTAL_GB,
        value: (os.totalmem() / 1024 ** 3).toFixed(2).toString(),
      },
    );

    const gpuInfo = await getGpuInfo();
    data.push({
      gemini_cli_key: EventMetadataKey.GEMINI_CLI_GPU_INFO,
      value: gpuInfo,
    });
    this.sessionData = data;

    // Flush after experiments finish loading from CCPA server
    // eslint-disable-next-line @typescript-eslint/no-floating-promises
    this.enqueueLogEventAfterExperimentsLoadAsync(
      this.createLogEvent(EventNames.START_SESSION, data),
    ).then(() => {
      this.flushToClearcut().catch((error) => {
        debugLogger.debug('Error flushing to Clearcut:', error);
      });
    });
  }

  logNewPromptEvent(event: UserPromptEvent): void {
    this.promptId = event.prompt_id;
    const data: EventValue[] = [
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_USER_PROMPT_LENGTH,
        value: JSON.stringify(event.prompt_length),
      },
    ];

    this.enqueueLogEvent(this.createLogEvent(EventNames.NEW_PROMPT, data));
    this.flushIfNeeded();
  }

  logToolCallEvent(event: ToolCallEvent): void {
    const data: EventValue[] = [
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_CALL_NAME,
        value: JSON.stringify(event.function_name),
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_CALL_DECISION,
        value: JSON.stringify(event.decision),
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_CALL_SUCCESS,
        value: JSON.stringify(event.success),
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_CALL_DURATION_MS,
        value: JSON.stringify(event.duration_ms),
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_CALL_ERROR_TYPE,
        value: JSON.stringify(event.error_type),
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_TYPE,
        value: JSON.stringify(event.tool_type),
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_CALL_CONTENT_LENGTH,
        value: JSON.stringify(event.content_length),
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_CALL_MCP_SERVER_NAME,
        value: JSON.stringify(event.mcp_server_name),
      },
    ];

    if (event.metadata) {
      const metadataMapping: { [key: string]: EventMetadataKey } = {
        model_added_lines: EventMetadataKey.GEMINI_CLI_AI_ADDED_LINES,
        model_removed_lines: EventMetadataKey.GEMINI_CLI_AI_REMOVED_LINES,
        model_added_chars: EventMetadataKey.GEMINI_CLI_AI_ADDED_CHARS,
        model_removed_chars: EventMetadataKey.GEMINI_CLI_AI_REMOVED_CHARS,
        user_added_lines: EventMetadataKey.GEMINI_CLI_USER_ADDED_LINES,
        user_removed_lines: EventMetadataKey.GEMINI_CLI_USER_REMOVED_LINES,
        user_added_chars: EventMetadataKey.GEMINI_CLI_USER_ADDED_CHARS,
        user_removed_chars: EventMetadataKey.GEMINI_CLI_USER_REMOVED_CHARS,
      };

      for (const [key, gemini_cli_key] of Object.entries(metadataMapping)) {
        if (event.metadata[key] !== undefined) {
          data.push({
            gemini_cli_key,
            value: JSON.stringify(event.metadata[key]),
          });
        }
      }
    }
    if (event.extension_id) {
      data.push({
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_ID,
        value: event.extension_id,
      });
    }

    const logEvent = this.createLogEvent(EventNames.TOOL_CALL, data);
    this.enqueueLogEvent(logEvent);
    this.flushIfNeeded();
  }

  logFileOperationEvent(event: FileOperationEvent): void {
    const data: EventValue[] = [
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_CALL_NAME,
        value: JSON.stringify(event.tool_name),
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_FILE_OPERATION_TYPE,
        value: JSON.stringify(event.operation),
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_FILE_OPERATION_LINES,
        value: JSON.stringify(event.lines),
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_FILE_OPERATION_MIMETYPE,
        value: JSON.stringify(event.mimetype),
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_FILE_OPERATION_EXTENSION,
        value: JSON.stringify(event.extension),
      },
    ];

    if (event.programming_language) {
      data.push({
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_PROGRAMMING_LANGUAGE,
        value: event.programming_language,
      });
    }

    const logEvent = this.createLogEvent(EventNames.FILE_OPERATION, data);
    this.enqueueLogEvent(logEvent);
    this.flushIfNeeded();
  }

  logApiRequestEvent(event: ApiRequestEvent): void {
    const data: EventValue[] = [
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_REQUEST_MODEL,
        value: JSON.stringify(event.model),
      },
    ];

    this.enqueueLogEvent(this.createLogEvent(EventNames.API_REQUEST, data));
    this.flushIfNeeded();
  }

  logApiResponseEvent(event: ApiResponseEvent): void {
    const data: EventValue[] = [
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_RESPONSE_MODEL,
        value: JSON.stringify(event.model),
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_RESPONSE_STATUS_CODE,
        value: JSON.stringify(event.status_code),
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_RESPONSE_DURATION_MS,
        value: JSON.stringify(event.duration_ms),
      },
      {
        gemini_cli_key:
          EventMetadataKey.GEMINI_CLI_API_RESPONSE_INPUT_TOKEN_COUNT,
        value: JSON.stringify(event.usage.input_token_count),
      },
      {
        gemini_cli_key:
          EventMetadataKey.GEMINI_CLI_API_RESPONSE_OUTPUT_TOKEN_COUNT,
        value: JSON.stringify(event.usage.output_token_count),
      },
      {
        gemini_cli_key:
          EventMetadataKey.GEMINI_CLI_API_RESPONSE_CACHED_TOKEN_COUNT,
        value: JSON.stringify(event.usage.cached_content_token_count),
      },
      {
        gemini_cli_key:
          EventMetadataKey.GEMINI_CLI_API_RESPONSE_THINKING_TOKEN_COUNT,
        value: JSON.stringify(event.usage.thoughts_token_count),
      },
      {
        gemini_cli_key:
          EventMetadataKey.GEMINI_CLI_API_RESPONSE_TOOL_TOKEN_COUNT,
        value: JSON.stringify(event.usage.tool_token_count),
      },
    ];

    this.enqueueLogEvent(this.createLogEvent(EventNames.API_RESPONSE, data));
    this.flushIfNeeded();
  }

  logApiErrorEvent(event: ApiErrorEvent): void {
    const data: EventValue[] = [
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_ERROR_MODEL,
        value: JSON.stringify(event.model),
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_ERROR_TYPE,
        value: JSON.stringify(event.error_type),
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_ERROR_STATUS_CODE,
        value: JSON.stringify(event.status_code),
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_ERROR_DURATION_MS,
        value: JSON.stringify(event.duration_ms),
      },
    ];

    this.enqueueLogEvent(this.createLogEvent(EventNames.API_ERROR, data));
    this.flushIfNeeded();
  }

  logChatCompressionEvent(event: ChatCompressionEvent): void {
    const data: EventValue[] = [
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_COMPRESSION_TOKENS_BEFORE,
        value: `${event.tokens_before}`,
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_COMPRESSION_TOKENS_AFTER,
        value: `${event.tokens_after}`,
      },
    ];

    this.enqueueLogEvent(
      this.createLogEvent(EventNames.CHAT_COMPRESSION, data),
    );
  }

  logFlashFallbackEvent(): void {
    this.enqueueLogEvent(this.createLogEvent(EventNames.FLASH_FALLBACK, []));
    this.flushToClearcut().catch((error) => {
      debugLogger.debug('Error flushing to Clearcut:', error);
    });
  }

  logRipgrepFallbackEvent(): void {
    this.enqueueLogEvent(this.createLogEvent(EventNames.RIPGREP_FALLBACK, []));
    this.flushToClearcut().catch((error) => {
      debugLogger.debug('Error flushing to Clearcut:', error);
    });
  }

  logLoopDetectedEvent(event: LoopDetectedEvent): void {
    const data: EventValue[] = [
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_LOOP_DETECTED_TYPE,
        value: JSON.stringify(event.loop_type),
      },
    ];

    if (event.confirmed_by_model) {
      data.push({
        gemini_cli_key:
          EventMetadataKey.GEMINI_CLI_LOOP_DETECTED_CONFIRMED_BY_MODEL,
        value: event.confirmed_by_model,
      });
    }

    this.enqueueLogEvent(this.createLogEvent(EventNames.LOOP_DETECTED, data));
    this.flushIfNeeded();
  }

  logLoopDetectionDisabledEvent(): void {
    const data: EventValue[] = [];

    this.enqueueLogEvent(
      this.createLogEvent(EventNames.LOOP_DETECTION_DISABLED, data),
    );
    this.flushIfNeeded();
  }

  logNextSpeakerCheck(event: NextSpeakerCheckEvent): void {
    const data: EventValue[] = [
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_RESPONSE_FINISH_REASON,
        value: JSON.stringify(event.finish_reason),
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_NEXT_SPEAKER_CHECK_RESULT,
        value: JSON.stringify(event.result),
      },
    ];

    this.enqueueLogEvent(
      this.createLogEvent(EventNames.NEXT_SPEAKER_CHECK, data),
    );
    this.flushIfNeeded();
  }

  logSlashCommandEvent(event: SlashCommandEvent): void {
    const data: EventValue[] = [
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_SLASH_COMMAND_NAME,
        value: JSON.stringify(event.command),
      },
    ];

    if (event.subcommand) {
      data.push({
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_SLASH_COMMAND_SUBCOMMAND,
        value: JSON.stringify(event.subcommand),
      });
    }

    if (event.status) {
      data.push({
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_SLASH_COMMAND_STATUS,
        value: JSON.stringify(event.status),
      });
    }

    if (event.extension_id) {
      data.push({
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_ID,
        value: event.extension_id,
      });
    }

    this.enqueueLogEvent(this.createLogEvent(EventNames.SLASH_COMMAND, data));
    this.flushIfNeeded();
  }

  logRewindEvent(event: RewindEvent): void {
    const data: EventValue[] = [
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_REWIND_OUTCOME,
        value: event.outcome,
      },
    ];

    this.enqueueLogEvent(this.createLogEvent(EventNames.REWIND, data));
    this.flushIfNeeded();
  }

  logMalformedJsonResponseEvent(event: MalformedJsonResponseEvent): void {
    const data: EventValue[] = [
      {
        gemini_cli_key:
          EventMetadataKey.GEMINI_CLI_MALFORMED_JSON_RESPONSE_MODEL,
        value: JSON.stringify(event.model),
      },
    ];

    this.enqueueLogEvent(
      this.createLogEvent(EventNames.MALFORMED_JSON_RESPONSE, data),
    );
    this.flushIfNeeded();
  }

  logIdeConnectionEvent(event: IdeConnectionEvent): void {
    const data: EventValue[] = [
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_IDE_CONNECTION_TYPE,
        value: JSON.stringify(event.connection_type),
      },
    ];

    // Flush after experiments finish loading from CCPA server
    // eslint-disable-next-line @typescript-eslint/no-floating-promises
    this.enqueueLogEventAfterExperimentsLoadAsync(
      this.createLogEvent(EventNames.START_SESSION, data),
    ).then(() => {
      this.flushToClearcut().catch((error) => {
        debugLogger.debug('Error flushing to Clearcut:', error);
      });
    });
  }

  logConversationFinishedEvent(event: ConversationFinishedEvent): void {
    const data: EventValue[] = [
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_SESSION_ID,
        value: this.config?.getSessionId() ?? '',
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_CONVERSATION_TURN_COUNT,
        value: JSON.stringify(event.turnCount),
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_APPROVAL_MODE,
        value: event.approvalMode,
      },
    ];

    this.enqueueLogEvent(
      this.createLogEvent(EventNames.CONVERSATION_FINISHED, data),
    );
    this.flushIfNeeded();
  }

  logEndSessionEvent(): void {
    // Flush immediately on session end.
    this.enqueueLogEvent(this.createLogEvent(EventNames.END_SESSION, []));
    this.flushToClearcut().catch((error) => {
      debugLogger.debug('Error flushing to Clearcut:', error);
    });
  }

  logInvalidChunkEvent(event: InvalidChunkEvent): void {
    const data: EventValue[] = [];

    if (event.error_message) {
      data.push({
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_INVALID_CHUNK_ERROR_MESSAGE,
        value: event.error_message,
      });
    }

    this.enqueueLogEvent(this.createLogEvent(EventNames.INVALID_CHUNK, data));
    this.flushIfNeeded();
  }

  logContentRetryEvent(event: ContentRetryEvent): void {
    const data: EventValue[] = [
      {
        gemini_cli_key:
          EventMetadataKey.GEMINI_CLI_CONTENT_RETRY_ATTEMPT_NUMBER,
        value: String(event.attempt_number),
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_CONTENT_RETRY_ERROR_TYPE,
        value: event.error_type,
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_CONTENT_RETRY_DELAY_MS,
        value: String(event.retry_delay_ms),
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_REQUEST_MODEL,
        value: event.model,
      },
    ];

    this.enqueueLogEvent(this.createLogEvent(EventNames.CONTENT_RETRY, data));
    this.flushIfNeeded();
  }

  logContentRetryFailureEvent(event: ContentRetryFailureEvent): void {
    const data: EventValue[] = [
      {
        gemini_cli_key:
          EventMetadataKey.GEMINI_CLI_CONTENT_RETRY_FAILURE_TOTAL_ATTEMPTS,
        value: String(event.total_attempts),
      },
      {
        gemini_cli_key:
          EventMetadataKey.GEMINI_CLI_CONTENT_RETRY_FAILURE_FINAL_ERROR_TYPE,
        value: event.final_error_type,
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_REQUEST_MODEL,
        value: event.model,
      },
    ];

    if (event.total_duration_ms) {
      data.push({
        gemini_cli_key:
          EventMetadataKey.GEMINI_CLI_CONTENT_RETRY_FAILURE_TOTAL_DURATION_MS,
        value: String(event.total_duration_ms),
      });
    }

    this.enqueueLogEvent(
      this.createLogEvent(EventNames.CONTENT_RETRY_FAILURE, data),
    );
    this.flushIfNeeded();
  }

  async logExtensionInstallEvent(event: ExtensionInstallEvent): Promise<void> {
    const data: EventValue[] = [
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_NAME,
        value: event.hashed_extension_name,
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_ID,
        value: event.extension_id,
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_VERSION,
        value: event.extension_version,
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_SOURCE,
        value: event.extension_source,
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_INSTALL_STATUS,
        value: event.status,
      },
    ];

    this.enqueueLogEvent(
      this.createBasicLogEvent(EventNames.EXTENSION_INSTALL, data),
    );
    await this.flushToClearcut().catch((error) => {
      debugLogger.debug('Error flushing to Clearcut:', error);
    });
  }

  async logExtensionUninstallEvent(
    event: ExtensionUninstallEvent,
  ): Promise<void> {
    const data: EventValue[] = [
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_NAME,
        value: event.hashed_extension_name,
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_ID,
        value: event.extension_id,
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_UNINSTALL_STATUS,
        value: event.status,
      },
    ];

    this.enqueueLogEvent(
      this.createBasicLogEvent(EventNames.EXTENSION_UNINSTALL, data),
    );
    await this.flushToClearcut().catch((error) => {
      debugLogger.debug('Error flushing to Clearcut:', error);
    });
  }

  async logExtensionUpdateEvent(event: ExtensionUpdateEvent): Promise<void> {
    const data: EventValue[] = [
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_NAME,
        value: event.hashed_extension_name,
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_ID,
        value: event.extension_id,
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_VERSION,
        value: event.extension_version,
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_PREVIOUS_VERSION,
        value: event.extension_previous_version,
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_SOURCE,
        value: event.extension_source,
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_UPDATE_STATUS,
        value: event.status,
      },
    ];

    this.enqueueLogEvent(
      this.createBasicLogEvent(EventNames.EXTENSION_UPDATE, data),
    );
    await this.flushToClearcut().catch((error) => {
      debugLogger.debug('Error flushing to Clearcut:', error);
    });
  }

  logToolOutputTruncatedEvent(event: ToolOutputTruncatedEvent): void {
    const data: EventValue[] = [
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_CALL_NAME,
        value: JSON.stringify(event.tool_name),
      },
      {
        gemini_cli_key:
          EventMetadataKey.GEMINI_CLI_TOOL_OUTPUT_TRUNCATED_ORIGINAL_LENGTH,
        value: JSON.stringify(event.original_content_length),
      },
      {
        gemini_cli_key:
          EventMetadataKey.GEMINI_CLI_TOOL_OUTPUT_TRUNCATED_TRUNCATED_LENGTH,
        value: JSON.stringify(event.truncated_content_length),
      },
      {
        gemini_cli_key:
          EventMetadataKey.GEMINI_CLI_TOOL_OUTPUT_TRUNCATED_THRESHOLD,
        value: JSON.stringify(event.threshold),
      },
    ];

    const logEvent = this.createLogEvent(
      EventNames.TOOL_OUTPUT_TRUNCATED,
      data,
    );
    this.enqueueLogEvent(logEvent);
    this.flushIfNeeded();
  }

  logToolOutputMaskingEvent(event: ToolOutputMaskingEvent): void {
    const data: EventValue[] = [
      {
        gemini_cli_key:
          EventMetadataKey.GEMINI_CLI_TOOL_OUTPUT_MASKING_TOKENS_BEFORE,
        value: event.tokens_before.toString(),
      },
      {
        gemini_cli_key:
          EventMetadataKey.GEMINI_CLI_TOOL_OUTPUT_MASKING_TOKENS_AFTER,
        value: event.tokens_after.toString(),
      },
      {
        gemini_cli_key:
          EventMetadataKey.GEMINI_CLI_TOOL_OUTPUT_MASKING_MASKED_COUNT,
        value: event.masked_count.toString(),
      },
      {
        gemini_cli_key:
          EventMetadataKey.GEMINI_CLI_TOOL_OUTPUT_MASKING_TOTAL_PRUNABLE_TOKENS,
        value: event.total_prunable_tokens.toString(),
      },
    ];

    this.enqueueLogEvent(
      this.createLogEvent(EventNames.TOOL_OUTPUT_MASKING, data),
    );
    this.flushIfNeeded();
  }

  logModelRoutingEvent(event: ModelRoutingEvent): void {
    const data: EventValue[] = [
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_ROUTING_DECISION,
        value: event.decision_model,
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_ROUTING_DECISION_SOURCE,
        value: event.decision_source,
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_ROUTING_LATENCY_MS,
        value: event.routing_latency_ms.toString(),
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_ROUTING_FAILURE,
        value: event.failed.toString(),
      },
    ];

    if (event.error_message) {
      data.push({
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_ROUTING_FAILURE_REASON,
        value: event.error_message,
      });
    }

    if (event.reasoning && this.config?.getTelemetryLogPromptsEnabled()) {
      data.push({
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_ROUTING_REASONING,
        value: event.reasoning,
      });
    }

    if (event.enable_numerical_routing !== undefined) {
      data.push({
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_ROUTING_NUMERICAL_ENABLED,
        value: event.enable_numerical_routing.toString(),
      });
    }

    if (event.classifier_threshold) {
      data.push({
        gemini_cli_key:
          EventMetadataKey.GEMINI_CLI_ROUTING_CLASSIFIER_THRESHOLD,
        value: event.classifier_threshold,
      });
    }

    this.enqueueLogEvent(this.createLogEvent(EventNames.MODEL_ROUTING, data));
    this.flushIfNeeded();
  }

  async logExtensionEnableEvent(event: ExtensionEnableEvent): Promise<void> {
    const data: EventValue[] = [
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_NAME,
        value: event.hashed_extension_name,
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_ID,
        value: event.extension_id,
      },
      {
        gemini_cli_key:
          EventMetadataKey.GEMINI_CLI_EXTENSION_ENABLE_SETTING_SCOPE,
        value: event.setting_scope,
      },
    ];

    this.enqueueLogEvent(
      this.createBasicLogEvent(EventNames.EXTENSION_ENABLE, data),
    );
    await this.flushToClearcut().catch((error) => {
      debugLogger.debug('Error flushing to Clearcut:', error);
    });
  }

  logModelSlashCommandEvent(event: ModelSlashCommandEvent): void {
    const data: EventValue[] = [
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_MODEL_SLASH_COMMAND,
        value: event.model_name,
      },
    ];

    this.enqueueLogEvent(
      this.createLogEvent(EventNames.MODEL_SLASH_COMMAND, data),
    );
    this.flushIfNeeded();
  }

  async logExtensionDisableEvent(event: ExtensionDisableEvent): Promise<void> {
    const data: EventValue[] = [
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_NAME,
        value: event.hashed_extension_name,
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_ID,
        value: event.extension_id,
      },
      {
        gemini_cli_key:
          EventMetadataKey.GEMINI_CLI_EXTENSION_DISABLE_SETTING_SCOPE,
        value: event.setting_scope,
      },
    ];

    this.enqueueLogEvent(
      this.createBasicLogEvent(EventNames.EXTENSION_DISABLE, data),
    );
    await this.flushToClearcut().catch((error) => {
      debugLogger.debug('Error flushing to Clearcut:', error);
    });
  }

  logEditStrategyEvent(event: EditStrategyEvent): void {
    const data: EventValue[] = [
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_EDIT_STRATEGY,
        value: event.strategy,
      },
    ];

    this.enqueueLogEvent(this.createLogEvent(EventNames.EDIT_STRATEGY, data));
    this.flushIfNeeded();
  }

  logEditCorrectionEvent(event: EditCorrectionEvent): void {
    const data: EventValue[] = [
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_EDIT_CORRECTION,
        value: event.correction,
      },
    ];

    this.enqueueLogEvent(this.createLogEvent(EventNames.EDIT_CORRECTION, data));
    this.flushIfNeeded();
  }

  logAgentStartEvent(event: AgentStartEvent): void {
    const data: EventValue[] = [
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_AGENT_ID,
        value: event.agent_id,
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_AGENT_NAME,
        value: event.agent_name,
      },
    ];

    this.enqueueLogEvent(this.createLogEvent(EventNames.AGENT_START, data));
    this.flushIfNeeded();
  }

  logAgentFinishEvent(event: AgentFinishEvent): void {
    const data: EventValue[] = [
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_AGENT_ID,
        value: event.agent_id,
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_AGENT_NAME,
        value: event.agent_name,
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_AGENT_DURATION_MS,
        value: event.duration_ms.toString(),
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_AGENT_TURN_COUNT,
        value: event.turn_count.toString(),
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_AGENT_TERMINATE_REASON,
        value: event.terminate_reason,
      },
    ];

    this.enqueueLogEvent(this.createLogEvent(EventNames.AGENT_FINISH, data));
    this.flushIfNeeded();
  }

  logRecoveryAttemptEvent(event: RecoveryAttemptEvent): void {
    const data: EventValue[] = [
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_AGENT_ID,
        value: event.agent_id,
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_AGENT_NAME,
        value: event.agent_name,
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_AGENT_RECOVERY_REASON,
        value: event.reason,
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_AGENT_RECOVERY_DURATION_MS,
        value: event.duration_ms.toString(),
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_AGENT_RECOVERY_SUCCESS,
        value: event.success.toString(),
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_AGENT_TURN_COUNT,
        value: event.turn_count.toString(),
      },
    ];

    this.enqueueLogEvent(
      this.createLogEvent(EventNames.RECOVERY_ATTEMPT, data),
    );
    this.flushIfNeeded();
  }

  logWebFetchFallbackAttemptEvent(event: WebFetchFallbackAttemptEvent): void {
    const data: EventValue[] = [
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_WEB_FETCH_FALLBACK_REASON,
        value: event.reason,
      },
    ];

    this.enqueueLogEvent(
      this.createLogEvent(EventNames.WEB_FETCH_FALLBACK_ATTEMPT, data),
    );
    this.flushIfNeeded();
  }

  logLlmLoopCheckEvent(event: LlmLoopCheckEvent): void {
    const data: EventValue[] = [
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_PROMPT_ID,
        value: event.prompt_id,
      },
      {
        gemini_cli_key:
          EventMetadataKey.GEMINI_CLI_LLM_LOOP_CHECK_FLASH_CONFIDENCE,
        value: event.flash_confidence.toString(),
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_LLM_LOOP_CHECK_MAIN_MODEL,
        value: event.main_model,
      },
      {
        gemini_cli_key:
          EventMetadataKey.GEMINI_CLI_LLM_LOOP_CHECK_MAIN_MODEL_CONFIDENCE,
        value: event.main_model_confidence.toString(),
      },
    ];

    this.enqueueLogEvent(this.createLogEvent(EventNames.LLM_LOOP_CHECK, data));
    this.flushIfNeeded();
  }

  logHookCallEvent(event: HookCallEvent): void {
    const data: EventValue[] = [
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_HOOK_EVENT_NAME,
        value: event.hook_event_name,
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_HOOK_DURATION_MS,
        value: event.duration_ms.toString(),
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_HOOK_SUCCESS,
        value: event.success.toString(),
      },
    ];

    if (event.exit_code !== undefined) {
      data.push({
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_HOOK_EXIT_CODE,
        value: event.exit_code.toString(),
      });
    }

    this.enqueueLogEvent(this.createLogEvent(EventNames.HOOK_CALL, data));
    this.flushIfNeeded();
  }

  logApprovalModeSwitchEvent(event: ApprovalModeSwitchEvent): void {
    const data: EventValue[] = [
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_ACTIVE_APPROVAL_MODE,
        value: event.from_mode,
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_APPROVAL_MODE_TO,
        value: event.to_mode,
      },
    ];

    this.enqueueLogEvent(
      this.createLogEvent(EventNames.APPROVAL_MODE_SWITCH, data),
    );
    this.flushIfNeeded();
  }

  logApprovalModeDurationEvent(event: ApprovalModeDurationEvent): void {
    const data: EventValue[] = [
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_ACTIVE_APPROVAL_MODE,
        value: event.mode,
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_APPROVAL_MODE_DURATION_MS,
        value: event.duration_ms.toString(),
      },
    ];

    this.enqueueLogEvent(
      this.createLogEvent(EventNames.APPROVAL_MODE_DURATION, data),
    );
    this.flushIfNeeded();
  }

  logPlanExecutionEvent(event: PlanExecutionEvent): void {
    const data: EventValue[] = [
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_APPROVAL_MODE,
        value: event.approval_mode,
      },
    ];

    this.enqueueLogEvent(this.createLogEvent(EventNames.PLAN_EXECUTION, data));
    this.flushIfNeeded();
  }

  /**
   * Adds default fields to data, and returns a new data array.  This fields
   * should exist on all log events.
   */
  addDefaultFields(data: EventValue[], totalAccounts: number): EventValue[] {
    const defaultLogMetadata: EventValue[] = [
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_SESSION_ID,
        value: this.config?.getSessionId() ?? '',
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_AUTH_TYPE,
        value: JSON.stringify(
          this.config?.getContentGeneratorConfig()?.authType,
        ),
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_GOOGLE_ACCOUNTS_COUNT,
        value: `${totalAccounts}`,
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_PROMPT_ID,
        value: this.promptId,
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_NODE_VERSION,
        value: process.versions.node,
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_USER_SETTINGS,
        value: this.getConfigJson(),
      },
      {
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_INTERACTIVE,
        value: this.config?.isInteractive().toString() ?? 'false',
      },
    ];
    if (this.config?.getExperiments()) {
      defaultLogMetadata.push({
        gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXPERIMENT_IDS,
        value: this.config?.getExperiments()?.experimentIds.toString() ?? 'NA',
      });
    }
    return [...data, ...defaultLogMetadata];
  }

  getProxyAgent() {
    const proxyUrl = this.config?.getProxy();
    if (!proxyUrl) return undefined;
    // undici which is widely used in the repo can only support http & https proxy protocol,
    // https://github.com/nodejs/undici/issues/2224
    if (proxyUrl.startsWith('http')) {
      return new HttpsProxyAgent(proxyUrl);
    } else {
      throw new Error('Unsupported proxy type');
    }
  }

  getConfigJson() {
    return safeJsonStringifyBooleanValuesOnly(this.config);
  }

  shutdown() {
    this.logEndSessionEvent();
  }

  private requeueFailedEvents(eventsToSend: LogEventEntry[][]): void {
    // Add the events back to the front of the queue to be retried, but limit retry queue size
    const eventsToRetry = eventsToSend.slice(-MAX_RETRY_EVENTS); // Keep only the most recent events

    // Log a warning if we're dropping events
    if (eventsToSend.length > MAX_RETRY_EVENTS && this.config?.getDebugMode()) {
      debugLogger.warn(
        `ClearcutLogger: Dropping ${
          eventsToSend.length - MAX_RETRY_EVENTS
        } events due to retry queue limit. Total events: ${
          eventsToSend.length
        }, keeping: ${MAX_RETRY_EVENTS}`,
      );
    }

    // Determine how many events can be re-queued
    const availableSpace = MAX_EVENTS - this.events.size;
    const numEventsToRequeue = Math.min(eventsToRetry.length, availableSpace);

    if (numEventsToRequeue === 0) {
      if (this.config?.getDebugMode()) {
        debugLogger.debug(
          `ClearcutLogger: No events re-queued (queue size: ${this.events.size})`,
        );
      }
      return;
    }

    // Get the most recent events to re-queue
    const eventsToRequeue = eventsToRetry.slice(
      eventsToRetry.length - numEventsToRequeue,
    );

    // Prepend events to the front of the deque to be retried first.
    // We iterate backwards to maintain the original order of the failed events.
    for (let i = eventsToRequeue.length - 1; i >= 0; i--) {
      this.events.unshift(eventsToRequeue[i]);
    }
    // Clear any potential overflow
    while (this.events.size > MAX_EVENTS) {
      this.events.pop();
    }

    if (this.config?.getDebugMode()) {
      debugLogger.debug(
        `ClearcutLogger: Re-queued ${numEventsToRequeue} events for retry (queue size: ${this.events.size})`,
      );
    }
  }
}

export const TEST_ONLY = {
  MAX_RETRY_EVENTS,
  MAX_EVENTS,
  refreshGpuInfo,
  resetCachedGpuInfoForTesting: () => {
    cachedGpuInfo = undefined;
  },
};
