//! Core types for pattern learning.
//!
//! When LLM extraction finds claims that regex extractors miss, we record
//! the pattern for potential promotion to a declarative extractor.

use std::collections::HashSet;

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

use crate::evidence::PatternEvidence;
use crate::lifecycle::KnowledgeLifecycle;
use crate::scope::ScopeLevel;
use crate::types::Language;

/// Value types for pattern placeholders.
///
/// Used to classify the type of value extracted from code patterns,
/// enabling proper placeholder generation during normalization.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum ValueType {
    /// Text/string value (e.g., "TLSv1.2", "admin")
    #[default]
    Text,
    /// Numeric value (e.g., 4096, 30)
    Number,
    /// Boolean value (true/false)
    Boolean,
}

impl std::fmt::Display for ValueType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ValueType::Text => write!(f, "text"),
            ValueType::Number => write!(f, "number"),
            ValueType::Boolean => write!(f, "boolean"),
        }
    }
}

/// Template for generating claims from a learned pattern.
///
/// Describes how to create an `ExtractedClaim` when the pattern matches.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClaimTemplate {
    /// Subject path template (e.g., "tls/min_version", "db/pool_size").
    ///
    /// This becomes part of the concept_path in the extracted claim.
    pub subject_template: String,

    /// Predicate describing what aspect is being claimed (e.g., "version", "enabled").
    pub predicate: String,

    /// Type of value this pattern extracts.
    pub value_type: ValueType,

    /// Description template explaining what this claim means.
    ///
    /// May include placeholders like `{value}` for dynamic content.
    pub description_template: String,
}

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

/// A pattern learned from LLM extraction that could become a declarative extractor.
///
/// Patterns are recorded when LLM successfully extracts claims from code where
/// regex extractors found nothing. When a pattern recurs across multiple projects
/// with high confidence, it becomes a candidate for promotion.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LearnedPattern {
    /// Unique identifier for this pattern.
    pub id: Uuid,

    /// Example code that triggered this pattern.
    ///
    /// Stored for reference and validation when promoting to extractor.
    pub example_code: String,

    /// Normalized pattern with literals replaced by typed placeholders.
    ///
    /// # Examples
    /// - `"const TLS_MIN_VERSION = \"1.0\""` -> `"const TLS_MIN_VERSION = <string:version>"`
    /// - `"pool_size: 25"` -> `"pool_size: <number>"`
    /// - `"verify_ssl: false"` -> `"verify_ssl: <boolean>"`
    pub normalized_pattern: String,

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

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

    /// When this pattern was first observed.
    #[serde(with = "chrono::serde::ts_seconds")]
    pub first_seen: DateTime<Utc>,

    /// When this pattern was last observed.
    #[serde(with = "chrono::serde::ts_seconds")]
    pub last_seen: DateTime<Utc>,

    /// BLAKE3 hashes of projects where this pattern was seen.
    ///
    /// Privacy-preserving: stores hashes, not project names.
    pub project_hashes: HashSet<String>,

    /// Total number of times this pattern was observed.
    pub occurrences: u32,

    /// Average confidence of LLM extractions for this pattern.
    ///
    /// Updated as a rolling average with each observation.
    pub avg_confidence: f32,

    /// Whether this pattern has been promoted to a declarative extractor.
    pub promoted: bool,

    /// If promoted, the name of the generated extractor.
    pub promoted_to: Option<String>,

    /// Evidence backing this pattern (RFC references, ADRs, specs, etc.).
    ///
    /// Patterns with higher evidence levels graduate faster and carry more authority.
    #[serde(default)]
    pub evidence: PatternEvidence,

    /// Scope level where this pattern was learned.
    ///
    /// Defaults to Project for backward compatibility.
    #[serde(default)]
    pub scope_level: ScopeLevel,

    /// Scope identifier (e.g., "acme-corp", "platform-team", "api-gateway").
    ///
    /// Combined with scope_level to uniquely identify the scope.
    #[serde(default)]
    pub scope_id: Option<String>,

    /// Lifecycle metadata for deprecation, supersession, and archival.
    ///
    /// Tracks whether this pattern is active, deprecated, superseded, or archived.
    /// Deprecated patterns continue to match but FLAG with migration guidance.
    #[serde(default)]
    pub lifecycle: KnowledgeLifecycle,
}

impl LearnedPattern {
    /// Create a new learned pattern from an LLM-extracted claim.
    pub fn new(
        example_code: impl Into<String>,
        normalized_pattern: impl Into<String>,
        claim_template: ClaimTemplate,
        language: Language,
        project_hash: impl Into<String>,
        confidence: f32,
    ) -> Self {
        let now = Utc::now();
        let mut project_hashes = HashSet::new();
        project_hashes.insert(project_hash.into());

        Self {
            id: Uuid::new_v4(),
            example_code: example_code.into(),
            normalized_pattern: normalized_pattern.into(),
            claim_template,
            language,
            first_seen: now,
            last_seen: now,
            project_hashes,
            occurrences: 1,
            avg_confidence: confidence,
            promoted: false,
            promoted_to: None,
            evidence: PatternEvidence::new(),
            scope_level: ScopeLevel::default(),
            scope_id: None,
            lifecycle: KnowledgeLifecycle::default(),
        }
    }

    /// Create a new learned pattern with evidence.
    pub fn with_evidence(
        example_code: impl Into<String>,
        normalized_pattern: impl Into<String>,
        claim_template: ClaimTemplate,
        language: Language,
        project_hash: impl Into<String>,
        confidence: f32,
        evidence: PatternEvidence,
    ) -> Self {
        let mut pattern = Self::new(
            example_code,
            normalized_pattern,
            claim_template,
            language,
            project_hash,
            confidence,
        );
        pattern.evidence = evidence;
        pattern
    }

    /// Record a new observation of this pattern.
    ///
    /// Updates occurrence count, project set, confidence average, and last_seen.
    pub fn record_observation(
        &mut self,
        project_hash: impl Into<String>,
        confidence: f32,
        timestamp: DateTime<Utc>,
    ) {
        self.project_hashes.insert(project_hash.into());
        self.last_seen = timestamp;

        // Incremental mean formula: new_avg = old_avg + (new_value - old_avg) / n
        // This is numerically stable and avoids precision loss from summing many values.
        self.occurrences += 1;
        self.avg_confidence += (confidence - self.avg_confidence) / self.occurrences as f32;
    }

    /// Number of unique projects where this pattern was seen.
    pub fn project_count(&self) -> usize {
        self.project_hashes.len()
    }

    /// Check if this pattern is eligible for promotion.
    ///
    /// A pattern is eligible when it meets minimum thresholds for
    /// project count and confidence. Evidence level affects the required
    /// project count: higher evidence levels graduate faster.
    pub fn is_promotion_candidate(&self, min_projects: usize, min_confidence: f32) -> bool {
        !self.promoted
            && self.project_count() >= self.effective_min_projects(min_projects)
            && self.avg_confidence >= min_confidence
    }

    /// Get effective minimum projects based on evidence level.
    ///
    /// Higher evidence levels require fewer projects to graduate.
    pub fn effective_min_projects(&self, base_min: usize) -> usize {
        // Use evidence-aware threshold if we have evidence
        let evidence_threshold = self.evidence.graduation_threshold();

        // Take the lower of evidence threshold and base threshold
        std::cmp::min(evidence_threshold, base_min)
    }

    /// Add evidence to this pattern.
    pub fn add_evidence(&mut self, evidence: PatternEvidence) {
        self.evidence.merge(evidence);
    }

    /// Days since this pattern was last seen.
    pub fn days_since_last_seen(&self) -> i64 {
        (Utc::now() - self.last_seen).num_days()
    }
}

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

    #[test]
    fn test_value_type_serde() {
        let json = serde_json::to_string(&ValueType::Text).expect("serialize");
        assert_eq!(json, "\"text\"");

        let parsed: ValueType = serde_json::from_str("\"number\"").expect("deserialize");
        assert_eq!(parsed, ValueType::Number);
    }

    #[test]
    fn test_claim_template_creation() {
        let template = ClaimTemplate::new(
            "tls/min_version",
            "version",
            ValueType::Text,
            "TLS minimum version set to {value}",
        );

        assert_eq!(template.subject_template, "tls/min_version");
        assert_eq!(template.predicate, "version");
        assert_eq!(template.value_type, ValueType::Text);
    }

    #[test]
    fn test_learned_pattern_creation() {
        let template = ClaimTemplate::new(
            "tls/min_version",
            "version",
            ValueType::Text,
            "TLS minimum version",
        );

        let pattern = LearnedPattern::new(
            "const TLS_MIN = \"1.0\"",
            "const TLS_MIN = <string:version>",
            template,
            Language::Rust,
            "abc123",
            0.9,
        );

        assert_eq!(pattern.occurrences, 1);
        assert_eq!(pattern.project_count(), 1);
        assert!((pattern.avg_confidence - 0.9).abs() < 0.001);
        assert!(!pattern.promoted);
    }

    #[test]
    fn test_record_observation() {
        let template = ClaimTemplate::new("db/pool_size", "size", ValueType::Number, "Pool size");

        let mut pattern = LearnedPattern::new(
            "pool_size: 25",
            "pool_size: <number>",
            template,
            Language::Yaml,
            "project1",
            0.8,
        );

        // Record from same project
        pattern.record_observation("project1", 0.9, Utc::now());
        assert_eq!(pattern.occurrences, 2);
        assert_eq!(pattern.project_count(), 1);
        assert!((pattern.avg_confidence - 0.85).abs() < 0.001);

        // Record from different project
        pattern.record_observation("project2", 0.7, Utc::now());
        assert_eq!(pattern.occurrences, 3);
        assert_eq!(pattern.project_count(), 2);
    }

    #[test]
    fn test_promotion_candidate() {
        let template = ClaimTemplate::new("auth/bypass", "enabled", ValueType::Boolean, "Bypass");

        let mut pattern = LearnedPattern::new(
            "skip_auth: true",
            "skip_auth: <boolean>",
            template,
            Language::Python,
            "project1",
            0.9,
        );

        // Not enough projects
        assert!(!pattern.is_promotion_candidate(5, 0.8));

        // Add more projects
        for i in 2..=6 {
            pattern.record_observation(format!("project{}", i), 0.85, Utc::now());
        }

        // Now eligible
        assert!(pattern.is_promotion_candidate(5, 0.8));

        // Mark as promoted
        pattern.promoted = true;
        assert!(!pattern.is_promotion_candidate(5, 0.8));
    }

    #[test]
    fn test_serialization_roundtrip() {
        let template = ClaimTemplate::new(
            "tls/min_version",
            "version",
            ValueType::Text,
            "TLS minimum version set to {value}",
        );

        let pattern = LearnedPattern::new(
            "const TLS_MIN = \"1.0\"",
            "const TLS_MIN = <string:version>",
            template,
            Language::Rust,
            "abc123",
            0.9,
        );

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

        assert_eq!(parsed.id, pattern.id);
        assert_eq!(parsed.normalized_pattern, pattern.normalized_pattern);
        assert_eq!(parsed.occurrences, pattern.occurrences);
    }
}
