//! File filtering and pattern learning functionality.

use std::path::Path;

use chrono::Utc;
use tracing::{info, warn};

use crate::config::AphoriaConfig;
use crate::learning::{
    normalize_pattern, ClaimTemplate, LearnedPattern, LocalPatternStore, PatternStore, ValueType,
};
use crate::types::{ExtractedClaim, Language, ScanMode};

/// Process extracted claims with optional pattern learning.
///
/// When pattern learning is enabled in persistent mode, this records LLM-extracted
/// claims as learned patterns for future declarative extraction.
pub struct ClaimProcessor {
    pattern_store: Option<LocalPatternStore>,
    project_hash: Option<String>,
    config: AphoriaConfig,
}

impl ClaimProcessor {
    /// Create a new claim processor with optional pattern learning.
    pub fn new(
        mode: ScanMode,
        config: &AphoriaConfig,
        project_root: &Path,
    ) -> Result<Self, crate::error::AphoriaError> {
        let pattern_store = if mode == ScanMode::Persistent && config.learning.enabled {
            match LocalPatternStore::new(&crate::learning::learning_store_dir()) {
                Ok(store) => {
                    info!("Pattern learning store initialized");
                    Some(store)
                }
                Err(e) => {
                    warn!(error = %e, "Failed to initialize pattern store, continuing without learning");
                    None
                }
            }
        } else {
            None
        };

        // Compute project hash once for pattern learning (privacy-preserving).
        // Uses the canonical project root path for stable identification across scans.
        let project_hash = if pattern_store.is_some() {
            Some(blake3_hash_hex(&project_root.display().to_string()))
        } else {
            None
        };

        Ok(Self { pattern_store, project_hash, config: config.clone() })
    }

    /// Record learned patterns for LLM-extracted claims.
    ///
    /// Returns the number of patterns recorded.
    pub fn record_patterns(&self, claims: &[ExtractedClaim], language: Language) -> usize {
        let Some(ref store) = self.pattern_store else {
            return 0;
        };

        let Some(ref proj_hash) = self.project_hash else {
            return 0;
        };

        let max_patterns = Some(self.config.learning.max_patterns);
        let mut patterns_recorded = 0;

        for claim in claims {
            if claim.confidence >= self.config.learning.min_confidence
                && record_learned_pattern(store, claim, language, proj_hash, max_patterns)
            {
                patterns_recorded += 1;
            }
        }

        patterns_recorded
    }

    /// Get the total pattern count in the store.
    pub fn pattern_count(&self) -> usize {
        self.pattern_store.as_ref().map(|s| s.pattern_count()).unwrap_or(0)
    }
}

/// Record a learned pattern from an LLM-extracted claim.
///
/// If a similar pattern already exists, updates it with the new observation.
/// Otherwise, creates a new pattern entry.
///
/// Returns true if a pattern was recorded or updated successfully.
fn record_learned_pattern(
    store: &LocalPatternStore,
    claim: &ExtractedClaim,
    language: Language,
    project_hash: &str,
    max_patterns: Option<usize>,
) -> bool {
    let normalized = normalize_pattern(&claim.matched_text);

    // Check for existing similar pattern
    if let Some(mut existing) = store.find_similar(&normalized, language, 0.8) {
        // Update existing pattern with new observation
        existing.record_observation(project_hash, claim.confidence, Utc::now());
        // Updates don't need max_patterns check (pattern already exists)
        if let Err(e) = store.record_pattern(&existing, None) {
            warn!(error = %e, "Failed to update existing pattern");
            return false;
        }
        return true;
    }

    // Create new pattern
    let template = ClaimTemplate::new(
        extract_subject_from_concept_path(&claim.concept_path),
        &claim.predicate,
        infer_value_type(&claim.value),
        &claim.description,
    );

    let pattern = LearnedPattern::new(
        &claim.matched_text,
        normalized,
        template,
        language,
        project_hash,
        claim.confidence,
    );

    // New patterns respect max_patterns limit
    if let Err(e) = store.record_pattern(&pattern, max_patterns) {
        warn!(error = %e, "Failed to record new pattern");
        return false;
    }

    true
}

/// Extract the subject portion from a concept path.
///
/// Concept paths have the form `code://rust/project/subject/topic`.
/// This extracts everything after the project segment.
fn extract_subject_from_concept_path(path: &str) -> String {
    // Remove scheme prefix (code://, rfc://, etc.)
    let after_scheme = path.split("://").nth(1).unwrap_or(path);

    // Split by '/' and skip the first two segments (language/project)
    let segments: Vec<&str> = after_scheme.split('/').collect();
    if segments.len() > 2 {
        segments[2..].join("/")
    } else {
        after_scheme.to_string()
    }
}

/// Infer the value type from an ObjectValue.
fn infer_value_type(value: &stemedb_core::types::ObjectValue) -> ValueType {
    use stemedb_core::types::ObjectValue;

    match value {
        ObjectValue::Boolean(_) => ValueType::Boolean,
        ObjectValue::Number(_) => ValueType::Number,
        ObjectValue::Text(_) | ObjectValue::Reference(_) => ValueType::Text,
    }
}

/// Compute BLAKE3 hash of a string, returning the first 16 hex characters.
fn blake3_hash_hex(input: &str) -> String {
    let hash = blake3::hash(input.as_bytes());
    hex::encode(&hash.as_bytes()[..8])
}
