//! AI Churn Pattern Detector
//!
//! Detects code with high modification frequency shortly after creation - a pattern
//! commonly seen with AI-generated code that gets quickly revised or corrected.
//!
//! The detector uses git blame + diff to analyze function-level changes and identify:
//! - Functions created and modified within 48 hours ("fix velocity")
//! - High churn ratio (lines_modified / lines_original) in first week
//! - Rapid iterative corrections typical of AI-generated code
//!
//! Key detection signal: time_to_first_fix < 48h AND modifications >= 3 → HIGH

use crate::detectors::base::{Detector, DetectorConfig};
use crate::graph::GraphClient;
use crate::models::{Finding, Severity};
use anyhow::Result;
use chrono::{DateTime, Duration, Utc};
use std::collections::HashMap;
use std::path::PathBuf;
use tracing::{debug, info, warn};
use uuid::Uuid;

/// Time thresholds in hours
const CRITICAL_FIX_VELOCITY_HOURS: i64 = 24;
const HIGH_FIX_VELOCITY_HOURS: i64 = 48;
const MEDIUM_FIX_VELOCITY_HOURS: i64 = 72;

/// Modification count thresholds
const CRITICAL_MOD_COUNT: usize = 5;
const HIGH_MOD_COUNT: usize = 3;

/// Churn ratio thresholds
const CRITICAL_CHURN_RATIO: f64 = 1.5;
const HIGH_CHURN_RATIO: f64 = 0.8;
const MEDIUM_CHURN_RATIO: f64 = 0.5;

/// Minimum score to create a finding (filters out noise)
const MIN_CHURN_SCORE: f64 = 0.8;

/// Analysis window in days
const DEFAULT_ANALYSIS_WINDOW_DAYS: i64 = 90;

/// Minimum function size to analyze
const DEFAULT_MIN_FUNCTION_LINES: usize = 5;

/// A single modification record
#[derive(Debug, Clone)]
pub struct Modification {
    pub timestamp: DateTime<Utc>,
    pub commit_sha: String,
    pub lines_added: usize,
    pub lines_deleted: usize,
}

/// Track churn statistics for a function
#[derive(Debug, Clone)]
pub struct FunctionChurnRecord {
    pub qualified_name: String,
    pub file_path: String,
    pub function_name: String,
    pub created_at: Option<DateTime<Utc>>,
    pub creation_commit: String,
    pub lines_original: usize,
    pub first_modification_at: Option<DateTime<Utc>>,
    pub first_modification_commit: String,
    pub modifications: Vec<Modification>,
}

impl FunctionChurnRecord {
    /// Time between creation and first modification
    pub fn time_to_first_fix(&self) -> Option<Duration> {
        match (&self.created_at, &self.first_modification_at) {
            (Some(created), Some(first_mod)) => Some(*first_mod - *created),
            _ => None,
        }
    }

    /// Time to first fix in hours
    pub fn time_to_first_fix_hours(&self) -> Option<f64> {
        self.time_to_first_fix()
            .map(|d| d.num_seconds() as f64 / 3600.0)
    }

    /// Count modifications within first week of creation
    pub fn modifications_first_week(&self) -> usize {
        let Some(created_at) = self.created_at else {
            return 0;
        };
        let week_cutoff = created_at + Duration::days(7);
        self.modifications
            .iter()
            .filter(|m| m.timestamp <= week_cutoff)
            .count()
    }

    /// Total lines changed (added + deleted) in first week
    pub fn lines_changed_first_week(&self) -> usize {
        let Some(created_at) = self.created_at else {
            return 0;
        };
        let week_cutoff = created_at + Duration::days(7);
        self.modifications
            .iter()
            .filter(|m| m.timestamp <= week_cutoff)
            .map(|m| m.lines_added + m.lines_deleted)
            .sum()
    }

    /// Ratio of lines changed to original lines in first week
    pub fn churn_ratio(&self) -> f64 {
        if self.lines_original == 0 {
            return 0.0;
        }
        self.lines_changed_first_week() as f64 / self.lines_original as f64
    }

    /// Key signal: fixed within 48h AND multiple modifications
    pub fn is_high_velocity_fix(&self) -> bool {
        let Some(ttf_hours) = self.time_to_first_fix_hours() else {
            return false;
        };
        ttf_hours < HIGH_FIX_VELOCITY_HOURS as f64 && self.modifications.len() >= 2
    }

    /// Combined score indicating AI churn pattern (0-1)
    pub fn ai_churn_score(&self) -> f64 {
        let mut score = 0.0;

        // Fast fix velocity is strong signal
        if let Some(ttf_hours) = self.time_to_first_fix_hours() {
            if ttf_hours < CRITICAL_FIX_VELOCITY_HOURS as f64 {
                score += 0.4;
            } else if ttf_hours < HIGH_FIX_VELOCITY_HOURS as f64 {
                score += 0.25;
            } else if ttf_hours < MEDIUM_FIX_VELOCITY_HOURS as f64 {
                score += 0.1;
            }
        }

        // Multiple early modifications
        let mods = self.modifications.len();
        if mods >= 4 {
            score += 0.3;
        } else if mods >= 2 {
            score += 0.2;
        } else if mods >= 1 {
            score += 0.1;
        }

        // High churn ratio
        let churn = self.churn_ratio();
        if churn > 1.0 {
            score += 0.3;
        } else if churn > 0.5 {
            score += 0.2;
        } else if churn > 0.3 {
            score += 0.1;
        }

        f64::min(score, 1.0)
    }
}

/// Detects AI-generated code patterns through fix velocity and churn analysis
pub struct AIChurnDetector {
    config: DetectorConfig,
    analysis_window_days: i64,
    min_function_lines: usize,
}

impl AIChurnDetector {
    /// Create a new detector with default settings
    pub fn new() -> Self {
        Self {
            config: DetectorConfig::new(),
            analysis_window_days: DEFAULT_ANALYSIS_WINDOW_DAYS,
            min_function_lines: DEFAULT_MIN_FUNCTION_LINES,
        }
    }

    /// Create with custom config
    pub fn with_config(config: DetectorConfig) -> Self {
        Self {
            analysis_window_days: config
                .get_option_or("analysis_window_days", DEFAULT_ANALYSIS_WINDOW_DAYS),
            min_function_lines: config
                .get_option_or("min_function_lines", DEFAULT_MIN_FUNCTION_LINES),
            config,
        }
    }

    /// Calculate severity based on fix velocity and churn metrics
    fn calculate_severity(&self, record: &FunctionChurnRecord) -> Severity {
        let ttf_hours = record.time_to_first_fix_hours();
        let mods = record.modifications.len();
        let churn = record.churn_ratio();

        // CRITICAL conditions
        if churn > CRITICAL_CHURN_RATIO {
            return Severity::Critical;
        }
        if let Some(ttf) = ttf_hours {
            if ttf < CRITICAL_FIX_VELOCITY_HOURS as f64 && mods >= CRITICAL_MOD_COUNT {
                return Severity::Critical;
            }
        }

        // HIGH conditions (key signal)
        if let Some(ttf) = ttf_hours {
            if ttf < HIGH_FIX_VELOCITY_HOURS as f64 && mods >= HIGH_MOD_COUNT {
                return Severity::High;
            }
        }
        if churn > HIGH_CHURN_RATIO {
            return Severity::High;
        }

        // MEDIUM conditions
        if let Some(ttf) = ttf_hours {
            if ttf < MEDIUM_FIX_VELOCITY_HOURS as f64 && mods >= 2 {
                return Severity::Medium;
            }
        }
        if churn > MEDIUM_CHURN_RATIO {
            return Severity::Medium;
        }

        // LOW - only if significant modification count
        if mods >= 4 {
            return Severity::Low;
        }

        Severity::Info
    }

    /// Create a finding for a high-churn function
    fn create_finding(&self, record: &FunctionChurnRecord) -> Option<Finding> {
        // Skip if score too low (noise filter)
        if record.ai_churn_score() < MIN_CHURN_SCORE {
            return None;
        }

        let severity = self.calculate_severity(record);
        if severity == Severity::Info {
            return None;
        }

        let ttf_hours = record.time_to_first_fix_hours();
        let ttf_str = ttf_hours
            .map(|h| format!("{:.1} hours", h))
            .unwrap_or_else(|| "N/A".to_string());

        let created_str = record
            .created_at
            .map(|dt| dt.format("%Y-%m-%d %H:%M").to_string())
            .unwrap_or_else(|| "Unknown".to_string());

        let mut description = format!(
            "Function `{}` in `{}` shows signs of rapid post-creation revision.\n\n\
             **Fix Velocity Metrics:**\n\
             - Created: {} (commit `{}`)\n\
             - Time to first fix: **{}**\n\
             - Total modifications in first week: **{}**\n\n\
             **Churn Analysis:**\n\
             - Original size: {} lines\n\
             - Lines changed in first week: {}\n\
             - Churn ratio: **{:.2}** ({:.0}% of original code)\n\
             - AI churn score: {:.2}",
            record.function_name,
            record.file_path,
            created_str,
            record.creation_commit,
            ttf_str,
            record.modifications_first_week(),
            record.lines_original,
            record.lines_changed_first_week(),
            record.churn_ratio(),
            record.churn_ratio() * 100.0,
            record.ai_churn_score(),
        );

        if record.is_high_velocity_fix() {
            description.push_str(
                "\n\n⚠️ **High fix velocity detected**: This function was modified within 48 hours of creation \
                 with multiple follow-up changes - a pattern strongly associated with AI-generated code \
                 that required human correction.",
            );
        }

        if record.churn_ratio() > CRITICAL_CHURN_RATIO {
            description.push_str(
                "\n\n⚠️ **Critical churn ratio**: More code was changed than originally written, \
                 indicating significant rewriting was needed.",
            );
        }

        // Modification timeline
        if !record.modifications.is_empty() {
            description.push_str("\n\n**Modification Timeline:**");
            for (i, m) in record.modifications.iter().take(5).enumerate() {
                let time_str = m.timestamp.format("%Y-%m-%d %H:%M").to_string();
                description.push_str(&format!(
                    "\n- {}: commit `{}` (+{} lines)",
                    time_str, m.commit_sha, m.lines_added
                ));
                if i == 4 && record.modifications.len() > 5 {
                    description.push_str(&format!(
                        "\n- ... and {} more modifications",
                        record.modifications.len() - 5
                    ));
                }
            }
        }

        let suggested_fix = match severity {
            Severity::Critical => {
                "This function shows strong signs of AI-generated code that required extensive correction. \
                 Consider:\n\
                 1. **Review thoroughly** for hidden bugs or incomplete logic\n\
                 2. **Add comprehensive tests** - the rapid changes suggest edge cases may be missed\n\
                 3. **Document the logic** - ensure the team understands what this code does\n\
                 4. **Consider rewriting** if the churn continues".to_string()
            }
            Severity::High => {
                "Review this function for correctness issues. Consider:\n\
                 1. Adding unit tests with edge cases\n\
                 2. Reviewing for logical errors\n\
                 3. Ensuring proper error handling".to_string()
            }
            _ => {
                "Monitor this function for continued churn. Consider adding tests \
                 to stabilize the implementation.".to_string()
            }
        };

        let estimated_effort = if matches!(severity, Severity::Low | Severity::Medium) {
            "Small (2-4 hours)"
        } else {
            "Medium (1-2 days)"
        };

        Some(Finding {
            id: Uuid::new_v4().to_string(),
            detector: "AIChurnDetector".to_string(),
            severity,
            title: format!("AI churn pattern in `{}`", record.function_name),
            description,
            affected_files: vec![PathBuf::from(&record.file_path)],
            line_start: None,
            line_end: None,
            suggested_fix: Some(suggested_fix),
            estimated_effort: Some(estimated_effort.to_string()),
            category: Some("ai_churn".to_string()),
            cwe_id: None,
            why_it_matters: Some(
                "Code that requires rapid fixing after creation often indicates AI-generated content \
                 that wasn't fully understood or tested before commit. This pattern is associated with \
                 hidden bugs, incomplete error handling, and logic that may not be fully correct."
                    .to_string(),
            ),
        })
    }
}

impl Default for AIChurnDetector {
    fn default() -> Self {
        Self::new()
    }
}

impl Detector for AIChurnDetector {
    fn name(&self) -> &'static str {
        "AIChurnDetector"
    }

    fn description(&self) -> &'static str {
        "Detects AI-generated code patterns through fix velocity and churn analysis"
    }

    fn category(&self) -> &'static str {
        "ai_generated"
    }

    fn config(&self) -> Option<&DetectorConfig> {
        Some(&self.config)
    }

    fn detect(&self, graph: &GraphClient) -> Result<Vec<Finding>> {
        debug!("Starting AI churn detection");

        // Query for functions with modification history
        // Note: This requires git history data in the graph
        let query = r#"
            MATCH (f:Function)
            WHERE f.name IS NOT NULL 
              AND f.filePath IS NOT NULL
              AND f.createdAt IS NOT NULL
            OPTIONAL MATCH (f)-[:MODIFIED_IN]->(c:Commit)
            WITH f, collect(c) AS commits
            WHERE size(commits) >= 2
            RETURN f.qualifiedName AS qualified_name,
                   f.name AS name,
                   f.filePath AS file_path,
                   f.loc AS loc,
                   f.createdAt AS created_at,
                   f.creationCommit AS creation_commit,
                   [c IN commits | {
                       sha: c.sha,
                       timestamp: c.timestamp,
                       linesAdded: c.linesAdded,
                       linesDeleted: c.linesDeleted
                   }] AS modifications
            LIMIT 500
        "#;

        let results = graph.execute(query)?;

        if results.is_empty() {
            debug!("No functions with modification history found");
            // Fall back to simpler query without git history
            return self.detect_without_git_history(graph);
        }

        let mut findings: Vec<Finding> = Vec::new();

        for row in results {
            let qualified_name = row
                .get("qualified_name")
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .to_string();

            let function_name = row
                .get("name")
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .to_string();

            let file_path = row
                .get("file_path")
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .to_string();

            let loc = row.get("loc").and_then(|v| v.as_u64()).unwrap_or(0) as usize;

            if loc < self.min_function_lines {
                continue;
            }

            let created_at = row
                .get("created_at")
                .and_then(|v| v.as_str())
                .and_then(|s| DateTime::parse_from_rfc3339(s).ok())
                .map(|dt| dt.with_timezone(&Utc));

            let creation_commit = row
                .get("creation_commit")
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .to_string();

            // Parse modifications
            let modifications: Vec<Modification> = row
                .get("modifications")
                .and_then(|v| v.as_array())
                .map(|arr| {
                    arr.iter()
                        .filter_map(|m| {
                            let sha = m.get("sha")?.as_str()?.to_string();
                            let timestamp = m
                                .get("timestamp")
                                .and_then(|v| v.as_str())
                                .and_then(|s| DateTime::parse_from_rfc3339(s).ok())
                                .map(|dt| dt.with_timezone(&Utc))?;
                            let lines_added =
                                m.get("linesAdded").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
                            let lines_deleted =
                                m.get("linesDeleted").and_then(|v| v.as_u64()).unwrap_or(0)
                                    as usize;
                            Some(Modification {
                                timestamp,
                                commit_sha: sha,
                                lines_added,
                                lines_deleted,
                            })
                        })
                        .collect()
                })
                .unwrap_or_default();

            // Sort modifications by timestamp
            let mut sorted_mods = modifications;
            sorted_mods.sort_by_key(|m| m.timestamp);

            let first_modification_at = sorted_mods.first().map(|m| m.timestamp);
            let first_modification_commit = sorted_mods
                .first()
                .map(|m| m.commit_sha.clone())
                .unwrap_or_default();

            let record = FunctionChurnRecord {
                qualified_name,
                file_path,
                function_name,
                created_at,
                creation_commit,
                lines_original: loc,
                first_modification_at,
                first_modification_commit,
                modifications: sorted_mods,
            };

            if let Some(finding) = self.create_finding(&record) {
                findings.push(finding);
            }
        }

        findings.sort_by(|a, b| b.severity.cmp(&a.severity));

        info!(
            "AIChurnDetector found {} high-churn patterns",
            findings.len()
        );

        Ok(findings)
    }
}

impl AIChurnDetector {
    /// Fallback detection without git history data
    fn detect_without_git_history(&self, _graph: &GraphClient) -> Result<Vec<Finding>> {
        warn!(
            "AIChurnDetector: No git history data in graph. \
             For full churn detection, ensure git history is indexed."
        );
        Ok(vec![])
    }
}

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

    fn make_record(ttf_hours: Option<f64>, mods: usize, churn_ratio: f64) -> FunctionChurnRecord {
        let now = Utc::now();
        let created_at = Some(now - Duration::days(7));
        let first_mod_at = ttf_hours.map(|h| now - Duration::days(7) + Duration::hours(h as i64));
        let lines_original = 100;
        let lines_changed = (churn_ratio * lines_original as f64) as usize;

        let modifications: Vec<Modification> = (0..mods)
            .map(|i| Modification {
                timestamp: first_mod_at.unwrap_or(now) + Duration::hours(i as i64),
                commit_sha: format!("abc{}", i),
                lines_added: lines_changed / mods.max(1),
                lines_deleted: 0,
            })
            .collect();

        FunctionChurnRecord {
            qualified_name: "test::func".to_string(),
            file_path: "test.py".to_string(),
            function_name: "func".to_string(),
            created_at,
            creation_commit: "initial".to_string(),
            lines_original,
            first_modification_at: first_mod_at,
            first_modification_commit: "first".to_string(),
            modifications,
        }
    }

    #[test]
    fn test_churn_score_high_velocity() {
        let record = make_record(Some(24.0), 3, 0.5);
        let score = record.ai_churn_score();
        assert!(
            score >= 0.7,
            "High velocity + multiple mods should score high"
        );
    }

    #[test]
    fn test_churn_score_high_churn_ratio() {
        let record = make_record(Some(100.0), 1, 1.5);
        let score = record.ai_churn_score();
        assert!(score >= 0.4, "High churn ratio should contribute to score");
    }

    #[test]
    fn test_is_high_velocity_fix() {
        let record = make_record(Some(24.0), 3, 0.3);
        assert!(record.is_high_velocity_fix());

        let record2 = make_record(Some(72.0), 3, 0.3);
        assert!(!record2.is_high_velocity_fix());
    }

    #[test]
    fn test_severity_calculation() {
        let detector = AIChurnDetector::new();

        // Critical: high churn ratio
        let record = make_record(Some(24.0), 5, 2.0);
        assert_eq!(detector.calculate_severity(&record), Severity::Critical);

        // High: fast fix + multiple mods
        let record = make_record(Some(36.0), 3, 0.5);
        assert_eq!(detector.calculate_severity(&record), Severity::High);
    }
}
