use crate::symbols::{QueryPattern, SymbolInfo, symbols_for_file};
use rayon::prelude::*;
use ruff_db::files::File;
use ty_project::{Db, parallel::ParallelIteratorExt};

/// Get all workspace symbols matching the query string.
/// Returns symbols from all files in the workspace, filtered by the query.
pub fn workspace_symbols(db: &dyn Db, query: &str) -> Vec<WorkspaceSymbolInfo> {
    // If the query is empty, return immediately to avoid expensive file scanning
    if query.is_empty() {
        return Vec::new();
    }

    let workspace_symbols_span = tracing::debug_span!("workspace_symbols");
    let _span = workspace_symbols_span.enter();

    let project = db.project();

    let query = QueryPattern::fuzzy(query);
    let files = project.files(db);
    let files: Vec<_> = files.iter().copied().collect();

    files
        .into_par_iter()
        .map_with_db(db, |db, file| {
            let symbols_for_file_span = tracing::debug_span!(
                parent: &workspace_symbols_span,
                "symbols_for_file",
                ?file
            );
            let _entered = symbols_for_file_span.entered();

            symbols_for_file(db, file)
                .search(&query)
                .map(|(_, symbol)| WorkspaceSymbolInfo {
                    symbol: symbol.to_owned(),
                    file,
                })
                .collect::<Vec<_>>()
        })
        .flat_map_iter(|symbols| symbols)
        .collect()
}

/// A symbol found in the workspace, including the file it was found in.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkspaceSymbolInfo {
    /// The symbol information
    pub symbol: SymbolInfo<'static>,
    /// The file containing the symbol
    pub file: File,
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::tests::CursorTest;
    use crate::tests::IntoDiagnostic;
    use insta::assert_snapshot;
    use ruff_db::diagnostic::{
        Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic,
        SubDiagnosticSeverity,
    };

    #[test]
    fn workspace_symbols_multi_file() {
        let test = CursorTest::builder()
            .source(
                "utils.py",
                "
def utility_function():
    '''A helpful utility function'''
    pass
",
            )
            .source(
                "models.py",
                "
class DataModel:
    '''A data model class'''
    def __init__(self):
        pass
",
            )
            .source(
                "constants.py",
                "
API_BASE_URL = 'https://api.example.com'
<CURSOR>",
            )
            .build();

        assert_snapshot!(test.workspace_symbols("ufunc"), @"
        info[workspace-symbols]: WorkspaceSymbolInfo
         --> utils.py:2:5
          |
        2 | def utility_function():
          |     ^^^^^^^^^^^^^^^^
          |
        info: Function utility_function
        ");

        assert_snapshot!(test.workspace_symbols("data"), @"
        info[workspace-symbols]: WorkspaceSymbolInfo
         --> models.py:2:7
          |
        2 | class DataModel:
          |       ^^^^^^^^^
          |
        info: Class DataModel
        ");

        assert_snapshot!(test.workspace_symbols("apibase"), @"
        info[workspace-symbols]: WorkspaceSymbolInfo
         --> constants.py:2:1
          |
        2 | API_BASE_URL = 'https://api.example.com'
          | ^^^^^^^^^^^^
          |
        info: Constant API_BASE_URL
        ");
    }

    #[test]
    fn members() {
        let test = CursorTest::builder()
            .source(
                "utils.py",
                "
class Test:
    def from_path(): ...
<CURSOR>",
            )
            .build();

        assert_snapshot!(test.workspace_symbols("from"), @"
        info[workspace-symbols]: WorkspaceSymbolInfo
         --> utils.py:3:9
          |
        3 |     def from_path(): ...
          |         ^^^^^^^^^
          |
        info: Method from_path
        ");
    }

    #[test]
    fn ignore_all() {
        let test = CursorTest::builder()
            .source(
                "utils.py",
                "
__all__ = []
class Test:
    def from_path(): ...
<CURSOR>",
            )
            .build();

        assert_snapshot!(test.workspace_symbols("from"), @"
        info[workspace-symbols]: WorkspaceSymbolInfo
         --> utils.py:4:9
          |
        4 |     def from_path(): ...
          |         ^^^^^^^^^
          |
        info: Method from_path
        ");
    }

    #[test]
    fn ignore_imports() {
        let test = CursorTest::builder()
            .source(
                "utils.py",
                "
import re
import json as json
from collections import defaultdict
foo = 1
<CURSOR>",
            )
            .build();

        assert_snapshot!(test.workspace_symbols("foo"), @"
        info[workspace-symbols]: WorkspaceSymbolInfo
         --> utils.py:5:1
          |
        5 | foo = 1
          | ^^^
          |
        info: Variable foo
        ");
        assert_snapshot!(test.workspace_symbols("re"), @"No symbols found");
        assert_snapshot!(test.workspace_symbols("json"), @"No symbols found");
        assert_snapshot!(test.workspace_symbols("default"), @"No symbols found");
    }

    impl CursorTest {
        fn workspace_symbols(&self, query: &str) -> String {
            let symbols = workspace_symbols(&self.db, query);

            if symbols.is_empty() {
                return "No symbols found".to_string();
            }

            self.render_diagnostics(symbols.into_iter().map(WorkspaceSymbolDiagnostic::new))
        }
    }

    struct WorkspaceSymbolDiagnostic {
        symbol_info: WorkspaceSymbolInfo,
    }

    impl WorkspaceSymbolDiagnostic {
        fn new(symbol_info: WorkspaceSymbolInfo) -> Self {
            Self { symbol_info }
        }
    }

    impl IntoDiagnostic for WorkspaceSymbolDiagnostic {
        fn into_diagnostic(self) -> Diagnostic {
            let symbol_kind_str = self.symbol_info.symbol.kind.to_string();

            let info_text = format!("{} {}", symbol_kind_str, self.symbol_info.symbol.name);

            let sub = SubDiagnostic::new(SubDiagnosticSeverity::Info, info_text);

            let mut main = Diagnostic::new(
                DiagnosticId::Lint(LintName::of("workspace-symbols")),
                Severity::Info,
                "WorkspaceSymbolInfo".to_string(),
            );
            main.annotate(Annotation::primary(
                Span::from(self.symbol_info.file).with_range(self.symbol_info.symbol.name_range),
            ));
            main.sub(sub);

            main
        }
    }
}
