import type { AnyValue, AnyValueMap } from "@opentelemetry/api-logs";
import { _getPIICounterRedactionMetric } from "./shared-metrics.js";

const EMAIL_REGEX = /[a-zA-Z0-9._%+-]+@([a-zA-Z0-9.-]+\.[a-z]{2,})/gi;

const decoder = new TextDecoder();
const encoder = new TextEncoder();

export type PIISource = "trace" | "log" | "metric";

/**
 * Redacts all email addresses in the input string and collects metadata.
 *
 * @param {string} value The input string potentially containing email addresses.
 * @returns {{
 *   redacted: string,
 *   count: number,
 *   domains: Record<string, number>
 * }}
 *
 * An object containing:
 *   - `redacted`: the string with email addresses replaced by `[REDACTED EMAIL]`
 *   - `count`: total number of email addresses redacted
 *   - `domains`: a map of domain names to the number of times they were redacted
 */
function _redactEmails(value: string): {
  redacted: string;
  count: number;
  domains: Record<string, number>;
} {
  let count = 0;
  const domains: Record<string, number> = {};

  const redacted = value.replace(EMAIL_REGEX, (_, domain) => {
    count++;
    domains[domain] = (domains[domain] || 0) + 1;
    return "[REDACTED EMAIL]";
  });

  return { redacted, count, domains };
}

/**
 * Checks whether a string contains URI-encoded components.
 *
 * @param {string} value - The string to inspect.
 * @returns {boolean} `true` if the string is encoded, `false` otherwise.
 */
function _containsEncodedComponents(value: string) {
  try {
    return decodeURI(value) !== decodeURIComponent(value);
  } catch {
    return false;
  }
}

/**
 * Cleans a string by redacting email addresses and emitting metrics for PII.
 *
 * If the string is URL-encoded, it will be decoded before redaction.
 * Metrics are emitted for each domain found in redacted email addresses.
 *
 * @param {string} value - The input string to sanitize.
 * @param {"trace" | "log"} source - The source context of the input, used in metrics.
 * @returns {string} The cleaned string with any email addresses replaced by `[REDACTED EMAIL]`.
 */
export function _cleanStringPII(value: AnyValue, source: PIISource): AnyValue {
  if (Array.isArray(value)) {
    return value.map((v) => _cleanStringPII(v, source));
  }

  if (typeof value !== "string") {
    return value;
  }

  let kind: "string" | "url" = "string";
  let decodedValue = value;

  if (_containsEncodedComponents(value)) {
    decodedValue = decodeURIComponent(value);
    kind = "url";
  }

  const { redacted, count, domains } = _redactEmails(decodedValue);

  if (count > 0) {
    for (const [domain, domainCount] of Object.entries(domains)) {
      _getPIICounterRedactionMetric().add(domainCount, {
        pii_type: "email",
        redaction_source: source,
        pii_email_domain: domain,
        pii_format: kind,
      });
    }
  }
  return redacted;
}

export function _cleanObjectPII(entry: object, source: PIISource) {
  if (!entry) {
    return entry;
  }

  return Object.fromEntries(
    Object.entries(entry).map(([k, v]) => [k, _cleanStringPII(v, source)]),
  );
}

export function _cleanLogBodyPII(value: AnyValue): AnyValue {
  if (typeof value === "string") {
    return _cleanStringPII(value, "log");
  }

  if (
    typeof value === "number" ||
    typeof value === "boolean" ||
    value == null
  ) {
    return value;
  }

  if (value instanceof Uint8Array) {
    try {
      const decoded = decoder.decode(value);
      const sanitized = _cleanStringPII(decoded, "log") as string;
      return encoder.encode(sanitized);
    } catch {
      return value;
    }
  }

  if (Array.isArray(value)) {
    return value.map(_cleanLogBodyPII);
  }

  if (typeof value === "object") {
    const sanitized: AnyValueMap = {};
    for (const [key, val] of Object.entries(value)) {
      sanitized[key] = _cleanLogBodyPII(val);
    }
    return sanitized;
  }

  return value;
}
