/**
 * Charset normalization for incoming email bodies. Two corrections, both
 * applied by rewriting the part's `charset=` declaration before the parser
 * decodes it:
 *
 *  1. Mis-declared UTF-8: PHPMailer-style senders declare `iso-8859-1`
 *     (the PHP default) but emit UTF-8 bytes. When the raw body really is
 *     valid UTF-8, rewrite the declaration to utf-8.
 *
 *  2. iso-8859-1 → windows-1252: senders that genuinely mean a legacy
 *     8-bit charset overwhelmingly emit Windows-1252, not strict
 *     ISO-8859-1 — smart quotes / em-dash / euro live in 0x80-0x9F, which
 *     ISO-8859-1 leaves as unusable C1 control codes. The WHATWG / browser
 *     standard is to decode `iso-8859-1` AS `windows-1252`; iconv-lite
 *     does not, so we rewrite the declaration. windows-1252 is a strict
 *     superset of printable ISO-8859-1, so a genuine ISO-8859-1 char is
 *     never lost.
 */

/** Returns either the original buffer (no change needed) or a copy with
 *  the leading charset declaration corrected. */
export function sniffAndFixCharset(raw: Buffer): Buffer {
    const HEAD_LIMIT = 16384;
    const head = raw.subarray(0, Math.min(HEAD_LIMIT, raw.length)).toString("latin1");
    const legacyRe = /charset\s*=\s*"?(iso-8859-1|us-ascii|windows-1252|latin1)"?/gi;
    if (!legacyRe.test(head)) return raw;

    // Correction 1 — mis-declared UTF-8. Only sound when the raw bytes ARE
    // the body bytes (an 8bit part): for a quoted-printable / base64 part
    // the raw .eml is pure ASCII, so isValidUtf8 passes vacuously and we'd
    // wrongly relabel a real Windows-1252 part as utf-8 (Bob 2026-05-21).
    // Requiring a real non-ASCII byte gates the UTF-8 sniff to where it
    // means something.
    if (hasNonAscii(raw) && isValidUtf8(raw)) {
        const fixed = head.replace(legacyRe, "charset=utf-8");
        return Buffer.concat([Buffer.from(fixed, "latin1"), raw.subarray(head.length)]);
    }

    // Correction 2 — iso-8859-1 / latin1 → windows-1252. Applies whether
    // the part is QP or 8bit; the rewrite is purely on the declaration.
    // (Bob 2026-05-22: an Intuit mail declared ISO-8859-1, QP-encoded a
    // 0x92 apostrophe → latin1 decode produced a U+0092 control char.)
    const isoRe = /charset\s*=\s*"?(iso-8859-1|latin1)"?/gi;
    if (isoRe.test(head)) {
        const fixed = head.replace(isoRe, "charset=windows-1252");
        return Buffer.concat([Buffer.from(fixed, "latin1"), raw.subarray(head.length)]);
    }

    return raw;
}

/** String form of sniffAndFixCharset — for the preview path, which parses
 *  the message as a string (extractPreview). Same two corrections, applied
 *  to the charset declaration in the text. The UTF-8-misdeclare correction
 *  can't be done on a string (the bytes are already decoded), so this only
 *  does correction 2 (iso-8859-1 → windows-1252), which is a pure
 *  declaration rewrite and the common case for previews. */
export function fixCharsetDeclString(s: string): string {
    return s.replace(/charset\s*=\s*"?(iso-8859-1|latin1)"?/gi, "charset=windows-1252");
}

/** True if the buffer contains at least one byte >= 0x80. A pure-ASCII
 *  buffer is trivially valid UTF-8, so the isValidUtf8 sniff tells us
 *  nothing about a quoted-printable / base64 body — gate on this first. */
function hasNonAscii(buf: Buffer): boolean {
    for (let i = 0; i < buf.length; i++) {
        if (buf[i] >= 0x80) return true;
    }
    return false;
}

/** Strict UTF-8 validity check: rejects overlong forms, invalid start
 *  bytes, and dangling continuations. Used to confirm the body is really
 *  UTF-8 before overriding a Latin-1 declaration. */
function isValidUtf8(buf: Buffer): boolean {
    let i = 0;
    while (i < buf.length) {
        const b = buf[i];
        if (b < 0x80) { i++; continue; }
        let need: number;
        if ((b & 0xE0) === 0xC0) { if (b < 0xC2) return false; need = 1; }
        else if ((b & 0xF0) === 0xE0) need = 2;
        else if ((b & 0xF8) === 0xF0) { if (b > 0xF4) return false; need = 3; }
        else return false;
        if (i + need >= buf.length) return false;
        for (let k = 1; k <= need; k++) {
            if ((buf[i + k] & 0xC0) !== 0x80) return false;
        }
        i += need + 1;
    }
    return true;
}
