//! Morpheus -- FURNACE Validation Agent
//!
//! Morpheus validates SCROLLs before execution, ensuring correctness and
//! preventing bad code generation. It performs four categories of validation:
//!
//! 1. **Constraint validation** -- checks MUST/MUST NOT/SHOULD/SHOULD NOT
//!    constraints against the knowledge graph.
//! 2. **Circular dependency detection** -- uses Tarjan's SCC algorithm to
//!    find cycles in the task dependency graph.
//! 3. **Type validation** -- verifies that referenced types exist in the
//!    codebase.
//! 4. **Breaking change detection** -- flags tasks that modify public APIs
//!    with downstream dependents.
//!
//! Key principle: *Validate early, fail fast, prevent bad code generation.*

pub mod breaking_change_detector;
pub mod constraint_validator;
pub mod dependency_checker;
pub mod type_validator;

use std::sync::Arc;

use anyhow::{Context, Result};
use chrono::Utc;
use tracing::{info, instrument, warn};

use crate::db::{KuzuGraph, SqliteDb};
use crate::knowledge::KnowledgeManager;
use crate::types::{
    Constraint, ScrollStatus, Severity, Task, ValidationReport, ValidationResult,
};

use self::breaking_change_detector::BreakingChangeDetector;
use self::constraint_validator::ConstraintValidator;
use self::dependency_checker::DependencyChecker;
use self::type_validator::TypeValidator;

// ---------------------------------------------------------------------------
// Morpheus
// ---------------------------------------------------------------------------

/// The Morpheus validation agent.
///
/// Morpheus is invoked by Aragorn before executing any SCROLL. If validation
/// fails the SCROLL must not proceed to execution.
pub struct Morpheus {
    /// SQLite database for SCROLL/task persistence.
    sqlite: Arc<SqliteDb>,

    /// Kuzu knowledge graph for dependency & type analysis.
    kuzu: Arc<KuzuGraph>,

    /// Validates constraints (MUST, MUST NOT, SHOULD, SHOULD NOT).
    constraint_validator: ConstraintValidator,

    /// Detects circular dependencies using Tarjan's SCC algorithm.
    dependency_checker: DependencyChecker,

    /// Validates type references against the knowledge graph.
    type_validator: TypeValidator,

    /// Detects modifications to public APIs that affect dependents.
    breaking_change_detector: BreakingChangeDetector,

    /// Optional knowledge base manager for KB-aware validation.
    kb_manager: Option<Arc<KnowledgeManager>>,
}

impl Morpheus {
    /// Create a new Morpheus instance backed by the given databases.
    pub fn new(sqlite: Arc<SqliteDb>, kuzu: Arc<KuzuGraph>) -> Self {
        Self {
            sqlite: sqlite.clone(),
            kuzu: kuzu.clone(),
            constraint_validator: ConstraintValidator::new(kuzu.clone()),
            dependency_checker: DependencyChecker::new(kuzu.clone()),
            type_validator: TypeValidator::new(kuzu.clone()),
            breaking_change_detector: BreakingChangeDetector::new(kuzu.clone()),
            kb_manager: None,
        }
    }

    /// Attach a KnowledgeManager for KB-aware validation.
    pub fn with_kb(mut self, kb_manager: Arc<KnowledgeManager>) -> Self {
        self.kb_manager = Some(kb_manager);
        self
    }

    // -----------------------------------------------------------------------
    // Primary entry point
    // -----------------------------------------------------------------------

    /// Validate a SCROLL identified by `scroll_id`.
    ///
    /// Loads the SCROLL and its tasks from SQLite, runs all four validation
    /// passes, persists the resulting `ValidationReport`, and returns it.
    #[instrument(skip(self), fields(scroll_id = %scroll_id))]
    pub async fn validate_scroll(&self, scroll_id: &str) -> Result<ValidationReport> {
        info!("Validating SCROLL: {}", scroll_id);

        // 1. Load the SCROLL from the database.
        let scroll = self
            .sqlite
            .get_scroll(scroll_id)
            .await
            .context("Failed to load SCROLL for validation")?;

        // 2. Load associated tasks (they are not embedded in the DB row).
        let tasks = self
            .sqlite
            .get_tasks_for_scroll(scroll_id)
            .await
            .context("Failed to load tasks for SCROLL")?;

        // 3. Run all validation passes (each with a 30s timeout guard).
        let constraint_results = tokio::time::timeout(
            std::time::Duration::from_secs(30),
            self.validate_constraints(&scroll.constraints),
        )
        .await
        .map_err(|_| {
            warn!("Constraint validation timed out after 30s");
            anyhow::anyhow!("Constraint validation timed out after 30s")
        })?
        .context("Constraint validation failed")?;

        let dependency_results = tokio::time::timeout(
            std::time::Duration::from_secs(30),
            self.check_circular_dependencies(&tasks),
        )
        .await
        .map_err(|_| {
            warn!("Dependency check timed out after 30s");
            anyhow::anyhow!("Dependency check timed out after 30s")
        })?
        .context("Dependency check failed")?;

        let type_results = tokio::time::timeout(
            std::time::Duration::from_secs(30),
            self.validate_types(&tasks),
        )
        .await
        .map_err(|_| {
            warn!("Type validation timed out after 30s");
            anyhow::anyhow!("Type validation timed out after 30s")
        })?
        .context("Type validation failed")?;

        let breaking_change_results = tokio::time::timeout(
            std::time::Duration::from_secs(30),
            self.detect_breaking_changes(&tasks),
        )
        .await
        .map_err(|_| {
            warn!("Breaking change detection timed out after 30s");
            anyhow::anyhow!("Breaking change detection timed out after 30s")
        })?
        .context("Breaking change detection failed")?;

        // 4. Build the report.
        let is_valid = Self::all_passed(&[
            &constraint_results,
            &dependency_results,
            &type_results,
            &breaking_change_results,
        ]);

        let report = ValidationReport {
            scroll_id: scroll_id.to_string(),
            is_valid,
            constraint_violations: constraint_results,
            circular_dependencies: dependency_results,
            type_errors: type_results,
            breaking_changes: breaking_change_results,
            validated_at: Utc::now(),
        };

        // 5. Persist the report.
        self.sqlite
            .save_validation_report(&report)
            .await
            .context("Failed to save validation report")?;

        // 6. Update SCROLL status if valid.
        if is_valid {
            self.sqlite
                .update_scroll_status(scroll_id, &ScrollStatus::Validated)
                .await
                .context("Failed to update SCROLL status to Validated")?;
            info!("SCROLL {} validated successfully", scroll_id);
        } else {
            warn!(
                "SCROLL {} failed validation: {}",
                scroll_id,
                report.summarize()
            );
        }

        Ok(report)
    }

    // -----------------------------------------------------------------------
    // Individual validation passes (public for direct use)
    // -----------------------------------------------------------------------

    /// Validate all constraints in the given list.
    #[instrument(skip(self, constraints), fields(count = constraints.len()))]
    pub async fn validate_constraints(
        &self,
        constraints: &[Constraint],
    ) -> Result<Vec<ValidationResult>> {
        self.constraint_validator.validate_all(constraints).await
    }

    /// Check for circular dependencies among the given tasks.
    #[instrument(skip(self, tasks), fields(count = tasks.len()))]
    pub async fn check_circular_dependencies(
        &self,
        tasks: &[Task],
    ) -> Result<Vec<ValidationResult>> {
        self.dependency_checker
            .check_circular_dependencies(tasks)
            .await
    }

    /// Validate type references in the given tasks.
    #[instrument(skip(self, tasks), fields(count = tasks.len()))]
    pub async fn validate_types(
        &self,
        tasks: &[Task],
    ) -> Result<Vec<ValidationResult>> {
        self.type_validator.validate_types(tasks).await
    }

    /// Detect breaking changes in the given tasks.
    #[instrument(skip(self, tasks), fields(count = tasks.len()))]
    pub async fn detect_breaking_changes(
        &self,
        tasks: &[Task],
    ) -> Result<Vec<ValidationResult>> {
        self.breaking_change_detector
            .detect_breaking_changes(tasks)
            .await
    }

    // -----------------------------------------------------------------------
    // KB validation
    // -----------------------------------------------------------------------

    /// Validate tasks against KB-sourced concepts in the graph.
    ///
    /// Checks whether Requirement-category concepts that relate to task target
    /// files are covered by the scroll's constraints. Returns warnings for
    /// uncovered requirements.
    pub async fn validate_against_kb(
        &self,
        constraints: &[Constraint],
        tasks: &[Task],
    ) -> Vec<ValidationResult> {
        let kb = match &self.kb_manager {
            Some(kb) if tokio::time::timeout(
                std::time::Duration::from_secs(5),
                kb.has_sources(),
            ).await.unwrap_or(false) => kb,
            _ => return Vec::new(),
        };
        let _ = kb; // KB manager used indirectly via kuzu graph

        let mut results = Vec::new();

        for task in tasks {
            let concepts = match self
                .kuzu
                .get_concepts_for_file(&task.target_file)
                .await
            {
                Ok(c) => c,
                Err(_) => continue,
            };

            for concept_row in &concepts {
                let category = concept_row
                    .get("c.category")
                    .map(|s| s.as_str())
                    .unwrap_or("");
                if category != "Requirement" {
                    continue;
                }

                let concept_name = concept_row
                    .get("c.name")
                    .cloned()
                    .unwrap_or_default();

                // Check if any constraint covers this requirement.
                let is_covered = constraints.iter().any(|c| {
                    c.description
                        .to_lowercase()
                        .contains(&concept_name.to_lowercase())
                });

                if !is_covered {
                    results.push(ValidationResult {
                        constraint_id: format!("kb-req-{}", concept_name),
                        passed: true, // Warning, not failure
                        message: format!(
                            "KB requirement '{}' for file '{}' is not explicitly covered by scroll constraints",
                            concept_name, task.target_file
                        ),
                        severity: Severity::Warning,
                    });
                }
            }
        }

        results
    }

    // -----------------------------------------------------------------------
    // Helpers
    // -----------------------------------------------------------------------

    /// Returns `true` if every `ValidationResult` in every slice has `passed == true`.
    fn all_passed(results: &[&Vec<ValidationResult>]) -> bool {
        results
            .iter()
            .all(|group| group.iter().all(|r| r.passed))
    }
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::{
        Constraint, ConstraintSource, ConstraintType, ExecutionMode, Scroll,
        ScrollMetadata, ScrollStatus, Severity, Task, TaskStatus, TaskType,
    };
    use chrono::Utc;
    use tempfile::TempDir;

    /// Helper: create an ephemeral Morpheus backed by real (empty) databases.
    async fn make_morpheus() -> (Morpheus, Arc<SqliteDb>, Arc<KuzuGraph>, TempDir) {
        let tmp = TempDir::new().expect("create temp dir");
        let db_path = tmp.path().join("test.db");
        let graph_path = tmp.path().join("kuzu");

        let sqlite = Arc::new(SqliteDb::new(&db_path).expect("create sqlite"));
        let kuzu = Arc::new(KuzuGraph::new(&graph_path).expect("create kuzu"));
        let morpheus = Morpheus::new(sqlite.clone(), kuzu.clone());

        (morpheus, sqlite, kuzu, tmp)
    }

    fn make_scroll(id: &str) -> Scroll {
        Scroll {
            id: id.to_string(),
            title: "Test Scroll".to_string(),
            intent: "Test validation".to_string(),
            constraints: vec![],
            tasks: vec![],
            metadata: ScrollMetadata {
                execution_mode: ExecutionMode::Forge,
                auto_forge_config: None,
                recipe_used: None,
                search_context: vec![],
            },
            status: ScrollStatus::Draft,
            created_at: Utc::now(),
            updated_at: Utc::now(),
            cleanup_after: None,
        }
    }

    fn make_task(id: &str, scroll_id: &str, deps: Vec<&str>) -> Task {
        Task {
            id: id.to_string(),
            scroll_id: scroll_id.to_string(),
            description: format!("Task {}", id),
            target_file: format!("src/{}.rs", id),
            task_type: TaskType::Create,
            status: TaskStatus::Pending,
            dependencies: deps.into_iter().map(String::from).collect(),
            generated_code: None,
            diff: None,
            created_at: Utc::now(),
            group_label: None,
        }
    }

    fn make_constraint(id: &str, ct: ConstraintType, pattern: &str) -> Constraint {
        Constraint {
            id: id.to_string(),
            constraint_type: ct,
            description: format!("Constraint: {}", pattern),
            pattern: pattern.to_string(),
            source: ConstraintSource::UserSpecified,
        }
    }

    // -- all_passed helper --

    #[test]
    fn test_all_passed_empty() {
        let empty: Vec<&Vec<ValidationResult>> = vec![];
        assert!(Morpheus::all_passed(&empty));
    }

    #[test]
    fn test_all_passed_all_pass() {
        let results = vec![ValidationResult {
            constraint_id: "c1".into(),
            passed: true,
            message: "ok".into(),
            severity: Severity::Info,
        }];
        assert!(Morpheus::all_passed(&[&results]));
    }

    #[test]
    fn test_all_passed_one_fails() {
        let good = vec![ValidationResult {
            constraint_id: "c1".into(),
            passed: true,
            message: "ok".into(),
            severity: Severity::Info,
        }];
        let bad = vec![ValidationResult {
            constraint_id: "c2".into(),
            passed: false,
            message: "fail".into(),
            severity: Severity::Error,
        }];
        assert!(!Morpheus::all_passed(&[&good, &bad]));
    }

    #[test]
    fn test_all_passed_multiple_groups_all_pass() {
        let group_a = vec![
            ValidationResult {
                constraint_id: "a1".into(),
                passed: true,
                message: "ok".into(),
                severity: Severity::Info,
            },
            ValidationResult {
                constraint_id: "a2".into(),
                passed: true,
                message: "ok".into(),
                severity: Severity::Info,
            },
        ];
        let group_b = vec![ValidationResult {
            constraint_id: "b1".into(),
            passed: true,
            message: "ok".into(),
            severity: Severity::Info,
        }];
        assert!(Morpheus::all_passed(&[&group_a, &group_b]));
    }

    // -- validate_scroll integration tests --

    #[tokio::test]
    async fn test_validate_scroll_empty_scroll() {
        let (morpheus, sqlite, _kuzu, _tmp) = make_morpheus().await;

        let scroll = make_scroll("s1");
        sqlite.create_scroll(&scroll).await.unwrap();

        let report = morpheus.validate_scroll("s1").await.unwrap();
        assert!(report.is_valid);
        assert_eq!(report.scroll_id, "s1");
        assert!(report.constraint_violations.is_empty());
        // dependency check should have at least a "no tasks" info result
        assert!(!report.circular_dependencies.is_empty());
        assert!(report.type_errors.is_empty());
        assert!(report.breaking_changes.is_empty());
    }

    #[tokio::test]
    async fn test_validate_scroll_with_valid_tasks() {
        let (morpheus, sqlite, _kuzu, _tmp) = make_morpheus().await;

        let scroll = make_scroll("s2");
        sqlite.create_scroll(&scroll).await.unwrap();

        let t1 = make_task("t1", "s2", vec![]);
        let t2 = make_task("t2", "s2", vec!["t1"]);
        sqlite.create_task(&t1).await.unwrap();
        sqlite.create_task(&t2).await.unwrap();

        let report = morpheus.validate_scroll("s2").await.unwrap();
        assert!(report.is_valid);
    }

    #[tokio::test]
    async fn test_validate_scroll_with_circular_deps() {
        let (morpheus, sqlite, _kuzu, _tmp) = make_morpheus().await;

        let scroll = make_scroll("s3");
        sqlite.create_scroll(&scroll).await.unwrap();

        let t1 = make_task("t1", "s3", vec!["t2"]);
        let t2 = make_task("t2", "s3", vec!["t1"]);
        sqlite.create_task(&t1).await.unwrap();
        sqlite.create_task(&t2).await.unwrap();

        let report = morpheus.validate_scroll("s3").await.unwrap();
        assert!(!report.is_valid);
        let cycle_failures: Vec<_> = report
            .circular_dependencies
            .iter()
            .filter(|r| !r.passed)
            .collect();
        assert!(!cycle_failures.is_empty());
    }

    #[tokio::test]
    async fn test_validate_scroll_with_must_constraint_violation() {
        let (morpheus, sqlite, _kuzu, _tmp) = make_morpheus().await;

        let mut scroll = make_scroll("s4");
        scroll.constraints.push(make_constraint(
            "c1",
            ConstraintType::Must,
            "NonExistentPattern",
        ));
        sqlite.create_scroll(&scroll).await.unwrap();

        let report = morpheus.validate_scroll("s4").await.unwrap();
        assert!(!report.is_valid);
        assert!(!report.constraint_violations.is_empty());
        assert!(!report.constraint_violations[0].passed);
    }

    #[tokio::test]
    async fn test_validate_scroll_with_must_not_satisfied() {
        let (morpheus, sqlite, _kuzu, _tmp) = make_morpheus().await;

        let mut scroll = make_scroll("s5");
        scroll.constraints.push(make_constraint(
            "c1",
            ConstraintType::MustNot,
            "ForbiddenPattern",
        ));
        sqlite.create_scroll(&scroll).await.unwrap();

        let report = morpheus.validate_scroll("s5").await.unwrap();
        // MustNot for a non-existent pattern should pass.
        assert!(
            report.constraint_violations.iter().all(|r| r.passed),
            "MustNot for absent pattern should pass"
        );
    }

    #[tokio::test]
    async fn test_validate_scroll_with_dangling_dependency() {
        let (morpheus, sqlite, _kuzu, _tmp) = make_morpheus().await;

        let scroll = make_scroll("s6");
        sqlite.create_scroll(&scroll).await.unwrap();

        let t1 = make_task("t1", "s6", vec!["nonexistent"]);
        sqlite.create_task(&t1).await.unwrap();

        let report = morpheus.validate_scroll("s6").await.unwrap();
        assert!(!report.is_valid);
    }

    #[tokio::test]
    async fn test_validate_scroll_nonexistent() {
        let (morpheus, _sqlite, _kuzu, _tmp) = make_morpheus().await;
        let result = morpheus.validate_scroll("nonexistent").await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn test_validate_scroll_updates_status_on_success() {
        let (morpheus, sqlite, _kuzu, _tmp) = make_morpheus().await;

        let scroll = make_scroll("s7");
        sqlite.create_scroll(&scroll).await.unwrap();

        let report = morpheus.validate_scroll("s7").await.unwrap();
        assert!(report.is_valid);

        // Check that the scroll status was updated.
        let updated = sqlite.get_scroll("s7").await.unwrap();
        assert_eq!(updated.status, ScrollStatus::Validated);
    }

    // -- Individual pass tests --

    #[tokio::test]
    async fn test_validate_constraints_directly() {
        let (morpheus, _sqlite, _kuzu, _tmp) = make_morpheus().await;

        let constraints = vec![
            make_constraint("c1", ConstraintType::Must, "Missing"),
            make_constraint("c2", ConstraintType::Should, "Advisory"),
        ];

        let results = morpheus.validate_constraints(&constraints).await.unwrap();
        assert_eq!(results.len(), 2);
    }

    #[tokio::test]
    async fn test_check_circular_dependencies_directly() {
        let (morpheus, _sqlite, _kuzu, _tmp) = make_morpheus().await;

        let tasks = vec![
            make_task("t1", "s", vec![]),
            make_task("t2", "s", vec!["t1"]),
        ];

        let results = morpheus
            .check_circular_dependencies(&tasks)
            .await
            .unwrap();
        assert!(results.iter().all(|r| r.passed));
    }

    #[tokio::test]
    async fn test_validate_types_directly() {
        let (morpheus, _sqlite, _kuzu, _tmp) = make_morpheus().await;

        let tasks = vec![make_task("t1", "s", vec![])];
        let results = morpheus.validate_types(&tasks).await.unwrap();
        assert_eq!(results.len(), 1);
        // Create task with no known types should be deferred.
        assert!(results[0].passed);
    }

    #[tokio::test]
    async fn test_detect_breaking_changes_directly() {
        let (morpheus, _sqlite, _kuzu, _tmp) = make_morpheus().await;

        let tasks = vec![make_task("t1", "s", vec![])];
        let results = morpheus.detect_breaking_changes(&tasks).await.unwrap();
        assert_eq!(results.len(), 1);
        assert!(results[0].passed);
    }

    // -- Report summarization tests --

    #[test]
    fn test_report_summarize_all_valid() {
        let report = ValidationReport {
            scroll_id: "s1".into(),
            is_valid: true,
            constraint_violations: vec![],
            circular_dependencies: vec![],
            type_errors: vec![],
            breaking_changes: vec![],
            validated_at: Utc::now(),
        };
        assert_eq!(report.summarize(), "All validations passed");
    }

    #[test]
    fn test_report_summarize_with_errors() {
        let report = ValidationReport {
            scroll_id: "s1".into(),
            is_valid: false,
            constraint_violations: vec![ValidationResult {
                constraint_id: "c1".into(),
                passed: false,
                message: "bad constraint".into(),
                severity: Severity::Error,
            }],
            circular_dependencies: vec![ValidationResult {
                constraint_id: "d1".into(),
                passed: false,
                message: "cycle found".into(),
                severity: Severity::Error,
            }],
            type_errors: vec![],
            breaking_changes: vec![],
            validated_at: Utc::now(),
        };
        let summary = report.summarize();
        assert!(summary.contains("2 errors"));
        assert!(summary.contains("bad constraint"));
        assert!(summary.contains("cycle found"));
    }
}
