use std::collections::BTreeSet;

use rusqlite::{Connection, params_from_iter};
use serde::Serialize;

const CALL_EDGE_KINDS: &[&str] = &["calls_name", "constructs"];
const MACRO_EDGE_KINDS: &[&str] = &["uses_macro"];
const REFERENCE_EDGE_KINDS: &[&str] =
    &["references_type", "imports", "exports", "contains", "implements"];
const OPTIONAL_EDGE_KINDS: &[&str] = &[
    "calls_name",
    "constructs",
    "uses_macro",
    "references_type",
    "imports",
    "exports",
    "contains",
    "implements",
];

#[derive(Debug, Clone, Default)]
pub struct GraphTraversalOptions {
    pub include_references: bool,
    pub include_unresolved: bool,
    pub include_macros: bool,
    pub include_common_methods: bool,
    pub edge_kinds: Option<Vec<String>>,
    pub resolution_mode: GraphResolutionMode,
    pub symbol_id: Option<i64>,
    pub logical_symbol_id: Option<i64>,
}

#[derive(Debug, Serialize)]
pub struct GraphTraversalReport {
    pub query: GraphTraversalQuery,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub logical_symbol: Option<LogicalSymbol>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub variants: Vec<LogicalSymbolVariant>,
    pub summary: GraphTraversalSummary,
    pub coverage: GraphCoverage,
    pub results: Vec<GraphHop>,
}

#[derive(Debug, Serialize)]
pub struct GraphTraversalQuery {
    pub tool: String,
    pub symbol_id: Option<i64>,
    pub logical_symbol_id: Option<i64>,
    pub symbol_path: String,
    pub resolution: String,
}

#[derive(Debug, Clone, Serialize)]
pub struct LogicalSymbol {
    pub logical_symbol_id: i64,
    pub qualified_name: String,
    pub variant_count: u64,
    pub group_reason: String,
}

#[derive(Debug, Clone, Serialize)]
pub struct LogicalSymbolVariant {
    pub symbol_id: i64,
    pub cfg_expr: Option<String>,
    pub signature_hash: Option<String>,
    pub start_line: i64,
    pub end_line: i64,
}

#[derive(Debug, Default, Serialize)]
pub struct GraphTraversalSummary {
    pub returned_count: u64,
    pub total_matching_edges: u64,
    pub truncated: bool,
    pub exact_verified: u64,
    pub syntactic: u64,
    pub name_only: u64,
    pub ambiguous: u64,
    pub unresolved: u64,
    pub false_positive_risk: String,
    pub completeness_risk: String,
}

#[derive(Debug, Default, Serialize)]
pub struct GraphCoverage {
    pub indexed_files: u64,
    pub parser_failures: u64,
    pub stale_files: u64,
    pub known_index_gaps: Vec<String>,
    pub parser_coverage_for_paths: Vec<GraphPathCoverage>,
}

#[derive(Debug, Serialize)]
pub struct GraphPathCoverage {
    pub path: String,
    pub language: String,
    pub parser_status: String,
    pub graph_status: String,
    pub last_indexed_revision: Option<String>,
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum GraphResolutionMode {
    Exact,
    #[default]
    Syntactic,
    Fuzzy,
}

impl GraphResolutionMode {
    pub fn parse(value: Option<&str>) -> anyhow::Result<Self> {
        match value.unwrap_or("syntactic") {
            "exact" => Ok(Self::Exact),
            "syntactic" => Ok(Self::Syntactic),
            "fuzzy" => Ok(Self::Fuzzy),
            other => anyhow::bail!(
                "unknown graph resolution mode `{other}`; expected exact, syntactic, or fuzzy"
            ),
        }
    }

    pub fn as_str(self) -> &'static str {
        match self {
            Self::Exact => "exact",
            Self::Syntactic => "syntactic",
            Self::Fuzzy => "fuzzy",
        }
    }
}

impl GraphTraversalOptions {
    pub fn callee_edge_kinds(&self) -> anyhow::Result<Vec<String>> {
        if let Some(edge_kinds) = &self.edge_kinds {
            validate_edge_kinds(edge_kinds)?;
            return Ok(edge_kinds.clone());
        }
        let mut edge_kinds =
            CALL_EDGE_KINDS.iter().map(|value| (*value).to_string()).collect::<Vec<_>>();
        if self.include_macros {
            edge_kinds.extend(MACRO_EDGE_KINDS.iter().map(|value| (*value).to_string()));
        }
        if self.include_references {
            edge_kinds.extend(REFERENCE_EDGE_KINDS.iter().map(|value| (*value).to_string()));
        }
        Ok(edge_kinds)
    }

    pub fn caller_edge_kinds(&self) -> anyhow::Result<Vec<String>> {
        self.callee_edge_kinds()
    }
}

#[derive(Debug, Serialize)]
pub struct CompareGraphTextReport {
    pub query: CompareGraphTextQuery,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub logical_symbol: Option<LogicalSymbol>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub variants: Vec<LogicalSymbolVariant>,
    pub summary: CompareGraphTextSummary,
    pub coverage: GraphCoverage,
    pub matched_hits: Vec<MatchedGraphTextHit>,
    pub text_only_hits: Vec<TextOnlyHit>,
    pub graph_only_edges: Vec<GraphOnlyEdge>,
    pub likely_parser_gaps: Vec<TextOnlyHit>,
    pub likely_false_positives: Vec<GraphOnlyEdge>,
}

#[derive(Debug, Serialize)]
pub struct CompareGraphTextQuery {
    pub symbol_id: Option<i64>,
    pub logical_symbol_id: Option<i64>,
    pub symbol_path: String,
    pub pattern: String,
    pub resolution: String,
    pub include_tests: bool,
}

#[derive(Debug, Default, Serialize)]
pub struct CompareGraphTextSummary {
    pub graph_hits: u64,
    pub graph_edges: u64,
    pub text_hits: u64,
    pub matched: u64,
    pub graph_only: u64,
    pub text_only: u64,
    pub text_mentions: u64,
    pub likely_parser_gaps: u64,
    pub likely_false_positives: u64,
    pub likely_index_gaps: u64,
    pub complete: bool,
    pub recommended_fallback: String,
    pub pattern_match_mode: String,
    pub warnings: Vec<String>,
}

#[derive(Debug, Serialize)]
pub struct MatchedGraphTextHit {
    pub path: String,
    pub line: i64,
    pub text: String,
    pub target: Option<String>,
    pub edge_kind: String,
    pub confidence: String,
    pub resolution: String,
}

#[derive(Debug, Clone, Serialize)]
pub struct TextOnlyHit {
    pub path: String,
    pub line: i64,
    pub text: String,
    pub reason: String,
    pub likely_gap: String,
}

#[derive(Debug, Clone, Serialize)]
pub struct GraphOnlyEdge {
    pub path: String,
    pub line: i64,
    pub target: Option<String>,
    pub edge_kind: String,
    pub confidence: String,
    pub resolution: String,
    pub evidence: Option<String>,
    pub reason: String,
    pub likely_reason: String,
}

#[derive(Debug, Clone, Serialize)]
pub struct GraphHop {
    pub edge_id: i64,
    pub from_symbol: Option<String>,
    pub to_symbol: Option<String>,
    pub edge_kind: String,
    pub confidence: String,
    pub edge_confidence: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub target: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub target_qualified_name: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub evidence: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub receiver_hint: Option<String>,
    pub resolution: String,
    pub verified_target_symbol: bool,
    pub shown_by_default: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub callsite: Option<Callsite>,
}

#[derive(Debug, Clone, Serialize)]
pub struct Callsite {
    pub path: String,
    pub line: i64,
    pub span: [i64; 2],
}

pub fn traverse(
    conn: &Connection,
    symbol: &str,
    reverse: bool,
    limit: u32,
) -> anyhow::Result<Vec<GraphHop>> {
    traverse_with_options(conn, symbol, reverse, limit, &GraphTraversalOptions::default())
}

pub fn traverse_with_options(
    conn: &Connection,
    symbol: &str,
    reverse: bool,
    limit: u32,
    options: &GraphTraversalOptions,
) -> anyhow::Result<Vec<GraphHop>> {
    let edge_kinds =
        if reverse { options.caller_edge_kinds()? } else { options.callee_edge_kinds()? };
    let quoted = quoted_placeholders(edge_kinds.len());
    let unique_short_name = unique_symbol_name(conn, short_name(symbol))?;
    let mode = options.resolution_mode;
    let sql = if reverse {
        let predicate = reverse_predicate(mode, options.logical_symbol_id.is_some());
        let tier = reverse_tier(mode);
        format!(
            "
            SELECT COALESCE(from_symbols.qualified_name, edges.from_name) AS from_symbol,
                   COALESCE(to_symbols.qualified_name, edges.to_name) AS to_symbol,
                   edges.id AS edge_id,
                   edges.edge_kind AS edge_kind,
                   edges.confidence AS confidence,
                   edges.to_name AS target,
                   edges.target_qualified_name AS target_qualified_name,
                   edges.evidence AS evidence,
                   edges.receiver_hint AS receiver_hint,
                   edges.resolution AS edge_resolution,
                   edges.to_symbol_id IS NOT NULL AS verified_target_symbol,
                   source_files.path AS callsite_path,
                   COALESCE(NULLIF(edges.source_start_line, 0), 1) AS callsite_start_line,
                   COALESCE(NULLIF(edges.source_end_line, 0), NULLIF(edges.source_start_line, 0), 1) AS callsite_end_line,
                   {tier} AS match_tier
            FROM edges
            JOIN files source_files ON source_files.id = edges.source_file_id
            LEFT JOIN symbols from_symbols ON from_symbols.id = edges.from_symbol_id
            LEFT JOIN symbols to_symbols ON to_symbols.id = edges.to_symbol_id
            WHERE edges.edge_kind IN ({quoted})
              AND ({predicate})
            ORDER BY match_tier,
                CASE edges.confidence
                    WHEN 'Exact' THEN 0
                    WHEN 'Syntactic' THEN 1
                    WHEN 'NameOnly' THEN 2
                    ELSE 3
                END,
                edges.edge_kind,
                edges.from_name
            LIMIT ?5
            "
        )
    } else {
        let predicate = forward_source_predicate(mode, options.logical_symbol_id.is_some());
        let target_filter = forward_target_filter(mode, options);
        let visibility_filter = forward_visibility_filter(options);
        format!(
            "
            SELECT COALESCE(from_symbols.qualified_name, edges.from_name) AS from_symbol,
                   COALESCE(to_symbols.qualified_name, edges.to_name) AS to_symbol,
                   edges.id AS edge_id,
                   edges.edge_kind AS edge_kind,
                   edges.confidence AS confidence,
                   edges.to_name AS target,
                   edges.target_qualified_name AS target_qualified_name,
                   edges.evidence AS evidence,
                   edges.receiver_hint AS receiver_hint,
                   edges.resolution AS edge_resolution,
                   edges.to_symbol_id IS NOT NULL AS verified_target_symbol,
                   source_files.path AS callsite_path,
                   COALESCE(NULLIF(edges.source_start_line, 0), 1) AS callsite_start_line,
                   COALESCE(NULLIF(edges.source_end_line, 0), NULLIF(edges.source_start_line, 0), 1) AS callsite_end_line,
                   0 AS match_tier
            FROM edges
            JOIN files source_files ON source_files.id = edges.source_file_id
            LEFT JOIN symbols from_symbols ON from_symbols.id = edges.from_symbol_id
            LEFT JOIN symbols to_symbols ON to_symbols.id = edges.to_symbol_id
            WHERE edges.edge_kind IN ({quoted})
              AND ({predicate})
              AND ({target_filter})
              AND ({visibility_filter})
              AND ?4 IN ('true', 'false')
            ORDER BY
                CASE edges.confidence
                    WHEN 'Exact' THEN 0
                    WHEN 'Syntactic' THEN 1
                    WHEN 'NameOnly' THEN 2
                    ELSE 3
                END,
                edges.edge_kind,
                edges.to_name
            LIMIT ?5
            "
        )
    };
    let params = traversal_params(
        symbol,
        limit,
        &edge_kinds,
        options.symbol_id,
        options.logical_symbol_id,
        unique_short_name,
    );
    let mut stmt = conn.prepare(&sql)?;
    let rows = stmt.query_map(params_from_iter(params), |row| {
        let edge_kind: String = row.get("edge_kind")?;
        let confidence: String = row.get("confidence")?;
        let verified_target_symbol = row.get("verified_target_symbol")?;
        let resolution = resolution_label(
            mode,
            row.get::<_, String>("edge_resolution")?,
            row.get("match_tier")?,
            verified_target_symbol,
        );
        let callsite_path: String = row.get("callsite_path")?;
        let callsite_start = row.get("callsite_start_line")?;
        let callsite_end = row.get("callsite_end_line")?;
        Ok(GraphHop {
            edge_id: row.get("edge_id")?,
            from_symbol: row.get("from_symbol")?,
            to_symbol: row.get("to_symbol")?,
            edge_kind: edge_kind.clone(),
            confidence: confidence.clone(),
            edge_confidence: confidence,
            target: row.get("target")?,
            target_qualified_name: row.get("target_qualified_name")?,
            evidence: row.get("evidence")?,
            receiver_hint: row.get("receiver_hint")?,
            resolution,
            verified_target_symbol,
            shown_by_default: CALL_EDGE_KINDS.contains(&edge_kind.as_str()),
            callsite: Some(Callsite {
                path: callsite_path,
                line: callsite_start,
                span: [callsite_start, callsite_end],
            }),
        })
    })?;
    let mut hops = Vec::new();
    for row in rows {
        hops.push(row?);
    }
    dedupe_hops(&mut hops);
    Ok(hops)
}

fn dedupe_hops(hops: &mut Vec<GraphHop>) {
    let mut seen = BTreeSet::new();
    hops.retain(|hop| {
        let callsite = hop.callsite.as_ref();
        seen.insert((
            hop.from_symbol.clone(),
            hop.to_symbol.clone(),
            hop.edge_id,
            hop.edge_kind.clone(),
            hop.target.clone(),
            hop.target_qualified_name.clone(),
            hop.receiver_hint.clone(),
            callsite.map(|value| value.path.clone()),
            callsite.map(|value| value.span),
        ))
    });
}

pub fn traversal_summary(
    conn: &Connection,
    symbol: &str,
    reverse: bool,
    limit: u32,
    options: &GraphTraversalOptions,
    returned_count: usize,
) -> anyhow::Result<GraphTraversalSummary> {
    let edge_kinds =
        if reverse { options.caller_edge_kinds()? } else { options.callee_edge_kinds()? };
    let quoted = quoted_placeholders(edge_kinds.len());
    let unique_short_name = unique_symbol_name(conn, short_name(symbol))?;
    let mode = options.resolution_mode;
    let sql = if reverse {
        let predicate = reverse_predicate(mode, options.logical_symbol_id.is_some());
        format!(
            "
            SELECT
                COUNT(*),
                SUM(CASE WHEN edges.to_symbol_id IS NOT NULL THEN 1 ELSE 0 END),
                SUM(CASE WHEN edges.confidence = 'Syntactic' THEN 1 ELSE 0 END),
                SUM(CASE WHEN edges.confidence = 'NameOnly' THEN 1 ELSE 0 END),
                SUM(CASE WHEN edges.confidence = 'Ambiguous' THEN 1 ELSE 0 END),
                SUM(CASE WHEN edges.to_symbol_id IS NULL THEN 1 ELSE 0 END)
            FROM edges
            LEFT JOIN symbols to_symbols ON to_symbols.id = edges.to_symbol_id
            WHERE edges.edge_kind IN ({quoted})
              AND ({predicate})
            "
        )
    } else {
        let predicate = forward_source_predicate(mode, options.logical_symbol_id.is_some());
        let target_filter = forward_target_filter(mode, options);
        let visibility_filter = forward_visibility_filter(options);
        format!(
            "
            SELECT
                COUNT(*),
                SUM(CASE WHEN edges.to_symbol_id IS NOT NULL THEN 1 ELSE 0 END),
                SUM(CASE WHEN edges.confidence = 'Syntactic' THEN 1 ELSE 0 END),
                SUM(CASE WHEN edges.confidence = 'NameOnly' THEN 1 ELSE 0 END),
                SUM(CASE WHEN edges.confidence = 'Ambiguous' THEN 1 ELSE 0 END),
                SUM(CASE WHEN edges.to_symbol_id IS NULL THEN 1 ELSE 0 END)
            FROM edges
            LEFT JOIN symbols from_symbols ON from_symbols.id = edges.from_symbol_id
            WHERE edges.edge_kind IN ({quoted})
              AND ({predicate})
              AND ({target_filter})
              AND ({visibility_filter})
              AND ?4 IN ('true', 'false')
            "
        )
    };
    let params = traversal_params(
        symbol,
        limit,
        &edge_kinds,
        options.symbol_id,
        options.logical_symbol_id,
        unique_short_name,
    );
    let mut summary = conn.query_row(&sql, params_from_iter(params), |row| {
        Ok(GraphTraversalSummary {
            returned_count: u64::try_from(returned_count).unwrap_or(u64::MAX),
            total_matching_edges: count_col(row, 0)?,
            truncated: false,
            exact_verified: count_col(row, 1)?,
            syntactic: count_col(row, 2)?,
            name_only: count_col(row, 3)?,
            ambiguous: count_col(row, 4)?,
            unresolved: count_col(row, 5)?,
            false_positive_risk: String::new(),
            completeness_risk: String::new(),
        })
    })?;
    let hidden_unresolved = hidden_unresolved_candidate_count(
        conn,
        symbol,
        reverse,
        &edge_kinds,
        options,
        unique_short_name,
    )?;
    summary.total_matching_edges = summary.total_matching_edges.saturating_add(hidden_unresolved);
    summary.unresolved = summary.unresolved.saturating_add(hidden_unresolved);
    summary.truncated = summary.total_matching_edges > u64::from(limit);
    summary.false_positive_risk = false_positive_risk(&summary, mode).to_string();
    summary.completeness_risk = completeness_risk(&summary).to_string();
    Ok(summary)
}

fn count_col(row: &rusqlite::Row<'_>, index: usize) -> rusqlite::Result<u64> {
    let value = row.get::<_, Option<i64>>(index)?.unwrap_or(0);
    Ok(u64::try_from(value).unwrap_or(0))
}

fn false_positive_risk(summary: &GraphTraversalSummary, mode: GraphResolutionMode) -> &'static str {
    if summary.ambiguous > 0 || mode == GraphResolutionMode::Fuzzy {
        "high"
    } else if summary.name_only > 0
        || summary.unresolved > 0
        || mode == GraphResolutionMode::Syntactic
    {
        "medium"
    } else {
        "low"
    }
}

fn completeness_risk(summary: &GraphTraversalSummary) -> &'static str {
    if summary.truncated
        || summary.unresolved > summary.exact_verified.saturating_add(summary.syntactic)
    {
        "high"
    } else if summary.unresolved > 0 || summary.name_only > 0 || summary.ambiguous > 0 {
        "medium"
    } else {
        "low"
    }
}

fn hidden_unresolved_candidate_count(
    conn: &Connection,
    symbol: &str,
    reverse: bool,
    edge_kinds: &[String],
    options: &GraphTraversalOptions,
    unique_short_name: bool,
) -> anyhow::Result<u64> {
    let mode = options.resolution_mode;
    let quoted = quoted_placeholders(edge_kinds.len());
    let sql = if reverse {
        let predicate = reverse_predicate(mode, options.logical_symbol_id.is_some());
        format!(
            "
            SELECT COUNT(*)
            FROM edges
            LEFT JOIN symbols to_symbols ON to_symbols.id = edges.to_symbol_id
            WHERE edges.edge_kind IN ({quoted})
              AND edges.to_symbol_id IS NULL
              AND NOT ({predicate})
              AND (
                edges.target_qualified_name = ?1
                OR edges.target_qualified_name LIKE ?2
                OR edges.to_name = ?3
              )
            "
        )
    } else {
        let source_predicate = forward_source_predicate(mode, options.logical_symbol_id.is_some());
        let target_filter = forward_target_filter(mode, options);
        let visibility_filter = forward_visibility_filter(options);
        format!(
            "
            SELECT COUNT(*)
            FROM edges
            LEFT JOIN symbols from_symbols ON from_symbols.id = edges.from_symbol_id
            WHERE edges.edge_kind IN ({quoted})
              AND ({source_predicate})
              AND edges.to_symbol_id IS NULL
              AND NOT (({target_filter}) AND ({visibility_filter}))
              AND ?4 IN ('true', 'false')
            "
        )
    };
    let params = traversal_params(
        symbol,
        0,
        edge_kinds,
        options.symbol_id,
        options.logical_symbol_id,
        unique_short_name,
    );
    let count = conn.query_row(&sql, params_from_iter(params), |row| count_col(row, 0))?;
    Ok(count)
}

fn validate_edge_kinds(edge_kinds: &[String]) -> anyhow::Result<()> {
    for edge_kind in edge_kinds {
        if !OPTIONAL_EDGE_KINDS.contains(&edge_kind.as_str()) {
            anyhow::bail!("unknown graph edge kind `{edge_kind}`");
        }
    }
    Ok(())
}

fn traversal_params(
    symbol: &str,
    limit: u32,
    edge_kinds: &[String],
    symbol_id: Option<i64>,
    logical_symbol_id: Option<i64>,
    unique_short_name: bool,
) -> Vec<String> {
    let qualified = symbol.to_string();
    let short = short_name(symbol).to_string();
    let fuzzy_qualified = format!("%::{qualified}");
    let allow_name_fallback = (!is_qualified_symbol(symbol)).to_string();
    let mut params = vec![
        qualified,
        fuzzy_qualified,
        short,
        allow_name_fallback,
        limit.to_string(),
        symbol_id.unwrap_or(-1).to_string(),
        unique_short_name.to_string(),
        logical_symbol_id.unwrap_or(-1).to_string(),
    ];
    params.extend(edge_kinds.iter().cloned());
    params
}

fn quoted_placeholders(count: usize) -> String {
    (0..count).map(|index| format!("?{}", index + 9)).collect::<Vec<_>>().join(", ")
}

fn reverse_predicate(mode: GraphResolutionMode, logical: bool) -> &'static str {
    if logical {
        return match mode {
            GraphResolutionMode::Exact => {
                "edges.to_symbol_id IS NOT NULL
                 AND edges.to_symbol_id IN (
                    SELECT symbol_id
                    FROM logical_symbol_members
                    WHERE logical_symbol_id = ?8
                 )"
            },
            GraphResolutionMode::Syntactic => {
                "(edges.to_symbol_id IN (
                    SELECT symbol_id
                    FROM logical_symbol_members
                    WHERE logical_symbol_id = ?8
                  )
                  OR edges.target_qualified_name = ?1)"
            },
            GraphResolutionMode::Fuzzy => {
                "edges.to_symbol_id IN (
                    SELECT symbol_id
                    FROM logical_symbol_members
                    WHERE logical_symbol_id = ?8
                 )
                 OR to_symbols.name = ?3
                 OR to_symbols.qualified_name = ?1
                 OR to_symbols.qualified_name LIKE ?2
                 OR edges.target_qualified_name = ?1
                 OR edges.target_qualified_name LIKE ?2
                 OR (?4 = 'true' AND edges.to_name = ?3)"
            },
        };
    }
    match mode {
        GraphResolutionMode::Exact => {
            "edges.to_symbol_id IS NOT NULL
             AND (edges.to_symbol_id = ?6 OR to_symbols.qualified_name = ?1)"
        },
        GraphResolutionMode::Syntactic => {
            "(edges.to_symbol_id = ?6
              OR to_symbols.qualified_name = ?1
              OR (?7 = 'true' AND to_symbols.name = ?3)
              OR edges.target_qualified_name = ?1)"
        },
        GraphResolutionMode::Fuzzy => {
            "to_symbols.name = ?3
             OR to_symbols.qualified_name = ?1
             OR to_symbols.qualified_name LIKE ?2
             OR edges.target_qualified_name = ?1
             OR edges.target_qualified_name LIKE ?2
             OR (?4 = 'true' AND edges.to_name = ?3)"
        },
    }
}

fn reverse_tier(mode: GraphResolutionMode) -> &'static str {
    match mode {
        GraphResolutionMode::Exact => "0",
        GraphResolutionMode::Syntactic => {
            "CASE
                WHEN edges.to_symbol_id IS NOT NULL THEN 0
                WHEN edges.target_qualified_name = ?1 THEN 1
                ELSE 4
             END"
        },
        GraphResolutionMode::Fuzzy => {
            "CASE
                WHEN edges.to_symbol_id IS NOT NULL THEN 0
                WHEN edges.target_qualified_name = ?1 OR edges.target_qualified_name LIKE ?2 THEN 1
                WHEN ?4 = 'true' AND edges.to_name = ?3 THEN 2
                ELSE 4
             END"
        },
    }
}

fn forward_source_predicate(mode: GraphResolutionMode, logical: bool) -> &'static str {
    if logical {
        return match mode {
            GraphResolutionMode::Exact => {
                "from_symbols.id IS NOT NULL
                 AND from_symbols.id IN (
                    SELECT symbol_id
                    FROM logical_symbol_members
                    WHERE logical_symbol_id = ?8
                 )"
            },
            GraphResolutionMode::Syntactic => {
                "from_symbols.id IN (
                    SELECT symbol_id
                    FROM logical_symbol_members
                    WHERE logical_symbol_id = ?8
                 )
                 OR edges.from_name = ?1"
            },
            GraphResolutionMode::Fuzzy => {
                "from_symbols.id IN (
                    SELECT symbol_id
                    FROM logical_symbol_members
                    WHERE logical_symbol_id = ?8
                 )
                 OR from_symbols.name = ?3
                 OR from_symbols.qualified_name = ?1
                 OR from_symbols.qualified_name LIKE ?2
                 OR edges.from_name = ?1
                 OR edges.from_name LIKE ?2"
            },
        };
    }
    match mode {
        GraphResolutionMode::Exact => {
            "from_symbols.id IS NOT NULL
             AND (from_symbols.id = ?6 OR from_symbols.qualified_name = ?1)"
        },
        GraphResolutionMode::Syntactic => {
            "from_symbols.id = ?6
             OR from_symbols.qualified_name = ?1
             OR (?7 = 'true' AND from_symbols.name = ?3)
             OR edges.from_name = ?1"
        },
        GraphResolutionMode::Fuzzy => {
            "from_symbols.name = ?3
             OR from_symbols.qualified_name = ?1
             OR from_symbols.qualified_name LIKE ?2
             OR edges.from_name = ?1
             OR edges.from_name LIKE ?2"
        },
    }
}

fn forward_target_filter(
    mode: GraphResolutionMode,
    options: &GraphTraversalOptions,
) -> &'static str {
    match mode {
        GraphResolutionMode::Exact => "edges.to_symbol_id IS NOT NULL",
        GraphResolutionMode::Syntactic => {
            if options.include_unresolved {
                "1 = 1"
            } else if options.include_macros {
                "
                edges.to_symbol_id IS NOT NULL
                OR edges.target_qualified_name IS NOT NULL
                OR edges.edge_kind = 'uses_macro'
                "
            } else {
                "edges.to_symbol_id IS NOT NULL OR edges.target_qualified_name IS NOT NULL"
            }
        },
        GraphResolutionMode::Fuzzy => "1 = 1",
    }
}

fn forward_visibility_filter(options: &GraphTraversalOptions) -> &'static str {
    match (
        options.include_unresolved,
        options.include_macros,
        options.include_common_methods,
    ) {
        (true, true, true) => "1 = 1",
        (true, true, false) => {
            "
            (
                edges.edge_kind != 'calls_name'
                OR edges.to_name NOT IN (
                    'clone', 'map', 'map_err', 'and_then', 'unwrap_or', 'unwrap_or_else',
                    'to_string', 'to_owned', 'as_ref', 'as_mut', 'get', 'insert',
                    'new', 'default', 'into', 'from', 'iter', 'collect', 'unwrap',
                    'expect', 'ok', 'err'
                )
                OR edges.to_symbol_id IS NOT NULL
            )
            "
        },
        (true, false, true) => "edges.edge_kind != 'uses_macro'",
        (true, false, false) => {
            "
            edges.edge_kind != 'uses_macro'
            AND (
                edges.edge_kind != 'calls_name'
                OR edges.to_name NOT IN (
                    'clone', 'map', 'map_err', 'and_then', 'unwrap_or', 'unwrap_or_else',
                    'to_string', 'to_owned', 'as_ref', 'as_mut', 'get', 'insert',
                    'new', 'default', 'into', 'from', 'iter', 'collect', 'unwrap',
                    'expect', 'ok', 'err'
                )
                OR edges.to_symbol_id IS NOT NULL
            )
            "
        },
        (false, true, true) => {
            "
            (
                edges.edge_kind = 'calls_name'
                AND (
                    edges.to_symbol_id IS NOT NULL
                    OR (edges.confidence = 'Syntactic' AND edges.target_qualified_name IS NOT NULL)
                )
            )
            OR (
                edges.edge_kind = 'constructs'
                AND edges.to_symbol_id IS NOT NULL
            )
            OR edges.edge_kind = 'uses_macro'
            OR edges.edge_kind NOT IN ('calls_name', 'constructs')
            "
        },
        (false, true, false) => {
            "
            (
                edges.edge_kind = 'calls_name'
                AND (
                    edges.to_symbol_id IS NOT NULL
                    OR (edges.confidence = 'Syntactic' AND edges.target_qualified_name IS NOT NULL)
                )
                AND (
                    edges.to_name NOT IN (
                        'clone', 'map', 'map_err', 'and_then', 'unwrap_or', 'unwrap_or_else',
                        'to_string', 'to_owned', 'as_ref', 'as_mut', 'get', 'insert',
                        'new', 'default', 'into', 'from', 'iter', 'collect', 'unwrap',
                        'expect', 'ok', 'err'
                    )
                    OR edges.to_symbol_id IS NOT NULL
                )
            )
            OR (
                edges.edge_kind = 'constructs'
                AND edges.to_symbol_id IS NOT NULL
            )
            OR edges.edge_kind = 'uses_macro'
            OR edges.edge_kind NOT IN ('calls_name', 'constructs')
            "
        },
        (false, false, true) => {
            "
            edges.edge_kind != 'uses_macro'
            AND (
                (
                    edges.edge_kind = 'calls_name'
                    AND (
                        edges.to_symbol_id IS NOT NULL
                        OR (edges.confidence = 'Syntactic' AND edges.target_qualified_name IS NOT NULL)
                    )
                )
                OR (
                    edges.edge_kind = 'constructs'
                    AND edges.to_symbol_id IS NOT NULL
                )
                OR edges.edge_kind NOT IN ('calls_name', 'constructs')
            )
            "
        },
        (false, false, false) => {
            "
            edges.edge_kind != 'uses_macro'
            AND (
                (
                    edges.edge_kind = 'calls_name'
                    AND (
                        edges.to_symbol_id IS NOT NULL
                        OR (edges.confidence = 'Syntactic' AND edges.target_qualified_name IS NOT NULL)
                    )
                    AND (
                        edges.to_name NOT IN (
                            'clone', 'map', 'map_err', 'and_then', 'unwrap_or', 'unwrap_or_else',
                            'to_string', 'to_owned', 'as_ref', 'as_mut', 'get', 'insert',
                            'new', 'default', 'into', 'from', 'iter', 'collect', 'unwrap',
                            'expect', 'ok', 'err'
                        )
                        OR edges.to_symbol_id IS NOT NULL
                    )
                )
                OR (
                    edges.edge_kind = 'constructs'
                    AND edges.to_symbol_id IS NOT NULL
                )
                OR edges.edge_kind NOT IN ('calls_name', 'constructs')
            )
            "
        },
    }
}

fn unique_symbol_name(conn: &Connection, name: &str) -> anyhow::Result<bool> {
    let count: i64 = conn.query_row(
        "SELECT COUNT(*) AS symbol_count FROM symbols WHERE name = ?1",
        [name],
        |row| row.get("symbol_count"),
    )?;
    Ok(count == 1)
}

fn resolution_label(
    mode: GraphResolutionMode,
    stored: String,
    tier: i64,
    verified_target_symbol: bool,
) -> String {
    if mode == GraphResolutionMode::Exact && verified_target_symbol {
        return "exact".to_string();
    }
    if stored != "unresolved" {
        return stored;
    }
    match tier {
        1 => "target_qualified_suffix".to_string(),
        2 => "target_name_fallback".to_string(),
        _ => stored,
    }
}

fn short_name(symbol: &str) -> &str {
    symbol.rsplit([':', '.', '#', '/']).find(|part| !part.is_empty()).unwrap_or(symbol)
}

fn is_qualified_symbol(symbol: &str) -> bool {
    symbol.contains("::")
        || symbol.contains(".rs:")
        || symbol.contains(".ts:")
        || symbol.contains(".tsx:")
        || symbol.contains(".kt:")
        || symbol.contains('/')
}
