use std::borrow::Cow;
// HTML named character references — HTML4 + common HTML5 (245 entries)
// Mirrors packages/js/src/entities.ts for parity between JS and Rust engines.
// Source: https://html.spec.whatwg.org/entities.json
//
// Intentionally omitted:
//   &emsp; &ensp; &thinsp; — space variants cause table cell layout issues
//   &lrm; &rlm; — invisible bidi formatting characters
//   &shy; — invisible soft hyphen
//   &zwj; &zwnj; — zero-width formatting characters

/// Look up a named HTML entity (without `&` prefix and `;` suffix).
/// Returns the decoded character, or `None` if unknown.
#[allow(clippy::too_many_lines)]
pub(crate) fn lookup_named_entity(name: &[u8]) -> Option<char> {
    match name {
        b"aacute" => Some('\u{00E1}'),
        b"Aacute" => Some('\u{00C1}'),
        b"acirc" => Some('\u{00E2}'),
        b"Acirc" => Some('\u{00C2}'),
        b"acute" => Some('\u{00B4}'),
        b"aelig" => Some('\u{00E6}'),
        b"AElig" => Some('\u{00C6}'),
        b"agrave" => Some('\u{00E0}'),
        b"Agrave" => Some('\u{00C0}'),
        b"alefsym" => Some('\u{2135}'),
        b"alpha" => Some('\u{03B1}'),
        b"Alpha" => Some('\u{0391}'),
        b"amp" => Some('&'),
        b"and" => Some('\u{2227}'),
        b"ang" => Some('\u{2220}'),
        b"apos" => Some('\''),
        b"aring" => Some('\u{00E5}'),
        b"Aring" => Some('\u{00C5}'),
        b"asymp" => Some('\u{2248}'),
        b"atilde" => Some('\u{00E3}'),
        b"Atilde" => Some('\u{00C3}'),
        b"auml" => Some('\u{00E4}'),
        b"Auml" => Some('\u{00C4}'),
        b"bdquo" => Some('\u{201E}'),
        b"beta" => Some('\u{03B2}'),
        b"Beta" => Some('\u{0392}'),
        b"brvbar" => Some('\u{00A6}'),
        b"bull" => Some('\u{2022}'),
        b"cap" => Some('\u{2229}'),
        b"ccedil" => Some('\u{00E7}'),
        b"Ccedil" => Some('\u{00C7}'),
        b"cedil" => Some('\u{00B8}'),
        b"cent" => Some('\u{00A2}'),
        b"chi" => Some('\u{03C7}'),
        b"Chi" => Some('\u{03A7}'),
        b"circ" => Some('\u{02C6}'),
        b"clubs" => Some('\u{2663}'),
        b"cong" => Some('\u{2245}'),
        b"copy" => Some('\u{00A9}'),
        b"crarr" => Some('\u{21B5}'),
        b"cup" => Some('\u{222A}'),
        b"curren" => Some('\u{00A4}'),
        b"dagger" => Some('\u{2020}'),
        b"Dagger" => Some('\u{2021}'),
        b"darr" => Some('\u{2193}'),
        b"dArr" => Some('\u{21D3}'),
        b"deg" => Some('\u{00B0}'),
        b"delta" => Some('\u{03B4}'),
        b"Delta" => Some('\u{0394}'),
        b"diams" => Some('\u{2666}'),
        b"divide" => Some('\u{00F7}'),
        b"eacute" => Some('\u{00E9}'),
        b"Eacute" => Some('\u{00C9}'),
        b"ecirc" => Some('\u{00EA}'),
        b"Ecirc" => Some('\u{00CA}'),
        b"egrave" => Some('\u{00E8}'),
        b"Egrave" => Some('\u{00C8}'),
        b"empty" => Some('\u{2205}'),
        b"epsilon" => Some('\u{03B5}'),
        b"Epsilon" => Some('\u{0395}'),
        b"equiv" => Some('\u{2261}'),
        b"eta" => Some('\u{03B7}'),
        b"Eta" => Some('\u{0397}'),
        b"eth" => Some('\u{00F0}'),
        b"ETH" => Some('\u{00D0}'),
        b"euml" => Some('\u{00EB}'),
        b"Euml" => Some('\u{00CB}'),
        b"euro" => Some('\u{20AC}'),
        b"exist" => Some('\u{2203}'),
        b"fnof" => Some('\u{0192}'),
        b"forall" => Some('\u{2200}'),
        b"frac12" => Some('\u{00BD}'),
        b"frac14" => Some('\u{00BC}'),
        b"frac34" => Some('\u{00BE}'),
        b"frasl" => Some('\u{2044}'),
        b"gamma" => Some('\u{03B3}'),
        b"Gamma" => Some('\u{0393}'),
        b"ge" => Some('\u{2265}'),
        b"gt" => Some('>'),
        b"harr" => Some('\u{2194}'),
        b"hArr" => Some('\u{21D4}'),
        b"hearts" => Some('\u{2665}'),
        b"hellip" => Some('\u{2026}'),
        b"iacute" => Some('\u{00ED}'),
        b"Iacute" => Some('\u{00CD}'),
        b"icirc" => Some('\u{00EE}'),
        b"Icirc" => Some('\u{00CE}'),
        b"iexcl" => Some('\u{00A1}'),
        b"igrave" => Some('\u{00EC}'),
        b"Igrave" => Some('\u{00CC}'),
        b"image" => Some('\u{2111}'),
        b"infin" => Some('\u{221E}'),
        b"int" => Some('\u{222B}'),
        b"iota" => Some('\u{03B9}'),
        b"Iota" => Some('\u{0399}'),
        b"iquest" => Some('\u{00BF}'),
        b"isin" => Some('\u{2208}'),
        b"iuml" => Some('\u{00EF}'),
        b"Iuml" => Some('\u{00CF}'),
        b"kappa" => Some('\u{03BA}'),
        b"Kappa" => Some('\u{039A}'),
        b"lambda" => Some('\u{03BB}'),
        b"Lambda" => Some('\u{039B}'),
        b"lang" => Some('\u{27E8}'),
        b"laquo" => Some('\u{00AB}'),
        b"larr" => Some('\u{2190}'),
        b"lArr" => Some('\u{21D0}'),
        b"lceil" => Some('\u{2308}'),
        b"ldquo" => Some('\u{201C}'),
        b"le" => Some('\u{2264}'),
        b"lfloor" => Some('\u{230A}'),
        b"lowast" => Some('\u{2217}'),
        b"loz" => Some('\u{25CA}'),
        b"lsaquo" => Some('\u{2039}'),
        b"lsquo" => Some('\u{2018}'),
        b"lt" => Some('<'),
        b"macr" => Some('\u{00AF}'),
        b"mdash" => Some('\u{2014}'),
        b"micro" => Some('\u{00B5}'),
        b"middot" => Some('\u{00B7}'),
        b"minus" => Some('\u{2212}'),
        b"mu" => Some('\u{03BC}'),
        b"Mu" => Some('\u{039C}'),
        b"nabla" => Some('\u{2207}'),
        b"nbsp" => Some(' '),
        b"ndash" => Some('\u{2013}'),
        b"ne" => Some('\u{2260}'),
        b"ni" => Some('\u{220B}'),
        b"not" => Some('\u{00AC}'),
        b"notin" => Some('\u{2209}'),
        b"nsub" => Some('\u{2284}'),
        b"ntilde" => Some('\u{00F1}'),
        b"Ntilde" => Some('\u{00D1}'),
        b"nu" => Some('\u{03BD}'),
        b"Nu" => Some('\u{039D}'),
        b"oacute" => Some('\u{00F3}'),
        b"Oacute" => Some('\u{00D3}'),
        b"ocirc" => Some('\u{00F4}'),
        b"Ocirc" => Some('\u{00D4}'),
        b"oelig" => Some('\u{0153}'),
        b"OElig" => Some('\u{0152}'),
        b"ograve" => Some('\u{00F2}'),
        b"Ograve" => Some('\u{00D2}'),
        b"oline" => Some('\u{203E}'),
        b"omega" => Some('\u{03C9}'),
        b"Omega" => Some('\u{03A9}'),
        b"omicron" => Some('\u{03BF}'),
        b"Omicron" => Some('\u{039F}'),
        b"oplus" => Some('\u{2295}'),
        b"or" => Some('\u{2228}'),
        b"ordf" => Some('\u{00AA}'),
        b"ordm" => Some('\u{00BA}'),
        b"oslash" => Some('\u{00F8}'),
        b"Oslash" => Some('\u{00D8}'),
        b"otilde" => Some('\u{00F5}'),
        b"Otilde" => Some('\u{00D5}'),
        b"otimes" => Some('\u{2297}'),
        b"ouml" => Some('\u{00F6}'),
        b"Ouml" => Some('\u{00D6}'),
        b"para" => Some('\u{00B6}'),
        b"part" => Some('\u{2202}'),
        b"permil" => Some('\u{2030}'),
        b"perp" => Some('\u{22A5}'),
        b"phi" => Some('\u{03C6}'),
        b"Phi" => Some('\u{03A6}'),
        b"pi" => Some('\u{03C0}'),
        b"Pi" => Some('\u{03A0}'),
        b"piv" => Some('\u{03D6}'),
        b"plusmn" => Some('\u{00B1}'),
        b"pound" => Some('\u{00A3}'),
        b"prime" => Some('\u{2032}'),
        b"Prime" => Some('\u{2033}'),
        b"prod" => Some('\u{220F}'),
        b"prop" => Some('\u{221D}'),
        b"psi" => Some('\u{03C8}'),
        b"Psi" => Some('\u{03A8}'),
        b"quot" => Some('"'),
        b"radic" => Some('\u{221A}'),
        b"rang" => Some('\u{27E9}'),
        b"raquo" => Some('\u{00BB}'),
        b"rarr" => Some('\u{2192}'),
        b"rArr" => Some('\u{21D2}'),
        b"rceil" => Some('\u{2309}'),
        b"rdquo" => Some('\u{201D}'),
        b"real" => Some('\u{211C}'),
        b"reg" => Some('\u{00AE}'),
        b"rfloor" => Some('\u{230B}'),
        b"rho" => Some('\u{03C1}'),
        b"Rho" => Some('\u{03A1}'),
        b"rsaquo" => Some('\u{203A}'),
        b"rsquo" => Some('\u{2019}'),
        b"sbquo" => Some('\u{201A}'),
        b"scaron" => Some('\u{0161}'),
        b"Scaron" => Some('\u{0160}'),
        b"sdot" => Some('\u{22C5}'),
        b"sect" => Some('\u{00A7}'),
        b"sigma" => Some('\u{03C3}'),
        b"Sigma" => Some('\u{03A3}'),
        b"sigmaf" => Some('\u{03C2}'),
        b"sim" => Some('\u{223C}'),
        b"spades" => Some('\u{2660}'),
        b"sub" => Some('\u{2282}'),
        b"sube" => Some('\u{2286}'),
        b"sum" => Some('\u{2211}'),
        b"sup" => Some('\u{2283}'),
        b"sup1" => Some('\u{00B9}'),
        b"sup2" => Some('\u{00B2}'),
        b"sup3" => Some('\u{00B3}'),
        b"supe" => Some('\u{2287}'),
        b"szlig" => Some('\u{00DF}'),
        b"tau" => Some('\u{03C4}'),
        b"Tau" => Some('\u{03A4}'),
        b"there4" => Some('\u{2234}'),
        b"theta" => Some('\u{03B8}'),
        b"Theta" => Some('\u{0398}'),
        b"thetasym" => Some('\u{03D1}'),
        b"thorn" => Some('\u{00FE}'),
        b"THORN" => Some('\u{00DE}'),
        b"tilde" => Some('\u{02DC}'),
        b"times" => Some('\u{00D7}'),
        b"trade" => Some('\u{2122}'),
        b"uacute" => Some('\u{00FA}'),
        b"Uacute" => Some('\u{00DA}'),
        b"uarr" => Some('\u{2191}'),
        b"uArr" => Some('\u{21D1}'),
        b"ucirc" => Some('\u{00FB}'),
        b"Ucirc" => Some('\u{00DB}'),
        b"ugrave" => Some('\u{00F9}'),
        b"Ugrave" => Some('\u{00D9}'),
        b"uml" => Some('\u{00A8}'),
        b"upsih" => Some('\u{03D2}'),
        b"upsilon" => Some('\u{03C5}'),
        b"Upsilon" => Some('\u{03A5}'),
        b"uuml" => Some('\u{00FC}'),
        b"Uuml" => Some('\u{00DC}'),
        b"weierp" => Some('\u{2118}'),
        b"xi" => Some('\u{03BE}'),
        b"Xi" => Some('\u{039E}'),
        b"yacute" => Some('\u{00FD}'),
        b"Yacute" => Some('\u{00DD}'),
        b"yen" => Some('\u{00A5}'),
        b"yuml" => Some('\u{00FF}'),
        b"Yuml" => Some('\u{0178}'),
        b"zeta" => Some('\u{03B6}'),
        b"Zeta" => Some('\u{0396}'),
        _ => None,
    }
}

#[inline]
pub(crate) fn decode_html_entities(text: &str) -> Cow<'_, str> {
    // Fast path: no ampersand means no entities to decode
    if !text.as_bytes().contains(&b'&') {
        return Cow::Borrowed(text);
    }
    Cow::Owned(decode_html_entities_alloc(text))
}

/// Resolve a numeric character reference value per the HTML standard:
/// C1 controls (0x80–0x9F) remap through the Windows-1252 legacy table,
/// and NUL / surrogates / out-of-range values decode to U+FFFD.
fn decode_numeric_ref(code: u32) -> char {
    // Windows-1252 replacements for 0x80–0x9F (unmapped slots keep their value).
    const WIN1252: [u32; 32] = [
        0x20AC, 0x0081, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021,
        0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0x008D, 0x017D, 0x008F,
        0x0090, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014,
        0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0x009D, 0x017E, 0x0178,
    ];
    let cp = if (0x80..=0x9F).contains(&code) {
        WIN1252[(code - 0x80) as usize]
    } else {
        code
    };
    if cp == 0 || cp > 0x0010_FFFF || (0xD800..=0xDFFF).contains(&cp) {
        return '\u{FFFD}';
    }
    char::from_u32(cp).unwrap_or('\u{FFFD}')
}

fn decode_html_entities_alloc(text: &str) -> String {
    let bytes = text.as_bytes();
    let len = bytes.len();
    let mut result = String::with_capacity(len);
    let mut i = 0;

    while i < len {
        if bytes[i] == b'&' {
            // Numeric character references: &#NNN; or &#xHHH;
            if i + 2 < len && bytes[i + 1] == b'#' {
                let start = i;
                i += 2;
                let is_hex = i < len && (bytes[i] == b'x' || bytes[i] == b'X');
                if is_hex {
                    i += 1;
                }
                let num_start = i;
                // Cap digit scan: 7 hex digits (U+10FFFF) or 8 decimal (max codepoint)
                let max_digits = if is_hex { 7 } else { 8 };
                let scan_limit = len.min(num_start + max_digits + 1);
                while i < scan_limit && bytes[i] != b';' {
                    i += 1;
                }
                if i < len && bytes[i] == b';' && i > num_start {
                    let num_str = &text[num_start..i];
                    let base = if is_hex { 16 } else { 10 };
                    if let Ok(code_point) = u32::from_str_radix(num_str, base) {
                        result.push(decode_numeric_ref(code_point));
                        i += 1;
                        continue;
                    }
                }
                i = start;
            } else {
                // Named entity: scan forward up to 33 bytes for ';'
                let mut semi = i + 1;
                let scan_end = len.min(i + 34);
                while semi < scan_end && bytes[semi] != b';' {
                    semi += 1;
                }
                if semi < len && bytes[semi] == b';' && semi > i + 1 {
                    let name = &bytes[i + 1..semi];
                    if let Some(c) = lookup_named_entity(name) {
                        result.push(c);
                        i = semi + 1;
                        continue;
                    }
                }
            }
            // No entity matched — push literal '&'
            result.push('&');
            i += 1;
            continue;
        }
        // Batch copy plain ASCII bytes until next '&' or non-ASCII
        let start = i;
        while i < len && bytes[i] != b'&' && bytes[i] < 0x80 {
            i += 1;
        }
        if i > start {
            result.push_str(&text[start..i]);
        }
        if i >= len { break; }
        // Handle non-ASCII multi-byte UTF-8 char
        if bytes[i] >= 0x80 {
            let Some(ch) = text[i..].chars().next() else { break; };
            result.push(ch);
            i += ch.len_utf8();
        }
    }
    result
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn no_ampersand_borrows() {
        assert!(matches!(decode_html_entities("plain text"), Cow::Borrowed(_)));
    }

    #[test]
    fn decodes_named_entities() {
        assert_eq!(decode_html_entities("a &amp; b"), "a & b");
        assert_eq!(decode_html_entities("&lt;tag&gt;"), "<tag>");
        assert_eq!(decode_html_entities("&quot;q&quot;"), "\"q\"");
    }

    #[test]
    fn decodes_numeric_references() {
        assert_eq!(decode_html_entities("&#65;"), "A");
        assert_eq!(decode_html_entities("&#x41;"), "A");
        assert_eq!(decode_html_entities("&#X41;"), "A");
    }

    #[test]
    fn numeric_references_follow_html_rules() {
        // C1 controls (0x80–0x9F) remap through Windows-1252
        assert_eq!(decode_html_entities("&#x80;"), "\u{20AC}"); // €
        assert_eq!(decode_html_entities("&#x99;"), "\u{2122}"); // ™
        // NUL, surrogates, and out-of-range values decode to U+FFFD
        assert_eq!(decode_html_entities("&#0;"), "\u{FFFD}");
        assert_eq!(decode_html_entities("&#xD800;"), "\u{FFFD}");
        assert_eq!(decode_html_entities("&#x110000;"), "\u{FFFD}");
    }

    #[test]
    fn unknown_entity_kept_literal() {
        assert_eq!(decode_html_entities("&notanentity;"), "&notanentity;");
        // bare ampersand is preserved
        assert_eq!(decode_html_entities("a & b"), "a & b");
    }
}
