//! Detector-local FP suppressor + real-key anchor for trufflehog Honeycomb hits.
//!
//! Trufflehog's Honeycomb detector substring-matches "Honeycomb" inside any
//! string and treats the surrounding window as a candidate credential. This
//! produces a saturated FP stream on Android apps, because the AOSP
//! framework class FQN family `android.graphics.drawable.HoneycombBitmapFactory`
//! / `android.support.v4.app.HoneycombCompat` (and similar Pascal-case
//! identifier shapes that begin with the literal word `Honeycomb`) appear in
//! every app that touches the bitmap pipeline or Android 3.0/Honeycomb-era
//! compatibility shims. A representative audit flagged
//! `HoneycombBitmapFactory` as a Critical Honeycomb credential — it is a
//! public AOSP class name with no key material whatsoever.
//!
//! The calibration layer ships a blanket downgrade: every unverified Honeycomb
//! hit emits at Low rather than Critical. That keeps FP volume bounded but
//! surfaces real Honeycomb keys at Low too — a precision regression. This
//! module is the precision anchor that supersedes the blanket downgrade for
//! the two cases it cannot disambiguate:
//!
//! 1. **Class-name shape** — `Honeycomb[A-Z][A-Za-z0-9_$]*` with all bytes
//!    in the ASCII identifier set. Downgraded Critical → Info with
//!    `id_tag = CREDENTIAL_HONEYCOMB_FP_ANDROID_CLASS` and
//!    `extra.fp_shape = "ANDROID_CLASS"`. Mirrors the Circle `_FP_<SHAPE>`
//!    convention from `credentials_fp.rs` — preserves the audit trail
//!    rather than dropping the line silently.
//! 2. **Documented key anchor** — Honeycomb publishes two key shapes:
//!    classic API key (32 lowercase hex characters, `[0-9a-f]{32}`
//!    exactly) and environment/ingest key (`hc[abc]i[ack]_` prefix + 28
//!    `[A-Za-z0-9]` characters; the prefix is structured —
//!    `hc<type><i><class>_`, where `type ∈ {a,b,c}` is region/tier and
//!    `class ∈ {a,c,k}` is auth class). A raw matching either anchor is
//!    re-promoted to Critical, overriding the row-1a Low downgrade.
//!    `id_tag = CREDENTIAL_HONEYCOMB`, `extra.anchor = "CLASSIC_HEX"`
//!    or `"INGEST_KEY"`.
//!
//! This module delivers the precision re-promotion that the calibration layer
//! defers: class-name shapes are downgraded to Info, while documented key
//! anchors are re-promoted to Critical.
//!
//! **Detector-local, not provenance-aware.** Like the Circle module, this
//! is content-shape suppression: it keys on the `Raw` text bytes of the
//! credential hit, not on the originating SDK class FQN. Orthogonal to the
//! umbrella `provenance-aware-suppression-layer` per R5.
//!
//! **Safe by construction.** Real Honeycomb keys cannot be Java
//! identifiers (lowercase hex / `hc..._` prefixed; Java identifiers don't
//! contain the underscore-after-prefix shape AND have a Pascal first byte
//! the anchor regex doesn't tolerate). The class-name shape gate cannot
//! match a real key (real keys fail the all-identifier-chars + Pascal-case
//! gate via the `_` in ingest keys or because they are pure-lowercase
//! hex, which fails the Pascal-case check).
//!

use std::sync::LazyLock;

use regex::Regex;

/// Detector names this module operates on. Trufflehog emits the canonical
/// `"Honeycomb"` for the Honeycomb observability service detector;
/// case-insensitive match folds future variants.
const HONEYCOMB_DETECTORS: &[&str] = &["honeycomb"];

/// FP-shape classification for trufflehog Honeycomb hits whose `Raw` matches
/// a Java/Kotlin class identifier rather than a documented Honeycomb key
/// shape. Stable string representation used as the audit-DB
/// `extra.fp_shape` value and as the `id_tag` suffix.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HoneycombFpShape {
    /// Pascal-case Java class identifier starting with the literal word
    /// `Honeycomb` and containing only ASCII identifier chars
    /// (`A-Za-z0-9_$`). Examples: `HoneycombBitmapFactory`,
    /// `HoneycombCompat`. AOSP framework + 3rd-party SDK class names; not
    /// credentials.
    AndroidClass,
}

impl HoneycombFpShape {
    /// Stable tag used in the audit-DB `extra.fp_shape` JSON field and as
    /// the `_FP_<SHAPE>` suffix on `id_tag`.
    pub fn as_tag(self) -> &'static str {
        match self {
            HoneycombFpShape::AndroidClass => "ANDROID_CLASS",
        }
    }
}

/// Real-Honeycomb-key anchor classification. Returned by
/// [`classify_honeycomb_raw_anchor`] when the `Raw` matches one of the two
/// documented Honeycomb key shapes — used by the ingestion path to
/// re-promote the finding to Critical even when sibling `#5`'s row-1a
/// blanket downgrade would otherwise route Honeycomb hits to Low.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HoneycombKeyAnchor {
    /// Classic API key: 32 lowercase hex characters.
    ClassicHex,
    /// Environment / Ingest key: `hc[abc]i[ack]_` prefix + 28
    /// `[A-Za-z0-9]` characters.
    IngestKey,
}

impl HoneycombKeyAnchor {
    /// Stable tag used in the audit-DB `extra.anchor` JSON field.
    pub fn as_tag(self) -> &'static str {
        match self {
            HoneycombKeyAnchor::ClassicHex => "CLASSIC_HEX",
            HoneycombKeyAnchor::IngestKey => "INGEST_KEY",
        }
    }
}

/// Returns true when `detector` (the trufflehog `DetectorName` field) is
/// the Honeycomb observability detector. Case-insensitive.
pub fn is_honeycomb_detector(detector: &str) -> bool {
    let lower = detector.to_ascii_lowercase();
    HONEYCOMB_DETECTORS.iter().any(|n| lower == *n)
}

/// Lazy-compiled regex for the documented Honeycomb classic API key shape.
/// 32 lowercase hex characters; full-anchor (start-of-string + end-of-string).
///
/// Source: Honeycomb classic API key documentation. Real keys are exactly
/// 32 chars of `[0-9a-f]`. Mixed-case or longer/shorter strings are not
/// classic Honeycomb keys.
static CLASSIC_HEX_RE: LazyLock<Result<Regex, regex::Error>> =
    LazyLock::new(|| Regex::new(r"^[0-9a-f]{32}$"));

/// Lazy-compiled regex for the documented Honeycomb environment/ingest
/// key shape: `hc[abc]i[ack]_` prefix + 28 `[A-Za-z0-9]` characters.
///
/// The prefix is structured: `hc` literal + region/tier letter + `i`
/// literal + auth-class letter + `_` literal. Length 6 prefix + 28 body
/// = 34 chars total. Full-anchor.
static INGEST_KEY_RE: LazyLock<Result<Regex, regex::Error>> =
    LazyLock::new(|| Regex::new(r"^hc[abc]i[ack]_[A-Za-z0-9]{28}$"));

/// Classify the `Raw` field of a Honeycomb trufflehog hit as a Java
/// class-name FP shape, or return `None` if the raw is not the
/// `Honeycomb[A-Z][A-Za-z0-9_$]*` Pascal-case identifier shape.
///
/// **Behavior contract:**
/// - Returns `Some(HoneycombFpShape::AndroidClass)` if `raw` starts with
///   the literal `Honeycomb`, the next byte is ASCII uppercase, AND every
///   byte in `raw` is an ASCII identifier char (`A-Za-z0-9_$`). Examples:
///   `HoneycombBitmapFactory`, `HoneycombCompat`,
///   `HoneycombMR1NotificationHelper`, `HoneycombGalleryActivity`.
/// - Returns `None` for any other shape, including:
///   - The literal word `Honeycomb` alone (no following Pascal-case suffix)
///   - `honeycomb` lowercase (not a Pascal-case class identifier)
///   - Strings containing non-identifier chars (`=`, `:`, `/`, `.`,
///     space) — real Honeycomb keys + JWT-shaped material won't match
///     anyway because of the literal-`Honeycomb` prefix requirement
///   - Real Honeycomb classic-hex keys (lowercase hex; no `Honeycomb`
///     prefix)
///   - Real Honeycomb ingest keys (`hc..._` prefix; no `Honeycomb` prefix)
///
/// **Typed-Err on byte-walk failure** — none possible; the predicate uses
/// only `str::starts_with` + bounds-checked `bytes().nth(...)` + `bytes().all(...)`.
/// No direct indexing, no `unwrap`/`expect`.
pub fn classify_honeycomb_raw_shape(raw: &str) -> Option<HoneycombFpShape> {
    if !raw.starts_with("Honeycomb") {
        return None;
    }
    // Byte 9 (right after the literal `Honeycomb`) must be ASCII uppercase
    // — that is the Pascal-case-suffix gate. Empty suffix (raw == "Honeycomb"
    // alone) is rejected.
    let next = raw.as_bytes().get(9)?;
    if !next.is_ascii_uppercase() {
        return None;
    }
    if !raw
        .bytes()
        .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'$')
    {
        return None;
    }
    Some(HoneycombFpShape::AndroidClass)
}

/// Classify the `Raw` field of a Honeycomb trufflehog hit as a real
/// Honeycomb-key anchor, or return `None` if no documented anchor matches.
///
/// **Behavior contract:**
/// - Returns `Some(HoneycombKeyAnchor::IngestKey)` if `raw` matches
///   `^hc[abc]i[ack]_[A-Za-z0-9]{28}$` — checked first because the prefix
///   is structurally specific.
/// - Returns `Some(HoneycombKeyAnchor::ClassicHex)` if `raw` is exactly
///   32 lowercase hex characters.
/// - Returns `None` for any other shape.
///
/// **Typed-Err on regex compile failure** — `LazyLock<Result<Regex, _>>`
/// surfaces a regex compile error as `Err`; the `match` arms collapse it
/// to "no anchor matched" rather than panicking. The patterns are
/// compile-time constants and resolve to `Ok` in practice; the wrapper
/// is defensive.
pub fn classify_honeycomb_raw_anchor(raw: &str) -> Option<HoneycombKeyAnchor> {
    let ingest_ok = match &*INGEST_KEY_RE {
        Ok(re) => re.is_match(raw),
        Err(_) => false,
    };
    if ingest_ok {
        return Some(HoneycombKeyAnchor::IngestKey);
    }
    let classic_ok = match &*CLASSIC_HEX_RE {
        Ok(re) => re.is_match(raw),
        Err(_) => false,
    };
    if classic_ok {
        return Some(HoneycombKeyAnchor::ClassicHex);
    }
    None
}

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

    // ── Detector-name classifier ────────────────────────────────────

    #[test]
    fn honeycomb_detector_case_insensitive() {
        assert!(is_honeycomb_detector("Honeycomb"));
        assert!(is_honeycomb_detector("HONEYCOMB"));
        assert!(is_honeycomb_detector("honeycomb"));
        assert!(is_honeycomb_detector("HoneyComb"));
    }

    #[test]
    fn honeycomb_detector_rejects_non_honeycomb() {
        assert!(!is_honeycomb_detector("Box"));
        assert!(!is_honeycomb_detector("Honeycombs")); // not exact match
        assert!(!is_honeycomb_detector("Honey"));
        assert!(!is_honeycomb_detector(""));
    }

    // ── Class-name FP shape ─────────────────────────────────────────

    /// The representative AOSP-class FP: AOSP framework class FQN.
    /// MUST classify as AndroidClass.
    #[test]
    fn classify_aosp_honeycomb_bitmap_factory() {
        assert_eq!(
            classify_honeycomb_raw_shape("HoneycombBitmapFactory"),
            Some(HoneycombFpShape::AndroidClass),
        );
    }

    #[test]
    fn classify_aosp_honeycomb_compat() {
        assert_eq!(
            classify_honeycomb_raw_shape("HoneycombCompat"),
            Some(HoneycombFpShape::AndroidClass),
        );
    }

    #[test]
    fn classify_extended_honeycomb_class_names() {
        // Other plausible Pascal-case `Honeycomb`-prefixed class names
        // — empirical FP shape extends to any Pascal-case suffix. The
        // gate requires byte 9 to be ASCII uppercase, so the suffix
        // must start with an uppercase letter (no `_`-leading suffix).
        let documented = [
            "HoneycombMR1NotificationHelper",
            "HoneycombGalleryActivity",
            "HoneycombV11Compat",
            "HoneycombBackport",
            "HoneycombA", // minimal Pascal-case suffix
        ];
        for raw in documented {
            assert_eq!(
                classify_honeycomb_raw_shape(raw),
                Some(HoneycombFpShape::AndroidClass),
                "expected AndroidClass for {raw}",
            );
        }
    }

    #[test]
    fn classify_dollar_inner_class_admitted() {
        // `Honeycomb$Inner` — Java/Kotlin inner-class identifier shape.
        // `$` is a valid identifier char.
        assert_eq!(
            classify_honeycomb_raw_shape("HoneycombFactory$Inner"),
            Some(HoneycombFpShape::AndroidClass),
        );
    }

    #[test]
    fn classify_lowercase_honeycomb_rejected() {
        // `honeycomb` lowercase — not a Pascal-case identifier; doesn't
        // start with the literal `Honeycomb`.
        assert_eq!(classify_honeycomb_raw_shape("honeycombbitmapfactory"), None);
    }

    #[test]
    fn classify_bare_honeycomb_word_rejected() {
        // The bare word `Honeycomb` alone has no Pascal-case suffix and
        // is rejected — it's not unambiguously a class name (could be a
        // marketing string, code comment, etc.). Falls through to `#5`'s
        // row-1a Low downgrade.
        assert_eq!(classify_honeycomb_raw_shape("Honeycomb"), None);
    }

    #[test]
    fn classify_honeycomb_with_lowercase_suffix_rejected() {
        // `Honeycombbitmapfactory` — no uppercase break after the prefix;
        // not Pascal-case.
        assert_eq!(
            classify_honeycomb_raw_shape("Honeycombbitmapfactory"),
            None,
        );
    }

    #[test]
    fn classify_honeycomb_with_punctuation_rejected() {
        // `Honeycomb=foo` contains `=`; not a pure identifier.
        assert_eq!(classify_honeycomb_raw_shape("HoneycombFactory=foo"), None);
        // `Honeycomb.Bar` contains `.`; FQN dot-notation is not pure
        // identifier (FQN ingestion would happen at the class-resolution
        // layer, not the credential-shape gate).
        assert_eq!(classify_honeycomb_raw_shape("Honeycomb.BitmapFactory"), None);
        // Space between `Honeycomb` and the suffix.
        assert_eq!(
            classify_honeycomb_raw_shape("Honeycomb BitmapFactory"),
            None,
        );
    }

    #[test]
    fn classify_honeycomb_with_slash_rejected() {
        // Internal JVM class FQN form uses `/`; not a pure identifier.
        assert_eq!(
            classify_honeycomb_raw_shape("Honeycomb/BitmapFactory"),
            None,
        );
    }

    #[test]
    fn classify_empty_string_is_not_class_name() {
        assert_eq!(classify_honeycomb_raw_shape(""), None);
    }

    // ── Real key anchors (must classify so ingest re-promotes) ──────

    #[test]
    fn classify_classic_hex_anchor() {
        // 32 lowercase hex chars = classic Honeycomb API key.
        let raw = "0123456789abcdef0123456789abcdef";
        assert_eq!(raw.len(), 32);
        assert_eq!(
            classify_honeycomb_raw_anchor(raw),
            Some(HoneycombKeyAnchor::ClassicHex),
        );
    }

    #[test]
    fn classify_classic_hex_uppercase_rejected() {
        // Honeycomb classic keys are lowercase hex specifically; uppercase
        // hex strings are likely test vectors / SHA digests in code,
        // not Honeycomb keys.
        let raw = "0123456789ABCDEF0123456789ABCDEF";
        assert_eq!(classify_honeycomb_raw_anchor(raw), None);
    }

    #[test]
    fn classify_classic_hex_short_rejected() {
        // 31 hex chars — too short.
        let raw = "0123456789abcdef0123456789abcde";
        assert_eq!(raw.len(), 31);
        assert_eq!(classify_honeycomb_raw_anchor(raw), None);
    }

    #[test]
    fn classify_classic_hex_long_rejected() {
        // 33 hex chars — too long.
        let raw = "0123456789abcdef0123456789abcdef0";
        assert_eq!(raw.len(), 33);
        assert_eq!(classify_honeycomb_raw_anchor(raw), None);
    }

    #[test]
    fn classify_ingest_key_anchor() {
        // Ingest key shape: `hc[abc]i[ack]_` + 28 alphanumeric.
        // Cover all 9 documented prefix combinations.
        let documented = [
            "hcaia_AbCdEfGhIjKlMnOpQrStUvWxYz01",
            "hcaic_AbCdEfGhIjKlMnOpQrStUvWxYz01",
            "hcaik_AbCdEfGhIjKlMnOpQrStUvWxYz01",
            "hcbia_AbCdEfGhIjKlMnOpQrStUvWxYz01",
            "hcbic_AbCdEfGhIjKlMnOpQrStUvWxYz01",
            "hcbik_AbCdEfGhIjKlMnOpQrStUvWxYz01",
            "hccia_AbCdEfGhIjKlMnOpQrStUvWxYz01",
            "hccic_AbCdEfGhIjKlMnOpQrStUvWxYz01",
            "hccik_AbCdEfGhIjKlMnOpQrStUvWxYz01",
        ];
        for raw in documented {
            assert_eq!(raw.len(), 6 + 28);
            assert_eq!(
                classify_honeycomb_raw_anchor(raw),
                Some(HoneycombKeyAnchor::IngestKey),
                "expected IngestKey for {raw}",
            );
        }
    }

    #[test]
    fn classify_ingest_key_with_wrong_type_letter_rejected() {
        // `d` is not in `[abc]` — not a valid Honeycomb prefix.
        let raw = "hcdia_AbCdEfGhIjKlMnOpQrStUvWxYz01";
        assert_eq!(classify_honeycomb_raw_anchor(raw), None);
    }

    #[test]
    fn classify_ingest_key_with_wrong_class_letter_rejected() {
        // `d` is not in `[ack]` — not a valid auth-class letter.
        let raw = "hcaid_AbCdEfGhIjKlMnOpQrStUvWxYz01";
        assert_eq!(classify_honeycomb_raw_anchor(raw), None);
    }

    #[test]
    fn classify_ingest_key_with_dash_in_body_rejected() {
        // `-` is not in `[A-Za-z0-9]` — body must be pure alphanumeric.
        let raw = "hcaik_AbCd-fGhIjKlMnOpQrStUvWxYz01";
        assert_eq!(raw.len(), 6 + 28);
        assert_eq!(classify_honeycomb_raw_anchor(raw), None);
    }

    #[test]
    fn classify_ingest_key_short_body_rejected() {
        // 25-char body — too short.
        let raw = "hcaik_AbCdEfGhIjKlMnOpQrStUvWxY";
        assert_eq!(raw.len(), 6 + 25);
        assert_eq!(classify_honeycomb_raw_anchor(raw), None);
    }

    // ── Class-name vs anchor disjointness ───────────────────────────

    #[test]
    fn class_name_and_anchor_are_disjoint_classic() {
        // Classic-hex anchor is pure lowercase; cannot match the
        // `Honeycomb`-Pascal-case prefix.
        let classic = "0123456789abcdef0123456789abcdef";
        assert_eq!(classify_honeycomb_raw_shape(classic), None);
        assert_eq!(
            classify_honeycomb_raw_anchor(classic),
            Some(HoneycombKeyAnchor::ClassicHex),
        );
    }

    #[test]
    fn class_name_and_anchor_are_disjoint_ingest() {
        // Ingest key starts with `hc...` lowercase; cannot match the
        // `Honeycomb`-Pascal-case prefix. Conversely, class names contain
        // no `_<28-alphanum>$` shape with the specific prefix bytes.
        let ingest = "hcaik_AbCdEfGhIjKlMnOpQrStUvWxYz01";
        assert_eq!(classify_honeycomb_raw_shape(ingest), None);
        assert_eq!(
            classify_honeycomb_raw_anchor(ingest),
            Some(HoneycombKeyAnchor::IngestKey),
        );
    }

    #[test]
    fn aosp_class_is_not_an_anchor() {
        // `HoneycombBitmapFactory` cannot match either anchor regex.
        let raw = "HoneycombBitmapFactory";
        assert_eq!(classify_honeycomb_raw_anchor(raw), None);
        assert_eq!(
            classify_honeycomb_raw_shape(raw),
            Some(HoneycombFpShape::AndroidClass),
        );
    }

    // ── Tag stability ───────────────────────────────────────────────

    #[test]
    fn fp_shape_tag_is_stable() {
        assert_eq!(HoneycombFpShape::AndroidClass.as_tag(), "ANDROID_CLASS");
    }

    #[test]
    fn key_anchor_tags_are_stable() {
        assert_eq!(HoneycombKeyAnchor::ClassicHex.as_tag(), "CLASSIC_HEX");
        assert_eq!(HoneycombKeyAnchor::IngestKey.as_tag(), "INGEST_KEY");
    }

    // ── Defensive edge cases ────────────────────────────────────────

    #[test]
    fn unicode_raw_does_not_panic_shape() {
        // Multi-byte UTF-8 input. The shape gate's prefix check returns
        // false (literal `Honeycomb` not at start in this case), and
        // even if it did, the `bytes().all(...)` walk over multi-byte
        // sequences fails the all-identifier-chars filter.
        let raw = "Honeycomb\u{1F600}Factory";
        assert_eq!(classify_honeycomb_raw_shape(raw), None);
    }

    #[test]
    fn unicode_raw_does_not_panic_anchor() {
        let raw = "hcaik_\u{1F600}_28_chars_xxxxxxxxxxxxxx";
        assert_eq!(classify_honeycomb_raw_anchor(raw), None);
    }

    #[test]
    fn bare_word_then_punctuation_rejected() {
        // `Honeycomb!` — punctuation right after the prefix; falls
        // through to row-1a downgrade rather than being mis-classified
        // as a class name.
        assert_eq!(classify_honeycomb_raw_shape("Honeycomb!"), None);
        // `Honeycomb_` — underscore right after the prefix; the next
        // byte is `_`, not ASCII uppercase, so the Pascal-case gate
        // rejects.
        assert_eq!(classify_honeycomb_raw_shape("Honeycomb_Foo"), None);
    }
}
