const SENSITIVE_KEY_NAMES = new Set([
  'password',
  'pass',
  'token',
  'secret',
  'apikey',
  'api_key',
  'auth_token',
  'authtoken',
  'access_token',
  'accesstoken',
  'authorization',
  'master_key',
  'masterkey',
  'private_key',
  'privatekey',
  'x-zyphon-app-key',
]);

const SECRET_REGEX_PATTERNS = [
  /Bearer\s+[a-zA-Z0-9_\-\.]+/gi,
  /zy_[0-9a-fA-F]{32}/g,
  /SG\.[A-Za-z0-9_\-]{20,}/g,
  /AC[0-9a-fA-F]{32}/g,
  /EAAB[A-Za-z0-9]+/g,
  /sk-[a-zA-Z0-9_\-]{16,}/g,
  /AKIA[0-9A-Z]{16}/g,
];

export class SecretRedactor {
  private knownSecrets = new Set<string>();

  /**
   * Register a dynamic secret value so it is scrubbed from any output.
   */
  public registerSecret(secret: string): void {
    if (secret && typeof secret === 'string' && secret.trim().length >= 4) {
      this.knownSecrets.add(secret.trim());
    }
  }

  public unregisterSecret(secret: string): void {
    if (secret) {
      this.knownSecrets.delete(secret.trim());
    }
  }

  public clearSecrets(): void {
    this.knownSecrets.clear();
  }

  /**
   * Redact sensitive strings and registered secrets from a raw string.
   */
  public redactString(input: string): string {
    if (!input || typeof input !== 'string') return input;

    let result = input;

    // 1. Scrub registered known secrets
    for (const secret of this.knownSecrets) {
      if (result.includes(secret)) {
        result = result.replaceAll(secret, '[REDACTED]');
      }
    }

    // 2. Scrub regex patterns
    for (const pattern of SECRET_REGEX_PATTERNS) {
      result = result.replaceAll(pattern, '[REDACTED]');
    }

    return result;
  }

  /**
   * Deeply redact an object, array, or primitive.
   */
  public redact<T>(value: T): T {
    if (value === null || value === undefined) {
      return value;
    }

    if (typeof value === 'string') {
      return this.redactString(value) as unknown as T;
    }

    if (typeof value !== 'object') {
      return value;
    }

    if (Array.isArray(value)) {
      return value.map((item) => this.redact(item)) as unknown as T;
    }

    if (value instanceof Error) {
      const clonedError = new Error(this.redactString(value.message));
      clonedError.name = value.name;
      if (value.stack) {
        clonedError.stack = this.redactString(value.stack);
      }
      return clonedError as unknown as T;
    }

    const output: Record<string, unknown> = {};
    for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
      const lowerKey = key.toLowerCase();
      if (SENSITIVE_KEY_NAMES.has(lowerKey)) {
        output[key] = '[REDACTED]';
      } else {
        output[key] = this.redact(val);
      }
    }

    return output as T;
  }
}

export const globalRedactor = new SecretRedactor();

