//! Name / dead-weight taxonomy — the classification predicates that decide which resolved names are
//! real gameplay functions vs. generated plumbing, and how much to trust a name guess. Every item here is
//! a pure `&str`-in / verdict-out predicate over the name/class vocabulary, with no engine or IO
//! dependency — each takes the game's `&GameProfile` for its retunable vocabulary (dead-weight namespaces,
//! serializer method names, query prefixes). This is the primary knob a fork retunes for a different game
//! (the vocabulary is data on the profile). Shared by the fold (`build_gamedata_cmd`), the experimental band
//! (`emit_experimental_band`), the live semantic sweep, and the corpus-model class scope.

use crate::model::Tier;
use crate::profile::GameProfile;
use serde::Deserialize;
use std::collections::{HashMap, HashSet};

/// A candidate whose RTTI class is not CS2 gameplay at all — foreign runtime/library code that leaked
/// into `libserver.so`, or a protobuf-generated message type whose whole vtable is serializer boilerplate
/// (`GetMetadata`/`New`/`Clear`/`MergeFrom`/…, zero hook value). Excluded at dump time so naming agents
/// never spend time (~64% of the CS2 candidate pool) on functions we already know are dead
/// weight — and so the same junk never enters a per-game run for Dota2/Deadlock.
pub(crate) fn is_dead_weight_class(prof: &GameProfile, class: &str) -> bool {
    // foreign namespaces (C++ runtime, libstdc++, Steam GC SDK, Valve container templates, the V8 vscript
    // backend whose `v8::` classes leak into libvscript's RTTI) — all retunable per game on the profile.
    if prof.foreign_namespaces.iter().any(|p| class.starts_with(p)) {
        return true;
    }
    // protobuf RPC message shape — a `_Response`/`_Request` class is always a wire message.
    if class.contains("_Response") || class.contains("_Request") {
        return true;
    }
    // protobuf-generated message classes (the wire/GC protocol) — every method is serializer plumbing. The
    // per-game user-message prefix (CS2: CCSUsrMsg) plus the shared Source-2 / Steam-GC message prefixes.
    let leaf = class.rsplit("::").next().unwrap_or(class);
    leaf.starts_with(prof.usermsg_prefix) || prof.proto_prefixes.iter().any(|p| leaf.starts_with(p))
}

/// A resolved NAME that is not CS2 gameplay — its owning class (or the whole name) is foreign/protobuf.
/// Complements the class-based dump-candidates prefilter for NON-virtual dead weight that has no RTTI
/// vtable class to filter on at dump time (free `GCSDK::*` / `google::protobuf::*` functions), caught
/// here once naming has resolved the class.
pub(crate) fn is_dead_weight_name(prof: &GameProfile, name: &str) -> bool {
    let cls = name.rsplit_once("::").map(|(c, _)| c).unwrap_or(name);
    is_dead_weight_class(prof, cls) || is_dead_weight_class(prof, name)
}

/// Classes with ≥3 serializer methods among `names` — protobuf message types whose class name matches no
/// foreign/CMsg prefix (e.g. `AccountActivity`, `CGCToGCMsgMasterAck`), detectable ONLY by their generated
/// method surface. The shared cluster detector both the fold and the experimental band flag plumbing with.
/// A HARD serializer method (`GetMetadata`/…) is decisive on its own; SOFT ones (`New`/`Clear`/…) can be
/// legit game methods, so both count toward the ≥3 cluster here but only HARD is a lone verdict elsewhere
/// (see [`is_serializer_plumbing`]). Both sets ride the `GameProfile` passed in, so a fork retunes them.
pub(crate) fn protobuf_message_classes<'a>(
    prof: &GameProfile,
    names: impl Iterator<Item = &'a str>,
) -> HashSet<String> {
    let mut ser_count: HashMap<&str, usize> = HashMap::new();
    for name in names {
        if let Some((cls, leaf)) = name.rsplit_once("::")
            && (prof.hard_serializer.contains(&leaf) || prof.soft_serializer.contains(&leaf))
        {
            *ser_count.entry(cls).or_default() += 1;
        }
    }
    ser_count
        .into_iter()
        .filter(|(_, c)| *c >= 3)
        .map(|(k, _)| k.to_string())
        .collect()
}

/// Is `name` protobuf serializer plumbing — a lone HARD serializer method, or a member of a class flagged
/// as a protobuf message by [`protobuf_message_classes`]? Complements the prefix/namespace test in
/// [`is_dead_weight_name`], which can't see method-name-only protobuf classes.
pub(crate) fn is_serializer_plumbing(
    prof: &GameProfile,
    name: &str,
    pb_classes: &HashSet<String>,
) -> bool {
    let hard = prof.hard_serializer;
    name.rsplit_once("::")
        .is_some_and(|(cls, leaf)| hard.contains(&leaf) || pb_classes.contains(cls))
}

/// A class clean enough to key a vtable-OFFSET entry: a real gameplay class (not dead weight), not a
/// `NetworkVar_`/template/alias chainer, and a bare name (the RTTI ground-truth class, no `::`).
pub(crate) fn clean_offset_class(prof: &GameProfile, cls: &str) -> bool {
    !is_dead_weight_class(prof, cls)
        && !cls.contains("NetworkVar_")
        && !cls.contains('<')
        && !cls.contains("Alias_")
        && !cls.contains("::")
}

/// The class portion of a fully-qualified function name — everything before the last `::`, or before the
/// last `_` for the flat `Class_Method` form, or the whole name if neither. The one name-vocabulary splitter
/// the gamedata offsets and the live sweep share, so callers don't re-derive the class inline.
pub(crate) fn class_of(name: &str) -> &str {
    if let Some(i) = name.rfind("::") {
        &name[..i]
    } else if let Some(i) = name.rfind('_') {
        &name[..i]
    } else {
        name
    }
}

/// The `ret=` class word out of an abi describe string ("int=1 float=0 ret=int" -> "int").
pub(crate) fn parse_ret(abi: &str) -> Option<&str> {
    abi.split_whitespace().find_map(|t| t.strip_prefix("ret="))
}

/// One row of the full-slice name universe (`candidates-names-cs2-full.json`): an address + the
/// AI/heuristic name guess for it, plus the signals that grade the guess. Distinct from `PromoName` —
/// this reads the UN-filtered set (promoted AND un-promoted), the raw material of the experimental band.
#[derive(Deserialize)]
pub(crate) struct FullName {
    pub(crate) addr: String,
    pub(crate) name: String,
    #[serde(default)]
    pub(crate) confidence: String,
    #[serde(default)]
    pub(crate) corroboration: String,
    #[serde(default)]
    pub(crate) self_named: bool,
    #[serde(default)]
    pub(crate) promote: bool,
}

/// The confidence LADDER for a name guess — a composite honesty tier, stronger than the model's own
/// confidence word: a name literally present in the function's bytes (`self-named`) is near-certain; a
/// dictionary-corroborated leaf is next; then the model's own high/medium/low. Returns `(rank, label)`,
/// lower rank = more trustworthy. This is the primary grouping key of the experimental band.
pub(crate) fn guess_tier(r: &FullName) -> (u8, Tier) {
    if r.self_named {
        (0, Tier::SelfNamed)
    } else if matches!(r.corroboration.as_str(), "exact" | "exact-free") {
        (1, Tier::Corroborated)
    } else if r.confidence == "high" {
        (2, Tier::High)
    } else if r.confidence == "medium" {
        (3, Tier::Medium)
    } else {
        (4, Tier::Low)
    }
}

/// A method name safe to blind-CALL with only `this` — a boolean predicate that returns a bool in RAX.
/// Deliberately EXCLUDES `Get*`: a getter can return a value BY VALUE (a string/struct), whose ABI
/// hides an output-buffer pointer in RDI (RVO) with `this` shifted to RSI — so calling it with the
/// object in RDI makes it WRITE the return value into the object. That is memory CORRUPTION, not a
/// faulting read, so `call_remote`'s signal-suppression can't catch it and the server dies later. The ABI-shape
/// lower bound can't distinguish this (a constant-returner reads no args and shows `int=0`), so the
/// gate is name-based: only the boolean predicates, which by convention return a bool and take no
/// output parameter. Fewer methods get the call-smoke-test, but the harness never corrupts the server.
pub(crate) fn is_query_method(prof: &GameProfile, name: &str) -> bool {
    let leaf = name.rsplit("::").next().unwrap_or(name);
    prof.query_prefixes.iter().any(|p| leaf.starts_with(p))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::profile::{CS2, DOTA};

    // This module is documented as "the primary knob a fork retunes for a different game", and nothing
    // else gates a retune: live validation only ever sees entries that SURVIVED classification, so an
    // over-broad predicate silently shrinks the output with no count to compare against. These pin the
    // decisions against both shipped profiles.

    #[test]
    fn real_gameplay_classes_are_not_dead_weight() {
        for prof in [&CS2, &DOTA] {
            for cls in [
                "CBaseEntity",
                "CCSPlayerPawn",
                "CGameRules",
                "CDOTA_BaseNPC",
            ] {
                assert!(
                    !is_dead_weight_class(prof, cls),
                    "{cls} misclassified as dead weight"
                );
            }
        }
    }

    #[test]
    fn protobuf_and_foreign_namespaces_are_dead_weight() {
        for prof in [&CS2, &DOTA] {
            for cls in ["CMsgVector", "v8::internal::Object", "std::vector<int>"] {
                assert!(
                    is_dead_weight_class(prof, cls),
                    "{cls} should be dead weight"
                );
            }
        }
    }

    #[test]
    fn one_hard_serializer_marks_plumbing_but_one_soft_does_not() {
        let prof = &CS2;
        let none = HashSet::new();
        let hard = format!("CFoo::{}", prof.hard_serializer[0]);
        let soft = format!("CFoo::{}", prof.soft_serializer[0]);
        // a lone HARD serializer method is decisive on its own
        assert!(is_serializer_plumbing(prof, &hard, &none));
        // a lone SOFT one is not — those names also occur on legitimate game classes
        assert!(!is_serializer_plumbing(prof, &soft, &none));
    }

    #[test]
    fn protobuf_clustering_needs_three_serializer_methods() {
        let prof = &CS2;
        let soft = prof.soft_serializer;
        assert!(
            soft.len() >= 3,
            "profile needs >=3 soft serializers for this rule to be reachable"
        );
        let two: Vec<String> = soft.iter().take(2).map(|m| format!("CTwo::{m}")).collect();
        let three: Vec<String> = soft
            .iter()
            .take(3)
            .map(|m| format!("CThree::{m}"))
            .collect();
        let all: Vec<&str> = two.iter().chain(three.iter()).map(String::as_str).collect();
        let flagged = protobuf_message_classes(prof, all.into_iter());
        assert!(
            flagged.contains("CThree"),
            "3 serializer methods should cluster as protobuf"
        );
        assert!(
            !flagged.contains("CTwo"),
            "2 methods is below the >=3 cluster threshold"
        );
    }

    #[test]
    fn class_of_prefers_scope_then_underscore_then_whole_name() {
        assert_eq!(class_of("CBaseEntity::TakeDamage"), "CBaseEntity");
        // a templated class keeps its template arguments
        assert_eq!(
            class_of("CHandle<CBaseEntity>::Get"),
            "CHandle<CBaseEntity>"
        );
        // no `::` falls back to the last underscore — this is how the ecosystem's flat
        // `CClass_Method` names still key a class
        assert_eq!(class_of("CCSPlayerPawn_Respawn"), "CCSPlayerPawn");
        // and with neither separator the whole name IS the class key
        assert_eq!(class_of("FreeFunction"), "FreeFunction");
    }

    #[test]
    fn query_methods_need_a_profile_prefix_not_just_get() {
        let prof = &CS2;
        // `is_query_method` gates the live CALL sweep — a false positive means blind-calling a method
        // that really takes arguments, so it must not fire on every `Get*`.
        let any_prefix_hit = prof
            .query_prefixes
            .iter()
            .any(|p| is_query_method(prof, &format!("CBaseEntity::{p}Something")));
        assert!(
            any_prefix_hit,
            "no profile query prefix matched its own pattern"
        );
        assert!(!is_query_method(prof, "CBaseEntity::Teleport"));
    }
}
