//! Core types for community corpus contributions.

use serde::{Deserialize, Serialize};

/// Serializable object value for community types.
///
/// This mirrors `stemedb_core::types::ObjectValue` but uses serde
/// instead of rkyv for network transport.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", content = "value")]
pub enum CommunityObjectValue {
    /// A text string value.
    Text(String),
    /// A numeric value (float).
    Number(f64),
    /// A boolean value.
    Boolean(bool),
}

impl From<&stemedb_core::types::ObjectValue> for CommunityObjectValue {
    fn from(value: &stemedb_core::types::ObjectValue) -> Self {
        use stemedb_core::types::ObjectValue;
        match value {
            ObjectValue::Text(s) => CommunityObjectValue::Text(s.clone()),
            ObjectValue::Number(n) => CommunityObjectValue::Number(*n),
            ObjectValue::Boolean(b) => CommunityObjectValue::Boolean(*b),
            ObjectValue::Reference(r) => {
                // References are converted to hex strings for community sharing
                CommunityObjectValue::Text(hex::encode(r))
            }
        }
    }
}

impl From<CommunityObjectValue> for stemedb_core::types::ObjectValue {
    fn from(value: CommunityObjectValue) -> Self {
        use stemedb_core::types::ObjectValue;
        match value {
            CommunityObjectValue::Text(s) => ObjectValue::Text(s),
            CommunityObjectValue::Number(n) => ObjectValue::Number(n),
            CommunityObjectValue::Boolean(b) => ObjectValue::Boolean(b),
        }
    }
}

/// Anonymized observation stripped of identifying metadata.
///
/// This is what gets sent to the community server. Critical privacy constraint:
/// the `anon_hash` is computed from (subject, predicate, value) ONLY - it must
/// NOT include file, line, or matched_text.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnonymizedObservation {
    /// Wildcarded subject path: `code://rust/*/tls/cert_verification`
    ///
    /// Project-specific segments are replaced with `*` to prevent
    /// correlation attacks across multiple observations.
    pub subject: String,

    /// The predicate (e.g., "enabled", "min_version").
    pub predicate: String,

    /// The extracted value.
    pub object: CommunityObjectValue,

    /// Confidence of extraction (0.0 to 1.0).
    pub confidence: f32,

    /// Hash of (subject, predicate, value) ONLY.
    ///
    /// CRITICAL: This hash must NOT include file, line, or matched_text.
    /// Those are the sensitive fields that would allow re-identification.
    /// The anon_hash enables server-side deduplication without leaking
    /// source location information.
    #[serde(with = "hex_array")]
    pub anon_hash: [u8; 32],

    /// Timestamp rounded to the nearest hour (Unix seconds).
    ///
    /// Rounding provides k-anonymity by grouping observations into
    /// hour-long buckets, preventing timing correlation attacks.
    pub timestamp_hour: u64,
}

/// Server-side pattern aggregate.
///
/// Aggregates observations from multiple projects to determine
/// community consensus on a particular pattern.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PatternAggregate {
    /// The anonymized subject path (with wildcarded project segment).
    pub subject: String,

    /// The predicate (e.g., "enabled", "min_version").
    pub predicate: String,

    /// The aggregated value.
    pub value: CommunityObjectValue,

    /// Number of distinct projects reporting this pattern.
    ///
    /// This is the key metric for community consensus - patterns
    /// seen across many projects are more likely to be safe defaults.
    pub project_count: u64,

    /// Total number of observations (may be > project_count if
    /// projects report the same pattern multiple times).
    pub observation_count: u64,

    /// Unix timestamp of first observation.
    pub first_seen: u64,

    /// Unix timestamp of most recent observation.
    pub last_seen: u64,
}

/// Serde module for hex encoding/decoding of [u8; 32] arrays.
mod hex_array {
    use serde::{Deserialize, Deserializer, Serializer};

    pub fn serialize<S>(data: &[u8; 32], serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_str(&hex::encode(data))
    }

    pub fn deserialize<'de, D>(deserializer: D) -> Result<[u8; 32], D::Error>
    where
        D: Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        let bytes = hex::decode(&s).map_err(serde::de::Error::custom)?;
        if bytes.len() != 32 {
            return Err(serde::de::Error::custom("expected 32 bytes"));
        }
        let mut arr = [0u8; 32];
        arr.copy_from_slice(&bytes);
        Ok(arr)
    }
}

impl PatternAggregate {
    /// Create a new aggregate from the first observation.
    pub fn new(
        subject: String,
        predicate: String,
        value: CommunityObjectValue,
        timestamp: u64,
    ) -> Self {
        Self {
            subject,
            predicate,
            value,
            project_count: 1,
            observation_count: 1,
            first_seen: timestamp,
            last_seen: timestamp,
        }
    }

    /// Check if this pattern has enough project diversity to be trusted.
    pub fn has_consensus(&self, min_projects: u64) -> bool {
        self.project_count >= min_projects
    }
}

// ============================================================================
// Cross-Project Learning Types
// ============================================================================

/// A learned pattern anonymized for cross-project sharing.
///
/// This is the payload sent to the hosted server when contributing patterns.
/// Privacy-sensitive fields like `example_code` and `project_hashes` are NOT
/// included - only anonymized statistical data.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SharedPattern {
    /// BLAKE3 hash of (normalized_pattern, language) - deduplication key.
    ///
    /// This hash uniquely identifies a pattern across organizations,
    /// enabling server-side deduplication without revealing the actual
    /// source code.
    pub pattern_hash: String, // hex-encoded

    /// Normalized pattern (literals replaced with placeholders).
    ///
    /// # Examples
    /// - `"pool_size: <number>"` (from `"pool_size: 25"`)
    /// - `"verify_ssl: <boolean>"` (from `"verify_ssl: false"`)
    pub normalized_pattern: String,

    /// Template for generating claims when this pattern matches.
    pub claim_template: SharedClaimTemplate,

    /// Programming language this pattern applies to.
    pub language: String,

    /// Number of unique projects where pattern was seen.
    ///
    /// This is the aggregated count from the contributing organization,
    /// NOT the individual project identifiers.
    pub project_count: usize,

    /// Total occurrences of the pattern.
    pub occurrences: u32,

    /// Average confidence across all observations.
    pub avg_confidence: f32,
}

/// Claim template for shared patterns.
///
/// A simplified version of `ClaimTemplate` for network transport.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SharedClaimTemplate {
    /// Subject path template (e.g., "tls/min_version", "db/pool_size").
    pub subject_template: String,

    /// Predicate describing what aspect is being claimed.
    pub predicate: String,

    /// Type of value this pattern extracts ("text", "number", "boolean").
    pub value_type: String,
}

impl SharedClaimTemplate {
    /// Create a new shared claim template.
    pub fn new(
        subject_template: impl Into<String>,
        predicate: impl Into<String>,
        value_type: impl Into<String>,
    ) -> Self {
        Self {
            subject_template: subject_template.into(),
            predicate: predicate.into(),
            value_type: value_type.into(),
        }
    }
}

/// A community extractor aggregated from patterns across organizations.
///
/// When patterns are seen across many organizations (default: 50+ projects),
/// they are promoted to community extractors and distributed back to
/// opted-in organizations.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommunityExtractor {
    /// Unique identifier for this extractor.
    pub id: String,

    /// Human-readable name for the extractor.
    pub name: String,

    /// Description of what this extractor detects.
    pub description: String,

    /// Languages this extractor applies to.
    pub languages: Vec<String>,

    /// The regex pattern for matching.
    pub pattern: String,

    /// Claim definition for matched code.
    pub claim: CommunityClaimDef,

    /// Confidence score for matches.
    pub confidence: f32,

    /// Provenance information about how this extractor was created.
    pub provenance: CommunityExtractorProvenance,
}

/// Claim definition for community extractors.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommunityClaimDef {
    /// Subject path template.
    pub subject: String,

    /// Predicate for the claim.
    pub predicate: String,

    /// Value type ("text", "number", "boolean").
    pub value_type: String,

    /// Description template.
    pub description: String,
}

/// Provenance information for community extractors.
///
/// Tracks how and when the extractor was created from aggregated patterns.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommunityExtractorProvenance {
    /// Number of contributing organizations.
    pub organization_count: u64,

    /// Total projects across all organizations.
    pub total_project_count: u64,

    /// Unix timestamp when the extractor was promoted.
    pub promoted_at: u64,

    /// Version number (incremented on updates).
    pub version: u32,
}

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

    #[test]
    fn test_pattern_aggregate_new() {
        let agg = PatternAggregate::new(
            "code://rust/*/tls/cert_verification".to_string(),
            "enabled".to_string(),
            CommunityObjectValue::Boolean(false),
            1706832000,
        );

        assert_eq!(agg.project_count, 1);
        assert_eq!(agg.observation_count, 1);
        assert_eq!(agg.first_seen, 1706832000);
        assert_eq!(agg.last_seen, 1706832000);
    }

    #[test]
    fn test_has_consensus() {
        let mut agg = PatternAggregate::new(
            "code://rust/*/jwt/audience".to_string(),
            "required".to_string(),
            CommunityObjectValue::Boolean(true),
            1000,
        );

        assert!(!agg.has_consensus(3));

        agg.project_count = 3;
        assert!(agg.has_consensus(3));

        agg.project_count = 5;
        assert!(agg.has_consensus(3));
    }

    #[test]
    fn test_community_object_value_from_core() {
        use stemedb_core::types::ObjectValue;

        let core_text = ObjectValue::Text("hello".to_string());
        let community = CommunityObjectValue::from(&core_text);
        assert_eq!(community, CommunityObjectValue::Text("hello".to_string()));

        let core_number = ObjectValue::Number(42.5);
        let community = CommunityObjectValue::from(&core_number);
        assert_eq!(community, CommunityObjectValue::Number(42.5));

        let core_bool = ObjectValue::Boolean(true);
        let community = CommunityObjectValue::from(&core_bool);
        assert_eq!(community, CommunityObjectValue::Boolean(true));
    }

    #[test]
    fn test_community_object_value_to_core() {
        use stemedb_core::types::ObjectValue;

        let community = CommunityObjectValue::Text("test".to_string());
        let core: ObjectValue = community.into();
        assert_eq!(core, ObjectValue::Text("test".to_string()));
    }

    #[test]
    fn test_anonymized_observation_serde_roundtrip() {
        let obs = AnonymizedObservation {
            subject: "code://rust/*/tls/cert".to_string(),
            predicate: "enabled".to_string(),
            object: CommunityObjectValue::Boolean(false),
            confidence: 0.95,
            anon_hash: [42u8; 32],
            timestamp_hour: 1706832000,
        };

        let json = serde_json::to_string(&obs).expect("serialize");
        let deserialized: AnonymizedObservation = serde_json::from_str(&json).expect("deserialize");

        assert_eq!(deserialized.subject, obs.subject);
        assert_eq!(deserialized.predicate, obs.predicate);
        assert_eq!(deserialized.object, obs.object);
        assert_eq!(deserialized.anon_hash, obs.anon_hash);
    }

    #[test]
    fn test_shared_pattern_serde_roundtrip() {
        let pattern = SharedPattern {
            pattern_hash: "abc123".to_string(),
            normalized_pattern: "pool_size: <number>".to_string(),
            claim_template: SharedClaimTemplate::new("db/pool_size", "size", "number"),
            language: "yaml".to_string(),
            project_count: 5,
            occurrences: 12,
            avg_confidence: 0.9,
        };

        let json = serde_json::to_string(&pattern).expect("serialize");
        let parsed: SharedPattern = serde_json::from_str(&json).expect("deserialize");

        assert_eq!(parsed.pattern_hash, pattern.pattern_hash);
        assert_eq!(parsed.normalized_pattern, pattern.normalized_pattern);
        assert_eq!(parsed.project_count, pattern.project_count);
        assert!((parsed.avg_confidence - 0.9).abs() < 0.001);
    }

    #[test]
    fn test_community_extractor_serde_roundtrip() {
        let extractor = CommunityExtractor {
            id: "ce-123".to_string(),
            name: "tls_min_version".to_string(),
            description: "Detects TLS minimum version settings".to_string(),
            languages: vec!["rust".to_string(), "python".to_string()],
            pattern: r#"TLS_MIN_VERSION\s*=\s*"([^"]+)""#.to_string(),
            claim: CommunityClaimDef {
                subject: "tls/min_version".to_string(),
                predicate: "version".to_string(),
                value_type: "text".to_string(),
                description: "TLS minimum version is {value}".to_string(),
            },
            confidence: 0.85,
            provenance: CommunityExtractorProvenance {
                organization_count: 25,
                total_project_count: 150,
                promoted_at: 1706832000,
                version: 1,
            },
        };

        let json = serde_json::to_string(&extractor).expect("serialize");
        let parsed: CommunityExtractor = serde_json::from_str(&json).expect("deserialize");

        assert_eq!(parsed.id, extractor.id);
        assert_eq!(parsed.name, extractor.name);
        assert_eq!(parsed.provenance.organization_count, 25);
        assert_eq!(parsed.provenance.total_project_count, 150);
    }
}
