/**
 * Nickname and team-name rules (ARCHITECTURE §2.9): trim, collapse whitespace, NFKC; 2–16
 * characters (24 for team names); letters of any script, digits, space and . _ - ' !; no URLs;
 * no profanity. Lengths count Unicode code points so non-Latin names aren't penalised.
 */
import { containsProfanity } from './profanity';
import { MAX_NICKNAME_LENGTH, MAX_TEAM_NAME_LENGTH, MIN_NICKNAME_LENGTH } from './types';

export type NameRejection = 'TOO_SHORT' | 'TOO_LONG' | 'INVALID_CHARACTERS' | 'URL' | 'RESERVED' | 'PROFANITY';

export type NameValidation =
  | { ok: true; /** The normalized name to store. */ nickname: string }
  | { ok: false; reason: NameRejection; message: string };

/** Letters (with combining marks for scripts that need them), decimal digits, space, . _ - ' ! */
const ALLOWED = /^[\p{L}\p{M}\p{Nd} ._'!-]+$/u;
const HAS_ALNUM = /[\p{L}\p{Nd}]/u;
const URL_LIKE =
  /https?|^www\b|[\p{L}\p{Nd}] ?\. ?(?:com|net|org|io|co|uk|us|ca|de|fr|gg|tv|me|ly|app|xyz|info|biz|ru|cn|site|online|link|club|shop|live|pro|to|ai)(?![\p{L}\p{Nd}])/iu;
/** Names that could impersonate staff or the product on the TV. */
const RESERVED = new Set(['host', 'admin', 'administrator', 'moderator', 'mod', 'staff', 'system', 'pintrush', 'bartender']);

const codePoints = (s: string): number => Array.from(s).length;

export function normalizeNickname(raw: string): string {
  return raw.normalize('NFKC').replace(/\s+/gu, ' ').trim();
}

/** Case-insensitive identity used for uniqueness ("Mike" and "MIKE" collide). */
export function nicknameKey(name: string): string {
  return normalizeNickname(name).toLowerCase();
}

function validateName(raw: string, maxLength: number, what: string): NameValidation {
  const name = normalizeNickname(raw);
  const len = codePoints(name);
  if (len < MIN_NICKNAME_LENGTH) return { ok: false, reason: 'TOO_SHORT', message: `${what} needs at least ${MIN_NICKNAME_LENGTH} characters` };
  if (len > maxLength) return { ok: false, reason: 'TOO_LONG', message: `${what} can be at most ${maxLength} characters` };
  if (!ALLOWED.test(name) || !HAS_ALNUM.test(name)) {
    return { ok: false, reason: 'INVALID_CHARACTERS', message: `${what} may use letters, numbers, spaces and . _ - ' !` };
  }
  if (URL_LIKE.test(name)) return { ok: false, reason: 'URL', message: `${what} can't contain a web address` };
  if (RESERVED.has(name.toLowerCase())) return { ok: false, reason: 'RESERVED', message: `${what} is reserved` };
  if (containsProfanity(name)) return { ok: false, reason: 'PROFANITY', message: `${what} isn't allowed` };
  return { ok: true, nickname: name };
}

export function validateNickname(raw: string): NameValidation {
  return validateName(raw, MAX_NICKNAME_LENGTH, 'Nickname');
}

export function validateTeamName(raw: string): NameValidation {
  return validateName(raw, MAX_TEAM_NAME_LENGTH, 'Team name');
}

/**
 * First free variant of `name`: "Mike", then "Mike 2", "Mike 3", … The base is trimmed so the
 * result never exceeds 16 characters. `takenKeys` holds nicknameKey() values already in use.
 */
export function dedupeNickname(name: string, takenKeys: Iterable<string>): string {
  const taken = new Set<string>();
  for (const k of takenKeys) taken.add(nicknameKey(k));
  const base = normalizeNickname(name);
  if (!taken.has(nicknameKey(base))) return base;
  // Terminates: at most taken.size candidates can collide.
  for (let n = 2; ; n++) {
    const suffix = ` ${n}`;
    const room = Math.max(1, MAX_NICKNAME_LENGTH - suffix.length);
    const trimmed = Array.from(base).slice(0, room).join('').trimEnd();
    const candidate = `${trimmed}${suffix}`;
    if (!taken.has(nicknameKey(candidate))) return candidate;
  }
}
