// Composition root — wires infra instances to usecase functions.
// Each function opens DB/VCS and delegates to the corresponding usecase.

use std::path::Path;

use anyhow::Result;

use astmap_core::diff::{DiffResult, SinceResult};
use astmap_core::model::{Config, SymbolResult};
use astmap_core::ScanResult;
use astmap_languages::DefaultLanguageRegistry;

use astmap_core::diff::DigestResult;
use astmap_core::model::{ImpactResult, InspectResult, WhyResult};

use crate::infra::discovery::IgnoreWalkDiscovery;
use crate::usecase;

// ── Operation functions ──

pub fn scan(project_dir: &Path, full: bool) -> Result<ScanResult> {
    let db = crate::infra::sqlite::open(project_dir)?;
    usecase::scan::scan(
        project_dir,
        full,
        db.as_ref(),
        &DefaultLanguageRegistry,
        &IgnoreWalkDiscovery,
    )
}

pub fn query(project_dir: &Path, pattern: &str, limit: i64) -> Result<Vec<SymbolResult>> {
    let db = crate::infra::sqlite::open_existing(project_dir)?;
    usecase::query::query(db.as_ref(), pattern, limit)
}

pub fn inspect(project_dir: &Path, symbol: &str) -> Result<InspectResult> {
    let db = crate::infra::sqlite::open_existing(project_dir)?;
    usecase::inspect::inspect(db.as_ref(), symbol)
}

pub fn impact(project_dir: &Path, symbol: &str, depth: i64) -> Result<ImpactResult> {
    let db = crate::infra::sqlite::open_existing(project_dir)?;
    usecase::impact::impact(db.as_ref(), symbol, depth)
}

pub fn why_path(project_dir: &Path, from: &str, to: &str, depth: i64) -> Result<WhyResult> {
    let db = crate::infra::sqlite::open_existing(project_dir)?;
    usecase::why_path::why_path(db.as_ref(), from, to, depth)
}

pub fn diff(project_dir: &Path, base: Option<&str>, depth: i64) -> Result<DiffResult> {
    let db = crate::infra::sqlite::open_existing(project_dir)?;
    let vcs = crate::infra::git::Git2VcsProvider::open(project_dir)?;
    let lang = DefaultLanguageRegistry;
    usecase::diff::diff(db.as_ref(), &vcs, &lang, base, depth)
}

pub fn since(project_dir: &Path, reference: &str, depth: i64) -> Result<SinceResult> {
    let db = crate::infra::sqlite::open(project_dir)?;
    let vcs = crate::infra::git::Git2VcsProvider::open(project_dir)?;
    let lang = DefaultLanguageRegistry;
    let ctx = usecase::VcsContext {
        scan_store: db.as_ref(),
        diff_store: db.as_ref(),
        vcs: &vcs,
        lang: &lang,
        discovery: &IgnoreWalkDiscovery,
    };
    usecase::since::since(project_dir, reference, depth, &ctx)
}

pub fn digest(project_dir: &Path, depth: i64) -> Result<DigestResult> {
    let db = crate::infra::sqlite::open(project_dir)?;
    let vcs = crate::infra::git::Git2VcsProvider::open(project_dir)?;
    let ctx = usecase::VcsContext {
        scan_store: db.as_ref(),
        diff_store: db.as_ref(),
        vcs: &vcs,
        lang: &DefaultLanguageRegistry,
        discovery: &IgnoreWalkDiscovery,
    };
    usecase::digest::digest(project_dir, depth, &ctx)
}

pub fn export_md(
    project_dir: &Path,
    file: Option<&str>,
    token_budget: Option<i64>,
) -> Result<String> {
    let db = crate::infra::sqlite::open_existing(project_dir)?;
    usecase::export::export_md(db.as_ref(), file, token_budget)
}

pub fn config_get(project_dir: &Path, key: &str) -> Result<Option<String>> {
    let db = crate::infra::sqlite::open(project_dir)?;
    usecase::config::config_get(db.as_ref(), key)
}

pub fn config_set(project_dir: &Path, key: &str, value: &str) -> Result<()> {
    let db = crate::infra::sqlite::open(project_dir)?;
    usecase::config::config_set(db.as_ref(), key, value)
}

pub fn config_list(project_dir: &Path) -> Result<Vec<Config>> {
    let db = crate::infra::sqlite::open(project_dir)?;
    usecase::config::config_list(db.as_ref())
}

pub fn watch(project_dir: &Path) -> Result<()> {
    let db = crate::infra::sqlite::open(project_dir)?;
    crate::infra::watcher::watch_and_scan(project_dir, db.as_ref())?;
    Ok(())
}

// ── Daemon operations (pure infra, no usecase layer) ──

pub async fn daemon_start(project_dir: &Path) -> Result<()> {
    crate::infra::daemon::start(project_dir).await?;
    Ok(())
}

pub fn daemon_stop(project_dir: &Path) -> Result<()> {
    crate::infra::daemon::stop(project_dir)?;
    Ok(())
}

pub fn daemon_status(project_dir: &Path) -> crate::infra::daemon::DaemonStatus {
    crate::infra::daemon::status(project_dir)
}

pub async fn subscribe(
    project_dir: &Path,
    filter: Option<&str>,
    json: bool,
    pretty: bool,
) -> Result<()> {
    use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
    use tokio::net::UnixStream;

    let sock = crate::infra::daemon::socket_path(project_dir);
    if !sock.exists() {
        anyhow::bail!("Daemon not running. Start it with: astmap daemon start");
    }

    let stream = UnixStream::connect(&sock).await?;
    let (reader, mut writer) = stream.into_split();
    let mut reader = BufReader::new(reader);

    // Read connected event
    let mut line = String::new();
    reader.read_line(&mut line).await?;

    // Send subscribe command (use serde_json to properly escape filter)
    let cmd = match filter {
        Some(f) => serde_json::json!({"command": "subscribe", "filter": f}).to_string(),
        None => serde_json::json!({"command": "subscribe"}).to_string(),
    };
    let cmd = format!("{cmd}\n");
    writer.write_all(cmd.as_bytes()).await?;

    // Read subscribed confirmation
    let mut line = String::new();
    reader.read_line(&mut line).await?;

    let use_json = json || (!pretty && !std::io::IsTerminal::is_terminal(&std::io::stdout()));

    // Stream events
    loop {
        let mut line = String::new();
        match reader.read_line(&mut line).await {
            Ok(0) => {
                eprintln!("Daemon disconnected.");
                break;
            }
            Ok(_) => {
                let trimmed = line.trim();
                if trimmed.is_empty() {
                    continue;
                }
                if use_json {
                    println!("{trimmed}");
                } else {
                    // Pretty-print: parse and format
                    if let Ok(evt) = serde_json::from_str::<serde_json::Value>(trimmed) {
                        print_pretty_event(&evt);
                    } else {
                        println!("{trimmed}");
                    }
                }
            }
            Err(e) => {
                eprintln!("Read error: {e}");
                break;
            }
        }
    }
    Ok(())
}

fn print_pretty_event(evt: &serde_json::Value) {
    let event_type = evt["event"].as_str().unwrap_or("unknown");

    match event_type {
        "impact.changed" => {
            let files = evt["trigger_files"]
                .as_array()
                .map(|a| {
                    a.iter()
                        .filter_map(|v| v.as_str())
                        .collect::<Vec<_>>()
                        .join(", ")
                })
                .unwrap_or_default();
            let seq = evt["seq"].as_u64().unwrap_or(0);
            println!("[{seq}] impact.changed: {files}");

            if let Some(symbols) = evt["changed_symbols"].as_array() {
                for sym in symbols {
                    let ct = sym["change_type"].as_str().unwrap_or("?");
                    let name = sym["name"].as_str().unwrap_or("?");
                    let kind = sym["kind"].as_str().unwrap_or("?");
                    let fp = sym["file_path"].as_str().unwrap_or("?");
                    let line = sym["line"].as_u64().unwrap_or(0);
                    let marker = match ct {
                        "added" => "+",
                        "modified" => "~",
                        "removed" => "-",
                        _ => "?",
                    };
                    println!("  {marker} {name} {kind} {fp}:{line}");
                }
            }

            if let Some(impacts) = evt["downstream_impacts"].as_array() {
                if !impacts.is_empty() {
                    println!("  downstream:");
                    for imp in impacts {
                        let name = imp["symbol_name"].as_str().unwrap_or("?");
                        let fp = imp["file_path"].as_str().unwrap_or("?");
                        let depth = imp["depth"].as_u64().unwrap_or(0);
                        println!("    -> {name} ({fp}, depth={depth})");
                    }
                }
            }
        }
        "index.rebuilt" => {
            let seq = evt["seq"].as_u64().unwrap_or(0);
            let files = evt["trigger_files"].as_array().map_or(0, |a| a.len());
            println!("[{seq}] index.rebuilt ({files} files changed)");
        }
        _ => {
            // index_updated or other events
            if let Some(data) = evt.get("data") {
                println!("{event_type}: {data}");
            } else {
                println!("{event_type}");
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::usecase::{query_symbols, resolve_symbol, resolve_symbol_id};
    use std::fs;

    fn setup_scanned_project() -> tempfile::TempDir {
        let dir = tempfile::tempdir().unwrap();
        let src = dir.path().join("src");
        fs::create_dir_all(&src).unwrap();
        fs::write(src.join("lib.rs"), "pub fn hello() {}\npub fn world() {}\n").unwrap();
        let db = crate::infra::sqlite::open(dir.path()).unwrap();
        astmap_core::scanner::scan(
            dir.path(),
            db.as_ref(),
            false,
            &DefaultLanguageRegistry,
            &IgnoreWalkDiscovery,
        )
        .unwrap();
        dir
    }

    #[test]
    fn test_resolve_symbol_by_name() {
        let dir = setup_scanned_project();
        let db = crate::infra::sqlite::open(dir.path()).unwrap();
        let sym = resolve_symbol(db.as_ref(), "hello").unwrap();
        assert!(sym.is_some());
        assert_eq!(sym.unwrap().name, "hello");
    }

    #[test]
    fn test_resolve_symbol_by_id() {
        let dir = setup_scanned_project();
        let db = crate::infra::sqlite::open(dir.path()).unwrap();
        let sym = resolve_symbol(db.as_ref(), "hello").unwrap().unwrap();
        let id_str = sym.id.to_string();
        let sym2 = resolve_symbol(db.as_ref(), &id_str).unwrap();
        assert!(sym2.is_some());
        assert_eq!(sym2.unwrap().name, "hello");
    }

    #[test]
    fn test_resolve_symbol_not_found() {
        let dir = setup_scanned_project();
        let db = crate::infra::sqlite::open(dir.path()).unwrap();
        let sym = resolve_symbol(db.as_ref(), "nonexistent").unwrap();
        assert!(sym.is_none());
    }

    #[test]
    fn test_query_symbols_fts_fallback() {
        let dir = setup_scanned_project();
        let db = crate::infra::sqlite::open(dir.path()).unwrap();
        let results = query_symbols(db.as_ref(), "hello", 10).unwrap();
        assert!(!results.is_empty());
        assert_eq!(results[0].name, "hello");
    }

    #[test]
    fn test_scan_operation() {
        let dir = tempfile::tempdir().unwrap();
        let src = dir.path().join("src");
        fs::create_dir_all(&src).unwrap();
        fs::write(src.join("main.rs"), "fn main() {}").unwrap();
        let result = scan(dir.path(), false).unwrap();
        assert!(result.symbols > 0);
    }

    #[test]
    fn test_query_operation() {
        let dir = setup_scanned_project();
        let results = query(dir.path(), "hello", 10).unwrap();
        assert!(!results.is_empty());
    }

    #[test]
    fn test_inspect_operation() {
        let dir = setup_scanned_project();
        let result = inspect(dir.path(), "hello").unwrap();
        assert_eq!(result.symbol.name, "hello");
    }

    #[test]
    fn test_inspect_not_found() {
        let dir = setup_scanned_project();
        let result = inspect(dir.path(), "nonexistent");
        assert!(result.is_err());
    }

    #[test]
    fn test_impact_operation() {
        let dir = setup_scanned_project();
        let result = impact(dir.path(), "hello", 3).unwrap();
        assert!(result.symbol_id > 0);
    }

    #[test]
    fn test_impact_not_found() {
        let dir = setup_scanned_project();
        let result = impact(dir.path(), "nonexistent", 3);
        assert!(result.is_err());
    }

    #[test]
    fn test_why_path_operation() {
        let dir = tempfile::tempdir().unwrap();
        let src = dir.path().join("src");
        fs::create_dir_all(&src).unwrap();
        fs::write(
            src.join("main.rs"),
            "fn main() { helper(); }\nfn helper() {}\n",
        )
        .unwrap();
        let _ = scan(dir.path(), true).unwrap();
        let result = why_path(dir.path(), "main", "helper", 5).unwrap();
        assert_eq!(result.from_name, "main");
        assert_eq!(result.to_name, "helper");
    }

    #[test]
    fn test_export_operation() {
        let dir = setup_scanned_project();
        let md = export_md(dir.path(), None, None).unwrap();
        assert!(md.contains("Code Structure Index"));
    }

    #[test]
    fn test_config_operations() {
        let dir = setup_scanned_project();
        config_set(dir.path(), "test.key", "val").unwrap();
        let val = config_get(dir.path(), "test.key").unwrap();
        assert_eq!(val, Some("val".to_string()));
        let list = config_list(dir.path()).unwrap();
        assert!(!list.is_empty());
    }

    // ── resolve_symbol_id tests ──

    #[test]
    fn test_resolve_symbol_id_by_name() {
        let dir = setup_scanned_project();
        let db = crate::infra::sqlite::open(dir.path()).unwrap();
        let id = resolve_symbol_id(db.as_ref(), "hello").unwrap();
        assert!(id > 0);
    }

    #[test]
    fn test_resolve_symbol_id_by_numeric_id() {
        let dir = setup_scanned_project();
        let db = crate::infra::sqlite::open(dir.path()).unwrap();
        let id = resolve_symbol_id(db.as_ref(), "hello").unwrap();
        let id2 = resolve_symbol_id(db.as_ref(), &id.to_string()).unwrap();
        assert_eq!(id, id2);
    }

    #[test]
    fn test_resolve_symbol_id_not_found_by_name() {
        let dir = setup_scanned_project();
        let db = crate::infra::sqlite::open(dir.path()).unwrap();
        let result = resolve_symbol_id(db.as_ref(), "nonexistent");
        assert!(result.is_err());
        let err_msg = result.unwrap_err().to_string();
        assert!(err_msg.contains("symbol not found"));
    }

    #[test]
    fn test_resolve_symbol_id_not_found_by_numeric_id() {
        let dir = setup_scanned_project();
        let db = crate::infra::sqlite::open(dir.path()).unwrap();
        let result = resolve_symbol_id(db.as_ref(), "99999");
        assert!(result.is_err());
        let err_msg = result.unwrap_err().to_string();
        assert!(err_msg.contains("symbol not found"));
    }

    // ── Git-dependent operation tests ──

    fn setup_git_project() -> tempfile::TempDir {
        let dir = tempfile::tempdir().unwrap();
        // Initialize git repo
        std::process::Command::new("git")
            .args(["init"])
            .current_dir(dir.path())
            .output()
            .unwrap();
        std::process::Command::new("git")
            .args(["config", "user.email", "test@test.com"])
            .current_dir(dir.path())
            .output()
            .unwrap();
        std::process::Command::new("git")
            .args(["config", "user.name", "Test"])
            .current_dir(dir.path())
            .output()
            .unwrap();

        let src = dir.path().join("src");
        fs::create_dir_all(&src).unwrap();
        fs::write(src.join("lib.rs"), "pub fn hello() {}\n").unwrap();

        // Initial commit
        std::process::Command::new("git")
            .args(["add", "."])
            .current_dir(dir.path())
            .output()
            .unwrap();
        std::process::Command::new("git")
            .args(["commit", "-m", "initial"])
            .current_dir(dir.path())
            .output()
            .unwrap();

        // Scan after initial commit
        scan(dir.path(), true).unwrap();
        dir
    }

    #[test]
    fn test_diff_working_tree() {
        let dir = setup_git_project();
        // Modify a file (uncommitted change)
        let src = dir.path().join("src");
        fs::write(src.join("lib.rs"), "pub fn hello() {}\npub fn added() {}\n").unwrap();

        let result = diff(dir.path(), None, 3).unwrap();
        // Should detect the uncommitted change
        assert!(!result.changed_files.is_empty());
    }

    #[test]
    fn test_diff_against_base() {
        let dir = setup_git_project();
        // Create a new commit with changes
        let src = dir.path().join("src");
        fs::write(src.join("lib.rs"), "pub fn hello() {}\npub fn world() {}\n").unwrap();
        std::process::Command::new("git")
            .args(["add", "."])
            .current_dir(dir.path())
            .output()
            .unwrap();
        std::process::Command::new("git")
            .args(["commit", "-m", "add world"])
            .current_dir(dir.path())
            .output()
            .unwrap();

        // Re-scan to pick up changes
        scan(dir.path(), true).unwrap();

        let result = diff(dir.path(), Some("HEAD~1"), 3).unwrap();
        assert!(!result.changed_files.is_empty());
    }

    #[test]
    fn test_since_operation() {
        let dir = setup_git_project();
        // Add a new commit
        let src = dir.path().join("src");
        fs::write(
            src.join("lib.rs"),
            "pub fn hello() {}\npub fn new_fn() {}\n",
        )
        .unwrap();
        std::process::Command::new("git")
            .args(["add", "."])
            .current_dir(dir.path())
            .output()
            .unwrap();
        std::process::Command::new("git")
            .args(["commit", "-m", "add new_fn"])
            .current_dir(dir.path())
            .output()
            .unwrap();

        let result = since(dir.path(), "HEAD~1", 3).unwrap();
        let has_changes =
            !result.added.is_empty() || !result.modified.is_empty() || !result.removed.is_empty();
        assert!(has_changes, "should detect changes since HEAD~1");
    }

    #[test]
    fn test_digest_operation() {
        let dir = setup_git_project();
        // digest should work even with no changes since last anchor
        let result = digest(dir.path(), 3).unwrap();
        // DigestResult has since and uncommitted fields
        // DigestResult.since is a SinceResult — just verify it doesn't panic
        let _ref = &result.since.reference;
        let _summary = &result.since.summary;
    }

    #[test]
    fn test_diff_no_changes() {
        let dir = setup_git_project();
        // No modifications — diff should return empty changed_files
        let result = diff(dir.path(), None, 3).unwrap();
        assert!(
            result.changed_files.is_empty(),
            "no uncommitted changes expected"
        );
    }

    // ── Scan edge cases ──

    #[test]
    fn test_scan_full_mode() {
        let dir = tempfile::tempdir().unwrap();
        let src = dir.path().join("src");
        fs::create_dir_all(&src).unwrap();
        fs::write(src.join("main.rs"), "fn main() {}").unwrap();
        let result1 = scan(dir.path(), false).unwrap();
        // Full scan should re-parse everything
        let result2 = scan(dir.path(), true).unwrap();
        assert_eq!(result1.symbols, result2.symbols);
    }

    #[test]
    fn test_export_with_file_filter() {
        let dir = setup_scanned_project();
        let md = export_md(dir.path(), Some("lib.rs"), None).unwrap();
        assert!(md.contains("hello") || md.contains("lib.rs"));
    }

    #[test]
    fn test_export_with_token_budget() {
        let dir = setup_scanned_project();
        let md = export_md(dir.path(), None, Some(100)).unwrap();
        // Should produce output (possibly truncated)
        assert!(!md.is_empty());
    }

    #[test]
    fn test_daemon_status_not_running() {
        let dir = tempfile::tempdir().unwrap();
        let status = daemon_status(dir.path());
        assert!(!status.running);
    }

    #[test]
    fn test_query_empty_db() {
        let dir = tempfile::tempdir().unwrap();
        // Create DB but don't scan anything
        let _db = crate::infra::sqlite::open(dir.path()).unwrap();
        let results = query(dir.path(), "anything", 10).unwrap();
        assert!(results.is_empty());
    }

    #[test]
    fn test_why_path_not_found() {
        let dir = setup_scanned_project();
        let result = why_path(dir.path(), "nonexistent", "hello", 5);
        assert!(result.is_err());
    }
}
