const SUSPICIOUS_TEXT_PATTERN =
  /(?:\u00C3[\u0080-\u00BF]|\u00C2[\u0080-\u00BF]|\u00E2[\u0080-\u00BF]{2}|\u00F0[\u0080-\u00BF]{3}|\\u[0-9a-fA-F]{4}|\uFFFD)/;

const SUSPICIOUS_GLOBAL_PATTERN =
  /(?:\u00C3[\u0080-\u00BF]|\u00C2[\u0080-\u00BF]|\u00E2[\u0080-\u00BF]{2}|\u00F0[\u0080-\u00BF]{3}|\\u[0-9a-fA-F]{4}|\uFFFD)/g;

const UNICODE_ESCAPE_PATTERN = /\\u([0-9a-fA-F]{4})/g;
const UNSAFE_CONTROL_CHARACTERS_REGEX = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g;
const utf8Decoder = new TextDecoder("utf-8", { fatal: false });

function isPlainObject(value: unknown): value is Record<string, unknown> {
  if (value === null || typeof value !== "object") {
    return false;
  }

  const prototype = Object.getPrototypeOf(value);
  return prototype === Object.prototype || prototype === null;
}

function suspiciousScore(value: string): number {
  return value.match(SUSPICIOUS_GLOBAL_PATTERN)?.length ?? 0;
}

function decodeUnicodeEscapes(value: string): string {
  if (!value.includes("\\u")) {
    return value;
  }

  return value.replace(UNICODE_ESCAPE_PATTERN, (_, hex: string) =>
    String.fromCharCode(Number.parseInt(hex, 16)),
  );
}

function stripUnsafeControlCharacters(value: string): string {
  return value.replace(UNSAFE_CONTROL_CHARACTERS_REGEX, "");
}

function reinterpretLatin1AsUtf8(value: string): string {
  const bytes = Uint8Array.from(value, (character) => character.charCodeAt(0) & 0xff);
  return utf8Decoder.decode(bytes);
}

export function normalizeUtf8Text(value: string | null | undefined): string {
  if (typeof value !== "string" || value.length === 0) {
    return value ?? "";
  }

  let normalized = stripUnsafeControlCharacters(decodeUnicodeEscapes(value));
  let currentScore = suspiciousScore(normalized);

  if (currentScore === 0) {
    return normalized;
  }

  for (let attempt = 0; attempt < 3; attempt += 1) {
    const candidate = stripUnsafeControlCharacters(
      decodeUnicodeEscapes(reinterpretLatin1AsUtf8(normalized)),
    );
    const candidateScore = suspiciousScore(candidate);

    if (candidateScore >= currentScore) {
      break;
    }

    normalized = candidate;
    currentScore = candidateScore;

    if (currentScore === 0) {
      break;
    }
  }

  return stripUnsafeControlCharacters(normalized);
}

export function normalizeUtf8Deep<T>(value: T): T {
  if (typeof value === "string") {
    return normalizeUtf8Text(value) as T;
  }

  if (Array.isArray(value)) {
    return value.map((item) => normalizeUtf8Deep(item)) as T;
  }

  if (isPlainObject(value)) {
    return Object.fromEntries(
      Object.entries(value).map(([key, entryValue]) => [key, normalizeUtf8Deep(entryValue)]),
    ) as T;
  }

  return value;
}

export function ensureUtf8ContentType(contentType?: string | null): string {
  if (!contentType || contentType.trim().length === 0) {
    return "application/json; charset=UTF-8";
  }

  if (/charset=/i.test(contentType)) {
    return contentType;
  }

  return contentType + "; charset=UTF-8";
}

export function isUtf8NormalizableContentType(contentType?: string | null): boolean {
  return /application\/json|text\/|application\/xml|application\/csv|application\/javascript/i.test(
    contentType ?? "",
  );
}
