//! Data model for the compliance scoring output.
//!
//! These are the plain types the renderers consume: the letter [`Grade`],
//! the [`Section`] enum, the per-section [`SectionScore`], the
//! [`ApplicabilitySummary`], the [`Rubric`] inputs, and the final
//! [`ComplianceScore`]. The scoring arithmetic that produces them lives in
//! [`super`] (`compliance::scoring`); this file is types only. Everything is
//! re-exported from [`super`] so external paths keep resolving unchanged.

use std::collections::{BTreeSet, HashMap};

use serde::{Deserialize, Serialize};

use super::registry::RuleRegistry;

/// String alias used for rule identifiers in the public output struct.
///
/// Kept as an alias instead of a newtype so the rubric stays
/// allocation-friendly: registries already hand out owned `String`s and
/// renderers want `&str` views. The alias documents intent at type sites
/// without forcing a conversion at every boundary.
pub type CheckId = String;

/// Letter grade emitted by the rubric.
///
/// Ordered worst to best for `Ord`-based comparisons so tests can express
/// "this run never grades below X" without a match arm.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum Grade {
    /// At least one MUST rule failed. Server is not RFC 2119 compliant.
    F,
    /// All MUST pass; under 50% of SHOULD rules pass.
    D,
    /// All MUST pass; between 50% and 79% of SHOULD rules pass.
    C,
    /// All MUST pass; between 80% and 99% of SHOULD rules pass.
    B,
    /// All MUST and SHOULD rules pass.
    A,
    /// A, plus at least one MAY rule passes in every applicable section.
    APlus,
}

impl Grade {
    /// Short ASCII label used by renderers and the JSON wire shape.
    pub fn as_str(&self) -> &'static str {
        match self {
            Grade::APlus => "A+",
            Grade::A => "A",
            Grade::B => "B",
            Grade::C => "C",
            Grade::D => "D",
            Grade::F => "F",
        }
    }
}

/// Rubric section bucket. Mirrors [`super::registry::RuleMetadata::section`]
/// but as a typed enum so HashMap keys cannot drift due to a typo in a
/// string literal.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum Section {
    /// Initialize handshake, capability negotiation, shutdown.
    Lifecycle,
    /// `tools/*` rules.
    Tools,
    /// `resources/*` rules.
    Resources,
    /// `prompts/*` rules.
    Prompts,
    /// Transport-layer and auth rules (headers, OAuth, TLS).
    TransportAuth,
}

impl Section {
    /// All sections in stable display order. Iterators ride this list so
    /// the reporter output is deterministic.
    pub const ALL: &'static [Section] = &[
        Section::Lifecycle,
        Section::Tools,
        Section::Resources,
        Section::Prompts,
        Section::TransportAuth,
    ];

    /// Parse the registry's section string. Returns `None` for unknown
    /// values (the registry validator already rejects those, but the
    /// scorer stays defensive).
    pub fn from_registry_str(s: &str) -> Option<Section> {
        match s {
            "lifecycle" => Some(Section::Lifecycle),
            "tools" => Some(Section::Tools),
            "resources" => Some(Section::Resources),
            "prompts" => Some(Section::Prompts),
            "transport-auth" => Some(Section::TransportAuth),
            _ => None,
        }
    }

    /// Canonical lowercase string used in the registry YAML.
    pub fn as_str(&self) -> &'static str {
        match self {
            Section::Lifecycle => "lifecycle",
            Section::Tools => "tools",
            Section::Resources => "resources",
            Section::Prompts => "prompts",
            Section::TransportAuth => "transport-auth",
        }
    }
}

/// Per-section pass/fail tallies used by the renderers.
///
/// Stores raw counts rather than ratios so renderers can decide how to
/// display them (X of Y vs percentage) without re-deriving the totals.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SectionScore {
    /// Section identifier; carried inline so reporters can iterate over
    /// the values without re-keying the map.
    pub section: Section,
    /// True when the negotiated capabilities exercise this section.
    /// When false the section is rendered as `N/A` and excluded from the
    /// grade math.
    pub applicable: bool,
    /// MUST rules that passed.
    pub must_passed: usize,
    /// MUST rules applicable in this section.
    pub must_total: usize,
    /// SHOULD rules that passed.
    pub should_passed: usize,
    /// SHOULD rules applicable in this section.
    pub should_total: usize,
    /// MAY rules that passed.
    pub may_passed: usize,
    /// MAY rules applicable in this section.
    pub may_total: usize,
    /// Per-section letter grade. Computed with the same table as the
    /// overall grade, scoped to the section's own counts.
    pub grade: Grade,
    /// MAY rule IDs in this section that did not pass. Renderers use
    /// this list to suggest which MAYs to implement next for the A+
    /// promotion.
    pub missing_may_ids: Vec<CheckId>,
}

/// Snapshot of which sections counted toward the grade and which were
/// excluded as not applicable.
///
/// Exists so a downstream dashboard can answer "what slice of MCP did
/// this run cover" without re-walking the per-section map. The numeric
/// `applicable_fraction` matches [`ComplianceScore::completeness_score`]
/// and is duplicated here for callers that prefer the summary shape.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ApplicabilitySummary {
    /// Capability keys the server declared at `initialize`. Stored in
    /// the summary so reports can show "scored against tools, prompts"
    /// without consulting the original rubric.
    pub declared_capabilities: BTreeSet<String>,
    /// Sections that contributed to the grade.
    pub applicable_sections: Vec<Section>,
    /// Sections that were skipped because their feature was not
    /// declared. A renderer marks these as N/A.
    pub not_applicable_sections: Vec<Section>,
    /// Fraction of [`Section::ALL`] that were applicable, in `[0, 1]`.
    pub applicable_fraction: f64,
}

/// Inputs the scorer needs that are not in the registry: the rubric
/// version string and the set of negotiated MCP capabilities.
///
/// Capability strings come straight from the negotiated `capabilities`
/// object on the initialize response (`tools`, `resources`, `prompts`,
/// etc.). The scorer treats `lifecycle` and `transport-auth` as always
/// applicable because every MCP server has a lifecycle and a transport.
#[derive(Debug, Clone)]
pub struct Rubric<'a> {
    /// Registry the rules came from.
    pub registry: &'a RuleRegistry,
    /// Rubric format version surfaced in reporter output.
    pub version: String,
    /// Capabilities the server declared during `initialize`. Used to
    /// decide whether the `tools`, `resources`, and `prompts` sections
    /// are applicable. Lowercase strings, for example `["tools",
    /// "resources"]`.
    pub declared_capabilities: BTreeSet<String>,
}

impl<'a> Rubric<'a> {
    /// Build a rubric with the conventional defaults.
    pub fn new(registry: &'a RuleRegistry, declared_capabilities: BTreeSet<String>) -> Self {
        Self {
            registry,
            version: "v1".into(),
            declared_capabilities,
        }
    }

    /// True when a section is applicable to this run.
    ///
    /// Lifecycle and transport-auth are always applicable. The
    /// feature-flavored sections (tools, resources, prompts) require the
    /// matching capability key to appear in the declared set.
    pub fn section_applicable(&self, section: Section) -> bool {
        match section {
            Section::Lifecycle | Section::TransportAuth => true,
            Section::Tools => self.declared_capabilities.contains("tools"),
            Section::Resources => self.declared_capabilities.contains("resources"),
            Section::Prompts => self.declared_capabilities.contains("prompts"),
        }
    }
}

/// Final scored run.
///
/// `numeric_score` is independent of `grade`: it carries the underlying
/// weighted percentage so dashboards can chart trends without re-running
/// the scorer. `missing_for_a_plus` is the flat list of MAY rule IDs the
/// run would need to pass to reach A+; per-section detail lives on each
/// [`SectionScore::missing_may_ids`].
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ComplianceScore {
    /// Overall letter grade.
    pub grade: Grade,
    /// Weighted percentage in `[0, 100]`.
    pub numeric_score: f64,
    /// Fraction of [`Section::ALL`] that were applicable. A stdio-only
    /// server typically lands at 0.6 (lifecycle + transport-auth +
    /// tools).
    pub completeness_score: f64,
    /// Per-section breakdown, keyed by section.
    pub sections: HashMap<Section, SectionScore>,
    /// Applicability snapshot mirroring `sections` in a renderer-friendly
    /// shape.
    pub applicability_summary: ApplicabilitySummary,
    /// Rubric version that produced the score.
    pub rubric_version: String,
    /// Rule IDs of every MUST that failed. Renderers lead with these
    /// when the grade is F.
    pub failed_must_checks: Vec<CheckId>,
    /// MAY rule IDs (across every applicable section) the run would
    /// need to pass to promote from A to A+. Empty when the run is
    /// below A or already at A+.
    pub missing_for_a_plus: Vec<CheckId>,
}
