// Usecase layer: pure business logic orchestration.
// Each submodule receives trait objects — no direct infra access.

pub mod config;
pub mod diff;
pub mod digest;
pub mod export;
pub mod impact;
pub mod inspect;
pub mod query;
pub mod scan;
pub mod since;
pub mod why_path;

use anyhow::{bail, Result};

use astmap_core::model::{Symbol, SymbolResult};
use astmap_core::{DbError, QueryStore};

// ── Shared context for VCS-aware usecases ──

/// Bundles the dependencies needed by usecases that compare against git history.
pub struct VcsContext<'a> {
    pub scan_store: &'a dyn astmap_core::ScanStore,
    pub diff_store: &'a dyn astmap_core::DiffStore,
    pub vcs: &'a dyn astmap_core::port::VcsProvider,
    pub lang: &'a dyn astmap_core::LanguageRegistry,
    pub discovery: &'a dyn astmap_core::port::FileDiscovery,
}

// ── Symbol resolution helpers ──

/// Resolve a symbol reference (name or numeric ID) to a Symbol.
pub fn resolve_symbol(db: &dyn QueryStore, input: &str) -> Result<Option<Symbol>, DbError> {
    if let Ok(id) = input.parse::<i64>() {
        db.get_symbol_by_id(id)
    } else {
        let results = db.search_symbols_by_name(input, 1)?;
        match results.into_iter().next() {
            Some(r) => db.get_symbol_by_id(r.id),
            None => Ok(None),
        }
    }
}

/// Resolve a symbol reference to its ID. Returns error if not found.
pub(crate) fn resolve_symbol_id(db: &dyn QueryStore, input: &str) -> Result<i64> {
    if let Ok(id) = input.parse::<i64>() {
        let sym = db.get_symbol_by_id(id)?;
        match sym {
            Some(s) => Ok(s.id),
            None => bail!("symbol not found: '{}'", input),
        }
    } else {
        let results = db.search_symbols_by_name(input, 1)?;
        match results.first() {
            Some(r) => Ok(r.id),
            None => bail!("symbol not found: '{}'", input),
        }
    }
}

/// Search symbols with FTS first, fall back to LIKE if FTS fails.
pub fn query_symbols(
    db: &dyn QueryStore,
    pattern: &str,
    limit: i64,
) -> Result<Vec<SymbolResult>, DbError> {
    db.search_symbols(pattern, limit)
        .or_else(|_| db.search_symbols_by_name(pattern, limit))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::infra::discovery::IgnoreWalkDiscovery;
    use astmap_languages::DefaultLanguageRegistry;
    use std::fs;

    fn setup_db(dir: &std::path::Path) -> Box<dyn QueryStore> {
        let db = crate::infra::sqlite::open(dir).unwrap();
        let src = dir.join("src");
        fs::create_dir_all(&src).unwrap();
        fs::write(src.join("lib.rs"), "pub fn hello() {}\npub fn world() {}").unwrap();
        astmap_core::scanner::scan(
            dir,
            db.as_ref(),
            true,
            &DefaultLanguageRegistry,
            &IgnoreWalkDiscovery,
        )
        .unwrap();
        db
    }

    // ── resolve_symbol ──

    #[test]
    fn resolve_symbol_by_name() {
        let dir = tempfile::tempdir().unwrap();
        let db = setup_db(dir.path());
        let sym = resolve_symbol(db.as_ref(), "hello").unwrap();
        assert!(sym.is_some());
        assert_eq!(sym.unwrap().name, "hello");
    }

    #[test]
    fn resolve_symbol_by_id() {
        let dir = tempfile::tempdir().unwrap();
        let db = setup_db(dir.path());
        let sym = resolve_symbol(db.as_ref(), "hello").unwrap().unwrap();
        let sym2 = resolve_symbol(db.as_ref(), &sym.id.to_string()).unwrap();
        assert_eq!(sym2.unwrap().name, "hello");
    }

    #[test]
    fn resolve_symbol_not_found() {
        let dir = tempfile::tempdir().unwrap();
        let db = setup_db(dir.path());
        let sym = resolve_symbol(db.as_ref(), "nonexistent").unwrap();
        assert!(sym.is_none());
    }

    #[test]
    fn resolve_symbol_invalid_id_not_found() {
        let dir = tempfile::tempdir().unwrap();
        let db = setup_db(dir.path());
        let sym = resolve_symbol(db.as_ref(), "99999").unwrap();
        assert!(sym.is_none());
    }

    // ── resolve_symbol_id ──

    #[test]
    fn resolve_symbol_id_by_name() {
        let dir = tempfile::tempdir().unwrap();
        let db = setup_db(dir.path());
        let id = resolve_symbol_id(db.as_ref(), "hello").unwrap();
        assert!(id > 0);
    }

    #[test]
    fn resolve_symbol_id_by_numeric_id() {
        let dir = tempfile::tempdir().unwrap();
        let db = setup_db(dir.path());
        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 resolve_symbol_id_name_not_found() {
        let dir = tempfile::tempdir().unwrap();
        let db = setup_db(dir.path());
        let err = resolve_symbol_id(db.as_ref(), "nonexistent").unwrap_err();
        assert!(err.to_string().contains("symbol not found"));
    }

    #[test]
    fn resolve_symbol_id_numeric_not_found() {
        let dir = tempfile::tempdir().unwrap();
        let db = setup_db(dir.path());
        let err = resolve_symbol_id(db.as_ref(), "99999").unwrap_err();
        assert!(err.to_string().contains("symbol not found"));
    }

    // ── query_symbols ──

    #[test]
    fn query_symbols_fts() {
        let dir = tempfile::tempdir().unwrap();
        let db = setup_db(dir.path());
        let results = query_symbols(db.as_ref(), "hello", 10).unwrap();
        assert!(!results.is_empty());
        assert_eq!(results[0].name, "hello");
    }

    #[test]
    fn query_symbols_fts_prefix() {
        let dir = tempfile::tempdir().unwrap();
        let db = setup_db(dir.path());
        // FTS5 prefix search with wildcard
        let results = query_symbols(db.as_ref(), "hel*", 10).unwrap();
        assert!(!results.is_empty());
    }

    #[test]
    fn query_symbols_no_match() {
        let dir = tempfile::tempdir().unwrap();
        let db = setup_db(dir.path());
        let results = query_symbols(db.as_ref(), "zzz_no_match", 10).unwrap();
        assert!(results.is_empty());
    }
}
