/**
 * Compact, original profanity filter for nicknames and team names (ARCHITECTURE §2.9).
 *
 * Pipeline: NFKC → lowercase → strip diacritics → leetspeak (0→o 1→i 3→e 4→a 5→s 7→t @→a $→s,
 * and "!" between letters → i) → split into letter-only tokens. Runs of single letters are also
 * joined ("f u c k", "s.h.i.t") so spacing tricks don't help.
 *
 * Repeated letters are handled run-aware rather than by naive collapsing: each run of a letter
 * in a listed word becomes "at least that many" (shit → s+h+i+t+, ass → a+s{2,}). That catches
 * "fuuuuck" and "sshhiitt" without turning "ass" into "as" and blocking an ordinary word.
 *
 * Two lists keep false positives (the "Scunthorpe problem") down:
 *  - STRONG words match anywhere inside a token; they have essentially no innocent hosts, and the
 *    few that exist are neutralised first by SAFE_PATTERNS (Scunthorpe, shiitake, Penistone, the
 *    Japanese surname ending "-shita" as in Yamashita/Matsushita, …). Safe patterns match exactly,
 *    not run-aware, so a stretched spelling like "shiiitake" cannot smuggle a word through.
 *  - MILD words match whole tokens only, because they hide inside common words and names:
 *    ass (Cassandra, class), rape (grape, drape), rapist (therapist), cock (Hancock, cocktail),
 *    dick (Dickens, Dickinson), anal (analyst), cum (cucumber), spic (spice), coon (raccoon),
 *    milf (Milford). So "Dick" alone is blocked while "Dickens" is fine — a deliberate trade-off.
 * Plural/inflected forms of mild words are listed explicitly (no generic "+s": "spic" + "es" would
 * block "spices"). Known gaps (homoglyphs from other scripts, words split by spaces mid-word) are
 * covered by host moderation: hosts can rename or kick.
 */

const LEET: Readonly<Record<string, string>> = {
  '0': 'o',
  '1': 'i',
  '3': 'e',
  '4': 'a',
  '5': 's',
  '7': 't',
  '@': 'a',
  $: 's',
};

const STRONG: readonly string[] = [
  'fuck', 'phuck', 'shit', 'cunt', 'nigg', 'bitch', 'biatch', 'whore', 'slut', 'skank', 'twat', 'wank',
  'bastard', 'bollock', 'jizz', 'porn', 'penis', 'vagina', 'dildo', 'cocksuck', 'dickhead', 'asshole',
  'arsehole', 'faggot', 'blowjob', 'handjob', 'rimjob', 'cumshot', 'buttplug', 'jerkoff', 'pedophil',
  'paedophil', 'hitler', 'wetback', 'raghead',
];

const MILD: readonly string[] = [
  'ass', 'arse', 'dick', 'dicks', 'cock', 'cocks', 'pussy', 'pussies', 'tits', 'boob', 'boobs', 'cum', 'cums',
  'anal', 'anus', 'piss', 'pissed', 'prick', 'pricks', 'fuk', 'fck', 'fcuk', 'kunt', 'fag', 'fags', 'dyke',
  'dykes', 'tranny', 'spic', 'spics', 'kike', 'kikes', 'chink', 'chinks', 'coon', 'coons', 'gook', 'gooks',
  'paki', 'pakis', 'beaner', 'beaners', 'nazi', 'nazis', 'kkk', 'rape', 'raped', 'rapes', 'rapist',
  'retard', 'retards', 'retarded', 'pedo', 'pedos', 'paedo', 'cuck', 'cucks', 'thot', 'milf', 'milfs',
];

/** Innocent hosts of a STRONG word (exact regex sources over folded tokens); removed before matching. */
const SAFE_PATTERNS: readonly string[] = [
  'scunthorp', // Scunthorpe
  'shiitak', // shiitake
  'shitak', // common misspelling "shitake"
  'shiite',
  'shita$', // Japanese surnames: Yamashita, Kinoshita, Matsushita, Morishita
  'peniston', // Penistone
  'swank', // swank, swanky
  'wankel', // Wankel engine
  'niggl', // niggle
  'snigg', // snigger
];

/** Build a run-aware pattern: every run of a letter must appear at least as many times. */
function runPattern(word: string): string {
  let out = '';
  for (let i = 0; i < word.length; ) {
    const ch = word[i] as string;
    let j = i;
    while (word[j] === ch) j++;
    const n = j - i;
    out += n === 1 ? `${ch}+` : `${ch}{${n},}`;
    i = j;
  }
  return out;
}

const STRONG_RE = STRONG.map((w) => ({ word: w, re: new RegExp(runPattern(w), 'u') }));
const MILD_RE = MILD.map((w) => ({ word: w, re: new RegExp(`^${runPattern(w)}$`, 'u') }));
const SAFE_RE = new RegExp(SAFE_PATTERNS.join('|'), 'gu');

/** Lowercased, diacritic-free, leetspeak-decoded text (separators preserved). */
export function foldForProfanity(text: string): string {
  const base = text.normalize('NFKC').toLowerCase().normalize('NFD').replace(/\p{M}+/gu, '');
  const decoded = Array.from(base, (ch) => LEET[ch] ?? ch).join('');
  return decoded.replace(/(?<=\p{L})!(?=\p{L})/gu, 'i');
}

function tokensOf(folded: string): string[] {
  const words = folded.split(/[^\p{L}]+/u).filter((w) => w.length > 0);
  const tokens = [...words];
  // Join runs of single letters: "f u c k" / "s.h.i.t" → one token.
  let run = '';
  for (const w of [...words, '']) {
    if (Array.from(w).length === 1) {
      run += w;
      continue;
    }
    if (Array.from(run).length >= 2) tokens.push(run);
    run = '';
  }
  return tokens;
}

/** The listed word a text trips on, or null. Useful for logs and tests. */
export function findProfanity(text: string): string | null {
  for (const token of tokensOf(foldForProfanity(text))) {
    for (const { word, re } of MILD_RE) if (re.test(token)) return word;
    // Neutralise known-innocent hosts before looking for strong words inside the token.
    for (const piece of token.replace(SAFE_RE, ' ').split(' ')) {
      if (!piece) continue;
      for (const { word, re } of STRONG_RE) if (re.test(piece)) return word;
    }
  }
  return null;
}

export function containsProfanity(text: string): boolean {
  return findProfanity(text) !== null;
}
