use crate::priority;
#[cfg(test)]
use crate::priority::UnifiedAnalysisUtils;
use anyhow::Result;
use std::cmp::Ordering;
use std::collections::HashSet;
use std::fs;
use std::io::Write;
use std::path::PathBuf;

pub fn output_json(
    analysis: &priority::UnifiedAnalysis,
    output_file: Option<PathBuf>,
) -> Result<()> {
    output_json_with_filters(analysis, None, None, output_file)
}

pub fn output_json_with_filters(
    analysis: &priority::UnifiedAnalysis,
    top: Option<usize>,
    tail: Option<usize>,
    output_file: Option<PathBuf>,
) -> Result<()> {
    output_json_with_format(analysis, top, tail, output_file, false)
}

pub fn output_json_with_format(
    analysis: &priority::UnifiedAnalysis,
    top: Option<usize>,
    tail: Option<usize>,
    output_file: Option<PathBuf>,
    include_scoring_details: bool,
) -> Result<()> {
    // Always use unified format (spec 202 - removed legacy format)
    let unified_output =
        crate::output::unified::convert_to_unified_format(analysis, include_scoring_details);

    // Apply filtering to unified output
    let filtered = apply_filters_to_unified_output(unified_output, top, tail);
    let json = serde_json::to_string_pretty(&filtered)?;

    if let Some(path) = output_file {
        if let Some(parent) = path.parent() {
            crate::io::ensure_dir(parent)?;
        }
        let mut file = fs::File::create(path)?;
        file.write_all(json.as_bytes())?;
    } else {
        println!("{json}");
    }
    Ok(())
}

fn apply_filters_to_unified_output(
    mut output: crate::output::unified::UnifiedOutput,
    top: Option<usize>,
    tail: Option<usize>,
) -> crate::output::unified::UnifiedOutput {
    if top.is_some() || tail.is_some() {
        output.items = filter_items_by_location_groups(output.items, top, tail);
    }

    // Update summary to reflect filtered items
    output.summary.total_items = output.items.len();
    output
}

#[derive(Debug, Clone)]
struct LocationGroupSelection {
    key: (String, Option<String>, Option<usize>),
    combined_score: f64,
    first_index: usize,
}

fn filter_items_by_location_groups(
    items: Vec<crate::output::unified::UnifiedDebtItemOutput>,
    top: Option<usize>,
    tail: Option<usize>,
) -> Vec<crate::output::unified::UnifiedDebtItemOutput> {
    let groups = sorted_function_location_groups(&items);
    let selected_keys = select_location_group_keys(groups, top, tail);

    items
        .into_iter()
        .filter(|item| selected_keys.contains(&item_location_key(item)))
        .collect()
}

fn sorted_function_location_groups(
    items: &[crate::output::unified::UnifiedDebtItemOutput],
) -> Vec<LocationGroupSelection> {
    let mut groups = Vec::<LocationGroupSelection>::new();

    for (index, item) in items.iter().enumerate() {
        let Some(key) = function_location_key(item) else {
            continue;
        };

        if let Some(group) = groups.iter_mut().find(|group| group.key == key) {
            group.combined_score += item.score();
        } else {
            groups.push(LocationGroupSelection {
                key,
                combined_score: item.score(),
                first_index: index,
            });
        }
    }

    groups.sort_by(compare_location_groups);
    groups
}

fn select_location_group_keys(
    groups: Vec<LocationGroupSelection>,
    top: Option<usize>,
    tail: Option<usize>,
) -> HashSet<(String, Option<String>, Option<usize>)> {
    let selected = if let Some(n) = top {
        groups.into_iter().take(n).collect()
    } else if let Some(n) = tail {
        let skip = groups.len().saturating_sub(n);
        groups.into_iter().skip(skip).collect()
    } else {
        groups
    };

    selected.into_iter().map(|group| group.key).collect()
}

fn compare_location_groups(a: &LocationGroupSelection, b: &LocationGroupSelection) -> Ordering {
    b.combined_score
        .partial_cmp(&a.combined_score)
        .unwrap_or(Ordering::Equal)
        .then_with(|| a.first_index.cmp(&b.first_index))
}

fn item_location_key(
    item: &crate::output::unified::UnifiedDebtItemOutput,
) -> (String, Option<String>, Option<usize>) {
    match item {
        crate::output::unified::UnifiedDebtItemOutput::Function(function) => (
            function.location.file.clone(),
            function.location.function.clone(),
            function.location.line,
        ),
        crate::output::unified::UnifiedDebtItemOutput::File(file) => {
            (file.location.file.clone(), None, None)
        }
    }
}

fn function_location_key(
    item: &crate::output::unified::UnifiedDebtItemOutput,
) -> Option<(String, Option<String>, Option<usize>)> {
    match item {
        crate::output::unified::UnifiedDebtItemOutput::Function(function) => Some((
            function.location.file.clone(),
            function.location.function.clone(),
            function.location.line,
        )),
        crate::output::unified::UnifiedDebtItemOutput::File(_) => None,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::output::unified::{
        DebtSummary, OutputMetadata, ScoreDistribution, TypeBreakdown, UnifiedDebtItemOutput,
        UnifiedOutput,
    };
    use crate::priority::{
        ActionableRecommendation, DebtType, FunctionRole, ImpactMetrics, Location, UnifiedDebtItem,
        UnifiedScore, call_graph::CallGraph,
    };
    use std::path::PathBuf;
    use tempfile::TempDir;

    fn create_test_item(name: &str, score: f64) -> UnifiedDebtItem {
        UnifiedDebtItem {
            location: Location {
                file: PathBuf::from("test.rs"),
                line: 10,
                function: name.to_string(),
            },
            debt_type: DebtType::ComplexityHotspot {
                cyclomatic: 15,
                cognitive: 25,
            },
            unified_score: UnifiedScore {
                complexity_factor: 50.0,
                coverage_factor: 80.0,
                dependency_factor: 50.0,
                role_multiplier: 2.0,
                final_score: score.max(0.0),
                base_score: None,
                exponential_factor: None,
                risk_boost: None,
                pre_adjustment_score: None,
                adjustment_applied: None,
                purity_factor: None,
                refactorability_factor: None,
                pattern_factor: None,
                // Spec 260: Score transparency fields
                debt_adjustment: None,
                pre_normalization_score: None,
                structural_multiplier: Some(1.0),
                has_coverage_data: false,
                contextual_risk_multiplier: None,
                pre_contextual_score: None,
                debt_type_multiplier: None,
            },
            function_role: FunctionRole::PureLogic,
            recommendation: ActionableRecommendation {
                primary_action: "Fix issue".to_string(),
                rationale: "Test reason".to_string(),
                implementation_steps: vec![],
                related_items: vec![],
                steps: None,
                estimated_effort_hours: None,
            },
            expected_impact: ImpactMetrics {
                complexity_reduction: 100.0,
                risk_reduction: 10.0,
                coverage_improvement: 100.0,
                lines_reduction: 500,
            },
            transitive_coverage: None,
            file_context: None,
            upstream_dependencies: 10,
            downstream_dependencies: 20,
            upstream_callers: vec![],
            downstream_callees: vec![],
            upstream_production_callers: vec![],
            upstream_test_callers: vec![],
            production_blast_radius: 0,
            nesting_depth: 5,
            function_length: 200,
            cyclomatic_complexity: 25,
            cognitive_complexity: 40,
            is_pure: Some(false),
            purity_confidence: Some(0.8),
            purity_level: None,
            god_object_indicators: None,
            tier: None,
            function_context: None,
            context_confidence: None,
            contextual_recommendation: None,
            pattern_analysis: None,
            context_multiplier: None,
            context_type: None,
            language_specific: None, // spec 190
            detected_pattern: None,
            contextual_risk: None, // spec 203
            file_line_count: None,
            responsibility_category: None,
            error_swallowing_count: None,
            error_swallowing_patterns: None,
            entropy_analysis: None,
            context_suggestion: None,
        }
    }

    fn create_test_analysis_with_items(count: usize) -> priority::UnifiedAnalysis {
        let call_graph = CallGraph::new();
        let mut analysis = priority::UnifiedAnalysis::new(call_graph);

        for i in 0..count {
            let mut item = create_test_item(&format!("func_{}", i), 100.0 - i as f64);
            // Give each item a unique line number to avoid duplicate detection
            item.location.line = 10 + i;
            analysis.add_item(item);
        }

        analysis.sort_by_priority();
        analysis
    }

    fn create_unified_output(items: Vec<UnifiedDebtItemOutput>) -> UnifiedOutput {
        UnifiedOutput {
            format_version: "3.0".to_string(),
            metadata: OutputMetadata {
                debtmap_version: "test".to_string(),
                generated_at: "test".to_string(),
                project_root: None,
                analysis_type: "unified".to_string(),
            },
            summary: DebtSummary {
                total_items: items.len(),
                total_debt_score: items.iter().map(UnifiedDebtItemOutput::score).sum(),
                debt_density: 0.0,
                total_loc: 0,
                by_type: TypeBreakdown {
                    file: 0,
                    function: items.len(),
                },
                by_category: Default::default(),
                score_distribution: ScoreDistribution::default(),
                cohesion: None,
            },
            items,
        }
    }

    /// Helper to extract function name from unified output item
    fn get_function_name(item: &UnifiedDebtItemOutput) -> Option<String> {
        match item {
            UnifiedDebtItemOutput::Function(f) => f.location.function.clone(),
            UnifiedDebtItemOutput::File(_) => None,
        }
    }

    #[test]
    fn test_top_filter_selects_complete_highest_location_group() {
        let items = vec![
            UnifiedDebtItemOutput::from_debt_item(
                &crate::priority::DebtItem::Function(Box::new(create_test_item("other", 40.0))),
                false,
            ),
            UnifiedDebtItemOutput::from_debt_item(
                &crate::priority::DebtItem::Function(Box::new(create_test_item("grouped", 30.0))),
                false,
            ),
            UnifiedDebtItemOutput::from_debt_item(
                &crate::priority::DebtItem::Function(Box::new(create_test_item("grouped", 29.0))),
                false,
            ),
        ];
        let output = create_unified_output(items);

        let filtered = apply_filters_to_unified_output(output, Some(1), None);

        assert_eq!(filtered.items.len(), 2);
        assert!(
            filtered
                .items
                .iter()
                .all(|item| get_function_name(item) == Some("grouped".to_string()))
        );
    }

    #[test]
    fn test_output_json_creates_parent_directories() {
        let temp_dir = TempDir::new().unwrap();
        let nested_path = temp_dir
            .path()
            .join("nested")
            .join("subdirs")
            .join("output.json");

        let call_graph = CallGraph::new();
        let analysis = priority::UnifiedAnalysis::new(call_graph);

        let result = output_json(&analysis, Some(nested_path.clone()));
        assert!(
            result.is_ok(),
            "Failed to write JSON to nested path: {:?}",
            result.err()
        );
        assert!(
            nested_path.exists(),
            "Output file was not created at nested path"
        );

        let content = fs::read_to_string(&nested_path).unwrap();
        assert!(!content.is_empty(), "Output file is empty");
    }

    #[test]
    fn test_output_json_with_head_parameter() {
        let temp_dir = TempDir::new().unwrap();
        let output_path = temp_dir.path().join("output.json");

        let analysis = create_test_analysis_with_items(10);

        // Test with head=3
        let result = output_json_with_filters(&analysis, Some(3), None, Some(output_path.clone()));
        assert!(result.is_ok(), "Failed to write JSON: {:?}", result.err());

        let content = fs::read_to_string(&output_path).unwrap();
        let parsed: UnifiedOutput = serde_json::from_str(&content).unwrap();

        assert_eq!(
            parsed.items.len(),
            3,
            "Expected 3 items with head=3, got {}",
            parsed.items.len()
        );

        // Verify we got the top 3 items (highest scores)
        assert_eq!(
            get_function_name(&parsed.items[0]),
            Some("func_0".to_string())
        );
        assert_eq!(
            get_function_name(&parsed.items[1]),
            Some("func_1".to_string())
        );
        assert_eq!(
            get_function_name(&parsed.items[2]),
            Some("func_2".to_string())
        );
    }

    #[test]
    fn test_output_json_with_tail_parameter() {
        let temp_dir = TempDir::new().unwrap();
        let output_path = temp_dir.path().join("output.json");

        let analysis = create_test_analysis_with_items(10);

        // Test with tail=3
        let result = output_json_with_filters(&analysis, None, Some(3), Some(output_path.clone()));
        assert!(result.is_ok(), "Failed to write JSON: {:?}", result.err());

        let content = fs::read_to_string(&output_path).unwrap();
        let parsed: UnifiedOutput = serde_json::from_str(&content).unwrap();

        assert_eq!(
            parsed.items.len(),
            3,
            "Expected 3 items with tail=3, got {}",
            parsed.items.len()
        );

        // Verify we got the last 3 items (lowest scores)
        assert_eq!(
            get_function_name(&parsed.items[0]),
            Some("func_7".to_string())
        );
        assert_eq!(
            get_function_name(&parsed.items[1]),
            Some("func_8".to_string())
        );
        assert_eq!(
            get_function_name(&parsed.items[2]),
            Some("func_9".to_string())
        );
    }

    #[test]
    fn test_output_json_without_filters() {
        let temp_dir = TempDir::new().unwrap();
        let output_path = temp_dir.path().join("output.json");

        let analysis = create_test_analysis_with_items(10);

        // Test without filters (should return all items)
        let result = output_json_with_filters(&analysis, None, None, Some(output_path.clone()));
        assert!(result.is_ok(), "Failed to write JSON: {:?}", result.err());

        let content = fs::read_to_string(&output_path).unwrap();
        let parsed: UnifiedOutput = serde_json::from_str(&content).unwrap();

        assert_eq!(
            parsed.items.len(),
            10,
            "Expected all 10 items without filters, got {}",
            parsed.items.len()
        );
    }

    #[test]
    fn test_output_json_head_larger_than_items() {
        let temp_dir = TempDir::new().unwrap();
        let output_path = temp_dir.path().join("output.json");

        let analysis = create_test_analysis_with_items(5);

        // Test with head=10 when only 5 items exist
        let result = output_json_with_filters(&analysis, Some(10), None, Some(output_path.clone()));
        assert!(result.is_ok(), "Failed to write JSON: {:?}", result.err());

        let content = fs::read_to_string(&output_path).unwrap();
        let parsed: UnifiedOutput = serde_json::from_str(&content).unwrap();

        assert_eq!(
            parsed.items.len(),
            5,
            "Expected 5 items (all available), got {}",
            parsed.items.len()
        );
    }

    #[test]
    fn test_output_json_tail_larger_than_items() {
        let temp_dir = TempDir::new().unwrap();
        let output_path = temp_dir.path().join("output.json");

        let analysis = create_test_analysis_with_items(5);

        // Test with tail=10 when only 5 items exist
        let result = output_json_with_filters(&analysis, None, Some(10), Some(output_path.clone()));
        assert!(result.is_ok(), "Failed to write JSON: {:?}", result.err());

        let content = fs::read_to_string(&output_path).unwrap();
        let parsed: UnifiedOutput = serde_json::from_str(&content).unwrap();

        assert_eq!(
            parsed.items.len(),
            5,
            "Expected 5 items (all available), got {}",
            parsed.items.len()
        );
    }

    #[test]
    fn test_output_json_includes_file_level_items() {
        use crate::priority::{FileDebtItem, FileDebtMetrics, FileImpact};

        let temp_dir = TempDir::new().unwrap();
        let output_path = temp_dir.path().join("output.json");

        let call_graph = CallGraph::new();
        let mut analysis = priority::UnifiedAnalysis::new(call_graph);

        // Add function-level items with lower scores
        for i in 0..3 {
            let mut item = create_test_item(&format!("func_{}", i), 50.0 + i as f64);
            item.location.line = 10 + i;
            analysis.add_item(item);
        }

        // Add file-level items with higher scores (should appear first in output)
        let file_item = FileDebtItem {
            metrics: FileDebtMetrics {
                path: PathBuf::from("god_object.rs"),
                total_lines: 5530,
                function_count: 179,
                class_count: 0,
                avg_complexity: 25.0,
                max_complexity: 85,
                total_complexity: 4500,
                coverage_percent: 0.3,
                uncovered_lines: 3871,
                god_object_analysis: Some(crate::organization::GodObjectAnalysis {
                    method_count: 179,
                    weighted_method_count: None,
                    field_count: 20,
                    responsibility_count: 15,
                    is_god_object: true,
                    god_object_score: 8500.0,
                    lines_of_code: 5533,
                    complexity_sum: 4500,
                    responsibilities: vec!["Too many responsibilities".to_string()],
                    responsibility_method_counts: Default::default(),
                    recommended_splits: vec![],
                    confidence: crate::organization::GodObjectConfidence::Definite,
                    purity_distribution: None,
                    module_structure: None,
                    detection_type: crate::organization::DetectionType::GodFile,
                    struct_name: None,
                    struct_line: None,
                    struct_location: None,
                    visibility_breakdown: None,
                    domain_count: 0,
                    domain_diversity: 0.0,
                    struct_ratio: 0.0,
                    analysis_method: crate::organization::SplitAnalysisMethod::None,
                    cross_domain_severity: None,
                    domain_diversity_metrics: None,
                    aggregated_entropy: None,
                    aggregated_error_swallowing_count: None,
                    aggregated_error_swallowing_patterns: None,
                    layering_impact: None,
                    anti_pattern_report: None,
                    complexity_metrics: None,   // Spec 211
                    trait_method_summary: None, // Spec 217
                }),
                function_scores: vec![],
                god_object_type: None,
                file_type: None,
                ..Default::default()
            },
            score: 606.0, // Higher than function items
            priority_rank: 1,
            recommendation: "Split this god object".to_string(),
            impact: FileImpact {
                complexity_reduction: 200.0,
                maintainability_improvement: 80.0,
                test_effort: 40.0,
            },
        };

        analysis.add_file_item(file_item);
        analysis.sort_by_priority();

        // Export to JSON
        let result = output_json_with_filters(&analysis, None, None, Some(output_path.clone()));
        assert!(result.is_ok(), "Failed to write JSON: {:?}", result.err());

        // Parse and verify using unified output format
        let content = fs::read_to_string(&output_path).unwrap();
        let parsed: UnifiedOutput = serde_json::from_str(&content).unwrap();

        // Should have 4 total items (3 function + 1 file)
        assert_eq!(
            parsed.items.len(),
            4,
            "Expected 4 items total (3 function + 1 file), got {}",
            parsed.items.len()
        );

        // File item with highest score should be first
        match &parsed.items[0] {
            UnifiedDebtItemOutput::File(file) => {
                assert_eq!(file.score, 606.0);
                assert_eq!(file.location.file, "god_object.rs");
            }
            _ => panic!("Expected first item to be a File debt item with highest score"),
        }

        // Remaining should be function items
        for i in 1..4 {
            match &parsed.items[i] {
                UnifiedDebtItemOutput::Function(_) => {
                    // This is expected
                }
                _ => panic!("Expected item {} to be a Function debt item", i),
            }
        }
    }
}
