import { ExportResult } from "@opentelemetry/core";
import { OTLPExporterBase } from "@opentelemetry/otlp-exporter-base";
import { ReadableLogRecord } from "@opentelemetry/sdk-logs";
import {
  PushMetricExporter,
  ResourceMetrics,
} from "@opentelemetry/sdk-metrics";
import { ReadableSpan, SpanExporter } from "@opentelemetry/sdk-trace-base";
import { NodeSDKConfig } from "../index.js";
import {
  _cleanLogBodyPII,
  _cleanObjectPII,
  _cleanStringPII,
} from "../internals/pii-detection.js";

export class PIIExporterDecorator
  extends OTLPExporterBase<
    (ReadableSpan | ReadableLogRecord)[] | ResourceMetrics
  >
  implements SpanExporter, PushMetricExporter
{
  private readonly _exporter;
  private readonly _config;

  constructor(
    exporter: OTLPExporterBase<
      (ReadableSpan | ReadableLogRecord)[] | ResourceMetrics
    >,
    config: NodeSDKConfig,
  ) {
    super(exporter["_delegate"]);
    this._exporter = exporter;
    this._config = config;
  }

  forceFlush(): Promise<void> {
    return this._exporter.forceFlush();
  }

  shutdown(): Promise<void> {
    return this._exporter.shutdown();
  }

  export(
    items: (ReadableSpan | ReadableLogRecord)[] | ResourceMetrics,
    resultCallback: (result: ExportResult) => void,
  ): void {
    if (!this._config.detection?.email) {
      this._exporter.export(items, resultCallback);
      return;
    }

    if (Array.isArray(items)) {
      const redactedItem = items.map((item) => {
        if (this._isReadableSpan(item)) {
          this._redactSpan(item);
        } else if (this._isReadableLogRecord(item)) {
          item = this._redactLogRecord(item) as ReadableLogRecord;
        }
        return item;
      });

      this._exporter.export(redactedItem, resultCallback);
      return;
    }

    if (this._isResourceMetrics(items)) {
      this._redactResourceMetrics(items);
      this._exporter.export(items, resultCallback);
    }
  }

  private _isReadableSpan(span: unknown): span is ReadableSpan {
    return (
      typeof span === "object" &&
      span !== null &&
      "name" in span &&
      "kind" in span &&
      "spanContext" in span &&
      "attributes" in span
    );
  }

  private _isReadableLogRecord(span: unknown): span is ReadableLogRecord {
    return (
      typeof span === "object" &&
      span !== null &&
      "body" in span &&
      "attributes" in span &&
      "severityText" in span &&
      "severityNumber" in span
    );
  }

  private _isResourceMetrics(obj: unknown): obj is ResourceMetrics {
    return (
      typeof obj === "object" &&
      obj !== null &&
      !Array.isArray(obj) &&
      "resource" in obj &&
      "scopeMetrics" in obj
    );
  }

  private _redactSpan(span: ReadableSpan) {
    Object.assign(span, {
      name: _cleanStringPII(span.name, "trace"),
      attributes: span.attributes && _cleanObjectPII(span.attributes, "trace"),
      resource: {
        attributes:
          span?.resource?.attributes &&
          _cleanObjectPII(span.resource.attributes, "trace"),
      },
      links: span?.links?.map((link) => {
        Object.assign(link, {
          attributes:
            link?.attributes && _cleanObjectPII(link.attributes, "trace"),
        });
      }),
      events: span?.events?.map((event) => {
        Object.assign(event, {
          name: _cleanStringPII(event.name, "trace"),
          attributes:
            event?.attributes && _cleanObjectPII(event.attributes, "trace"),
        });
        return event;
      }),
    });
  }

  private _redactLogRecord(log: ReadableLogRecord) {
    return {
      ...log,
      body: _cleanLogBodyPII(log.body),
      attributes: log.attributes && _cleanObjectPII(log.attributes, "log"),
      resource: log.resource && {
        ...log.resource,
        attributes: _cleanObjectPII(log.resource.attributes, "log"),
      },
    };
  }

  private _redactResourceMetrics(metric: ResourceMetrics) {
    Object.assign(metric, {
      resource: {
        attributes:
          metric?.resource?.attributes &&
          _cleanObjectPII(metric.resource.attributes, "metric"),
      },
    });
  }
}
