/**
 * FR-093 (2026-06-09) — Universal secret/key masking utility (bedrock level).
 *
 * Lehetove teszi az ALL-FDP-systems-szintu secret-szivargas-detection +
 * maszkolas-t egy bedrock-szinten. A korabban Overseer / runner-worker
 * helyi util-jai (FR-092) most ezt importaljak — egy SOR konfiguracio +
 * find-and-replace.
 *
 * User feedback ('az osszes rendszerunknek a lenyege az univerzalizalt,
 * mindenhol hasznalhato, egysegesített megoldások'): every secret-pattern
 * ennek a egyetlen DyFM_SecretMask_Util-nak a felelossege.
 *
 * Strategy — KETTOS:
 *   1. **PATTERN-based**: ismert secret-formatumokra automatikus felismeres.
 *   2. **VALUE-based**: a process.env-jenek secret-erteket overlay-eli minden szovegben.
 *
 * Identifiability config (`DyFM_SecretMask_Settings.partialReveal`):
 *   - DEFAULT: `{ prefix: 4, suffix: 4 }` — a kulcsok elso 4 + utolso 4 char-ja
 *     megorzodik, kozeppe `[redacted-<KIND>]` keruln, pl.:
 *       `ghp_abcdefghij1234567890abcdefghij12345678` → `ghp_abcd...5678 [redacted-github-pat]`
 *   - User-extensible: csokkentett vagy 0-os reveal a tobb-biztonsagos
 *     deploymentekhez (`{ prefix: 0, suffix: 0 }` → tisztan opaque).
 *
 * Per memory `[[feedback_rich_error_handling]]`: a maszkolt token jelzes ne
 * legyen `***` opaque; legyen `[redacted-<KIND>]` hogy ranezessel lassuk MILYEN
 * tipusu szivargast szuntunk meg.
 *
 * Pattern-mapping per memory `[[feedback_individual_plans]]` — single source of
 * truth: minden uj secret-format hozzaadando a `DyFM_SECRET_PATTERNS` array-hez;
 * a tobbi rendszerben (Overseer/runner/bedrock packages) automatikusan
 * megjelenik a fsm-bump utan.
 */

export interface DyFM_SecretMask_PartialReveal_Interface {
  /** Megorzendo elso karakter-szam (0 = teljesen elrejt). */
  prefix: number;
  /** Megorzendo utolso karakter-szam (0 = teljesen elrejt). */
  suffix: number;
}

export interface DyFM_SecretMask_Settings_Interface {
  /**
   * Identifiability-controls. Default `{ prefix: 4, suffix: 4 }`. Beallitva
   * pl. `{ prefix: 0, suffix: 0 }` opaque maszkolas-hoz.
   */
  partialReveal: DyFM_SecretMask_PartialReveal_Interface;

  /**
   * Min karakter-hossz amire az env-overlay alkalmaz. Rovid ertekek false
   * positivot adnak (pl. `KEY=foo` ahol foo a project-name). Default: 8.
   */
  minEnvValueLength: number;

  /**
   * `process.env`-bol kulcsneveket szuro regex. Default match: minden olyan
   * env-var ahol a NEV titkos-jellegu (TOKEN/SECRET/KEY/PASSWORD/PAT/HMAC stb.).
   */
  envNameSecretRx: RegExp;
}

export const DyFM_DEFAULT_SECRET_MASK_SETTINGS: DyFM_SecretMask_Settings_Interface = {
  partialReveal: {
    prefix: 4,
    suffix: 4,
  },
  minEnvValueLength: 8,
  envNameSecretRx:  /(?:TOKEN|SECRET|KEY|PASSWORD|PASSWD|PAT|HMAC|AUTH|PRIVATE|CREDENTIAL|API_KEY|WEBHOOK)/i,
};

interface SecretPattern_Interface {
  /** Diagnosztikai cimke a `[redacted-<KIND>]` markerben. */
  kind: string;
  /** Detekcios regex (global flag is REQUIRED — a global flow minden match-et kezel). */
  regex: RegExp;
  /**
   * `redactWith` — meghivva minden match-re a "matched" stringgel + settings-szel.
   * Visszaad a csere-string-et. Default: partial-reveal-szel `<prefix>...<suffix> [redacted-<KIND>]`.
   */
  redactWith?: (matched: string, settings: DyFM_SecretMask_Settings_Interface) => string;
  /**
   * `wrapPrefix` — ha a regex captures (`$1` group-ja stb.) a literal-prefixet jelzi
   * (pl. `Bearer ` a Bearer-token-nel), akkor a redact-result `[$1][reveal][marker]`-rel
   * kerul ossze; a regex group az `wrapPrefix` index-en mutatott group. Default 0 = no wrap.
   */
  wrapPrefix?: number;
}

const defaultRevealReplacement = (
  matched: string,
  kind: string,
  settings: DyFM_SecretMask_Settings_Interface,
): string => {
  const p: number = Math.max(0, settings.partialReveal.prefix);
  const s: number = Math.max(0, settings.partialReveal.suffix);
  // Ha a kulcs tul rovid az osszes reveal-osszeghez kepest, ne reveal-juk
  // (kulonben az egesz secret elerhetonek osszetapadna).
  if (matched.length <= p + s + 4) {
    return `[redacted-${kind}]`;
  }
  if (p === 0 && s === 0) {
    return `[redacted-${kind}]`;
  }
  const head: string = matched.slice(0, p);
  const tail: string = matched.slice(matched.length - s);
  return `${head}...${tail} [redacted-${kind}]`;
};

export const DyFM_SECRET_PATTERNS: SecretPattern_Interface[] = [
  // URL-embedded basic auth — sajat replace-pattern megorzi a scheme-et + host-ot.
  {
    kind:        'url-cred',
    regex:       /\b(https?:\/\/)([^/\s:@]+):([^\s@]+)@/g,
    redactWith:  (_m: string): string => '$1[redacted-url-cred]@',
  },
  // GitHub PATs
  { kind: 'github-pat', regex: /\bgh[ps]_[A-Za-z0-9]{36,}\b/g },
  { kind: 'github-pat-fine', regex: /\bgithub_pat_[A-Za-z0-9_]{82,}\b/g },
  { kind: 'github-oauth', regex: /\bgho_[A-Za-z0-9]{36,}\b/g },
  { kind: 'github-refresh', regex: /\bghr_[A-Za-z0-9]{36,}\b/g },
  // NPM token
  { kind: 'npm-token', regex: /\bnpm_[A-Za-z0-9]{36,}\b/g },
  // Docker Hub PAT
  { kind: 'docker-pat', regex: /\bdckr_pat_[A-Za-z0-9_\-]{20,}\b/g },
  // Slack token
  { kind: 'slack-token', regex: /\bxox[abprs]-[A-Za-z0-9-]{10,}\b/g },
  // Anthropic key (BEFORE OpenAI — `sk-ant-*` more specific than `sk-*`)
  { kind: 'anthropic-key', regex: /\bsk-ant-[A-Za-z0-9\-_]{32,}\b/g },
  // OpenAI / generic `sk-*` key
  { kind: 'openai-key', regex: /\bsk-[A-Za-z0-9]{32,}\b/g },
  // Google API key
  { kind: 'google-api-key', regex: /\bAIza[0-9A-Za-z\-_]{35}\b/g },
  // Discord webhook URL (id + token in URL)
  {
    kind:       'discord-webhook',
    regex:      /\bhttps?:\/\/(?:discord(?:app)?\.com|discordapp\.com)\/api\/webhooks\/\d+\/[A-Za-z0-9_\-]+/g,
    redactWith: (_m: string): string => '[redacted-discord-webhook]',
  },
  // Bearer token in HTTP header style — megorizzuk a 'Bearer ' prefixet (literally), a token-t pedig reveal-eljuk.
  {
    kind:       'bearer-token',
    regex:      /\b(Bearer)\s+([A-Za-z0-9\-._~+/=]{20,})/gi,
    redactWith: (matched: string, settings: DyFM_SecretMask_Settings_Interface): string => {
      const tokenStart: number = matched.search(/\s+/);
      const prefix: string = matched.slice(0, tokenStart + 1); // 'Bearer '
      const token: string = matched.slice(tokenStart + 1);
      return prefix + defaultRevealReplacement(token, 'token', settings);
    },
  },
  // JWT (eyJ + .* + .*)
  { kind: 'jwt', regex: /\beyJ[A-Za-z0-9_\-]{10,}\.[A-Za-z0-9_\-]{10,}\.[A-Za-z0-9_\-]{10,}\b/g },
  // AWS Access Key
  { kind: 'aws-access-key', regex: /\bAKIA[0-9A-Z]{16}\b/g },
  // Env-style secret-name = value
  {
    kind:       'env-style-secret',
    regex:      /\b([A-Z][A-Z0-9_]*(?:TOKEN|SECRET|KEY|PASSWORD|PASSWD|PAT|HMAC|AUTH|PRIVATE)[A-Z0-9_]*)\s*[:=]\s*['"]?([^\s'"]+)['"]?/g,
    redactWith: (matched: string, settings: DyFM_SecretMask_Settings_Interface): string => {
      // Pull out the key name + value via re-match (avoids capturing groups in redactWith signature)
      const m: RegExpMatchArray | null = matched.match(/\b([A-Z][A-Z0-9_]*(?:TOKEN|SECRET|KEY|PASSWORD|PASSWD|PAT|HMAC|AUTH|PRIVATE)[A-Z0-9_]*)\s*[:=]\s*['"]?([^\s'"]+)/);
      if (!m) { return matched; }
      const keyName: string = m[1];
      const value: string = m[2];
      const sep: string = matched.includes(': ') ? '=' : '=';
      const revealed: string = defaultRevealReplacement(value, 'env-value', settings);
      return `${keyName}${sep}${revealed}`;
    },
  },
];

/**
 * `process.env`-bol kigyujti azokat a kulcsokat ahol a NEV titkos-jellegu
 * ÉS a value >= minEnvValueLength. Visszaad egy listat — ezeket szovegben
 * minden elofordulasban partial-reveal-el felulrak-juk.
 */
const collectEnvSecretValues = (
  settings: DyFM_SecretMask_Settings_Interface,
): { value: string; key: string }[] => {
  const out: { value: string; key: string }[] = [];
  for (const [key, raw] of Object.entries(process.env)) {
    if (!raw || raw.length < settings.minEnvValueLength) { continue; }
    if (!settings.envNameSecretRx.test(key)) { continue; }
    if (/^(true|false|0|1)$/i.test(raw)) { continue; }
    out.push({ value: raw, key: key });
  }
  // Sort by value length DESC — hosszabb ertekekkel kezdjuk, igy egy rovid
  // prefix nem sealeze el a hosszabb kornyezetet.
  out.sort((a, b): number => b.value.length - a.value.length);
  return out;
};

/**
 * `DyFM_maskSecrets` — universal pre-Discord-send + pre-log-persist masking.
 *
 * @param text bemenet (ha nem string, JSON-szerializalaskor maskoljuk)
 * @param settings  konfiguracio (default a DyFM_DEFAULT_SECRET_MASK_SETTINGS)
 * @returns maszkolt szoveg
 */
export function DyFM_maskSecrets(
  text: unknown,
  settings: DyFM_SecretMask_Settings_Interface = DyFM_DEFAULT_SECRET_MASK_SETTINGS,
): string {
  if (text === null || text === undefined) { return ''; }
  let str: string;
  if (typeof text === 'string') {
    str = text;
  } else {
    try { str = JSON.stringify(text); }
    catch { str = String(text); }
  }

  // 1. PATTERN-based maszkolas
  for (const p of DyFM_SECRET_PATTERNS) {
    str = str.replace(p.regex, (matched: string, ...groups: string[]): string => {
      // Drop the `offset` + `wholeString` args at the end of `groups`
      if (p.redactWith) {
        return p.redactWith(matched, settings)
          .replace(/\$1/g, groups[0] ?? '')
          .replace(/\$2/g, groups[1] ?? '');
      }
      // Default: partial-reveal with kind
      return defaultRevealReplacement(matched, p.kind, settings);
    });
  }

  // 2. ENV-secret value-alapu overlay
  const envSecrets: { value: string; key: string }[] = collectEnvSecretValues(settings);
  for (const { value, key } of envSecrets) {
    if (!str.includes(value)) { continue; }
    const revealed: string = defaultRevealReplacement(value, `env-${key}`, settings);
    str = str.split(value).join(revealed);
  }

  return str;
}

/**
 * Recursive object-masker — JSON-szeru struktura minden string-ertekeben alkalmazza
 * a DyFM_maskSecrets-t. Pl. errors-table additionalContent / logTail tomb / cdpReport
 * objektum elotti masking-hez.
 */
export function DyFM_maskSecretsDeep<T = unknown>(
  input: T,
  settings: DyFM_SecretMask_Settings_Interface = DyFM_DEFAULT_SECRET_MASK_SETTINGS,
): T {
  if (input === null || input === undefined) { return input; }
  if (typeof input === 'string') {
    return DyFM_maskSecrets(input, settings) as unknown as T;
  }
  if (typeof input !== 'object') { return input; }
  if (Array.isArray(input)) {
    return (input as unknown[]).map((it: unknown): unknown => DyFM_maskSecretsDeep(it, settings)) as unknown as T;
  }
  const out: Record<string, unknown> = {};
  for (const [k, v] of Object.entries(input as Record<string, unknown>)) {
    out[k] = DyFM_maskSecretsDeep(v, settings);
  }
  return out as unknown as T;
}
