//! Authored claim types for human-written claims.
//!
//! Unlike `Observation` (grep results from extractors), an `AuthoredClaim`
//! is a deliberate assertion with provenance, invariants, and consequences.
//!
//! Example: "All wallet atomics MUST use SeqCst" is a claim.
//! "ordering = SeqCst found at line 42" is an observation.

use serde::{Deserialize, Serialize};
use stemedb_core::types::{ObjectValue, SourceClass};

use crate::AphoriaError;

/// A human-authored claim with full provenance.
///
/// Authored claims live in `.aphoria/claims.toml` and represent deliberate
/// architectural decisions, safety invariants, or policy requirements.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthoredClaim {
    /// Human-readable slug (e.g., "wallet-seqcst-001").
    pub id: String,

    /// Concept path (e.g., "maxwell/wallet/atomics/ordering").
    pub concept_path: String,

    /// Predicate (e.g., "required_ordering").
    pub predicate: String,

    /// The claimed value.
    pub value: AuthoredValue,

    /// How to compare the claim value against observations.
    #[serde(default)]
    pub comparison: ComparisonMode,

    /// Who/what established this claim (e.g., "Safety analysis by lead developer").
    pub provenance: String,

    /// The invariant this claim enforces (e.g., "All wallet atomics MUST use SeqCst").
    pub invariant: String,

    /// What happens if violated (e.g., "Double-spend race condition").
    pub consequence: String,

    /// Authority tier as a string: "regulatory", "clinical", "observational", "expert", "community", "anecdotal".
    pub authority_tier: String,

    /// Supporting evidence references (e.g., ["wallet ADR-003", "Intel SDM Vol 4"]).
    #[serde(default)]
    pub evidence: Vec<String>,

    /// Category: "safety", "architecture", "imports", "constants", "derives", etc.
    pub category: String,

    /// Claim lifecycle status.
    #[serde(default)]
    pub status: ClaimStatus,

    /// ID of claim this supersedes (if any).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub supersedes: Option<String>,

    /// Author who created this claim.
    pub created_by: String,

    /// ISO 8601 creation timestamp.
    pub created_at: String,

    /// ISO 8601 last-update timestamp.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub updated_at: Option<String>,
}

/// TOML-friendly value types (no nested enums).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum AuthoredValue {
    /// Boolean value.
    Bool(bool),
    /// Numeric value.
    Number(f64),
    /// Text value.
    Text(String),
}

impl std::fmt::Display for AuthoredValue {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            AuthoredValue::Bool(b) => write!(f, "{b}"),
            AuthoredValue::Number(n) => write!(f, "{n}"),
            AuthoredValue::Text(s) => write!(f, "{s}"),
        }
    }
}

impl AuthoredValue {
    /// Convert to Episteme's `ObjectValue` for bridge conversion.
    pub fn to_object_value(&self) -> ObjectValue {
        match self {
            AuthoredValue::Bool(b) => ObjectValue::Boolean(*b),
            AuthoredValue::Number(n) => ObjectValue::Number(*n),
            AuthoredValue::Text(s) => ObjectValue::Text(s.clone()),
        }
    }

    /// Parse from a CLI string value.
    ///
    /// Tries boolean, then number, then falls back to text.
    pub fn parse(s: &str) -> Self {
        if let Ok(b) = s.parse::<bool>() {
            return AuthoredValue::Bool(b);
        }
        if let Ok(n) = s.parse::<f64>() {
            return AuthoredValue::Number(n);
        }
        AuthoredValue::Text(s.to_string())
    }
}

/// How to compare an authored claim's value against observations.
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ComparisonMode {
    /// Observation value must equal claim value.
    #[default]
    Equals,
    /// Observation value must differ from claim value.
    NotEquals,
    /// At least one observation must exist at this path.
    Present,
    /// No observation should exist at this path.
    Absent,
    /// Observation value must contain claim value as substring.
    /// For lists (comma-separated), checks if claim value appears as an element.
    /// Example: claim "Serialize", observation "Clone,Debug,Serialize" → PASS
    Contains,
    /// Observation value must NOT contain claim value as substring.
    /// For lists (comma-separated), checks that claim value does NOT appear.
    /// Example: claim "Clone", observation "Debug" → PASS
    /// Example: claim "Clone", observation "Clone,Debug" → CONFLICT
    NotContains,
}

impl std::fmt::Display for ComparisonMode {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ComparisonMode::Equals => write!(f, "equals"),
            ComparisonMode::NotEquals => write!(f, "not_equals"),
            ComparisonMode::Present => write!(f, "present"),
            ComparisonMode::Absent => write!(f, "absent"),
            ComparisonMode::Contains => write!(f, "contains"),
            ComparisonMode::NotContains => write!(f, "not_contains"),
        }
    }
}

/// Claim lifecycle status.
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ClaimStatus {
    /// Claim is active and enforced.
    #[default]
    Active,
    /// Claim has been deprecated (no longer enforced).
    Deprecated,
    /// Claim has been superseded by a newer claim.
    Superseded,
}

impl std::fmt::Display for ClaimStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ClaimStatus::Active => write!(f, "active"),
            ClaimStatus::Deprecated => write!(f, "deprecated"),
            ClaimStatus::Superseded => write!(f, "superseded"),
        }
    }
}

/// Parse an authority tier string into a `SourceClass`.
///
/// Accepted values: "regulatory", "clinical", "observational", "team_policy", "expert", "community", "anecdotal".
pub fn parse_authority_tier(s: &str) -> Result<SourceClass, AphoriaError> {
    match s.to_lowercase().as_str() {
        "regulatory" => Ok(SourceClass::Regulatory),
        "clinical" => Ok(SourceClass::Clinical),
        "observational" => Ok(SourceClass::Observational),
        "team_policy" => Ok(SourceClass::TeamPolicy),
        "expert" => Ok(SourceClass::Expert),
        "community" => Ok(SourceClass::Community),
        "anecdotal" => Ok(SourceClass::Anecdotal),
        _ => Err(AphoriaError::Claims(format!(
            "Unknown authority tier '{s}'. Expected: regulatory, clinical, observational, team_policy, expert, community, anecdotal"
        ))),
    }
}

/// Format a `SourceClass` for display (e.g., "Expert (Tier 3)").
pub fn format_authority_tier(source_class: SourceClass) -> String {
    let name = match source_class {
        SourceClass::Regulatory => "Regulatory",
        SourceClass::Clinical => "Clinical",
        SourceClass::Observational => "Observational",
        SourceClass::TeamPolicy => "TeamPolicy",
        SourceClass::Expert => "Expert",
        SourceClass::Community => "Community",
        SourceClass::Anecdotal => "Anecdotal",
    };
    let tier = source_class.tier_fractional();
    if tier.fract() == 0.0 {
        format!("{name} (Tier {tier})", tier = tier as u8)
    } else {
        format!("{name} (Tier {tier})")
    }
}

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

    #[test]
    fn test_authored_value_parse() {
        assert_eq!(AuthoredValue::parse("true"), AuthoredValue::Bool(true));
        assert_eq!(AuthoredValue::parse("false"), AuthoredValue::Bool(false));
        assert_eq!(AuthoredValue::parse("42"), AuthoredValue::Number(42.0));
        assert_eq!(AuthoredValue::parse("2.71"), AuthoredValue::Number(2.71));
        assert_eq!(AuthoredValue::parse("SeqCst"), AuthoredValue::Text("SeqCst".to_string()));
    }

    #[test]
    fn test_authored_value_to_object_value() {
        assert_eq!(AuthoredValue::Bool(true).to_object_value(), ObjectValue::Boolean(true));
        assert_eq!(AuthoredValue::Number(42.0).to_object_value(), ObjectValue::Number(42.0));
        assert_eq!(
            AuthoredValue::Text("hello".to_string()).to_object_value(),
            ObjectValue::Text("hello".to_string())
        );
    }

    #[test]
    fn test_parse_authority_tier() {
        assert_eq!(parse_authority_tier("regulatory").ok(), Some(SourceClass::Regulatory));
        assert_eq!(parse_authority_tier("Expert").ok(), Some(SourceClass::Expert));
        assert_eq!(parse_authority_tier("CLINICAL").ok(), Some(SourceClass::Clinical));
        assert_eq!(parse_authority_tier("team_policy").ok(), Some(SourceClass::TeamPolicy));
        assert!(parse_authority_tier("unknown").is_err());
    }

    #[test]
    fn test_format_authority_tier() {
        assert_eq!(format_authority_tier(SourceClass::Expert), "Expert (Tier 3)");
        assert_eq!(format_authority_tier(SourceClass::Regulatory), "Regulatory (Tier 0)");
        assert_eq!(format_authority_tier(SourceClass::TeamPolicy), "TeamPolicy (Tier 2.5)");
    }

    #[test]
    fn test_claim_status_display() {
        assert_eq!(ClaimStatus::Active.to_string(), "active");
        assert_eq!(ClaimStatus::Deprecated.to_string(), "deprecated");
        assert_eq!(ClaimStatus::Superseded.to_string(), "superseded");
    }

    #[test]
    fn test_authored_value_display() {
        assert_eq!(AuthoredValue::Bool(true).to_string(), "true");
        assert_eq!(AuthoredValue::Number(2.71).to_string(), "2.71");
        assert_eq!(AuthoredValue::Text("SeqCst".to_string()).to_string(), "SeqCst");
    }
}
