// packages/core-shared/src/instrumentation/otel/pii-scrub-processor.ts
//
// PII scrub processors for OTel spans and log records.
// These run FIRST in their respective processor chains so downstream exporters
// (including the Sentry exporter) see scrubbed data. This replaces the old
// Sentry beforeSend / beforeSendTransaction hooks — scrubbing now
// happens at the OTel layer, vendor-agnostic.

import type {
  ReadableSpan,
  SpanProcessor,
} from "@opentelemetry/sdk-trace-base";
import type { Span } from "@opentelemetry/api";
import type { Context } from "@opentelemetry/api";
import type { LogRecord, LogRecordProcessor } from "@opentelemetry/sdk-logs";
import {
  PII_KEY_SUBSTRINGS,
  REDACTED_VALUE,
  IPV4_REGEX,
  IPV6_REGEX,
  REDACTED_IP,
} from "./pii-fields";

function isPiiKey(key: string): boolean {
  const lower = key.toLowerCase();
  return PII_KEY_SUBSTRINGS.some((s) => lower.includes(s));
}

function containsPiiSubstring(s: string): boolean {
  const lower = s.toLowerCase();
  return PII_KEY_SUBSTRINGS.some((sub) => lower.includes(sub));
}

/**
 * Scrubs IP addresses from a string value using regex replacement.
 * Called for attribute values whose KEYS did not match a PII substring — the
 * old Sentry beforeSend hook performed this kind of value-level scrubbing; we
 * replicate it here so IP addresses embedded in non-IP-keyed attributes
 * (e.g. "request.note": "from 10.0.0.1") are still redacted.
 */
function scrubValue(value: unknown): unknown {
  if (typeof value !== "string") return value;
  // The regexes are global (`/g`) so they must be reset between calls via new RegExp
  // or by relying on the fact that each string replace runs against a fresh lastIndex.
  // String.prototype.replace with a regex literal (global) resets lastIndex automatically.
  let scrubbed = value.replace(IPV4_REGEX, REDACTED_IP);
  scrubbed = scrubbed.replace(IPV6_REGEX, REDACTED_IP);
  return scrubbed;
}

function scrubAttributes(
  attrs: Record<string, unknown>,
): Record<string, unknown> {
  const out: Record<string, unknown> = {};
  for (const [key, value] of Object.entries(attrs)) {
    if (isPiiKey(key)) {
      out[key] = REDACTED_VALUE;
    } else {
      out[key] = scrubValue(value);
    }
  }
  return out;
}

/**
 * Runs FIRST in the span processor chain so downstream exporters see scrubbed attributes.
 * Redacts any span attribute whose key contains a PII substring (case-insensitive).
 * Attribute-key-based PII redaction.
 */
export class PiiScrubSpanProcessor implements SpanProcessor {
  forceFlush(): Promise<void> {
    return Promise.resolve();
  }

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

  onStart(_span: Span, _parentContext: Context): void {
    // no-op — scrub on completion when all attributes are set
  }

  onEnd(span: ReadableSpan): void {
    const scrubbed = scrubAttributes(
      span.attributes as Record<string, unknown>,
    );
    Object.assign(span.attributes, scrubbed);
  }
}

/**
 * Runs FIRST in the log processor chain.
 * - Strips PII from attributes (key-based substring match, case-insensitive).
 * - Strips PII from the log body string (substring match — if any PII substring
 *   appears in the body, the entire body is redacted to avoid partial leakage).
 * Attribute-key-based PII redaction; body-level redaction.
 */
export class PiiScrubLogRecordProcessor implements LogRecordProcessor {
  forceFlush(): Promise<void> {
    return Promise.resolve();
  }

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

  onEmit(record: LogRecord): void {
    if (record.attributes) {
      const scrubbed = scrubAttributes(
        record.attributes as Record<string, unknown>,
      );
      Object.assign(record.attributes, scrubbed);
    }
    if (typeof record.body === "string") {
      if (containsPiiSubstring(record.body)) {
        // Body contains a PII keyword (email, password, etc.) — redact entirely
        // to avoid partial leakage.
        record.body = REDACTED_VALUE;
      } else {
        // No PII keyword, but may still contain IP addresses embedded in text.
        // Apply value-level regex scrubbing.
        record.body = scrubValue(record.body) as string;
      }
    }
  }
}
