//! The canonical derived-gamedata model — the single in-memory representation the derivation produces
//! and the emitters consume. Deliberately format-agnostic (no serde_json shapes here): `render::*`
//! turns it into CSSharp JSON, Metamod VDF, etc. Keeping the shape here (not smeared through `json!`
//! call sites) is what lets one derivation feed every output format and a standalone generator.

use std::collections::BTreeMap;

/// A byte-pattern signature located in a specific library.
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Sig {
    pub library: String, // "server", "engine2", … (the module the pattern scans)
    pub linux: String,   // space-hex pattern with `?` wildcards, e.g. "55 48 89 ? E5"
}

/// One gamedata function: a vtable-method offset, a scan signature, or (rarely) both.
#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Entry {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub signature: Option<Sig>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub offset: Option<i64>, // vtable slot index (or a carried member offset)
}

impl Entry {
    /// A signature-only locator (the deriver's sig-XOR-offset invariant as a constructor).
    pub fn signature(library: impl Into<String>, linux: impl Into<String>) -> Entry {
        Entry {
            signature: Some(Sig {
                library: library.into(),
                linux: linux.into(),
            }),
            offset: None,
        }
    }

    /// A vtable-offset-only locator.
    pub fn offset(linux: i64) -> Entry {
        Entry {
            signature: None,
            offset: Some(linux),
        }
    }
}

/// The default output game-key when none is set — CS2's Steam content-dir token. Games-keyed emitters
/// (Metamod/Plugify) fall back to this so an older model JSON (no `game_key`) still renders as CS2.
fn default_game_key() -> String {
    "csgo".to_string()
}

/// The derived gamedata for one build: function name -> entry. BTreeMap so iteration/output is
/// deterministically key-sorted, matching serde_json's Map key ordering byte-for-byte.
///
/// Serializing this IS the canonical "model JSON" — the single per-build artifact the deriver
/// publishes and the standalone `source2rosetta-gen` reads back to produce every framework's format.
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct Gamedata {
    pub entries: BTreeMap<String, Entry>,
    /// The game token the game-keyed emitters wrap output in (Metamod `Games { <game_key> {..} }`,
    /// Plugify `{ "<game_key>": {..} }`). Persisted here because `source2rosetta-gen` renders from the model
    /// JSON alone, with no access to the deriver's `GameProfile`. Defaults to CS2's `csgo`.
    #[serde(default = "default_game_key")]
    pub game_key: String,
}

impl Default for Gamedata {
    fn default() -> Self {
        Self {
            entries: BTreeMap::new(),
            game_key: default_game_key(),
        }
    }
}

/// Why the derivation could not produce a shipped locator — the closed domain the `unresolved` tier reports.
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum FlagReason {
    /// A catalogued signature that no longer resolves uniquely / wasn't recovered in the target.
    SigDrifted,
    /// A vtable offset whose recency-weighted vote fell below the confidence bar.
    OffsetLowConf,
    /// No chainable anchor / no reference history at all.
    Unresolved,
    /// The sig SHIPPED, but its ABI prototype shape drifted from the model consensus (review the prototype).
    AbiDrift,
}

impl FlagReason {
    /// The kebab id — the same string the `kebab-case` serialization emits, for callers that carry the
    /// reason across to the monolith's `Unresolved.reason` (a `String`, kept stable for byte-reproducibility).
    pub fn as_str(self) -> &'static str {
        match self {
            FlagReason::SigDrifted => "sig-drifted",
            FlagReason::OffsetLowConf => "offset-low-conf",
            FlagReason::Unresolved => "unresolved",
            FlagReason::AbiDrift => "abi-drift",
        }
    }
}

/// A catalogue entry the derivation could NOT confidently produce. Emitted as a first-class sidecar
/// (never guessed into the gamedata — safety > recall) so it is both reviewable and machine-readable.
#[derive(Debug, Clone, serde::Serialize)]
pub struct Flagged {
    pub name: String,
    pub reason: FlagReason,
    /// The signal we do have: the carried value, vote confidence, "no reference", …
    pub detail: String,
}

impl Flagged {
    pub fn new(name: impl Into<String>, reason: FlagReason, detail: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            reason,
            detail: detail.into(),
        }
    }
}

impl Gamedata {
    pub fn set_signature(
        &mut self,
        name: impl Into<String>,
        library: impl Into<String>,
        linux: impl Into<String>,
    ) {
        self.entries.entry(name.into()).or_default().signature = Some(Sig {
            library: library.into(),
            linux: linux.into(),
        });
    }

    pub fn set_offset(&mut self, name: impl Into<String>, linux: i64) {
        self.entries.entry(name.into()).or_default().offset = Some(linux);
    }

    pub fn len(&self) -> usize {
        self.entries.len()
    }

    pub fn is_empty(&self) -> bool {
        self.entries.is_empty()
    }
}

// ===========================================================================================
// The monolith model — the shipped `gamedata-<game>.json`. Four confidence tiers with provenance
// + live-validation folded inline; `source2rosetta-gen` renders it into any framework format, and it is
// equally readable as-is by a consumer. A `MonoEntry` EMBEDS `Entry`, so the locator shape stays
// single-sourced on `render::locator_value` and never diverges. Lib-agnostic: an entry carries its
// `library` in the signature locator, so the monolith spans every derived library, not just libserver.
// ===========================================================================================

/// A monolith entry's confidence tier — its finer label within a section, serialized as kebab strings
/// (`core`, `self-named`, `dict-exact`, `contextual`, `corroborated`, `high`, `medium`, `low`).
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Tier {
    Core,
    SelfNamed,
    /// Dictionary-corroborated by the FOLD (an exact hit in the harvested name catalogue).
    DictExact,
    Contextual,
    /// Dictionary-corroborated by the EXPERIMENTAL band, and also the label the macOS ground-truth
    /// transfer carries. Kept distinct from [`Tier::DictExact`] rather than merged: the two are produced
    /// by different paths, and folding them together would additionally conflate ground-truth symbols
    /// with dictionary guesses. Anything counting "corroborated" for display must count BOTH — see
    /// [`Tier::is_dict_corroborated`].
    Corroborated,
    High,
    Medium,
    Low,
}

impl Tier {
    /// Is this tier a dictionary/ground-truth corroboration, under either of its two labels?
    pub fn is_dict_corroborated(self) -> bool {
        matches!(self, Tier::DictExact | Tier::Corroborated)
    }
}

/// Per-entry provenance, folded inline. Optional by tier: a `core` entry carries almost nothing, an
/// `experimental` guess carries the full grading.
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct Provenance {
    pub tier: Tier,
    /// Raw address in THIS build — a debugging/trace anchor (the monolith is per-build, so it's coherent).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub addr: Option<String>,
    /// The confidence LABEL as the harvest records it — `"high"` / `"medium"` / `"low"` — a separate axis
    /// from `tier` (a self-named entry can still be low-confidence). A string, not a number.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub confidence: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub self_named: Option<bool>,
    /// Return is struct-by-value → unsafe to naive-call (the `RetClass::ByValue` flag).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub by_value: Option<bool>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub ret_class: Option<String>,
    /// "catalogue" | "source2rosetta-nameext" | "contribution:<date>" | …
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub source: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub rtti_class: Option<String>,
    /// high_confidence tier only — the naming rationale.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub rationale: Option<String>,
    /// experimental tier only — how the dictionary corroborated the guess (`"exact"` / `"bare"` / `"none"`).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub corroboration: Option<String>,
    /// experimental tier only — the same name was guessed at more than one address.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub collision: Option<bool>,
    /// experimental tier only — protobuf/serializer/foreign plumbing (flagged, kept for completeness).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub dead_weight: Option<bool>,
    /// core only — the target's ABI prototype-shape differs from the model's consensus (`"target [..] vs
    /// history [..]"`). The signature still ships (a drifted arg-list is a loader-hook seam a byte-sig can't
    /// see, not a wrong locator), but a consumer that ptrace-calls it should re-check the prototype.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub abi_drift: Option<String>,
}

impl Tier {
    /// The tier's kebab id — the same string the `kebab-case` serialization emits, for a deriver that needs
    /// it as a plain `&str` (e.g. a count-by-tier tally) without going through serde.
    pub fn as_str(self) -> &'static str {
        match self {
            Tier::Core => "core",
            Tier::SelfNamed => "self-named",
            Tier::DictExact => "dict-exact",
            Tier::Contextual => "contextual",
            Tier::Corroborated => "corroborated",
            Tier::High => "high",
            Tier::Medium => "medium",
            Tier::Low => "low",
        }
    }

    /// Parse a tier from its kebab id (the inverse of the `kebab-case` serialization) — so a deriver holding
    /// a tier as a computed string can lift it to the typed enum without a serde_json round-trip. `None` for
    /// an unrecognised id (the caller decides whether that is a hard error).
    pub fn from_id(s: &str) -> Option<Tier> {
        Some(match s {
            "core" => Tier::Core,
            "self-named" => Tier::SelfNamed,
            "dict-exact" => Tier::DictExact,
            "contextual" => Tier::Contextual,
            "corroborated" => Tier::Corroborated,
            "high" => Tier::High,
            "medium" => Tier::Medium,
            "low" => Tier::Low,
            _ => return None,
        })
    }
}

impl Provenance {
    /// A provenance with only its tier set (all optional fields `None`) — the base for `core` entries and
    /// the start point for functional-update construction (`Provenance { source: …, ..with_tier(t) }`).
    pub fn with_tier(tier: Tier) -> Self {
        Self {
            tier,
            addr: None,
            confidence: None,
            self_named: None,
            by_value: None,
            ret_class: None,
            source: None,
            rtti_class: None,
            rationale: None,
            corroboration: None,
            collision: None,
            dead_weight: None,
            abi_drift: None,
        }
    }
}

/// One monolith function: a locator (the `signature`/`offset` model shape, flattened in from [`Entry`] so
/// the shape is single-sourced) plus its provenance and live-validation verdict.
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct MonoEntry {
    #[serde(flatten)]
    pub locator: Entry,
    /// experimental offsets only: the vtable class the slot lives on (a reader's eyeball check).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub class: Option<String>,
    pub provenance: Provenance,
    /// Live-validation verdict: `Some(true)` passed, `Some(false)` dropped confident-bad, `None` unvalidated.
    #[serde(default)]
    pub validated: Option<bool>,
}

/// A catalogued function the derivation could not confidently produce — kept in-file (never a shipped
/// locator) so the monolith is the complete catalogue picture.
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct Unresolved {
    pub reason: String, // "sig-drifted" | "offset-low-conf" | "unresolved" | …
    pub detail: String,
}

/// Entry counts per section — a struct (not a map) so it serializes in this logical order, deterministically.
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct Counts {
    pub core: usize,
    pub high_confidence: usize,
    pub experimental: usize,
    pub unresolved: usize,
}

/// The monolith's intrinsic release identity. NO wall-clock field — volatile release metadata (`produced_at`,
/// `status`, urls, sha256, `based_on`) lives in the per-buildid manifest, so the monolith is fully
/// byte-reproducible.
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct MonoMeta {
    pub game_key: String,
    pub game: String, // display name
    pub source_build: String,
    /// `<game>-<buildid>-<patch>` — the buildid is embedded here; the per-buildid MANIFEST carries it as its
    /// own field (volatile metadata is kept OUT of this byte-reproducible monolith).
    pub version: String,
    pub counts: Counts,
}

/// The full derived gamedata for one build — the shipped `gamedata-<game>.json`. Four confidence tiers, each
/// a key-sorted map. `source2rosetta-gen` renders it into any framework format; a consumer can equally read it
/// directly, gating `experimental` behind a runtime toggle off each entry's tier.
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct Monolith {
    pub meta: MonoMeta,
    pub core: BTreeMap<String, MonoEntry>,
    pub high_confidence: BTreeMap<String, MonoEntry>,
    pub experimental: BTreeMap<String, MonoEntry>,
    pub unresolved: BTreeMap<String, Unresolved>,
}

/// Which of the monolith's three SHIPPABLE tiers a render includes — cumulative, most-confident first
/// (`unresolved` is never rendered; it has no locator). The `--tier` arg of `source2rosetta-gen`.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TierSelect {
    /// `core` only — the guaranteed, live-validated set.
    Core,
    /// core + high_confidence — adds the promoted name-extrapolations.
    HighConfidence,
    /// core + high_confidence + experimental — every locatable guess.
    Experimental,
}

impl TierSelect {
    /// Parse the `--tier` id (the monolith tier names, plus a couple of intuitive aliases).
    pub fn from_id(s: &str) -> Option<TierSelect> {
        match s {
            "core" => Some(TierSelect::Core),
            "high_confidence" | "high-confidence" | "stable" => Some(TierSelect::HighConfidence),
            "experimental" | "full" => Some(TierSelect::Experimental),
            _ => None,
        }
    }
}

/// Every `--tier` id `from_id` accepts (canonical names) — for help text.
pub const TIER_IDS: &[&str] = &["core", "high_confidence", "experimental"];

impl Monolith {
    /// Flatten the tiers up to `select` into one `name -> Entry` map — the input the flat framework emitters
    /// (metamod/modsharp/swiftly/plugify) consume. Drops confident-bad entries (`validated == Some(false)`);
    /// `unresolved` is never included (no locator); a more-confident tier wins a name collision.
    pub fn select(&self, select: TierSelect) -> Gamedata {
        let mut gd = Gamedata {
            entries: BTreeMap::new(),
            game_key: self.meta.game_key.clone(),
        };
        let mut add = |m: &BTreeMap<String, MonoEntry>| {
            for (name, e) in m {
                if e.validated == Some(false) {
                    continue;
                }
                gd.entries
                    .entry(name.clone())
                    .or_insert_with(|| e.locator.clone());
            }
        };
        add(&self.core);
        if select != TierSelect::Core {
            add(&self.high_confidence);
        }
        if select == TierSelect::Experimental {
            add(&self.experimental);
        }
        gd
    }
}

/// One typed schema field (the field NAME is the map key). `offset` is static; `ty`/`kind`/`size` are
/// runtime-resolved (empty/zero when derived offline without a live process).
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct Field {
    pub offset: i32,
    #[serde(rename = "type", default, skip_serializing_if = "String::is_empty")]
    pub ty: String,
    #[serde(default)]
    pub kind: FieldKind,
    pub size: usize,
    pub name_hash: u64,
}

/// How a schema field holds its value — a closed runtime domain (Source-2 `CSchemaType` category).
/// Serializes to lowercase tokens, the values `netvars-<game>.json` consumers expect.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FieldKind {
    /// Builtin / atomic / declared class / declared enum — held inline (the common case).
    #[default]
    Ref,
    /// A pointer to the value.
    Ptr,
    /// A fixed-size inline array.
    FixedArray,
}

/// The typed schema — the shipped `netvars-<game>.json`. Merges field offsets with runtime types:
/// class -> field -> [`Field`].
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct Schema {
    pub meta: SchemaMeta,
    pub classes: BTreeMap<String, BTreeMap<String, Field>>,
}

/// The typed schema's intrinsic identity (no wall-clock field, same rationale as [`MonoMeta`]).
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct SchemaMeta {
    pub game_key: String,
    pub source_build: String,
    pub typed: usize,
    pub untyped: usize,
}

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

    #[test]
    fn mono_entry_flattens_locator_and_kebabs_tier() {
        let e = MonoEntry {
            locator: Entry {
                signature: None,
                offset: Some(158),
            },
            class: None,
            provenance: Provenance {
                source: Some("catalogue".into()),
                ..Provenance::with_tier(Tier::Core)
            },
            validated: Some(true),
        };
        let v = serde_json::to_value(&e).unwrap();
        assert_eq!(v["offset"], 158); // locator flattened to the top level
        assert!(v.get("signature").is_none()); // None locator field omitted
        assert_eq!(v["provenance"]["tier"], "core"); // kebab-case enum
        assert_eq!(v["provenance"]["source"], "catalogue");
        assert!(v["provenance"].get("confidence").is_none()); // None provenance field skipped
        assert_eq!(v["validated"], true);
    }

    #[test]
    fn experimental_entry_keeps_class_and_serializes_null_validated() {
        let e = MonoEntry {
            locator: Entry {
                signature: None,
                offset: Some(40),
            },
            class: Some("CFoo".into()),
            provenance: Provenance {
                confidence: Some("low".into()),
                self_named: Some(false),
                collision: Some(true),
                dead_weight: Some(false),
                ..Provenance::with_tier(Tier::Low)
            },
            validated: None,
        };
        let v = serde_json::to_value(&e).unwrap();
        assert_eq!(v["class"], "CFoo");
        assert_eq!(v["provenance"]["tier"], "low");
        assert_eq!(v["provenance"]["collision"], true);
        assert_eq!(v["validated"], serde_json::Value::Null); // present as null, not omitted
    }

    #[test]
    fn monolith_round_trips() {
        let mut m = Monolith {
            meta: MonoMeta {
                game_key: "csgo".into(),
                game: "CS2".into(),
                source_build: "2026-07-15_003539".into(),
                version: "cs2-12345-0".into(),
                counts: Counts {
                    core: 1,
                    high_confidence: 0,
                    experimental: 0,
                    unresolved: 1,
                },
            },
            core: BTreeMap::new(),
            high_confidence: BTreeMap::new(),
            experimental: BTreeMap::new(),
            unresolved: BTreeMap::new(),
        };
        m.core.insert(
            "A::b".into(),
            MonoEntry {
                locator: Entry {
                    signature: Some(Sig {
                        library: "server".into(),
                        linux: "55 48 89 E5".into(),
                    }),
                    offset: None,
                },
                class: None,
                provenance: Provenance {
                    source: Some("catalogue".into()),
                    ..Provenance::with_tier(Tier::Core)
                },
                validated: Some(true),
            },
        );
        m.unresolved.insert(
            "C::d".into(),
            Unresolved {
                reason: "sig-drifted".into(),
                detail: "no unique/recovered signature in target".into(),
            },
        );
        let s = serde_json::to_string_pretty(&m).unwrap();
        let back: Monolith = serde_json::from_str(&s).unwrap();
        assert_eq!(back.core.len(), 1);
        assert_eq!(
            back.core["A::b"]
                .locator
                .signature
                .as_ref()
                .unwrap()
                .library,
            "server"
        );
        assert_eq!(back.unresolved["C::d"].reason, "sig-drifted");
        assert_eq!(back.meta.counts.unresolved, 1);
    }
}
