use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};

use serde::{Deserialize, Serialize};

use crate::core::graph_index::ProjectIndex;
use crate::core::knowledge::ProjectKnowledge;
use crate::project_metadata;
use crate::server_client;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BootstrapFact {
    pub category: String,
    pub key: String,
    pub value: String,
    pub confidence: f32,
    pub evidence: String,
    pub source_type: String,
    pub source_scope: String,
    pub promotion_identity: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BootstrapPreview {
    pub project_root: String,
    pub stack: Vec<String>,
    pub entrypoints: Vec<String>,
    pub tests: Vec<String>,
    pub infra: Vec<String>,
    pub modules: Vec<String>,
    pub workflow_signals: Vec<String>,
    pub facts: Vec<BootstrapFact>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BootstrapApplyReport {
    pub project_root: String,
    pub stored: usize,
    pub queued_server_promotions: bool,
    pub facts: Vec<BootstrapFact>,
}

pub fn resolve_project_root(path: Option<&str>) -> Result<String, String> {
    let candidate = match path {
        Some(value) => PathBuf::from(value),
        None => std::env::current_dir().map_err(|e| format!("cannot resolve current directory: {e}"))?,
    };

    candidate
        .canonicalize()
        .map(|path| path.to_string_lossy().to_string())
        .map_err(|e| format!("cannot resolve project root {}: {e}", candidate.display()))
}

pub fn build_preview(project_root: &str) -> Result<BootstrapPreview, String> {
    let root = Path::new(project_root);
    let metadata = project_metadata::build_project_metadata(root).map_err(|e| e.to_string())?;
    let index = Some(crate::core::graph_index::scan(project_root));

    let stack = detect_stack(&metadata.summary.markers, &metadata.summary.languages);
    let entrypoints = detect_entrypoints(root, index.as_ref());
    let tests = detect_tests(root, index.as_ref());
    let infra = detect_infra(&metadata.summary.markers, root);
    let modules = detect_modules(index.as_ref(), root);
    let workflow_signals = detect_workflow_signals(&metadata.summary.markers, root);
    let facts = build_fact_candidates(
        project_root,
        &stack,
        &entrypoints,
        &tests,
        &infra,
        &modules,
        &workflow_signals,
        &metadata.summary.markers,
    );

    Ok(BootstrapPreview {
        project_root: project_root.to_string(),
        stack,
        entrypoints,
        tests,
        infra,
        modules,
        workflow_signals,
        facts,
    })
}

pub fn apply_preview(
    project_root: &str,
    preview: &BootstrapPreview,
) -> Result<BootstrapApplyReport, String> {
    let session_id = crate::core::session::SessionState::load_latest_for_project_root(project_root)
        .map(|session| session.id)
        .unwrap_or_else(|| "project-bootstrap".to_string());

    let mut knowledge = ProjectKnowledge::load_or_create(project_root);
    for fact in &preview.facts {
        let _ = knowledge.remember(
            &fact.category,
            &fact.key,
            &fact.value,
            &session_id,
            fact.confidence,
        );
    }
    let _ = knowledge.run_memory_lifecycle();
    knowledge.save()?;

    let items: Vec<serde_json::Value> = preview
        .facts
        .iter()
        .map(|fact| {
            serde_json::json!({
                "category": fact.category,
                "key": fact.key,
                "value": fact.value,
                "confidence": fact.confidence,
                "source_type": fact.source_type,
                "source_scope": fact.source_scope,
                "promotion_identity": fact.promotion_identity,
            })
        })
        .collect();

    let mut queued_server_promotions = false;
    if !items.is_empty() {
        let ctx = crate::git_context::discover_project_context(Path::new(project_root));
        let mut args = serde_json::Map::new();
        args.insert("action".to_string(), serde_json::Value::String("promote".to_string()));
        args.insert("items".to_string(), serde_json::Value::Array(items));
        queued_server_promotions =
            server_client::queue_or_call_tool("ctx_knowledge", args, &ctx).is_ok();
    }

    Ok(BootstrapApplyReport {
        project_root: project_root.to_string(),
        stored: preview.facts.len(),
        queued_server_promotions,
        facts: preview.facts.clone(),
    })
}

pub fn format_preview(preview: &BootstrapPreview) -> String {
    let mut lines = vec![
        format!("PROJECT BOOTSTRAP PREVIEW  {}", preview.project_root),
        String::new(),
    ];

    push_section(&mut lines, "STACK", &preview.stack);
    push_section(&mut lines, "ENTRYPOINTS", &preview.entrypoints);
    push_section(&mut lines, "TESTS", &preview.tests);
    push_section(&mut lines, "INFRA", &preview.infra);
    push_section(&mut lines, "MODULES", &preview.modules);
    push_section(&mut lines, "WORKFLOW SIGNALS", &preview.workflow_signals);

    lines.push("CANDIDATE FACTS:".to_string());
    for fact in &preview.facts {
        lines.push(format!(
            "  - [{}/{}] {}  ({:.0}% | {})",
            fact.category,
            fact.key,
            fact.value,
            fact.confidence * 100.0,
            fact.evidence
        ));
    }
    lines.push(String::new());
    lines.push(
        "Preview only. Nothing stored yet. Apply with: nebu-ctx project-bootstrap apply"
            .to_string(),
    );

    lines.join("\n")
}

pub fn format_apply(report: &BootstrapApplyReport) -> String {
    format!(
        "Stored {} bootstrap facts for {}. Server promotion queued: {}.",
        report.stored,
        report.project_root,
        if report.queued_server_promotions {
            "yes"
        } else {
            "no"
        }
    )
}

fn push_section(lines: &mut Vec<String>, title: &str, items: &[String]) {
    lines.push(format!("{title}:"));
    if items.is_empty() {
        lines.push("  - none detected".to_string());
    } else {
        for item in items {
            lines.push(format!("  - {item}"));
        }
    }
    lines.push(String::new());
}

fn detect_stack(
    markers: &[String],
    languages: &[crate::models::ProjectLanguageStat],
) -> Vec<String> {
    let mut stack = Vec::new();
    let language_names: Vec<String> = languages
        .iter()
        .map(|language| format!("{}({})", language.language, language.file_count))
        .collect();
    if !language_names.is_empty() {
        stack.push(format!("languages: {}", language_names.join(", ")));
    }
    if markers.iter().any(|marker| marker == "Cargo.toml") {
        stack.push("build: cargo / rust".to_string());
    }
    if markers.iter().any(|marker| marker == "package.json") {
        stack.push("app: node package.json".to_string());
    }
    if markers
        .iter()
        .any(|marker| marker == "pyproject.toml" || marker == "requirements.txt")
    {
        stack.push("app: python project".to_string());
    }
    if markers.iter().any(|marker| marker.ends_with(".slnx")) {
        stack.push("app: .NET solution".to_string());
    }
    stack
}

fn detect_entrypoints(root: &Path, index: Option<&ProjectIndex>) -> Vec<String> {
    let mut entrypoints = BTreeSet::new();
    for rel in [
        "src/main.rs",
        "src/lib.rs",
        "src/index.ts",
        "src/main.ts",
        "src/main.tsx",
        "src/index.js",
        "src/main.py",
        "main.py",
        "Program.cs",
        "server/src/NebuCtx.Server.Host/Program.cs",
    ] {
        if root.join(rel).exists() {
            entrypoints.insert(rel.to_string());
        }
    }

    // Detect root-level script files common in small automation repos (Apps Script, Node helpers).
    if let Ok(entries) = std::fs::read_dir(root) {
        for entry in entries.flatten() {
            let path = entry.path();
            if !path.is_file() {
                continue;
            }
            let ext = path
                .extension()
                .and_then(|e| e.to_str())
                .unwrap_or("")
                .to_ascii_lowercase();
            let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
            if matches!(ext.as_str(), "gs" | "js" | "ts" | "py" | "sh")
                && !name.starts_with('.')
            {
                entrypoints.insert(name.to_string());
            }
        }
    }

    if let Some(index) = index {
        for file in index.files.values() {
            if file.exports.iter().any(|export| export == "main") {
                let rel = relativize(root, &file.path);
                if !rel.contains("node_modules") {
                    entrypoints.insert(rel);
                }
            }
        }
    }

    entrypoints.into_iter().collect()
}

fn detect_tests(root: &Path, index: Option<&ProjectIndex>) -> Vec<String> {
    let mut tests = BTreeSet::new();
    for rel in ["tests", "server/tests", "client/tests", ".github/workflows"] {
        if root.join(rel).exists() {
            tests.insert(rel.to_string());
        }
    }
    if let Some(index) = index {
        for file in index.files.values() {
            let rel = relativize(root, &file.path);
            if rel.contains("node_modules") {
                continue;
            }
            if rel.contains("test") || rel.contains("spec") {
                tests.insert(rel);
            }
        }
    }
    tests.into_iter().take(8).collect()
}

fn detect_infra(markers: &[String], root: &Path) -> Vec<String> {
    let mut infra = BTreeSet::new();
    for marker in markers {
        if matches!(marker.as_str(), "compose.yaml" | "docker-compose.yml") {
            infra.insert(format!("container: {marker}"));
        }
    }
    for rel in ["Dockerfile", "docker-entrypoint.sh", ".github/workflows", "homeassistant"] {
        if root.join(rel).exists() {
            infra.insert(rel.to_string());
        }
    }
    infra.into_iter().collect()
}

fn detect_modules(index: Option<&ProjectIndex>, root: &Path) -> Vec<String> {
    if let Some(index) = index {
        let mut by_dir = BTreeMap::<String, usize>::new();
        for file in index.files.values() {
            let rel = relativize(root, &file.path);
            // Exclude vendor/generated directories from module surface reporting.
            if rel.starts_with("node_modules") || rel.contains("/node_modules/") {
                continue;
            }
            let dir = Path::new(&rel)
                .parent()
                .map(|path| path.to_string_lossy().to_string())
                .unwrap_or_else(|| ".".to_string());
            *by_dir.entry(dir).or_default() += 1;
        }

        return by_dir
            .into_iter()
            .filter(|(dir, count)| dir != "." && *count >= 2)
            .map(|(dir, count)| format!("{dir} ({count} files)"))
            .take(8)
            .collect();
    }

    let mut modules = Vec::new();
    for rel in ["client/src", "server/src", "tests", "docs", "homeassistant"] {
        if root.join(rel).exists() {
            modules.push(rel.to_string());
        }
    }
    modules
}

fn detect_workflow_signals(markers: &[String], root: &Path) -> Vec<String> {
    let mut signals = BTreeSet::new();
    if markers.iter().any(|marker| marker == "Cargo.toml") {
        signals.insert("rust workflow present".to_string());
    }
    if markers.iter().any(|marker| marker.ends_with(".slnx")) {
        signals.insert("dotnet workflow present".to_string());
    }
    if root.join("tests").exists() {
        signals.insert("repo-level tests directory".to_string());
    }
    if root.join(".github/workflows").exists() {
        signals.insert("github actions configured".to_string());
    }
    if root.join("client/assets/skills").exists() {
        signals.insert("agent skill assets in repo".to_string());
    }
    signals.into_iter().collect()
}

fn build_fact_candidates(
    project_root: &str,
    stack: &[String],
    entrypoints: &[String],
    tests: &[String],
    infra: &[String],
    modules: &[String],
    workflow_signals: &[String],
    markers: &[String],
) -> Vec<BootstrapFact> {
    let source_scope = format!("project-bootstrap:{project_root}");
    let mut facts = Vec::new();

    if !stack.is_empty() {
        facts.push(make_fact(
            "architecture",
            "stack",
            &stack.join("; "),
            0.95,
            "derived from project markers and languages",
            &source_scope,
        ));
    }
    if !entrypoints.is_empty() {
        facts.push(make_fact(
            "architecture",
            "entrypoints",
            &entrypoints.join(", "),
            0.9,
            "derived from common entrypoint files and index exports",
            &source_scope,
        ));
    }
    if !tests.is_empty() {
        facts.push(make_fact(
            "testing",
            "test-surfaces",
            &tests.join(", "),
            0.85,
            "derived from test directories and indexed file names",
            &source_scope,
        ));
    }
    if !infra.is_empty() {
        facts.push(make_fact(
            "deployment",
            "infra-surfaces",
            &infra.join(", "),
            0.85,
            "derived from infrastructure markers",
            &source_scope,
        ));
    }
    if !modules.is_empty() {
        facts.push(make_fact(
            "architecture",
            "modules",
            &modules.join(", "),
            0.8,
            "derived from indexed directories and module roots",
            &source_scope,
        ));
    }
    if !workflow_signals.is_empty() {
        facts.push(make_fact(
            "conventions",
            "workflow-signals",
            &workflow_signals.join(", "),
            0.8,
            "derived from repository automation and workflow files",
            &source_scope,
        ));
    }
    if !markers.is_empty() {
        facts.push(make_fact(
            "conventions",
            "project-markers",
            &markers.join(", "),
            0.75,
            "derived from marker files at repository root",
            &source_scope,
        ));
    }

    facts
}

fn make_fact(
    category: &str,
    key: &str,
    value: &str,
    confidence: f32,
    evidence: &str,
    source_scope: &str,
) -> BootstrapFact {
    BootstrapFact {
        category: category.to_string(),
        key: key.to_string(),
        value: value.to_string(),
        confidence,
        evidence: evidence.to_string(),
        source_type: "project_bootstrap".to_string(),
        source_scope: source_scope.to_string(),
        promotion_identity: server_client::deterministic_promotion_identity(
            "project_bootstrap",
            source_scope,
            category,
            key,
        ),
    }
}

fn relativize(root: &Path, path: &str) -> String {
    let path = Path::new(path);
    path.strip_prefix(root)
        .map(|rel| rel.to_string_lossy().to_string())
        .unwrap_or_else(|_| path.to_string_lossy().to_string())
}

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

    #[test]
    fn preview_detects_basic_stack_and_candidates() {
        let _lock = crate::core::data_dir::test_env_lock();
        let tmp = tempfile::tempdir().unwrap();
        let root = tmp.path();
        std::fs::write(root.join("Cargo.toml"), "[package]\nname='demo'\n").unwrap();
        std::fs::create_dir_all(root.join("src")).unwrap();
        std::fs::create_dir_all(root.join("tests")).unwrap();
        std::fs::write(root.join("src/main.rs"), "fn main() {}\n").unwrap();
        std::fs::write(root.join("Dockerfile"), "FROM scratch\n").unwrap();

        let preview = build_preview(&root.to_string_lossy()).unwrap();
        assert!(preview.stack.iter().any(|line| line.contains("cargo / rust")));
        assert!(preview.entrypoints.iter().any(|line| line == "src/main.rs"));
        assert!(preview.tests.iter().any(|line| line.contains("tests")));
        assert!(preview.infra.iter().any(|line| line.contains("Dockerfile")));
        assert!(preview.facts.iter().any(|fact| fact.key == "stack"));
    }

    #[test]
    fn apply_preview_stores_local_knowledge_and_queues_server_call() {
        let _lock = crate::core::data_dir::test_env_lock();
        let tmp = tempfile::tempdir().unwrap();
        std::env::set_var("NEBU_CTX_DATA_DIR", tmp.path().join("data"));
        std::env::set_var("NEBU_CTX_HOME", tmp.path().join("home"));
        let root = tmp.path().join("project");
        std::fs::create_dir_all(&root).unwrap();
        std::fs::write(root.join("Cargo.toml"), "[package]\nname='demo'\n").unwrap();

        let preview = BootstrapPreview {
            project_root: root.to_string_lossy().to_string(),
            stack: vec!["build: cargo / rust".to_string()],
            entrypoints: vec!["src/main.rs".to_string()],
            tests: vec![],
            infra: vec![],
            modules: vec![],
            workflow_signals: vec![],
            facts: vec![make_fact(
                "architecture",
                "stack",
                "build: cargo / rust",
                0.95,
                "derived from markers",
                &format!("project-bootstrap:{}", root.to_string_lossy()),
            )],
        };

        let report = apply_preview(&root.to_string_lossy(), &preview).unwrap();
        assert_eq!(report.stored, 1);

        let stored = ProjectKnowledge::load(&root.to_string_lossy()).unwrap();
        assert!(stored
            .facts
            .iter()
            .any(|fact| fact.key == "stack" && fact.value.contains("cargo")));

        let entry = crate::core::sync_outbox::load_entries()
            .unwrap()
            .into_iter()
            .find(|item| item.kind == crate::core::sync_outbox::OutboxOperationKind::ServerToolCall)
            .unwrap();
        assert_eq!(entry.payload["tool_name"], "ctx_knowledge");
        assert_eq!(entry.payload["arguments"]["action"], "promote");
    }

    #[test]
    fn preview_refreshes_stale_index_paths() {
        let _lock = crate::core::data_dir::test_env_lock();
        let tmp = tempfile::tempdir().unwrap();
        let data_dir = tmp.path().join("data");
        std::fs::create_dir_all(&data_dir).unwrap();
        std::env::set_var("NEBU_CTX_DATA_DIR", &data_dir);

        let root = tmp.path().join("project");
        std::fs::create_dir_all(root.join("client/assets/skills/project-bootstrap/scripts")).unwrap();
        std::fs::create_dir_all(root.join("skills/project-bootstrap/scripts")).unwrap();
        std::fs::create_dir_all(root.join("src")).unwrap();
        std::fs::write(root.join("Cargo.toml"), "[package]\nname='demo'\n").unwrap();
        std::fs::write(root.join("src/main.rs"), "fn main() {}\n").unwrap();
        std::fs::write(root.join("client/assets/skills/project-bootstrap/scripts/install.sh"), "#!/bin/sh\n").unwrap();
        std::fs::write(root.join("skills/project-bootstrap/scripts/install.sh"), "#!/bin/sh\n").unwrap();

        let project_root = root.to_string_lossy().to_string();
        let index_dir = crate::core::graph_index::ProjectIndex::index_dir(&project_root).unwrap();
        std::fs::create_dir_all(&index_dir).unwrap();
        let stale_index = serde_json::json!({
            "version": 6,
            "project_root": project_root,
            "last_scan": "now",
            "files": {
                "client/assets/skills/nebu-ctx/scripts/install.sh": {
                    "path": "client/assets/skills/nebu-ctx/scripts/install.sh",
                    "hash": "stale",
                    "language": "sh",
                    "line_count": 1,
                    "token_count": 1,
                    "exports": ["main"],
                    "summary": "stale"
                },
                "skills/nebu-ctx/scripts/install.sh": {
                    "path": "skills/nebu-ctx/scripts/install.sh",
                    "hash": "stale",
                    "language": "sh",
                    "line_count": 1,
                    "token_count": 1,
                    "exports": ["main"],
                    "summary": "stale"
                }
            },
            "edges": [],
            "symbols": {}
        });
        std::fs::write(index_dir.join("index.json"), serde_json::to_string(&stale_index).unwrap()).unwrap();

        let preview = build_preview(&root.to_string_lossy()).unwrap();
        let joined_entrypoints = preview.entrypoints.join("\n");
        assert!(joined_entrypoints.contains("src/main.rs"));
        assert!(!joined_entrypoints.contains("client/assets/skills/nebu-ctx/scripts/install.sh"));
        assert!(!joined_entrypoints.contains("skills/nebu-ctx/scripts/install.sh"));

        std::env::remove_var("NEBU_CTX_DATA_DIR");
    }
}
