//! Breaking Change Detector
//!
//! Analyzes tasks to determine whether they modify public APIs in ways that
//! would break downstream dependents. Uses the Kuzu knowledge graph to find
//! files that depend on each task's target file and checks whether the task
//! description or generated code indicates a public API modification.

use std::sync::Arc;

use anyhow::Result;
use tracing::{debug, instrument, warn};

use crate::db::KuzuGraph;
use crate::types::{Severity, Task, TaskType, ValidationResult};

/// Keywords in task descriptions that suggest a public API is being changed.
const API_CHANGE_KEYWORDS: &[&str] = &[
    "export",
    "public",
    "pub fn",
    "pub struct",
    "pub enum",
    "pub trait",
    "pub type",
    "interface",
    "function signature",
    "remove function",
    "rename function",
    "rename struct",
    "rename method",
    "change signature",
    "change return type",
    "change parameter",
    "remove parameter",
    "add required parameter",
    "remove field",
    "rename field",
    "deprecate",
    "breaking",
];

/// Detects whether tasks introduce breaking changes to public APIs by
/// cross-referencing the knowledge graph's dependency edges.
pub struct BreakingChangeDetector {
    kuzu: Arc<KuzuGraph>,
}

impl BreakingChangeDetector {
    pub fn new(kuzu: Arc<KuzuGraph>) -> Self {
        Self { kuzu }
    }

    /// Analyze all tasks for potential breaking changes.
    #[instrument(skip(self, tasks), fields(task_count = tasks.len()))]
    pub async fn detect_breaking_changes(
        &self,
        tasks: &[Task],
    ) -> Result<Vec<ValidationResult>> {
        let mut results = Vec::new();

        for task in tasks {
            let result = self.check_task_for_breaking_changes(task).await?;
            results.push(result);
        }

        debug!(
            total = results.len(),
            passed = results.iter().filter(|r| r.passed).count(),
            "Breaking change detection complete"
        );

        Ok(results)
    }

    /// Check a single task for breaking changes.
    async fn check_task_for_breaking_changes(
        &self,
        task: &Task,
    ) -> Result<ValidationResult> {
        // Create tasks cannot break existing APIs (they introduce new files).
        if task.task_type == TaskType::Create {
            return Ok(ValidationResult {
                constraint_id: format!("breaking_{}", task.id),
                passed: true,
                message: "Create task does not affect existing APIs".to_string(),
                severity: Severity::Info,
            });
        }

        // Test tasks are not breaking changes.
        if task.task_type == TaskType::Test {
            return Ok(ValidationResult {
                constraint_id: format!("breaking_{}", task.id),
                passed: true,
                message: "Test task does not affect public APIs".to_string(),
                severity: Severity::Info,
            });
        }

        // Find files that depend on the target file.
        let dependents = self.kuzu.get_dependents(&task.target_file).await?;

        if dependents.is_empty() {
            return Ok(ValidationResult {
                constraint_id: format!("breaking_{}", task.id),
                passed: true,
                message: format!(
                    "No dependent files for '{}'; no breaking change risk",
                    task.target_file
                ),
                severity: Severity::Info,
            });
        }

        // Determine whether the task modifies the public API.
        let description_modifies = self.modifies_public_api(&task.description);
        let code_modifies = task
            .generated_code
            .as_deref()
            .map(|c| self.modifies_public_api(c))
            .unwrap_or(false);

        // For Delete tasks that have dependents, always flag.
        if task.task_type == TaskType::Delete {
            warn!(
                task_id = %task.id,
                dependents = dependents.len(),
                "Delete task removes a file with dependents"
            );
            return Ok(ValidationResult {
                constraint_id: format!("breaking_{}", task.id),
                passed: false,
                message: format!(
                    "BREAKING CHANGE: deleting '{}' affects {} dependent file(s): {}",
                    task.target_file,
                    dependents.len(),
                    truncated_list(&dependents, 5)
                ),
                severity: Severity::Error,
            });
        }

        // Modify task with API-level changes.
        if description_modifies || code_modifies {
            let severity = if description_modifies {
                // Description explicitly mentions API changes -> Warning
                // (may be intentional).
                Severity::Warning
            } else {
                // Code-level detection only -> also Warning, needs human
                // review.
                Severity::Warning
            };

            warn!(
                task_id = %task.id,
                dependents = dependents.len(),
                "Potential breaking change detected"
            );

            Ok(ValidationResult {
                constraint_id: format!("breaking_{}", task.id),
                passed: false,
                message: format!(
                    "Potential BREAKING CHANGE: modifying public API in '{}' \
                     affects {} dependent file(s): {}",
                    task.target_file,
                    dependents.len(),
                    truncated_list(&dependents, 5)
                ),
                severity,
            })
        } else {
            Ok(ValidationResult {
                constraint_id: format!("breaking_{}", task.id),
                passed: true,
                message: format!(
                    "Implementation-only changes to '{}' ({} dependent file(s), not breaking)",
                    task.target_file,
                    dependents.len()
                ),
                severity: Severity::Info,
            })
        }
    }

    /// Heuristic check: does the text suggest a public API modification?
    fn modifies_public_api(&self, text: &str) -> bool {
        let lower = text.to_lowercase();
        API_CHANGE_KEYWORDS
            .iter()
            .any(|keyword| lower.contains(keyword))
    }
}

/// Format a list of file paths, truncating to `max` entries with a "..." suffix.
fn truncated_list(items: &[String], max: usize) -> String {
    if items.len() <= max {
        items.join(", ")
    } else {
        let shown: Vec<&str> = items.iter().take(max).map(|s| s.as_str()).collect();
        format!("{} ... and {} more", shown.join(", "), items.len() - max)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::{FileEntity, FunctionDef, ImportDef, Task, TaskStatus, TaskType};
    use chrono::Utc;
    use std::path::Path;

    fn make_task(id: &str, task_type: TaskType, desc: &str, target: &str) -> Task {
        Task {
            id: id.to_string(),
            scroll_id: "scroll_1".to_string(),
            description: desc.to_string(),
            target_file: target.to_string(),
            task_type,
            status: TaskStatus::Pending,
            dependencies: vec![],
            generated_code: None,
            diff: None,
            created_at: Utc::now(),
            group_label: None,
        }
    }

    fn next_test_id() -> u64 {
        static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
        COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst)
    }

    async fn make_detector_with_graph() -> (BreakingChangeDetector, Arc<KuzuGraph>) {
        let path = format!("/tmp/furnace_test_bcd_kuzu_{}_{}", std::process::id(), next_test_id());
        let kuzu = Arc::new(
            KuzuGraph::new(Path::new(&path))
                .expect("failed to create test KuzuGraph"),
        );
        let detector = BreakingChangeDetector::new(kuzu.clone());
        (detector, kuzu)
    }

    #[tokio::test]
    async fn test_create_task_no_breaking_change() {
        let (d, _) = make_detector_with_graph().await;
        let task = make_task("t1", TaskType::Create, "Create new file", "src/new.rs");
        let results = d.detect_breaking_changes(&[task]).await.unwrap();
        assert_eq!(results.len(), 1);
        assert!(results[0].passed);
    }

    #[tokio::test]
    async fn test_test_task_no_breaking_change() {
        let (d, _) = make_detector_with_graph().await;
        let task = make_task("t1", TaskType::Test, "Add test", "tests/test.rs");
        let results = d.detect_breaking_changes(&[task]).await.unwrap();
        assert_eq!(results.len(), 1);
        assert!(results[0].passed);
    }

    #[tokio::test]
    async fn test_modify_without_dependents() {
        let (d, _) = make_detector_with_graph().await;
        let task = make_task(
            "t1",
            TaskType::Modify,
            "Change function signature",
            "src/isolated.rs",
        );
        let results = d.detect_breaking_changes(&[task]).await.unwrap();
        assert_eq!(results.len(), 1);
        // No dependents in the graph -> passes.
        assert!(results[0].passed);
    }

    #[tokio::test]
    async fn test_modify_with_dependents_and_api_change() {
        let (d, kuzu) = make_detector_with_graph().await;

        // Set up a dependency: consumer.rs imports from api.rs
        let api_file = FileEntity {
            path: "src/api.rs".to_string(),
            language: "rust".to_string(),
            functions: vec![FunctionDef {
                name: "get_data".to_string(),
                signature: "pub fn get_data() -> Vec<u8>".to_string(),
                line_start: 1,
                line_end: 5,
                is_public: true,
            }],
            types: vec![],
            imports: vec![],
            exports: vec!["get_data".to_string()],
        };
        let consumer_file = FileEntity {
            path: "src/consumer.rs".to_string(),
            language: "rust".to_string(),
            functions: vec![],
            types: vec![],
            imports: vec![ImportDef {
                source: "src/api.rs".to_string(),
                items: vec!["get_data".to_string()],
            }],
            exports: vec![],
        };

        kuzu.add_file_to_graph(&api_file).await.unwrap();
        kuzu.add_file_to_graph(&consumer_file).await.unwrap();

        let task = make_task(
            "t1",
            TaskType::Modify,
            "Change function signature of get_data",
            "src/api.rs",
        );
        let results = d.detect_breaking_changes(&[task]).await.unwrap();
        assert_eq!(results.len(), 1);
        assert!(!results[0].passed);
        assert!(results[0].message.contains("BREAKING CHANGE"));
    }

    #[tokio::test]
    async fn test_delete_with_dependents() {
        let (d, kuzu) = make_detector_with_graph().await;

        let lib_file = FileEntity {
            path: "src/lib_util.rs".to_string(),
            language: "rust".to_string(),
            functions: vec![],
            types: vec![],
            imports: vec![],
            exports: vec![],
        };
        let user_file = FileEntity {
            path: "src/user.rs".to_string(),
            language: "rust".to_string(),
            functions: vec![],
            types: vec![],
            imports: vec![ImportDef {
                source: "src/lib_util.rs".to_string(),
                items: vec!["helper".to_string()],
            }],
            exports: vec![],
        };

        kuzu.add_file_to_graph(&lib_file).await.unwrap();
        kuzu.add_file_to_graph(&user_file).await.unwrap();

        let task = make_task(
            "t1",
            TaskType::Delete,
            "Remove lib_util.rs",
            "src/lib_util.rs",
        );
        let results = d.detect_breaking_changes(&[task]).await.unwrap();
        assert_eq!(results.len(), 1);
        assert!(!results[0].passed);
        assert_eq!(results[0].severity, Severity::Error);
    }

    #[tokio::test]
    async fn test_modify_implementation_only_not_breaking() {
        let (d, kuzu) = make_detector_with_graph().await;

        let target = FileEntity {
            path: "src/impl_only.rs".to_string(),
            language: "rust".to_string(),
            functions: vec![],
            types: vec![],
            imports: vec![],
            exports: vec![],
        };
        let consumer = FileEntity {
            path: "src/impl_consumer.rs".to_string(),
            language: "rust".to_string(),
            functions: vec![],
            types: vec![],
            imports: vec![ImportDef {
                source: "src/impl_only.rs".to_string(),
                items: vec!["something".to_string()],
            }],
            exports: vec![],
        };

        kuzu.add_file_to_graph(&target).await.unwrap();
        kuzu.add_file_to_graph(&consumer).await.unwrap();

        // Description does NOT mention API changes.
        let task = make_task(
            "t1",
            TaskType::Modify,
            "Optimize internal loop in impl_only module",
            "src/impl_only.rs",
        );
        let results = d.detect_breaking_changes(&[task]).await.unwrap();
        assert_eq!(results.len(), 1);
        assert!(results[0].passed);
        assert!(results[0].message.contains("Implementation-only"));
    }

    #[tokio::test]
    async fn test_empty_tasks() {
        let (d, _) = make_detector_with_graph().await;
        let results = d.detect_breaking_changes(&[]).await.unwrap();
        assert!(results.is_empty());
    }

    #[test]
    fn test_truncated_list_short() {
        let items: Vec<String> = vec!["a.rs".into(), "b.rs".into()];
        assert_eq!(truncated_list(&items, 5), "a.rs, b.rs");
    }

    #[test]
    fn test_truncated_list_long() {
        let items: Vec<String> = (0..10).map(|i| format!("file_{}.rs", i)).collect();
        let result = truncated_list(&items, 3);
        assert!(result.contains("..."));
        assert!(result.contains("7 more"));
    }

    #[test]
    fn test_modifies_public_api_keywords() {
        let d = BreakingChangeDetector::new(Arc::new(
            KuzuGraph::new(Path::new(&format!("/tmp/furnace_test_bcd_kw_{}_{}", std::process::id(), next_test_id())))
                .expect("create graph"),
        ));
        assert!(d.modifies_public_api("Change the public interface"));
        assert!(d.modifies_public_api("Remove function get_data"));
        assert!(d.modifies_public_api("Add pub fn new_method"));
        assert!(d.modifies_public_api("Rename struct FooBar to BazQux"));
        assert!(!d.modifies_public_api("Fix internal logic bug"));
        assert!(!d.modifies_public_api("Optimize loop performance"));
    }
}
