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

import { type JWTInput } from 'google-auth-library';
import { TraceExporter } from '@google-cloud/opentelemetry-cloud-trace-exporter';
import { MetricExporter } from '@google-cloud/opentelemetry-cloud-monitoring-exporter';
import { Logging, type Log } from '@google-cloud/logging';
import {
  hrTimeToMilliseconds,
  ExportResultCode,
  type ExportResult,
} from '@opentelemetry/core';
import type {
  ReadableLogRecord,
  LogRecordExporter,
} from '@opentelemetry/sdk-logs';
import type { ResourceMetrics } from '@opentelemetry/sdk-metrics';

/**
 * Google Cloud Trace exporter that extends the official trace exporter
 */
export class GcpTraceExporter extends TraceExporter {
  constructor(projectId?: string, credentials?: JWTInput) {
    super({
      projectId,
      credentials,
      resourceFilter: /^gcp\./,
    });
  }

  async forceFlush(): Promise<void> {
    return Promise.resolve();
  }
}

/**
 * Google Cloud Monitoring exporter that extends the official metrics exporter
 */
export class GcpMetricExporter extends MetricExporter {
  constructor(projectId?: string, credentials?: JWTInput) {
    super({
      projectId,
      credentials,
      prefix: 'custom.googleapis.com/gemini_cli',
    });
  }

  override export(
    metrics: ResourceMetrics,
    resultCallback: (result: ExportResult) => void,
  ): void {
    super.export(metrics, (result: ExportResult) => {
      if (result.code === ExportResultCode.FAILED && result.error) {
        // Suppress errors related to writing too frequently, as they are
        // expected when the CLI shuts down quickly after a periodic export.
        const errorMessage = result.error.message || String(result.error);
        if (
          process.env['GEMINI_STRICT_TELEMETRY_LIMITS'] === 'true' &&
          errorMessage.includes(
            'written more frequently than the maximum sampling period',
          )
        ) {
          resultCallback({ code: ExportResultCode.SUCCESS });
          return;
        }
      }
      resultCallback(result);
    });
  }
}

/**
 * Deeply truncates strings in an object to prevent GCP log size limit errors.
 */
function truncateLogPayload(payload: unknown, limit = 200000): unknown {
  if (typeof payload === 'string') {
    return payload.length > limit
      ? payload.substring(0, limit) + '... (truncated due to size)'
      : payload;
  }
  if (Array.isArray(payload)) {
    return payload.map((item) => truncateLogPayload(item, limit));
  }
  if (payload !== null && typeof payload === 'object') {
    const truncatedObj: Record<string, unknown> = {};
    for (const [key, value] of Object.entries(payload)) {
      // Keys are also strings, but usually small. Truncate values.
      truncatedObj[key] = truncateLogPayload(value, limit);
    }
    return truncatedObj;
  }
  return payload;
}

/**
 * Google Cloud Logging exporter that uses the Cloud Logging client
 */
export class GcpLogExporter implements LogRecordExporter {
  private logging: Logging;
  private log: Log;
  private pendingWrites: Array<Promise<void>> = [];

  constructor(projectId?: string, credentials?: JWTInput) {
    this.logging = new Logging({ projectId, credentials });
    this.log = this.logging.log('gemini_cli');
  }

  export(
    logs: ReadableLogRecord[],
    resultCallback: (result: ExportResult) => void,
  ): void {
    try {
      const entries = logs.map((log) => {
        const rawPayload = {
          ...log.attributes,
          ...log.resource?.attributes,
          message: log.body,
        };

        const isStrictTelemetry =
          process.env['GEMINI_STRICT_TELEMETRY_LIMITS'] === 'true';

        let finalPayload: unknown = rawPayload;

        if (isStrictTelemetry) {
          // Enforce a strict cap on the entire payload to avoid 256KB limit crashes in CI.
          let safePayload = truncateLogPayload(rawPayload, 10000);
          let payloadString = JSON.stringify(safePayload);

          if (payloadString && payloadString.length > 100000) {
            // If still too large, apply a stricter limit
            safePayload = truncateLogPayload(rawPayload, 2000);
            payloadString = JSON.stringify(safePayload);

            if (payloadString && payloadString.length > 100000) {
              safePayload = truncateLogPayload(rawPayload, 5000);
              payloadString = JSON.stringify(safePayload);

              if (payloadString && payloadString.length > 100000) {
                // Fallback: strip structure and send a truncated raw string
                safePayload = {
                  _warning: 'Payload heavily truncated due to strict limits',
                  data: payloadString.substring(0, 50000) + '... (truncated)',
                };
              }
            }
          }
          finalPayload = safePayload;
        }

        const entry = this.log.entry(
          {
            severity: this.mapSeverityToCloudLogging(log.severityNumber),
            timestamp: new Date(hrTimeToMilliseconds(log.hrTime)),
            resource: {
              type: 'global',
              labels: {
                project_id: this.logging.projectId,
              },
            },
          },
          // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
          finalPayload as Record<string, unknown>,
        );
        return entry;
      });

      const writePromise = this.log
        .write(entries)
        .then(() => {
          resultCallback({ code: ExportResultCode.SUCCESS });
        })
        .catch((error: Error) => {
          resultCallback({
            code: ExportResultCode.FAILED,
            error,
          });
        })
        .finally(() => {
          const index = this.pendingWrites.indexOf(writePromise);
          if (index > -1) {
            // eslint-disable-next-line @typescript-eslint/no-floating-promises
            this.pendingWrites.splice(index, 1);
          }
        });
      this.pendingWrites.push(writePromise);
    } catch (error) {
      resultCallback({
        code: ExportResultCode.FAILED,
        // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
        error: error as Error,
      });
    }
  }

  async forceFlush(): Promise<void> {
    if (this.pendingWrites.length > 0) {
      await Promise.all(this.pendingWrites);
    }
  }

  async shutdown(): Promise<void> {
    await this.forceFlush();
    this.pendingWrites = [];
  }

  private mapSeverityToCloudLogging(severityNumber?: number): string {
    if (!severityNumber) return 'DEFAULT';

    // Map OpenTelemetry severity numbers to Cloud Logging severity levels
    // https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitynumber
    if (severityNumber >= 21) return 'CRITICAL';
    if (severityNumber >= 17) return 'ERROR';
    if (severityNumber >= 13) return 'WARNING';
    if (severityNumber >= 9) return 'INFO';
    if (severityNumber >= 5) return 'DEBUG';
    return 'DEFAULT';
  }
}
