//! Pure model-catalogue compatibility types.

use core::cmp::Ordering;
use std::collections::HashMap;

use serde::{Deserialize, Serialize};

use crate::profile::StructuredAdapter;

/// Parsed CLI version used for local compatibility checks.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliVersion {
    /// Original version string reported by the CLI.
    pub raw: String,
    parts: Vec<u64>,
}

impl CliVersion {
    /// Parses a version from a string containing at least one digit.
    ///
    /// # Errors
    /// Returns [`ModelCatalogueError::InvalidVersion`] when no numeric version
    /// segment can be found.
    pub fn parse(raw: impl Into<String>) -> Result<Self, ModelCatalogueError> {
        let raw = raw.into();
        let start = raw
            .char_indices()
            .find_map(|(idx, ch)| ch.is_ascii_digit().then_some(idx))
            .ok_or_else(|| ModelCatalogueError::InvalidVersion(raw.clone()))?;
        let version = raw[start..]
            .chars()
            .take_while(|ch| ch.is_ascii_digit() || *ch == '.')
            .collect::<String>();
        let parts = version
            .split('.')
            .filter(|part| !part.is_empty())
            .map(str::parse::<u64>)
            .collect::<Result<Vec<_>, _>>()
            .map_err(|_| ModelCatalogueError::InvalidVersion(raw.clone()))?;
        if parts.is_empty() {
            return Err(ModelCatalogueError::InvalidVersion(raw));
        }
        Ok(Self { raw, parts })
    }
}

impl PartialOrd for CliVersion {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for CliVersion {
    fn cmp(&self, other: &Self) -> Ordering {
        let max_len = self.parts.len().max(other.parts.len());
        for idx in 0..max_len {
            match self
                .parts
                .get(idx)
                .copied()
                .unwrap_or(0)
                .cmp(&other.parts.get(idx).copied().unwrap_or(0))
            {
                Ordering::Equal => {}
                ordering => return ordering,
            }
        }
        Ordering::Equal
    }
}

/// Compatibility state between a local CLI version and a model id.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum ModelCompatibility {
    /// The known minimum CLI version is satisfied.
    Compatible,
    /// The CLI version is absent, or the model is not covered by the matrix.
    Unknown,
    /// The model is covered by the matrix but appears newer than the local CLI.
    LikelyTooRecent,
}

/// Origin of a model-catalogue entry.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum ModelCatalogSource {
    /// Curated static seed maintained by IdeA.
    Catalogue,
    /// Best-effort provider API discovery.
    Provider,
}

/// Matrix mapping adapter/model ids to their minimum known CLI version.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CompatibilityMatrix {
    /// Matrix schema/data version.
    pub version: u32,
    /// Claude Code entries keyed by model id.
    #[serde(default)]
    pub claude: HashMap<String, String>,
    /// Codex CLI entries keyed by model id.
    #[serde(default)]
    pub codex: HashMap<String, String>,
}

impl CompatibilityMatrix {
    /// Looks up the minimum CLI version for an adapter/model pair.
    #[must_use]
    pub fn minimum_version(&self, adapter: StructuredAdapter, model_id: &str) -> Option<&str> {
        match adapter {
            StructuredAdapter::Claude => self.claude.get(model_id).map(String::as_str),
            StructuredAdapter::Codex => self.codex.get(model_id).map(String::as_str),
            StructuredAdapter::OpenCode | StructuredAdapter::OpenAiCompatible => None,
        }
    }
}

/// Evaluates local compatibility using only pure matrix data.
#[must_use]
pub fn evaluate_compatibility(
    matrix: &CompatibilityMatrix,
    adapter: StructuredAdapter,
    model_id: &str,
    cli_version: Option<&CliVersion>,
) -> ModelCompatibility {
    let Some(cli_version) = cli_version else {
        return ModelCompatibility::Unknown;
    };
    let Some(minimum) = matrix.minimum_version(adapter, model_id) else {
        return ModelCompatibility::Unknown;
    };
    let Ok(minimum) = CliVersion::parse(minimum.to_owned()) else {
        return ModelCompatibility::Unknown;
    };
    if &minimum <= cli_version {
        ModelCompatibility::Compatible
    } else {
        ModelCompatibility::LikelyTooRecent
    }
}

/// Errors from pure model-catalogue parsing.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ModelCatalogueError {
    /// Version strings must contain at least one numeric segment.
    #[error("invalid CLI version: {0}")]
    InvalidVersion(String),
}

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

    fn matrix() -> CompatibilityMatrix {
        CompatibilityMatrix {
            version: 1,
            claude: HashMap::from([("claude-sonnet-5".to_owned(), "1.2.0".to_owned())]),
            codex: HashMap::from([("gpt-5-codex".to_owned(), "0.44.0".to_owned())]),
        }
    }

    #[test]
    fn cli_versions_compare_by_numeric_parts() {
        assert!(CliVersion::parse("codex 0.10.0").unwrap() > CliVersion::parse("0.9.9").unwrap());
        assert_eq!(
            CliVersion::parse("1.2")
                .unwrap()
                .cmp(&CliVersion::parse("1.2.0").unwrap()),
            Ordering::Equal
        );
    }

    #[test]
    fn compatibility_is_unknown_without_version_or_matrix_entry() {
        let matrix = matrix();
        assert_eq!(
            evaluate_compatibility(&matrix, StructuredAdapter::Codex, "gpt-5-codex", None),
            ModelCompatibility::Unknown
        );
        assert_eq!(
            evaluate_compatibility(
                &matrix,
                StructuredAdapter::Codex,
                "future-model",
                Some(&CliVersion::parse("999.0.0").unwrap())
            ),
            ModelCompatibility::Unknown
        );
    }

    #[test]
    fn compatibility_detects_supported_and_too_recent_models() {
        let matrix = matrix();
        assert_eq!(
            evaluate_compatibility(
                &matrix,
                StructuredAdapter::Codex,
                "gpt-5-codex",
                Some(&CliVersion::parse("0.44.0").unwrap())
            ),
            ModelCompatibility::Compatible
        );
        assert_eq!(
            evaluate_compatibility(
                &matrix,
                StructuredAdapter::Claude,
                "claude-sonnet-5",
                Some(&CliVersion::parse("1.1.9").unwrap())
            ),
            ModelCompatibility::LikelyTooRecent
        );
    }
}
