use graphmind_config::{paths, resolve_project_slug};
use colored::Colorize;
use graphmind_db::queries::GraphQueries;
use graphmind_db::schema::init_database;

fn open_db(slug: &str) -> rusqlite::Connection {
    let db_path = paths::graph_db_path(slug);
    let db_path_str = db_path.to_string_lossy().to_string();
    if !db_path.exists() {
        eprintln!(
            "{} No graph found for {}. Run 'graphmind build' first.",
            "Error:".red().bold(),
            slug
        );
        std::process::exit(1);
    }
    init_database(&db_path_str).unwrap_or_else(|e| {
        eprintln!("{} Failed to open database: {}", "Error:".red().bold(), e);
        std::process::exit(1);
    })
}

pub fn query_symbol(name: &str, slug: Option<&str>, file: Option<&str>, kind: Option<&str>, limit: usize, offset: usize) {
    let slug = require_slug(slug);
    let db = open_db(&slug);
    let q = GraphQueries::new(&db);

    let symbols = q.find_symbol_filtered(name, file, kind);
    if symbols.is_empty() {
        println!("{} No symbol found: {}", "!".yellow(), name);
        return;
    }

    for s in &symbols {
        println!(
            "{} {} [{}] in {}:{}",
            ">>".cyan().bold(),
            s.name.bold(),
            s.kind.yellow(),
            s.file.dimmed(),
            s.line_start
        );
        if let Some(ref sig) = s.signature {
            if !sig.is_empty() {
                println!("  {}", sig.dimmed());
            }
        }

        // Show callers/callees (deduplicated, with limit/offset)
        let callers = q.callers_filtered(&s.name, file);
        let mut seen = std::collections::HashSet::new();
        let unique_callers: Vec<_> = callers.iter().filter(|c| seen.insert((&c.name, &c.file, c.line_start))).collect();
        let call_callers: Vec<_> = unique_callers.iter().copied().filter(|c| c.edge_kind != "imports").collect();
        let import_callers: Vec<_> = unique_callers.iter().copied().filter(|c| c.edge_kind == "imports").collect();
        if !call_callers.is_empty() {
            println!("  {} ({}):", "Callers".green(), call_callers.len());
            for c in call_callers.iter().skip(offset).take(limit) {
                println!("    {} [{}] {}", c.name, c.edge_kind.dimmed(), c.file.dimmed());
            }
            if call_callers.len() > offset + limit {
                println!("    ... +{} more (use --offset/--limit)", call_callers.len() - offset - limit);
            }
        }
        if !import_callers.is_empty() {
            println!("  {} ({}):", "Imported by".cyan(), import_callers.len());
            for c in import_callers.iter().take(limit) {
                println!("    {} {}", c.name, c.file.dimmed());
            }
            if import_callers.len() > limit {
                println!("    ... +{} more", import_callers.len() - limit);
            }
        }
        let callees = q.callees_filtered(&s.name, file);
        seen.clear();
        let unique_callees: Vec<_> = callees.iter().filter(|c| seen.insert((&c.name, &c.file, c.line_start))).collect();
        let call_callees: Vec<_> = unique_callees.iter().copied().filter(|c| c.edge_kind != "imports").collect();
        let import_callees: Vec<_> = unique_callees.iter().copied().filter(|c| c.edge_kind == "imports").collect();
        if !call_callees.is_empty() {
            println!("  {} ({}):", "Callees".green(), call_callees.len());
            for c in call_callees.iter().skip(offset).take(limit) {
                println!("    {} [{}] {}", c.name, c.edge_kind.dimmed(), c.file.dimmed());
            }
            if call_callees.len() > offset + limit {
                println!("    ... +{} more (use --offset/--limit)", call_callees.len() - offset - limit);
            }
        }
        if !import_callees.is_empty() {
            println!("  {} ({}):", "Imports".cyan(), import_callees.len());
            for c in import_callees.iter().take(limit) {
                println!("    {} {}", c.name, c.file.dimmed());
            }
            if import_callees.len() > limit {
                println!("    ... +{} more", import_callees.len() - limit);
            }
        }
    }
}

pub struct FnDetailOpts<'a> {
    pub no_tests: bool,
    pub file: Option<&'a str>,
    pub kind: Option<&'a str>,
    pub limit: usize,
    pub offset: usize,
    pub include_content: bool,
}

pub fn fn_detail(name: &str, slug: Option<&str>, opts: &FnDetailOpts<'_>) {
    let no_tests = opts.no_tests;
    let file = opts.file;
    let kind = opts.kind;
    let limit = opts.limit;
    let offset = opts.offset;
    let include_content = opts.include_content;
    let slug = require_slug(slug);
    let db = open_db(&slug);
    let q = GraphQueries::new(&db);

    let symbols = q.find_symbol_filtered(name, file, kind);
    if symbols.is_empty() {
        println!("{} No symbol found: {}", "!".yellow(), name);
        return;
    }

    for s in &symbols {
        if no_tests && (s.file.contains("test") || s.file.contains("spec")) {
            continue;
        }

        println!(
            "{} {} [{}] in {}:{}-{}",
            ">>".cyan().bold(),
            s.name.bold(),
            s.kind.yellow(),
            s.file.dimmed(),
            s.line_start,
            s.line_end
        );
        if let Some(ref sig) = s.signature {
            if !sig.is_empty() {
                println!("  sig: {}", sig);
            }
        }
        if let Some(ref doc) = s.doc {
            if !doc.is_empty() {
                println!("  doc: {}", doc.dimmed());
            }
        }
        if include_content {
            if let Some(ref content) = s.content {
                if !content.is_empty() {
                    println!("{}", content);
                }
            }
        }

        let callers = q.callers_filtered(&s.name, file);
        let mut seen = std::collections::HashSet::new();
        let unique_callers: Vec<_> = callers.iter().filter(|c| seen.insert((&c.name, &c.file, c.line_start))).collect();
        let call_callers: Vec<_> = unique_callers.iter().copied().filter(|c| c.edge_kind != "imports").collect();
        let import_callers: Vec<_> = unique_callers.iter().copied().filter(|c| c.edge_kind == "imports").collect();
        if !call_callers.is_empty() {
            println!("  {} ({}):", "Callers".green(), call_callers.len());
            for c in call_callers.iter().skip(offset).take(limit) {
                println!("    {} [{}] {}", c.name, c.edge_kind.dimmed(), c.file.dimmed());
            }
            if call_callers.len() > offset + limit {
                println!("    ... +{} more (use --limit/--offset)", call_callers.len() - offset - limit);
            }
        }
        if !import_callers.is_empty() {
            println!("  {} ({}):", "Imported by".cyan(), import_callers.len());
            for c in import_callers.iter().take(limit) {
                println!("    {} {}", c.name, c.file.dimmed());
            }
            if import_callers.len() > limit {
                println!("    ... +{} more", import_callers.len() - limit);
            }
        }
        let callees = q.callees_filtered(&s.name, file);
        seen.clear();
        let unique_callees: Vec<_> = callees.iter().filter(|c| seen.insert((&c.name, &c.file, c.line_start))).collect();
        let call_callees: Vec<_> = unique_callees.iter().copied().filter(|c| c.edge_kind != "imports").collect();
        let import_callees: Vec<_> = unique_callees.iter().copied().filter(|c| c.edge_kind == "imports").collect();
        if !call_callees.is_empty() {
            println!("  {} ({}):", "Callees".green(), call_callees.len());
            for c in call_callees.iter().skip(offset).take(limit) {
                println!("    {} [{}] {}", c.name, c.edge_kind.dimmed(), c.file.dimmed());
            }
            if call_callees.len() > offset + limit {
                println!("    ... +{} more (use --limit/--offset)", call_callees.len() - offset - limit);
            }
        }
        if !import_callees.is_empty() {
            println!("  {} ({}):", "Imports".cyan(), import_callees.len());
            for c in import_callees.iter().take(limit) {
                println!("    {} {}", c.name, c.file.dimmed());
            }
            if import_callees.len() > limit {
                println!("    ... +{} more", import_callees.len() - limit);
            }
        }
    }
}

pub fn deps(file: &str, slug: Option<&str>) {
    let slug = require_slug(slug);
    let db = open_db(&slug);
    let q = GraphQueries::new(&db);

    let deps = q.file_deps(file);
    let rev_deps = q.file_reverse_deps(file);

    if deps.is_empty() && rev_deps.is_empty() {
        println!("{} No dependencies found for {}", "!".yellow(), file);
        return;
    }

    if !deps.is_empty() {
        println!("{} {} depends on:", ">>".cyan().bold(), file.bold());
        for d in &deps {
            println!("  {} [{}] x{}", d.file, d.kind.dimmed(), d.count);
        }
    }

    if !rev_deps.is_empty() {
        println!("\n{} Depended on by:", "<<".cyan().bold());
        for d in &rev_deps {
            println!("  {} [{}] x{}", d.file, d.kind.dimmed(), d.count);
        }
    }
}

pub fn impact(file: &str, slug: Option<&str>, depth: usize) {
    let slug = require_slug(slug);
    let db = open_db(&slug);
    let q = GraphQueries::new(&db);

    let impacted = q.impact(file, depth);
    if impacted.is_empty() {
        println!("{} No impact found for {}", "!".yellow(), file);
        return;
    }

    println!(
        "{} {} files impacted by changes to {}:",
        ">>".cyan().bold(),
        impacted.len().to_string().green(),
        file.bold()
    );
    for f in &impacted {
        println!("  {f}");
    }
}

pub fn fn_impact(name: &str, slug: Option<&str>, depth: usize) {
    let slug = require_slug(slug);
    let db = open_db(&slug);
    let q = GraphQueries::new(&db);

    let symbols = q.find_symbol(name);
    if symbols.is_empty() {
        println!("{} No symbol found: {}", "!".yellow(), name);
        return;
    }

    let mut all_impacted = std::collections::HashSet::new();
    for s in &symbols {
        let impacted = q.impact(&s.file, depth);
        for f in impacted {
            all_impacted.insert(f);
        }
    }

    if all_impacted.is_empty() {
        println!("{} No impact found for symbol {}", "!".yellow(), name);
        return;
    }

    println!(
        "{} {} files impacted by changes to symbol {}:",
        ">>".cyan().bold(),
        all_impacted.len().to_string().green(),
        name.bold()
    );
    for f in &all_impacted {
        println!("  {f}");
    }
}

pub fn map(slug: Option<&str>) {
    let slug = require_slug(slug);
    let db = open_db(&slug);
    let q = GraphQueries::new(&db);

    let stats = q.stats();
    let langs = q.language_breakdown();
    let top = q.top_connected(20);

    println!("{} Graph map for {}", ">>".cyan().bold(), slug.cyan());
    println!(
        "  {} symbols | {} edges | {} files",
        stats.symbols.to_string().green(),
        stats.edges.to_string().green(),
        stats.files.to_string().green()
    );

    if !langs.is_empty() {
        println!("\n  {}:", "Languages".bold());
        for l in &langs {
            println!("    {} ({})", l.language, l.count);
        }
    }

    if !top.is_empty() {
        println!("\n  {}:", "Top connected files".bold());
        for t in &top {
            println!("    {} ({})", t.file, t.connections);
        }
    }
}

pub fn cycles(slug: Option<&str>) {
    let slug = require_slug(slug);
    let db = open_db(&slug);
    let q = GraphQueries::new(&db);

    let cycles = q.detect_cycles();
    if cycles.is_empty() {
        println!("{} No cycles detected", "OK".green().bold());
        return;
    }

    println!(
        "{} {} circular dependencies detected:",
        "!".yellow().bold(),
        cycles.len().to_string().red()
    );
    for c in &cycles {
        println!("  {} <-> {}", c.from_file.red(), c.to_file.red());
    }
}

pub fn outline(file: &str, slug: Option<&str>) {
    let slug = require_slug(slug);
    let db = open_db(&slug);
    let q = GraphQueries::new(&db);

    let nodes = q.outline(file);
    if nodes.is_empty() {
        println!("{} No symbols found in {}", "!".yellow(), file);
        return;
    }

    println!("{} outline: {}\n", ">>".cyan().bold(), file.bold());
    fn render_tree(nodes: &[graphmind_db::queries::OutlineNode], depth: usize) {
        for node in nodes {
            let indent = "  ".repeat(depth);
            let sig = node.signature.as_deref().unwrap_or("");
            let sig_part = if sig.is_empty() { String::new() } else { format!("({})", sig) };
            println!("{}{} {}{} :{}", indent, node.kind.yellow(), node.name.bold(), sig_part.dimmed(), node.line_start);
            if !node.children.is_empty() {
                render_tree(&node.children, depth + 1);
            }
        }
    }
    render_tree(&nodes, 1);
}

pub fn who_calls(symbol: &str, slug: Option<&str>, depth: usize) {
    let slug = require_slug(slug);
    let db = open_db(&slug);
    let q = GraphQueries::new(&db);

    let (chain, max_reached) = q.who_calls_chain(symbol, depth);
    if chain.is_empty() {
        println!("{} No callers found for: {}", "!".yellow(), symbol);
        return;
    }

    println!(
        "{} who calls {} (depth {}, {} callers):\n",
        ">>".cyan().bold(),
        symbol.bold(),
        depth,
        chain.len()
    );
    for node in &chain {
        let indent = "  ".repeat(node.depth + 1);
        println!(
            "{}← {} [{}] {}:{}",
            indent,
            node.name.bold(),
            node.kind.yellow(),
            node.file.dimmed(),
            node.line_start
        );
    }
    if max_reached {
        println!("\n  {} (max depth {} reached — increase --depth for more)", "!".yellow(), depth);
    }
}

pub fn dead_code(slug: Option<&str>, kind: Option<&str>, limit: i64) {
    let slug = require_slug(slug);
    let db = open_db(&slug);
    let q = GraphQueries::new(&db);

    let dead = q.dead_code(kind, limit);
    if dead.is_empty() {
        println!("{} No dead code found", "OK".green().bold());
        return;
    }

    let kind_label = kind.unwrap_or("Function/Method");
    println!(
        "{} {} dead symbols (kind: {}):\n",
        ">>".cyan().bold(),
        dead.len().to_string().yellow(),
        kind_label
    );
    for s in &dead {
        println!(
            "  {} [{}] {}:{}",
            s.name.bold(),
            s.kind.yellow(),
            s.file.dimmed(),
            s.line_start
        );
        if let Some(ref sig) = s.signature {
            if !sig.is_empty() {
                println!("    {}", sig.dimmed());
            }
        }
    }
}

pub fn similar(symbol: &str, slug: Option<&str>, limit: i64) {
    let slug = require_slug(slug);
    let db = open_db(&slug);
    let q = GraphQueries::new(&db);

    let found = q.find_symbol(symbol);
    if found.is_empty() {
        println!("{} No symbol found: {}", "!".yellow(), symbol);
        return;
    }

    let results = q.similar_symbols(found[0].id, limit);
    if results.is_empty() {
        println!("{} No similar symbols found for: {}", "!".yellow(), symbol);
        return;
    }

    println!(
        "{} {} similar to \"{}\":\n",
        ">>".cyan().bold(),
        results.len().to_string().green(),
        symbol.bold()
    );
    for s in &results {
        println!(
            "  {} [{}] {}:{} ({:.2})",
            s.name.bold(),
            s.kind.yellow(),
            s.file.dimmed(),
            s.line_start,
            s.score
        );
    }
}

pub fn listeners(event: &str, slug: Option<&str>) {
    let slug = require_slug(slug);
    let db = open_db(&slug);
    let q = GraphQueries::new(&db);

    let found = q.find_listeners(event);
    if found.is_empty() {
        println!("{} No listeners found for: {}", "!".yellow(), event);
        return;
    }

    println!(
        "{} {} listener(s) for \"{}\":\n",
        ">>".cyan().bold(),
        found.len().to_string().green(),
        event.bold()
    );
    for s in &found {
        println!(
            "  {} [{}] {}:{}",
            s.name.bold(),
            s.kind.yellow(),
            s.file.dimmed(),
            s.line_start
        );
        if let Some(ref sig) = s.signature {
            if !sig.is_empty() {
                println!("    {}", sig.dimmed());
            }
        }
    }
}

pub fn file_content(file: &str, slug: Option<&str>) {
    let slug = require_slug(slug);
    let project = match graphmind_config::Registry::get(&slug) {
        Some(p) => p,
        None => {
            println!("{} Project {} not found", "!".yellow(), slug);
            return;
        }
    };
    let path = std::path::Path::new(&project.path).join(file);
    match std::fs::read_to_string(&path) {
        Ok(content) => {
            println!("{} {} [{}]", ">>".cyan().bold(), file.bold(), slug.dimmed());
            println!("{}", content);
        }
        Err(e) => {
            println!("{} Cannot read {}: {}", "!".yellow(), path.display(), e);
        }
    }
}

fn require_slug(slug: Option<&str>) -> String {
    match resolve_project_slug(&[slug]) {
        Some(s) => s,
        None => {
            eprintln!(
                "{} No project specified and none could be resolved",
                "Error:".red().bold()
            );
            std::process::exit(1);
        }
    }
}
