//! SCIP index source resolution, spawning, and verification pass.
//!
//! `apply_scip_verification` runs *after* tree-sitter and uses SCIP as a
//! cross-checker: edges confirmed by SCIP get confidence bumped to 1.0 and
//! their caller Symbols are stamped `@scip-verified`.

use std::path::{Path, PathBuf};

use anyhow::{Context, Result};
use ckg_storage::Storage;

use crate::commands::index::Args;

/// Where the SCIP index for a repo comes from.
pub(super) enum ScipSource {
    Path(PathBuf),
    None,
}

/// Resolve the SCIP source for `repo_root` given the CLI `args`.
///
/// Priority order:
/// 1. `--scip <file>` — explicit path, used as-is.
/// 2. `--no-scip` — skip entirely.
/// 3. `--scip-auto` — spawn the matching indexer; error if none found.
/// 4. Default — opportunistic spawn; silently fall back to tree-sitter.
pub(super) fn resolve_scip_source(repo_root: &Path, args: &Args) -> Result<ScipSource> {
    if let Some(p) = &args.scip {
        return Ok(ScipSource::Path(p.clone()));
    }
    if args.no_scip {
        return Ok(ScipSource::None);
    }
    if args.scip_auto {
        let p = super::scip_indexer::run_scip_indexer(repo_root, /*strict=*/ true)?;
        match p {
            Some(p) => Ok(ScipSource::Path(p)),
            None => anyhow::bail!(
                "--scip-auto: no SCIP indexer detected for {}",
                repo_root.display()
            ),
        }
    } else {
        // Default: opportunistic. Skip if no matching indexer on PATH.
        match super::scip_indexer::run_scip_indexer(repo_root, /*strict=*/ false)? {
            Some(p) => Ok(ScipSource::Path(p)),
            None => Ok(ScipSource::None),
        }
    }
}

/// Run SCIP ingestion *after* the tree-sitter pass and use it as a
/// **verifier**: any tree-sitter Calls edge whose `(caller_file, caller_name,
/// callee_name)` triple matches a SCIP edge gets its confidence bumped to 1.0
/// and a `@scip-verified` doc tag stamped on its source Symbol.
///
/// Returns `(symbols_tagged, edges_upgraded, edges_merged)` where
/// `edges_upgraded` is the count of overlapping (TS+SCIP) edges bumped to
/// conf 1.0 and `edges_merged` is the count of SCIP-only edges newly
/// inserted into the graph at conf 1.0.
pub(super) fn apply_scip_verification(
    storage: &Storage,
    repo_id: &str,
    scip_path: &Path,
) -> Result<(usize, usize, usize)> {
    use std::collections::{HashMap, HashSet};
    let scip_out = ckg_scip::ingest_file(repo_id, scip_path)
        .with_context(|| format!("ingesting SCIP at {}", scip_path.display()))?;

    // SCIP fact set: ((caller_file, caller_name), callee_name).
    // SCIP Symbol ids encode the file + symbol path; extract (file, name) by
    // joining each SCIP edge's src/dst against the SCIP Symbol table.
    let scip_id_to_meta: HashMap<&String, (&String, &String)> = scip_out
        .symbols
        .iter()
        .map(|s| (&s.id, (&s.file, &s.name)))
        .collect();
    let mut scip_facts: HashSet<(String, String, String)> = HashSet::new();
    for e in scip_out
        .edges
        .iter()
        .filter(|e| matches!(e.kind, ckg_core::EdgeKind::Calls))
    {
        if let (Some((sf, sn)), Some((_, dn))) =
            (scip_id_to_meta.get(&e.src), scip_id_to_meta.get(&e.dst))
        {
            scip_facts.insert(((*sf).clone(), (*sn).clone(), (*dn).clone()));
        } else if let Some((sf, sn)) = scip_id_to_meta.get(&e.src) {
            // dst may be a foreign symbol not in `symbols`; fall back to its
            // last descriptor parsed from the SCIP id string.
            let dst_name = e
                .dst
                .rsplit(['/', '.', '#', ' ', '(', '[', ']'])
                .next()
                .unwrap_or(&e.dst)
                .trim_end_matches([')', '.'])
                .to_string();
            if !dst_name.is_empty() {
                scip_facts.insert(((*sf).clone(), (*sn).clone(), dst_name));
            }
        }
    }
    if scip_facts.is_empty() {
        return Ok((0, 0, 0));
    }

    // Build (id → (file, name)) for tree-sitter symbols.
    let ts_syms = storage.run_immutable("?[id, file, name] := *Symbol{id, file, name}")?;
    let mut id_to_meta: HashMap<String, (String, String)> = HashMap::new();
    for r in &ts_syms.rows {
        if let (
            Some(cozo::DataValue::Str(id)),
            Some(cozo::DataValue::Str(f)),
            Some(cozo::DataValue::Str(n)),
        ) = (r.first(), r.get(1), r.get(2))
        {
            id_to_meta.insert(id.to_string(), (f.to_string(), n.to_string()));
        }
    }

    // Set of file basenames that SCIP covered → only verify edges whose
    // caller file matches (avoids inadvertently marking files SCIP didn't
    // touch). Use full paths from SCIP.
    let scip_files: HashSet<&String> = scip_out.symbols.iter().map(|s| &s.file).collect();

    // Walk tree-sitter Calls edges; collect upgrades.
    let calls =
        storage.run_immutable("?[src, dst, confidence] := *Calls{src, dst, confidence}")?;
    let mut upgrades: Vec<(String, String, f64)> = Vec::new();
    let mut verified_callers: HashSet<String> = HashSet::new();
    for r in calls.rows {
        let (Some(cozo::DataValue::Str(src)), Some(cozo::DataValue::Str(dst)), Some(_)) =
            (r.first(), r.get(1), r.get(2))
        else {
            continue;
        };
        let Some((src_file, src_name)) = id_to_meta.get(src.as_str()) else {
            continue;
        };
        if !scip_files.iter().any(|sf| sf.as_str() == src_file.as_str()) {
            continue;
        }
        // The dst may be a node id (resolved) or a bare name (unresolved).
        let dst_name = if let Some((_, dn)) = id_to_meta.get(dst.as_str()) {
            dn.clone()
        } else {
            // CLI-H9: use '::' split first for Rust qualified names
            // (e.g. `module::Type::method` → `method`), then fall back
            // to SCIP descriptor delimiters.
            dst.rsplit("::")
                .next()
                .and_then(|s| s.rsplit(['/', '.', '#', ' ', '(', '[', ']']).next())
                .unwrap_or(dst)
                .trim_end_matches([')', '.'])
                .to_string()
        };
        if scip_facts.contains(&(src_file.clone(), src_name.clone(), dst_name)) {
            upgrades.push((src.to_string(), dst.to_string(), 1.0));
            verified_callers.insert(src.to_string());
        }
    }
    // No early return on empty upgrades — SCIP-only merge may still
    // contribute edges below.

    // Apply upgrades by re-`:put`ing the same Calls rows (Cozo `:put`
    // overwrites on the (src, dst) primary key). Skip when there are no
    // upgrades — Cozo rejects empty `<- $rows` lists.
    if !upgrades.is_empty() {
        let rm_rows: Vec<cozo::DataValue> = upgrades
            .iter()
            .map(|(s, d, _)| {
                cozo::DataValue::List(vec![
                    cozo::DataValue::from(s.as_str()),
                    cozo::DataValue::from(d.as_str()),
                ])
            })
            .collect();
        let mut p = std::collections::BTreeMap::new();
        p.insert("rows".into(), cozo::DataValue::List(rm_rows));
        let _ = storage.run_with("?[src, dst] <- $rows :rm Calls {src, dst}", p);

        let put_rows: Vec<cozo::DataValue> = upgrades
            .iter()
            .map(|(s, d, c)| {
                cozo::DataValue::List(vec![
                    cozo::DataValue::from(s.as_str()),
                    cozo::DataValue::from(d.as_str()),
                    cozo::DataValue::from(*c),
                ])
            })
            .collect();
        let mut p = std::collections::BTreeMap::new();
        p.insert("rows".into(), cozo::DataValue::List(put_rows));
        storage.run_with(
            "?[src, dst, confidence] <- $rows :put Calls {src, dst => confidence}",
            p,
        )?;
    }

    // Pass 3: persist every SCIP symbol not already in the tree-sitter
    // table. Vendored / framework-internal files get an `@external` doc
    // tag — primary classification via the adapter registry, fallback
    // to the hardcoded path list when the relevant lang adapter isn't
    // active (e.g. a Python project where PythonAdapter didn't fire on
    // fingerprint detection). The registry-driven path subsumes the
    // hardcoded list once full-scip-persistence phase 2 lands.
    let registry = ckg_lang::AdapterRegistry::detect(
        &std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
    );
    let scip_only_ids = persist_external_symbols(storage, &scip_out, &id_to_meta, &registry)?;

    // Merge SCIP Calls edges that tree-sitter missed. With pass 3
    // having persisted every SCIP symbol under its own SCIP id, edges
    // whose endpoints don't map to a tree-sitter row can fall back to
    // the raw SCIP id — no edges are dropped now.
    let merged_callers = merge_scip_only_calls(
        storage,
        &scip_out,
        &id_to_meta,
        &scip_id_to_meta,
        &scip_only_ids,
    )?;
    let merged_count = merged_callers.len();

    // Stamp doc tags on caller symbols so downstream queries can
    // distinguish edge provenance:
    //   @scip-verified — at least one outgoing call confirmed by SCIP
    //                    (overlapped with tree-sitter)
    //   @scip-only     — at least one outgoing call only SCIP saw
    //                    (tree-sitter missed it; merged in at conf 1.0)
    // A symbol can carry both tags simultaneously.
    let updated_syms = stamp_scip_tags(storage, &verified_callers, &merged_callers)?;
    storage.put_symbols(&updated_syms)?;

    Ok((updated_syms.len(), upgrades.len(), merged_count))
}

/// Read every Symbol that's in either the `verified` or `merged_callers`
/// set, append the appropriate `@scip-verified` / `@scip-only` doc tag
/// (without duplicating an existing one), and return the rebuilt rows
/// for `put_symbols`.
fn stamp_scip_tags(
    storage: &Storage,
    verified: &std::collections::HashSet<String>,
    merged: &std::collections::HashSet<String>,
) -> Result<Vec<ckg_core::Symbol>> {
    let symbols = storage.run_immutable(
        "?[id, qname, name, kind, file, line, col, is_public, doc, hash] \
         := *Symbol{id, qname, name, kind, file, line, col, is_public, doc, hash}",
    )?;
    let mut out: Vec<ckg_core::Symbol> = Vec::new();
    for r in symbols.rows {
        let Some(cozo::DataValue::Str(id_s)) = r.first() else {
            continue;
        };
        let id = id_s.as_str();
        let needs_verified = verified.contains(id);
        let needs_only = merged.contains(id);
        if !needs_verified && !needs_only {
            continue;
        }
        let s = |i: usize| match r.get(i) {
            Some(cozo::DataValue::Str(s)) => s.to_string(),
            _ => String::new(),
        };
        let i = |i: usize| match r.get(i) {
            Some(cozo::DataValue::Num(n)) => match n {
                cozo::Num::Int(v) => *v as u32,
                cozo::Num::Float(v) => *v as u32,
            },
            _ => 0,
        };
        let b = |i: usize| matches!(r.get(i), Some(cozo::DataValue::Bool(true)));
        let mut doc = s(8);
        if needs_verified && !doc.contains("@scip-verified") {
            if doc.is_empty() {
                doc = "@scip-verified".into();
            } else {
                doc.push_str("\n@scip-verified");
            }
        }
        if needs_only && !doc.contains("@scip-only") {
            if doc.is_empty() {
                doc = "@scip-only".into();
            } else {
                doc.push_str("\n@scip-only");
            }
        }
        let kind = match s(3).as_str() {
            "function" => ckg_core::Kind::Function,
            "method" => ckg_core::Kind::Method,
            "class" => ckg_core::Kind::Class,
            "struct" => ckg_core::Kind::Struct,
            "trait" => ckg_core::Kind::Trait,
            "interface" => ckg_core::Kind::Interface,
            "enum" => ckg_core::Kind::Enum,
            "module" => ckg_core::Kind::Module,
            "constant" => ckg_core::Kind::Constant,
            "variable" => ckg_core::Kind::Variable,
            "type_alias" => ckg_core::Kind::TypeAlias,
            "route" => ckg_core::Kind::Route,
            "process" => ckg_core::Kind::Process,
            "constructor" => ckg_core::Kind::Constructor,
            _ => ckg_core::Kind::Function,
        };
        out.push(ckg_core::Symbol {
            id: id.to_string(),
            qname: s(1),
            name: s(2),
            kind,
            file: s(4),
            line: i(5),
            col: i(6),
            is_public: b(7),
            doc,
            hash: s(9),
        });
    }
    Ok(out)
}

/// Per-language vendor / dependency directory substrings. Files under
/// any of these are flagged `@external` so queries can filter them.
/// Conservative — only matches well-known package-manager paths.
/// Phase 2 of full-scip-persistence moves these into per-language
/// adapters; phase 1 (this) keeps them hardcoded for the smallest blast
/// radius. See plans/260512-1440-full-scip-persistence/.
fn is_vendored_path(file: &str) -> bool {
    const VENDOR_SUBSTRINGS: &[&str] = &[
        "/vendor/bundle/",   // Ruby (bundler-installed gems)
        "/.bundle/",         // Ruby (bundler internal)
        "/node_modules/",    // JS / TS
        "/.venv/",           // Python (virtualenv)
        "/venv/",            // Python
        "/site-packages/",   // Python
        "/.tox/",            // Python (tox)
        "/.cargo/registry/", // Rust
        "/.rustup/",         // Rust toolchain stdlib
        "/vendor/",          // Go (vendored deps); also Rails non-bundle vendor
        "/pkg/mod/",         // Go (module cache)
    ];
    VENDOR_SUBSTRINGS.iter().any(|s| file.contains(s))
}

/// Persist every SCIP symbol not yet in the graph, tagging vendored
/// files with `@external`. Returns the set of newly inserted SCIP IDs
/// so the edge-merge step knows which dst IDs are now valid endpoints.
///
/// Vendor classification: tries the adapter registry first (per-language
/// `is_vendored_file`). Falls back to the hardcoded substring list for
/// languages whose adapter isn't active in this repo — keeps the
/// per-language path knowledge usable even on polyglot indices where
/// fingerprint detection missed an adapter.
fn persist_external_symbols(
    storage: &Storage,
    scip_out: &ckg_scip::ScipOutput,
    ts_id_to_meta: &std::collections::HashMap<String, (String, String)>,
    registry: &ckg_lang::AdapterRegistry,
) -> Result<std::collections::HashSet<String>> {
    let mut new_syms: Vec<ckg_core::Symbol> = Vec::new();
    let mut new_ids: std::collections::HashSet<String> =
        std::collections::HashSet::new();
    for s in &scip_out.symbols {
        if ts_id_to_meta.contains_key(&s.id) {
            continue;
        }
        let mut sym = s.clone();
        let is_external = registry.any_vendored_file(&sym.file) || is_vendored_path(&sym.file);
        if is_external {
            if sym.doc.is_empty() {
                sym.doc = "@external".into();
            } else if !sym.doc.contains("@external") {
                sym.doc.push_str("\n@external");
            }
        }
        new_ids.insert(sym.id.clone());
        new_syms.push(sym);
    }
    if !new_syms.is_empty() {
        storage.put_symbols(&new_syms)?;
    }
    Ok(new_ids)
}

/// Insert SCIP Calls edges that have no tree-sitter counterpart, mapping
/// each SCIP-ID endpoint to either a tree-sitter symbol ID by (file,
/// name) lookup, OR the raw SCIP id when the endpoint was persisted by
/// pass 3 (`persist_external_symbols`). Returns the set of caller IDs
/// for the merged edges so downstream tagging can stamp `@scip-only`.
fn merge_scip_only_calls(
    storage: &Storage,
    scip_out: &ckg_scip::ScipOutput,
    ts_id_to_meta: &std::collections::HashMap<String, (String, String)>,
    scip_id_to_meta: &std::collections::HashMap<&String, (&String, &String)>,
    scip_only_ids: &std::collections::HashSet<String>,
) -> Result<std::collections::HashSet<String>> {
    use std::collections::{HashMap, HashSet};

    // Reverse index: (file, name) → tree-sitter symbol id.
    let mut ts_lookup: HashMap<(String, String), String> = HashMap::new();
    // Fallback: name → first matching tree-sitter id (for callees whose
    // file scip-ruby couldn't pin down — common for methods defined in
    // a parent / mixin module).
    let mut ts_by_name: HashMap<String, String> = HashMap::new();
    for (id, (f, n)) in ts_id_to_meta {
        ts_lookup.insert((f.clone(), n.clone()), id.clone());
        ts_by_name.entry(n.clone()).or_insert_with(|| id.clone());
    }

    // Existing tree-sitter Calls edges as a (src, dst) set so we don't
    // double-insert what verification already covered.
    let existing = storage.run_immutable("?[src, dst] := *Calls{src, dst}")?;
    let mut have_edge: HashSet<(String, String)> = HashSet::new();
    for r in &existing.rows {
        if let (Some(cozo::DataValue::Str(s)), Some(cozo::DataValue::Str(d))) =
            (r.first(), r.get(1))
        {
            have_edge.insert((s.to_string(), d.to_string()));
        }
    }

    let mut new_rows: Vec<cozo::DataValue> = Vec::new();
    let mut inserted: HashSet<(String, String)> = HashSet::new();
    for e in scip_out
        .edges
        .iter()
        .filter(|e| matches!(e.kind, ckg_core::EdgeKind::Calls))
    {
        // Source endpoint: tree-sitter id by (file, name); fall back
        // to the raw SCIP id when pass 3 just inserted it.
        let src_endpoint: String = if let Some((src_file, src_name)) = scip_id_to_meta.get(&e.src) {
            if let Some(ts_id) =
                ts_lookup.get(&((*src_file).clone(), (*src_name).clone()))
            {
                ts_id.clone()
            } else if scip_only_ids.contains(&e.src) {
                e.src.clone()
            } else {
                continue;
            }
        } else if scip_only_ids.contains(&e.src) {
            e.src.clone()
        } else {
            continue;
        };

        // Destination endpoint: same fallback chain. Last-descriptor
        // name lookup remains a final fallback for "foreign" SCIP refs
        // whose symbol row isn't in `scip_out.symbols`.
        let dst_endpoint: String = if let Some((df, dn)) = scip_id_to_meta.get(&e.dst) {
            if let Some(ts_id) = ts_lookup.get(&((*df).clone(), (*dn).clone())) {
                ts_id.clone()
            } else if let Some(ts_id) = ts_by_name.get(*dn) {
                ts_id.clone()
            } else if scip_only_ids.contains(&e.dst) {
                e.dst.clone()
            } else {
                continue;
            }
        } else if scip_only_ids.contains(&e.dst) {
            e.dst.clone()
        } else {
            // Foreign symbol — last descriptor → name-based ts lookup.
            let last = e
                .dst
                .rsplit(['/', '.', '#', ' ', '(', '[', ']'])
                .next()
                .unwrap_or(&e.dst)
                .trim_end_matches([')', '.'])
                .to_string();
            if let Some(ts_id) = ts_by_name.get(&last) {
                ts_id.clone()
            } else {
                continue;
            }
        };

        let pair = (src_endpoint, dst_endpoint);
        if have_edge.contains(&pair) || !inserted.insert(pair.clone()) {
            continue;
        }
        new_rows.push(cozo::DataValue::List(vec![
            cozo::DataValue::from(pair.0.as_str()),
            cozo::DataValue::from(pair.1.as_str()),
            cozo::DataValue::from(1.0_f64),
        ]));
    }

    let merged_callers: HashSet<String> =
        inserted.iter().map(|(src, _)| src.clone()).collect();
    if new_rows.is_empty() {
        return Ok(merged_callers);
    }
    let mut p = std::collections::BTreeMap::new();
    p.insert("rows".into(), cozo::DataValue::List(new_rows));
    storage.run_with(
        "?[src, dst, confidence] <- $rows :put Calls {src, dst => confidence}",
        p,
    )?;
    Ok(merged_callers)
}

