//! The competitive bakeoff matrix model and its loader (WOR-1428).
//!
//! The matrix is the single source of truth in `registry.yml`, embedded at
//! build time. It captures, for a pinned snapshot, how the MCP testing / eval /
//! security / runtime landscape compares on a fixed set of capability axes, plus
//! mcptest's own position and the Linear ticket behind every mcptest gap.

use std::collections::BTreeMap;

use serde::{Deserialize, Serialize};

/// The canonical matrix, embedded from `registry.yml`.
const EMBEDDED_YAML: &str = include_str!("registry.yml");

/// A single capability support value.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Support {
    /// Fully supported.
    Yes,
    /// Partially supported.
    Partial,
    /// Not supported.
    No,
    /// Available only behind a hosted account, not local OSS.
    Hosted,
    /// Not applicable to this tool's role.
    Na,
}

impl Support {
    /// A short cell glyph for the markdown table.
    pub fn glyph(self) -> &'static str {
        match self {
            Support::Yes => "yes",
            Support::Partial => "partial",
            Support::No => "no",
            Support::Hosted => "hosted",
            Support::Na => "n/a",
        }
    }

    /// Whether this counts as a red/yellow gap for mcptest (needs a ticket).
    pub fn is_gap(self) -> bool {
        matches!(self, Support::Partial | Support::No)
    }
}

/// One capability axis the scorecard compares on.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Criterion {
    /// Stable id, referenced by every capability map.
    pub id: String,
    /// Display name.
    pub name: String,
    /// One-line description.
    pub description: String,
}

/// One competitor row.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Competitor {
    /// Stable id.
    pub id: String,
    /// Display name.
    pub name: String,
    /// Project URL.
    pub url: String,
    /// The pinned version snapshot this row was verified at.
    pub version: String,
    /// SPDX-ish license label.
    pub license: String,
    /// `local`, `hosted`, or `both`.
    pub hosting: String,
    /// Support per criterion id.
    pub capabilities: BTreeMap<String, Support>,
}

/// One mcptest capability, with the gap ticket when it is not fully supported.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct McptestCapability {
    /// Support value.
    pub support: Support,
    /// Linear ticket behind a partial/no gap.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub gap_issue: Option<String>,
}

/// mcptest's own position in the matrix.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct McptestRow {
    /// mcptest version.
    pub version: String,
    /// Support per criterion id, with gap tickets.
    pub capabilities: BTreeMap<String, McptestCapability>,
}

/// The full bakeoff matrix.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BakeoffMatrix {
    /// Schema id.
    pub schema_version: String,
    /// The date the matrix data was last verified.
    pub as_of: String,
    /// The capability axes.
    pub criteria: Vec<Criterion>,
    /// The competitor rows.
    pub competitors: Vec<Competitor>,
    /// mcptest's own row.
    pub mcptest: McptestRow,
}

impl BakeoffMatrix {
    /// Load the embedded matrix, validating its cross-references.
    pub fn embedded() -> Result<Self, BakeoffError> {
        let matrix: BakeoffMatrix =
            serde_yaml::from_str(EMBEDDED_YAML).map_err(|e| BakeoffError::Parse(e.to_string()))?;
        matrix.validate()?;
        Ok(matrix)
    }

    /// Enforce the invariants a malformed edit could break.
    ///
    /// Every criterion must be scored by mcptest, and every mcptest gap
    /// (partial/no) must carry a `gap_issue`. This is the rule that keeps the
    /// scorecard honest: a gap with no ticket fails to load.
    pub fn validate(&self) -> Result<(), BakeoffError> {
        for criterion in &self.criteria {
            let cap = self
                .mcptest
                .capabilities
                .get(&criterion.id)
                .ok_or_else(|| {
                    BakeoffError::Invalid(format!("mcptest does not score `{}`", criterion.id))
                })?;
            if cap.support.is_gap() && cap.gap_issue.is_none() {
                return Err(BakeoffError::Invalid(format!(
                    "mcptest gap on `{}` has no gap_issue",
                    criterion.id
                )));
            }
        }
        Ok(())
    }

    /// The mcptest gaps, as `(criterion, support, issue)` triples.
    pub fn gaps(&self) -> Vec<(&Criterion, Support, &str)> {
        self.criteria
            .iter()
            .filter_map(|c| {
                let cap = self.mcptest.capabilities.get(&c.id)?;
                if cap.support.is_gap() {
                    Some((c, cap.support, cap.gap_issue.as_deref().unwrap_or("")))
                } else {
                    None
                }
            })
            .collect()
    }
}

/// Errors raised loading or validating the bakeoff matrix.
#[derive(Debug, thiserror::Error)]
pub enum BakeoffError {
    /// The embedded YAML did not parse.
    #[error("failed to parse the bakeoff matrix: {0}")]
    Parse(String),
    /// The matrix broke an invariant.
    #[error("invalid bakeoff matrix: {0}")]
    Invalid(String),
}
