/**
 * Shared, pure byte-level binary detector. No file/network IO — callers pass a
 * head Buffer (Read reads it from disk, WebFetch streams it from the response).
 *
 * Detection order (highest precision first):
 *   1. magic-byte signature        → binary
 *   2. UTF-8 BOM                    → text(utf-8)
 *   3. UTF-16 BOM                   → text(utf-16le|be)
 *   4. NUL byte in head            → binary
 *   5. strict UTF-8 decode failure → binary
 *   6. control-char ratio > 0.30   → binary
 *   7. otherwise                   → text(utf-8)
 *
 * Note: BOMless UTF-16 is treated as binary (NUL bytes trip step 4); only BOM-tagged UTF-16 is recognized as text.
 */

export const SNIFF_HEAD_BYTES = 8 * 1024;
export const CONTROL_CHAR_RATIO_THRESHOLD = 0.3;

export type TextEncodingLabel = 'utf-8' | 'utf-16le' | 'utf-16be';

export type BinaryVerdict =
  | { binary: true; reason: string }
  | { binary: false; encoding: TextEncodingLabel };

const MAGIC_SIGNATURES: Array<{ reason: string; bytes: number[] }> = [
  { reason: 'magic:ole2', bytes: [0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1] },
  { reason: 'magic:zip', bytes: [0x50, 0x4b, 0x03, 0x04] },
  { reason: 'magic:zip', bytes: [0x50, 0x4b, 0x05, 0x06] },
  { reason: 'magic:zip', bytes: [0x50, 0x4b, 0x07, 0x08] },
  { reason: 'magic:pdf', bytes: [0x25, 0x50, 0x44, 0x46, 0x2d] },
  { reason: 'magic:png', bytes: [0x89, 0x50, 0x4e, 0x47] },
  { reason: 'magic:jpeg', bytes: [0xff, 0xd8, 0xff] },
  { reason: 'magic:gif', bytes: [0x47, 0x49, 0x46, 0x38] },
  { reason: 'magic:gzip', bytes: [0x1f, 0x8b] },
  { reason: 'magic:7z', bytes: [0x37, 0x7a, 0xbc, 0xaf, 0x27, 0x1c] },
  { reason: 'magic:rar', bytes: [0x52, 0x61, 0x72, 0x21] },
  { reason: 'magic:riff', bytes: [0x52, 0x49, 0x46, 0x46] },
];

function startsWith(head: Buffer, sig: number[]): boolean {
  if (head.length < sig.length) return false;
  for (let i = 0; i < sig.length; i++) {
    if (head[i] !== sig[i]) return false;
  }
  return true;
}

function matchMagic(head: Buffer): string | null {
  for (const { reason, bytes } of MAGIC_SIGNATURES) {
    if (startsWith(head, bytes)) return reason;
  }
  return null;
}

function controlCharRatio(head: Buffer): number {
  if (head.length === 0) return 0;
  let ctrl = 0;
  for (const byte of head) {
    if (
      byte <= 0x08 ||
      byte === 0x0b ||
      byte === 0x0c ||
      (byte >= 0x0e && byte <= 0x1f) ||
      byte === 0x7f
    ) {
      ctrl++;
    }
  }
  return ctrl / head.length;
}

/**
 * Strict text decode. Returns the decoded string, or null if the bytes are not
 * valid in the given encoding. Uses streaming mode so a multibyte sequence that
 * is merely *truncated* at the buffer boundary (e.g. an 8KB / 5MB cut) does not
 * count as invalid — only genuinely malformed mid-stream bytes fail.
 */
export function decodeText(buf: Buffer, encoding: TextEncodingLabel): string | null {
  if (encoding === 'utf-16be' && !utf16beSupported()) {
    // Node small-icu builds lack the 'utf-16be' label. Byte-swap BE→LE and
    // decode as utf-16le (a correct recovery, not a guess).
    const swapped = Buffer.from(buf); // copy so we don't mutate the caller's buffer
    for (let i = 0; i + 1 < swapped.length; i += 2) {
      const tmp = swapped[i];
      swapped[i] = swapped[i + 1];
      swapped[i + 1] = tmp;
    }
    return decodeStrict(swapped, 'utf-16le');
  }
  return decodeStrict(buf, encoding);
}

function decodeStrict(buf: Buffer, encoding: TextEncodingLabel): string | null {
  try {
    return new TextDecoder(encoding, { fatal: true }).decode(buf, { stream: true });
  } catch {
    return null;
  }
}

let utf16beSupportedCache: boolean | null = null;
function utf16beSupported(): boolean {
  if (utf16beSupportedCache === null) {
    try {
      new TextDecoder('utf-16be');
      utf16beSupportedCache = true;
    } catch {
      utf16beSupportedCache = false;
    }
  }
  return utf16beSupportedCache;
}

export function looksLikeBinaryBytes(head: Buffer): BinaryVerdict {
  const magic = matchMagic(head);
  if (magic) return { binary: true, reason: magic };

  if (startsWith(head, [0xef, 0xbb, 0xbf])) return { binary: false, encoding: 'utf-8' };
  if (startsWith(head, [0xff, 0xfe])) return { binary: false, encoding: 'utf-16le' };
  if (startsWith(head, [0xfe, 0xff])) return { binary: false, encoding: 'utf-16be' };

  if (head.includes(0)) return { binary: true, reason: 'nul-byte' };

  if (head.length > 0 && decodeText(head, 'utf-8') === null) {
    return { binary: true, reason: 'utf8-decode-fail' };
  }

  const ratio = controlCharRatio(head);
  if (ratio > CONTROL_CHAR_RATIO_THRESHOLD) {
    return { binary: true, reason: `control-ratio:${ratio.toFixed(2)}` };
  }

  return { binary: false, encoding: 'utf-8' };
}
