mod historical;
mod items;
mod neighbors;
mod select;
use std::collections::{BTreeMap, BTreeSet};

pub(crate) use historical::*;
pub(crate) use items::*;
pub(crate) use neighbors::*;
use rusqlite::{Connection, OptionalExtension, params};
pub(crate) use select::*;
use serde::Serialize;

use crate::query::graph::{self, GraphHop, GraphResolutionMode, GraphTraversalOptions};
use crate::query::memory::{self, RepoMemoryEvidence};
use crate::query::symbol::SymbolHit;

#[derive(Debug, Serialize)]
pub struct ImpactItem {
    pub path: String,
    pub language: String,
    pub kind: String,
    pub symbol: Option<String>,
    pub category: String,
    pub reason: String,
    pub evidence: Vec<String>,
}

#[derive(Debug, Clone)]
pub struct ImpactSurfaceOptions {
    pub resolution_mode: GraphResolutionMode,
    pub include_tests: bool,
    pub include_docs: bool,
    pub include_git: bool,
    pub include_papertrail: bool,
    pub include_text_fallback: bool,
    pub include_memories: bool,
}

#[derive(Debug, Serialize)]
pub struct ImpactSurfaceReport {
    pub query: ImpactSurfaceQuery,
    pub direct_semantic_callers: Vec<GraphHop>,
    pub direct_semantic_callees: Vec<GraphHop>,
    pub import_export_dependents: Vec<ImpactItem>,
    pub tests_touching_symbol_path: Vec<ImpactItem>,
    pub docs_mentioning_symbol_path: Vec<ImpactItem>,
    pub text_fallback_hits: Vec<ImpactItem>,
    pub recent_commits_touching_symbol_path: Vec<ImpactItem>,
    pub github_rationale_issues_prs: Vec<ImpactItem>,
    pub repo_memories: RepoMemoryEvidence,
    pub completeness_and_caveats: ImpactCompleteness,
}

#[derive(Debug, Serialize)]
pub struct ImpactSurfaceQuery {
    pub symbol_id: Option<i64>,
    pub symbol_path: Option<String>,
    pub query: Option<String>,
    pub resolution: String,
    pub include_tests: bool,
    pub include_docs: bool,
    pub include_git: bool,
    pub include_papertrail: bool,
    pub include_text_fallback: bool,
    pub include_memories: bool,
}

#[derive(Debug, Default, Serialize)]
pub struct ImpactCompleteness {
    pub exact_graph_callers: u64,
    pub graph_callees: u64,
    pub text_fallback_hits: u64,
    pub parser_failures: u64,
    pub stale_files: u64,
    pub memory_status: ImpactMemoryStatus,
    pub caveats: Vec<String>,
}

#[derive(Debug, Default, Serialize)]
pub struct ImpactMemoryStatus {
    pub active: u64,
    pub stale: u64,
}

impl Default for ImpactSurfaceOptions {
    fn default() -> Self {
        Self {
            resolution_mode: GraphResolutionMode::Syntactic,
            include_tests: true,
            include_docs: true,
            include_git: true,
            include_papertrail: true,
            include_text_fallback: true,
            include_memories: true,
        }
    }
}

pub fn impact_surface(
    conn: &Connection,
    query: &str,
    limit: u32,
) -> anyhow::Result<Vec<ImpactItem>> {
    impact_surface_with_options(conn, query, limit, GraphResolutionMode::Syntactic)
}

pub fn impact_surface_report_for_symbol(
    conn: &Connection,
    symbol: &SymbolHit,
    limit: u32,
    options: &ImpactSurfaceOptions,
) -> anyhow::Result<ImpactSurfaceReport> {
    let graph_options = GraphTraversalOptions {
        resolution_mode: options.resolution_mode,
        symbol_id: Some(symbol.symbol_id),
        logical_symbol_id: symbol.logical_symbol_id,
        ..Default::default()
    };
    let direct_semantic_callers =
        graph::traverse_with_options(conn, &symbol.qualified_name, true, limit, &graph_options)?;
    let direct_semantic_callees =
        graph::traverse_with_options(conn, &symbol.qualified_name, false, limit, &graph_options)?;
    let names = vec![symbol.name.clone(), symbol.qualified_name.clone()];
    let import_export_dependents =
        import_export_items(conn, symbol.symbol_id, &symbol.qualified_name, &names, limit)?;
    let tests_touching_symbol_path =
        if options.include_tests { test_items(conn, symbol, &names, limit)? } else { Vec::new() };
    let docs_mentioning_symbol_path =
        if options.include_docs { docs_items(conn, symbol, &names, limit)? } else { Vec::new() };
    let text_fallback_hits = if options.include_text_fallback {
        text_fallback_items(conn, symbol, &names, limit)?
    } else {
        Vec::new()
    };
    let recent_commits_touching_symbol_path = if options.include_git {
        git_commit_items(conn, std::slice::from_ref(&symbol.path), limit)?
    } else {
        Vec::new()
    };
    let github_rationale_issues_prs = if options.include_papertrail {
        let mut items = github_ref_items(conn, std::slice::from_ref(&symbol.path), limit)?;
        items.extend(github_rationale_items(conn, &symbol.qualified_name, limit)?);
        items.truncate(usize::try_from(limit).unwrap_or(usize::MAX));
        items
    } else {
        Vec::new()
    };
    let repo_memories = if options.include_memories {
        let caller_edge_ids =
            direct_semantic_callers.iter().map(|hop| hop.edge_id).collect::<Vec<_>>();
        let callee_edge_ids =
            direct_semantic_callees.iter().map(|hop| hop.edge_id).collect::<Vec<_>>();
        memory::memory_evidence_for_symbol_and_edges(
            conn,
            symbol,
            &caller_edge_ids,
            &callee_edge_ids,
            limit,
        )?
    } else {
        RepoMemoryEvidence {
            direct: Vec::new(),
            path_crossed: Vec::new(),
            call_path_crossed: Vec::new(),
            stale: Vec::new(),
        }
    };
    let mut caveats = vec![
        "Graph evidence is tree-sitter/syntactic, not compiler-grade name resolution.".to_string(),
    ];
    if options.resolution_mode == GraphResolutionMode::Exact
        && direct_semantic_callers.is_empty()
        && !text_fallback_hits.is_empty()
    {
        caveats.push(format!(
            "No exact graph callers found. Text search found {} symbol/path hits. This likely \
             indicates graph extraction or resolution gaps.",
            text_fallback_hits.len()
        ));
    }
    Ok(ImpactSurfaceReport {
        query: ImpactSurfaceQuery {
            symbol_id: Some(symbol.symbol_id),
            symbol_path: Some(symbol.qualified_name.clone()),
            query: None,
            resolution: options.resolution_mode.as_str().to_string(),
            include_tests: options.include_tests,
            include_docs: options.include_docs,
            include_git: options.include_git,
            include_papertrail: options.include_papertrail,
            include_text_fallback: options.include_text_fallback,
            include_memories: options.include_memories,
        },
        completeness_and_caveats: ImpactCompleteness {
            exact_graph_callers: u64::try_from(direct_semantic_callers.len()).unwrap_or(u64::MAX),
            graph_callees: u64::try_from(direct_semantic_callees.len()).unwrap_or(u64::MAX),
            text_fallback_hits: u64::try_from(text_fallback_hits.len()).unwrap_or(u64::MAX),
            parser_failures: parser_failure_count(conn)?,
            stale_files: 0,
            memory_status: ImpactMemoryStatus {
                active: u64::try_from(
                    repo_memories.direct.len()
                        + repo_memories.path_crossed.len()
                        + repo_memories.call_path_crossed.len(),
                )
                .unwrap_or(u64::MAX),
                stale: u64::try_from(repo_memories.stale.len()).unwrap_or(u64::MAX),
            },
            caveats,
        },
        direct_semantic_callers,
        direct_semantic_callees,
        import_export_dependents,
        tests_touching_symbol_path,
        docs_mentioning_symbol_path,
        text_fallback_hits,
        recent_commits_touching_symbol_path,
        github_rationale_issues_prs,
        repo_memories,
    })
}

pub fn impact_surface_with_options(
    conn: &Connection,
    query: &str,
    limit: u32,
    resolution_mode: GraphResolutionMode,
) -> anyhow::Result<Vec<ImpactItem>> {
    impact_surface_from_targets(conn, query, None, limit, resolution_mode)
}

pub fn impact_surface_for_symbol(
    conn: &Connection,
    symbol: &SymbolHit,
    limit: u32,
    resolution_mode: GraphResolutionMode,
) -> anyhow::Result<Vec<ImpactItem>> {
    let target = SymbolTarget {
        id: symbol.symbol_id,
        file_id: symbol.file_id,
        path: symbol.path.clone(),
        language: symbol.language.clone(),
        file_kind: symbol.file_kind.clone(),
        name: symbol.name.clone(),
        qualified_name: symbol.qualified_name.clone(),
    };
    impact_surface_from_targets(
        conn,
        &symbol.qualified_name,
        Some(vec![target]),
        limit,
        resolution_mode,
    )
}

fn impact_surface_from_targets(
    conn: &Connection,
    query: &str,
    selected_targets: Option<Vec<SymbolTarget>>,
    limit: u32,
    resolution_mode: GraphResolutionMode,
) -> anyhow::Result<Vec<ImpactItem>> {
    let max_items = usize::try_from(limit).unwrap_or(usize::MAX);
    let mut surface = ImpactSurface::default();
    let targets = match selected_targets {
        Some(targets) => targets,
        None => exact_symbols(conn, query)?,
    };
    let target_names = target_names(query, &targets);

    for symbol in &targets {
        surface.push(
            ImpactCategory::DirectStructural,
            FileSymbol {
                path: symbol.path.clone(),
                language: symbol.language.clone(),
                kind: symbol.file_kind.clone(),
                symbol: Some(symbol.qualified_name.clone()),
            },
            "exact_symbol_definition",
            format!("defined as {}", symbol.qualified_name),
        );
    }

    graph_neighbors(conn, &targets, &target_names, true, resolution_mode, &mut surface)?;
    graph_neighbors(conn, &targets, &target_names, false, resolution_mode, &mut surface)?;
    import_export_dependents(conn, &targets, &target_names, &mut surface)?;
    same_file_siblings(conn, &targets, &mut surface)?;

    if surface.len() < max_items {
        let remaining = max_items.saturating_sub(surface.len());
        textual_fallback(conn, query, &mut surface, remaining)?;
    }

    let current_paths = surface.current_paths();
    historical_evidence(conn, &current_paths, query, &mut surface, max_items)?;

    Ok(surface.into_items(max_items))
}

pub fn ffi_surface(conn: &Connection, limit: u32) -> anyhow::Result<Vec<ImpactItem>> {
    let mut stmt = conn.prepare(
        "
        WITH rust_exports AS (
            SELECT DISTINCT
                   files.path AS path,
                   files.language AS language,
                   files.kind AS kind,
                   symbols.qualified_name AS symbol,
                   CASE
                       WHEN symbols.kind = 'impl' THEN 'rust_uniffi_exported_impl'
                       ELSE 'rust_uniffi_export'
                   END AS reason
            FROM symbols
            JOIN files ON files.id = symbols.file_id
            JOIN symbol_facts
              ON symbol_facts.symbol_id = symbols.id
             AND symbol_facts.fact_kind = 'rust_attr'
             AND symbol_facts.fact_value = 'uniffi_export'
            WHERE files.language = 'rust'
              AND symbols.kind IN ('function', 'method', 'impl', 'struct', 'enum', 'trait')
        ),
        rust_exported_impl_members AS (
            SELECT DISTINCT
                   files.path AS path,
                   files.language AS language,
                   files.kind AS kind,
                   members.qualified_name AS symbol,
                   'rust_uniffi_impl_member' AS reason
            FROM symbols AS impls
            JOIN files ON files.id = impls.file_id
            JOIN symbol_facts
              ON symbol_facts.symbol_id = impls.id
             AND symbol_facts.fact_kind = 'rust_attr'
             AND symbol_facts.fact_value = 'uniffi_export'
            JOIN symbols AS members
              ON members.file_id = impls.file_id
             AND members.start_byte > impls.start_byte
             AND members.end_byte < impls.end_byte
             AND members.kind IN ('function', 'method')
            WHERE files.language = 'rust'
              AND impls.kind = 'impl'
        ),
        binding_refs AS (
            -- Generated/binding artifacts detected by path. Detection is generic on purpose:
            -- matching specific native-symbol substrings in chunk text was project-specific and
            -- self-matched any source that merely mentions those names (e.g. this query). The
            -- `#[uniffi::export]` symbol facts above are the principled, language-level signal.
            SELECT DISTINCT
                   files.path AS path,
                   files.language AS language,
                   files.kind AS kind,
                   chunks.symbol_path AS symbol,
                   'generated_binding_artifact' AS reason
            FROM files
            JOIN chunks ON chunks.file_id = files.id
            WHERE files.path LIKE '%/src/generated/%'
               OR files.path LIKE '%/generated/%'
               OR files.path LIKE '%generated-manifest.json'
        )
        SELECT path, language, kind, symbol, reason FROM rust_exports
        UNION
        SELECT path, language, kind, symbol, reason FROM rust_exported_impl_members
        UNION
        SELECT path, language, kind, symbol, reason FROM binding_refs
        ORDER BY reason, kind DESC, path
        LIMIT ?1
        ",
    )?;
    rows_to_items(stmt.query_map([limit], |row| {
        let reason: String = row.get(4)?;
        Ok(ImpactItem {
            path: row.get(0)?,
            language: row.get(1)?,
            kind: row.get(2)?,
            symbol: row.get(3)?,
            category: ImpactCategory::ProbableTextual.as_str().to_string(),
            reason: reason.clone(),
            evidence: ffi_surface_evidence(&reason),
        })
    })?)
}

fn ffi_surface_evidence(reason: &str) -> Vec<String> {
    let mut evidence = vec![format!("ffi_surface evidence class: {reason}")];
    match reason {
        "rust_uniffi_impl_member" => {
            evidence.push(
                "member symbol is inside a chunk containing an exported UniFFI impl".to_string(),
            );
            evidence.push(
                "this row is not claiming the member itself has a #[uniffi::export] attribute"
                    .to_string(),
            );
        },
        "rust_uniffi_exported_impl" => {
            evidence.push(
                "exported impl/type surface; member rows are reported separately when symbols are \
                 available"
                    .to_string(),
            );
        },
        _ => {},
    }
    evidence
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum ImpactCategory {
    DirectStructural,
    ProbableTextual,
    HistoricalPapertrail,
}

impl ImpactCategory {
    fn as_str(self) -> &'static str {
        match self {
            Self::DirectStructural => "Direct structural impact",
            Self::ProbableTextual => "Probable textual impact",
            Self::HistoricalPapertrail => "Historical/papertrail evidence",
        }
    }
}

#[derive(Debug, Clone)]
pub(crate) struct FileSymbol {
    path: String,
    language: String,
    kind: String,
    symbol: Option<String>,
}

#[derive(Debug, Clone)]
pub(crate) struct SymbolTarget {
    id: i64,
    file_id: i64,
    path: String,
    language: String,
    file_kind: String,
    name: String,
    qualified_name: String,
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct ImpactKey {
    category: &'static str,
    path: String,
    symbol: Option<String>,
    reason: String,
}

#[derive(Default)]
pub(crate) struct ImpactSurface {
    items: BTreeMap<ImpactKey, ImpactItem>,
}

impl ImpactSurface {
    fn len(&self) -> usize {
        self.items.len()
    }

    fn push(
        &mut self,
        category: ImpactCategory,
        file_symbol: FileSymbol,
        reason: impl Into<String>,
        evidence: impl Into<String>,
    ) {
        let reason = reason.into();
        let key = ImpactKey {
            category: category.as_str(),
            path: file_symbol.path.clone(),
            symbol: file_symbol.symbol.clone(),
            reason: reason.clone(),
        };
        let item = self.items.entry(key).or_insert_with(|| ImpactItem {
            path: file_symbol.path,
            language: file_symbol.language,
            kind: file_symbol.kind,
            symbol: file_symbol.symbol,
            category: category.as_str().to_string(),
            reason,
            evidence: Vec::new(),
        });
        let evidence = evidence.into();
        if !item.evidence.iter().any(|value| value == &evidence) {
            item.evidence.push(evidence);
        }
    }

    fn current_paths(&self) -> Vec<String> {
        let mut paths = BTreeSet::new();
        for item in self.items.values() {
            if item.category != ImpactCategory::HistoricalPapertrail.as_str() {
                paths.insert(item.path.clone());
            }
        }
        paths.into_iter().collect()
    }

    fn into_items(self, limit: usize) -> Vec<ImpactItem> {
        let mut items = self.items.into_values().collect::<Vec<_>>();
        items.sort_by_key(|item| {
            (
                category_rank(&item.category),
                reason_rank(&item.reason),
                item.path.clone(),
                item.symbol.clone().unwrap_or_default(),
            )
        });
        items.truncate(limit);
        items
    }
}
