//! Voting and Consensus Engine for Multi-Detector Validation
//!
//! Aggregates findings from multiple detectors to determine consensus
//! and confidence scores using configurable voting strategies.
//!
//! # Voting Strategies
//!
//! - `Majority`: 2+ detectors agree = consensus
//! - `Weighted`: Detectors have different weights based on accuracy
//! - `Threshold`: Only include findings above confidence threshold
//! - `Unanimous`: All detectors must agree
//!
//! # Example
//!
//! ```ignore
//! let engine = VotingEngine::new();
//! let (consensus_findings, stats) = engine.vote(all_findings);
//! ```

use crate::models::{Finding, Severity};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use tracing::{debug, info};

/// Voting strategy for consensus determination
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum VotingStrategy {
    /// 2+ detectors agree = consensus
    #[default]
    Majority,
    /// Weight by detector accuracy
    Weighted,
    /// Only high-confidence findings
    Threshold,
    /// All detectors must agree
    Unanimous,
}

/// Method for calculating aggregate confidence
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum ConfidenceMethod {
    /// Simple average
    Average,
    /// Weighted by detector accuracy
    #[default]
    Weighted,
    /// Prior + evidence strength
    Bayesian,
    /// Maximum (aggressive)
    Max,
    /// Minimum (conservative)
    Min,
}

/// Method for resolving severity conflicts
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum SeverityResolution {
    /// Use highest severity
    #[default]
    Highest,
    /// Use lowest (conservative)
    Lowest,
    /// Most common severity
    MajorityVote,
    /// Weight by confidence
    WeightedVote,
}

/// Weight configuration for a detector
#[derive(Debug, Clone)]
pub struct DetectorWeight {
    pub name: String,
    pub weight: f64,
    pub accuracy: f64,
}

impl DetectorWeight {
    pub fn new(name: impl Into<String>, weight: f64, accuracy: f64) -> Self {
        Self {
            name: name.into(),
            weight,
            accuracy,
        }
    }
}

impl Default for DetectorWeight {
    fn default() -> Self {
        Self {
            name: "default".to_string(),
            weight: 1.0,
            accuracy: 0.80,
        }
    }
}

/// Result of consensus calculation for a finding group
#[derive(Debug, Clone)]
pub struct ConsensusResult {
    pub has_consensus: bool,
    pub confidence: f64,
    pub severity: Severity,
    pub contributing_detectors: Vec<String>,
    pub vote_count: usize,
    pub total_detectors: usize,
    pub agreement_ratio: f64,
}

/// Statistics from voting engine run
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct VotingStats {
    pub total_input: usize,
    pub total_output: usize,
    pub groups_analyzed: usize,
    pub single_detector_findings: usize,
    pub multi_detector_findings: usize,
    pub boosted_by_consensus: usize,
    pub rejected_low_confidence: usize,
    pub strategy: String,
    pub confidence_method: String,
    pub threshold: f64,
}

/// Default detector weights based on typical accuracy
fn default_detector_weights() -> HashMap<String, DetectorWeight> {
    let weights = vec![
        // Graph-based detectors (lower false positive rate)
        ("CircularDependencyDetector", 1.2, 0.95),
        ("GodClassDetector", 1.1, 0.85),
        ("FeatureEnvyDetector", 1.0, 0.80),
        ("ShotgunSurgeryDetector", 1.0, 0.85),
        ("InappropriateIntimacyDetector", 1.0, 0.80),
        ("ArchitecturalBottleneckDetector", 1.1, 0.90),
        // Hybrid detectors (external tool + graph)
        ("RuffLintDetector", 1.3, 0.98),
        ("RuffImportDetector", 1.2, 0.95),
        ("MypyDetector", 1.3, 0.99),
        ("BanditDetector", 1.1, 0.85),
        ("SemgrepDetector", 1.2, 0.90),
        ("RadonDetector", 1.0, 0.95),
        ("JscpdDetector", 1.1, 0.90),
        ("VultureDetector", 0.9, 0.75),
        ("PylintDetector", 1.0, 0.85),
    ];

    let mut map = HashMap::new();
    for (name, weight, accuracy) in weights {
        map.insert(
            name.to_string(),
            DetectorWeight::new(name, weight, accuracy),
        );
    }
    map.insert("default".to_string(), DetectorWeight::default());
    map
}

/// Engine for aggregating findings and determining consensus
///
/// Supports multiple voting strategies and confidence scoring methods
/// to determine when multiple detectors agree on an issue.
pub struct VotingEngine {
    strategy: VotingStrategy,
    confidence_method: ConfidenceMethod,
    severity_resolution: SeverityResolution,
    confidence_threshold: f64,
    min_detectors_for_boost: usize,
    detector_weights: HashMap<String, DetectorWeight>,
}

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

impl VotingEngine {
    /// Create a new voting engine with default settings
    pub fn new() -> Self {
        Self {
            strategy: VotingStrategy::default(),
            confidence_method: ConfidenceMethod::default(),
            severity_resolution: SeverityResolution::default(),
            confidence_threshold: 0.6,
            min_detectors_for_boost: 2,
            detector_weights: default_detector_weights(),
        }
    }

    /// Create with custom configuration
    pub fn with_config(
        strategy: VotingStrategy,
        confidence_method: ConfidenceMethod,
        severity_resolution: SeverityResolution,
        confidence_threshold: f64,
        min_detectors_for_boost: usize,
    ) -> Self {
        Self {
            strategy,
            confidence_method,
            severity_resolution,
            confidence_threshold,
            min_detectors_for_boost,
            detector_weights: default_detector_weights(),
        }
    }

    /// Apply voting to findings and return consensus findings
    pub fn vote(&self, findings: Vec<Finding>) -> (Vec<Finding>, VotingStats) {
        if findings.is_empty() {
            return (
                vec![],
                VotingStats {
                    total_input: 0,
                    total_output: 0,
                    ..Default::default()
                },
            );
        }

        // Group findings by entity
        let groups = self.group_by_entity(&findings);

        let mut consensus_findings = Vec::new();
        let mut rejected_count = 0;
        let mut boosted_count = 0;

        for group_findings in groups.values() {
            if group_findings.len() == 1 {
                // Single detector - check threshold
                let finding = &group_findings[0];
                let confidence = self.get_finding_confidence(finding);

                if confidence >= self.confidence_threshold {
                    consensus_findings.push(finding.clone());
                } else {
                    rejected_count += 1;
                }
            } else {
                // Multiple findings in this group - check for consensus
                let consensus = self.calculate_consensus(group_findings);

                if consensus.has_consensus && consensus.confidence >= self.confidence_threshold {
                    // True consensus: multiple detectors agree - merge into one finding
                    let merged = self.create_consensus_finding(group_findings, &consensus);
                    consensus_findings.push(merged);
                    boosted_count += 1;
                } else if consensus.vote_count == 1 {
                    // No consensus: all findings from same detector
                    // Keep the highest-severity finding as a regular (non-consensus) finding
                    let mut sorted = group_findings.to_vec();
                    sorted.sort_by(|a, b| b.severity.cmp(&a.severity));
                    let best = &sorted[0];
                    let confidence = self.get_finding_confidence(best);
                    if confidence >= self.confidence_threshold {
                        consensus_findings.push(best.clone());
                    } else {
                        rejected_count += 1;
                    }
                } else {
                    // Multiple detectors but below consensus threshold - reject
                    rejected_count += 1;
                }
            }
        }

        let stats = VotingStats {
            total_input: findings.len(),
            total_output: consensus_findings.len(),
            groups_analyzed: groups.len(),
            single_detector_findings: groups.values().filter(|g| g.len() == 1).count(),
            multi_detector_findings: groups.values().filter(|g| g.len() > 1).count(),
            boosted_by_consensus: boosted_count,
            rejected_low_confidence: rejected_count,
            strategy: format!("{:?}", self.strategy),
            confidence_method: format!("{:?}", self.confidence_method),
            threshold: self.confidence_threshold,
        };

        info!(
            "VotingEngine: {} -> {} findings ({} boosted, {} rejected)",
            findings.len(),
            consensus_findings.len(),
            boosted_count,
            rejected_count
        );

        (consensus_findings, stats)
    }

    /// Group findings by the entity they target
    fn group_by_entity(&self, findings: &[Finding]) -> HashMap<String, Vec<Finding>> {
        let mut groups: HashMap<String, Vec<Finding>> = HashMap::new();

        for finding in findings {
            let key = self.get_entity_key(finding);
            groups.entry(key).or_default().push(finding.clone());
        }

        groups
    }

    /// Generate unique key for entity identification
    fn get_entity_key(&self, finding: &Finding) -> String {
        // Get issue category to prevent merging different issue types
        let category = self.get_issue_category(finding);

        // Extract entity name from title (e.g., "God class: Flask" -> "Flask")
        let entity_name = self.extract_entity_name(&finding.title);

        // Build key from affected nodes/files
        let location = if !finding.affected_files.is_empty() {
            let file = finding.affected_files[0].to_string_lossy();
            match (finding.line_start, finding.line_end) {
                (Some(start), Some(end)) => {
                    // Use exact line range for precise matching
                    // Only group findings that overlap significantly
                    format!("{}:{}-{}", file, start, end)
                }
                (Some(start), None) => {
                    // Single line - use exact line
                    format!("{}:{}", file, start)
                }
                _ => file.to_string(),
            }
        } else {
            "unknown".to_string()
        };

        // Include entity name in key if available for better deduplication
        if !entity_name.is_empty() {
            format!("{}::{}::{}", category, entity_name, location)
        } else {
            format!("{}::{}", category, location)
        }
    }

    /// Extract entity name from finding title (function/class name)
    fn extract_entity_name(&self, title: &str) -> String {
        // Common patterns: "Issue Type: entity_name", "Issue Type in entity_name"
        if let Some(pos) = title.find(": ") {
            let after_colon = &title[pos + 2..];
            // Take first word (the entity name)
            after_colon
                .split_whitespace()
                .next()
                .unwrap_or("")
                .trim_matches(|c: char| !c.is_alphanumeric() && c != '_')
                .to_string()
        } else {
            String::new()
        }
    }

    /// Determine the category/type of issue for grouping
    fn get_issue_category(&self, finding: &Finding) -> &str {
        let detector = finding.detector.to_lowercase();

        if detector.contains("circular") || detector.contains("dependency") {
            "circular_dependency"
        } else if detector.contains("god") || detector.contains("class") {
            "god_class"
        } else if detector.contains("dead") || detector.contains("vulture") {
            "dead_code"
        } else if detector.contains("security") || detector.contains("bandit") {
            "security"
        } else if detector.contains("complexity") || detector.contains("radon") {
            "complexity"
        } else if detector.contains("duplicate") || detector.contains("clone") {
            "duplication"
        } else if detector.contains("type") || detector.contains("mypy") {
            "type_error"
        } else if detector.contains("lint") || detector.contains("ruff") {
            "lint"
        } else {
            "other"
        }
    }

    /// Calculate consensus for a group of findings
    fn calculate_consensus(&self, findings: &[Finding]) -> ConsensusResult {
        let detectors: Vec<&str> = findings.iter().map(|f| f.detector.as_str()).collect();
        let unique_detectors: HashSet<&str> = detectors.iter().copied().collect();
        let unique_vec: Vec<String> = unique_detectors.iter().map(|s| s.to_string()).collect();

        // Calculate confidence
        let confidence = self.calculate_confidence(findings);

        // Resolve severity
        let severity = self.resolve_severity(findings);

        // Check if consensus achieved based on strategy
        let has_consensus = self.check_consensus(findings, &unique_vec);

        let agreement_ratio = unique_detectors.len() as f64 / findings.len().max(1) as f64;

        ConsensusResult {
            has_consensus,
            confidence,
            severity,
            contributing_detectors: unique_vec,
            vote_count: unique_detectors.len(),
            total_detectors: findings.len(),
            agreement_ratio,
        }
    }

    /// Check if consensus is achieved based on voting strategy
    fn check_consensus(&self, findings: &[Finding], unique_detectors: &[String]) -> bool {
        let detector_count = unique_detectors.len();

        // CRITICAL: Consensus requires multiple DIFFERENT detectors
        // Multiple findings from the same detector is NOT consensus
        if detector_count < 2 {
            return false;
        }

        match self.strategy {
            VotingStrategy::Unanimous => {
                // All findings must be from different detectors
                detector_count >= 2 && detector_count == findings.len()
            }
            VotingStrategy::Majority => {
                // At least 2 detectors agree
                detector_count >= 2
            }
            VotingStrategy::Weighted => {
                // Calculate weighted vote score from UNIQUE detectors only
                let total_weight: f64 = unique_detectors
                    .iter()
                    .map(|d| self.get_detector_weight(d))
                    .sum();
                // Need combined weight >= 2.0 for consensus
                total_weight >= 2.0
            }
            VotingStrategy::Threshold => {
                // Check if aggregate confidence meets threshold
                let confidence = self.calculate_confidence(findings);
                confidence >= self.confidence_threshold && detector_count >= 2
            }
        }
    }

    /// Calculate aggregate confidence using configured method
    fn calculate_confidence(&self, findings: &[Finding]) -> f64 {
        let mut confidences = Vec::new();
        let mut weights = Vec::new();

        for finding in findings {
            let conf = self.get_finding_confidence(finding);
            let weight = self.get_detector_weight(&finding.detector);
            confidences.push(conf);
            weights.push(weight);
        }

        if confidences.is_empty() {
            return 0.0;
        }

        let base = match self.confidence_method {
            ConfidenceMethod::Average => confidences.iter().sum::<f64>() / confidences.len() as f64,

            ConfidenceMethod::Weighted => {
                let total_weight: f64 = weights.iter().sum();
                if total_weight > 0.0 {
                    confidences
                        .iter()
                        .zip(weights.iter())
                        .map(|(c, w)| c * w)
                        .sum::<f64>()
                        / total_weight
                } else {
                    confidences.iter().sum::<f64>() / confidences.len() as f64
                }
            }

            ConfidenceMethod::Max => confidences.iter().cloned().fold(0.0, f64::max),

            ConfidenceMethod::Min => confidences.iter().cloned().fold(1.0, f64::min),

            ConfidenceMethod::Bayesian => {
                // Bayesian update with detector-family de-correlation (#52).
                // Correlated detectors (same family/prefix) should not count as
                // independent evidence.
                let mut by_family: HashMap<String, Vec<f64>> = HashMap::new();
                for f in findings {
                    let family = f
                        .detector
                        .split(['[', '+', ':'])
                        .next()
                        .unwrap_or(f.detector.as_str())
                        .to_string();
                    by_family
                        .entry(family)
                        .or_default()
                        .push(self.get_finding_confidence(f));
                }

                let family_confidences: Vec<f64> = by_family
                    .values()
                    .map(|vals| vals.iter().sum::<f64>() / vals.len() as f64)
                    .collect();

                let mut prior = 0.5;
                for conf in family_confidences {
                    let likelihood = conf;
                    let denom = prior * likelihood + (1.0 - prior) * (1.0 - likelihood);
                    if denom > 0.0 {
                        prior = (prior * likelihood) / denom;
                    }
                }
                prior
            }
        };

        // Apply consensus boost if multiple detectors agree
        let unique_detectors: HashSet<&str> =
            findings.iter().map(|f| f.detector.as_str()).collect();
        if unique_detectors.len() >= self.min_detectors_for_boost {
            // Boost: +5% per additional detector, max +20%
            let boost = ((unique_detectors.len() - 1) as f64 * 0.05).min(0.20);
            (base + boost).min(1.0)
        } else {
            base
        }
    }

    /// Resolve severity conflicts between detectors
    fn resolve_severity(&self, findings: &[Finding]) -> Severity {
        if findings.is_empty() {
            return Severity::Medium;
        }

        match self.severity_resolution {
            SeverityResolution::Highest => findings
                .iter()
                .map(|f| f.severity)
                .max()
                .unwrap_or(Severity::Medium),

            SeverityResolution::Lowest => findings
                .iter()
                .map(|f| f.severity)
                .min()
                .unwrap_or(Severity::Medium),

            SeverityResolution::MajorityVote => {
                // Most common severity
                let mut counts: HashMap<Severity, usize> = HashMap::new();
                for finding in findings {
                    *counts.entry(finding.severity).or_insert(0) += 1;
                }
                counts
                    .into_iter()
                    .max_by_key(|(_, count)| *count)
                    .map(|(sev, _)| sev)
                    .unwrap_or(Severity::Medium)
            }

            SeverityResolution::WeightedVote => {
                // Weight by confidence
                let mut severity_scores: HashMap<Severity, f64> = HashMap::new();
                for finding in findings {
                    let conf = self.get_finding_confidence(finding);
                    let weight = self.get_detector_weight(&finding.detector);
                    *severity_scores.entry(finding.severity).or_insert(0.0) += conf * weight;
                }
                severity_scores
                    .into_iter()
                    .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
                    .map(|(sev, _)| sev)
                    .unwrap_or(Severity::Medium)
            }
        }
    }

    /// Get confidence score for a finding
    fn get_finding_confidence(&self, finding: &Finding) -> f64 {
        // Read from finding if available, otherwise use detector accuracy as proxy
        if let Some(conf) = finding.confidence {
            return conf.clamp(0.0, 1.0);
        }

        // Fall back to detector's accuracy rating as confidence proxy
        self.detector_weights
            .get(&finding.detector)
            .or_else(|| self.detector_weights.get("default"))
            .map(|w| w.accuracy)
            .unwrap_or(0.7)
    }

    /// Get weight for a detector
    fn get_detector_weight(&self, detector_name: &str) -> f64 {
        self.detector_weights
            .get(detector_name)
            .or_else(|| self.detector_weights.get("default"))
            .map(|w| w.weight)
            .unwrap_or(1.0)
    }

    /// Check if a function name is a utility/helper pattern that should cap severity at High
    fn is_utility_function_name(title: &str) -> bool {
        // Extract function name from title like "Architectural Bottleneck: is_sql_context"
        let func_name = title
            .split(':')
            .next_back()
            .unwrap_or("")
            .trim()
            .to_lowercase();

        // Utility function prefixes - high connectivity is expected
        const UTILITY_PREFIXES: &[&str] = &[
            "is_",
            "has_",
            "check_",
            "validate_",
            "should_",
            "can_",
            "find_",
            "calculate_",
            "compute_",
            "scan_",
            "extract_",
            "normalize_",
            "get_",
            "set_",
            "parse_",
            "format_",
            // Service/business logic prefixes
            "resolve_",
            "schedule_",
            "add_",
            "update_",
            "delete_",
            "remove_",
            "apply_",
            "use",
            "fetch_",
            "load_",
            "save_",
            "send_",
            "notify_",
        ];

        UTILITY_PREFIXES.iter().any(|p| func_name.starts_with(p))
    }

    /// Create merged finding from consensus
    fn create_consensus_finding(
        &self,
        findings: &[Finding],
        consensus: &ConsensusResult,
    ) -> Finding {
        // Use highest severity finding as base
        let mut sorted_findings = findings.to_vec();
        sorted_findings.sort_by(|a, b| b.severity.cmp(&a.severity));
        let base = &sorted_findings[0];

        // Cap severity at High for utility functions (they're expected to be widely used)
        let mut final_severity = consensus.severity;
        if final_severity == Severity::Critical && Self::is_utility_function_name(&base.title) {
            final_severity = Severity::High;
        }

        // Create descriptive detector name
        let detector_names: Vec<&str> = consensus
            .contributing_detectors
            .iter()
            .take(3)
            .map(|s| s.as_str())
            .collect();

        let detector_str = if consensus.contributing_detectors.len() > 3 {
            format!(
                "Consensus[{}+{}more]",
                detector_names.join("+"),
                consensus.contributing_detectors.len() - 3
            )
        } else {
            format!("Consensus[{}]", detector_names.join("+"))
        };

        let consensus_note = format!(
            "\n\n**Consensus Analysis**\n\
             - {} detectors agree on this issue\n\
             - Confidence: {:.0}%\n\
             - Detectors: {}",
            consensus.vote_count,
            consensus.confidence * 100.0,
            consensus.contributing_detectors.join(", ")
        );

        Finding {
            id: base.id.clone(),
            detector: detector_str,
            severity: final_severity,
            title: format!("{} [{} detectors]", base.title, consensus.vote_count),
            description: format!("{}{}", base.description, consensus_note),
            affected_files: base.affected_files.clone(),
            line_start: base.line_start,
            line_end: base.line_end,
            suggested_fix: self.merge_suggestions(findings),
            estimated_effort: base.estimated_effort.clone(),
            category: base.category.clone(),
            cwe_id: base.cwe_id.clone(),
            why_it_matters: base.why_it_matters.clone(),
            confidence: Some(consensus.confidence),
            ..Default::default()
        }
    }

    /// Merge fix suggestions from multiple findings
    fn merge_suggestions(&self, findings: &[Finding]) -> Option<String> {
        let mut suggestions = Vec::new();
        let mut seen = HashSet::new();

        for f in findings {
            if let Some(ref fix) = f.suggested_fix {
                if !seen.contains(fix) {
                    suggestions.push(format!("[{}] {}", f.detector, fix));
                    seen.insert(fix.clone());
                }
            }
        }

        if suggestions.is_empty() {
            findings.first().and_then(|f| f.suggested_fix.clone())
        } else {
            Some(suggestions.join("\n\n"))
        }
    }
}

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

    #[test]
    fn test_utility_function_detection() {
        let test_cases = vec![
            ("Architectural Bottleneck: is_sql_context", true),
            ("Architectural Bottleneck: is_hash_mention_not_usage", true),
            ("Architectural Bottleneck: scan_file", true),
            ("Architectural Bottleneck: find_dead_classes", true),
            ("Architectural Bottleneck: check_line_for_patterns", true),
            ("Architectural Bottleneck: calculate_health_scores", true),
            ("Architectural Bottleneck: remove_finding_impact", true), // matches remove_ prefix
            ("Some Other Finding", false),
        ];

        for (title, expected) in test_cases {
            let result = VotingEngine::is_utility_function_name(title);
            assert_eq!(
                result, expected,
                "Title '{}' expected {} but got {}",
                title, expected, result
            );
        }
    }
}
