use crate::analyzer::cognitive_complexity;
use crate::analyzer::common::language_for_file as file_language;
use crate::analyzer::usages::{
    ExportEntry, ExportIndex, ImportBinder, ImportBinding, ImportKind, ReexportStar,
};
use crate::analyzer::{
    AnalyzerConfig, CodeUnit, CodeUnitType, IAnalyzer, ImportAnalysisProvider, ImportInfo,
    Language, LanguageAdapter, Project, ProjectFile, TestAssertionSmell, TestAssertionWeights,
    TestDetectionProvider, TreeSitterAnalyzer, TypeHierarchyProvider, build_reverse_import_index,
    direct_descendants_via_ancestors,
};
use crate::hash::{HashMap, HashSet};
use crate::profiling;
use crate::text_utils::{compute_line_starts, find_line_index_for_offset};
use crate::{
    analyzer::clone_detection::{
        CloneCandidateData, CloneCandidateProfile, compact_clone_excerpt,
        compute_ast_refinement_similarity_percent, detect_structural_clone_smells,
    },
    analyzer::{CloneSmell, CloneSmellWeights},
};
use moka::sync::Cache;
use regex::Regex;
use std::collections::BTreeSet;
use std::mem::size_of;
use std::path::Path;
use std::sync::{Arc, LazyLock, OnceLock};
use tree_sitter::{Language as TsLanguage, Node, Parser, Tree};

use super::javascript_analyzer::build_weighted_cache;
/// Tree-sitter node-kind mapping used by the cognitive-complexity scorer
/// for Python. Mirrors `ai.brokk.analyzer.python.CognitiveComplexityAnalysis`.
static PYTHON_COGNITIVE_CONFIG: LazyLock<cognitive_complexity::Config> =
    LazyLock::new(|| cognitive_complexity::Config {
        if_types: &["if_statement"],
        alternate_if_types: &["elif_clause"],
        loop_types: &["for_statement", "while_statement"],
        catch_types: &["except_clause"],
        conditional_types: &["conditional_expression"],
        case_types: &["case_clause"],
        binary_types: &["boolean_operator"],
        logical_operators: &["and", "or"],
        named_function_boundary_types: &["function_definition"],
        anonymous_function_types: &["lambda"],
        named_function_boundary_predicate: Some(python_is_decorated_function_boundary),
        ..cognitive_complexity::Config::empty()
    });

fn python_is_decorated_function_boundary(node: Node<'_>) -> bool {
    if node.kind() != "decorated_definition" {
        return false;
    }
    let mut cursor = node.walk();
    node.named_children(&mut cursor)
        .any(|child| child.kind() == "function_definition")
}

#[derive(Debug, Clone, Default)]
pub struct PythonAdapter;

impl LanguageAdapter for PythonAdapter {
    fn language(&self) -> Language {
        Language::Python
    }

    fn query_directory(&self) -> &'static str {
        "resources/treesitter/python"
    }

    fn parser_language(&self) -> TsLanguage {
        tree_sitter_python::LANGUAGE.into()
    }

    fn file_extension(&self) -> &'static str {
        "py"
    }

    fn contains_tests(
        &self,
        _file: &ProjectFile,
        source: &str,
        _tree: &Tree,
        _parsed: &crate::analyzer::tree_sitter_analyzer::ParsedFile,
    ) -> bool {
        python_source_contains_tests(source)
    }

    fn extract_call_receiver(&self, reference: &str) -> Option<String> {
        let trimmed = reference.trim();
        let before_args = trimmed
            .split_once('(')
            .map(|(head, _)| head)
            .unwrap_or(trimmed);
        before_args
            .rsplit_once('.')
            .map(|(receiver, _)| receiver.to_string())
    }

    fn parse_file(
        &self,
        file: &ProjectFile,
        source: &str,
        tree: &Tree,
    ) -> crate::analyzer::tree_sitter_analyzer::ParsedFile {
        let module_fq = python_module_name(file);
        let mut parsed = crate::analyzer::tree_sitter_analyzer::ParsedFile::new(module_fq.clone());
        let root = tree.root_node();

        collect_python_identifiers(root, source, &mut parsed.type_identifiers);

        let module_code_unit = module_code_unit(file, &module_fq);
        if let Some(module) = module_code_unit.clone() {
            parsed.add_code_unit(module, root, source, None, None);
        }

        let mut visitor = PythonVisitor {
            file,
            source,
            package_name: &module_fq,
            parsed: &mut parsed,
            module: module_code_unit,
        };
        visitor.visit_container(root, &[], 0);

        parsed
    }

    fn cognitive_complexity_config(&self) -> Option<&'static cognitive_complexity::Config> {
        Some(&PYTHON_COGNITIVE_CONFIG)
    }
}

#[derive(Clone)]
pub struct PythonAnalyzer {
    inner: TreeSitterAnalyzer<PythonAdapter>,
    memo_budget: u64,
    imported_code_units: Cache<ProjectFile, Arc<HashSet<CodeUnit>>>,
    referencing_files: Cache<ProjectFile, Arc<HashSet<ProjectFile>>>,
    direct_ancestors: Cache<CodeUnit, Arc<Vec<CodeUnit>>>,
    direct_descendants: Cache<CodeUnit, Arc<HashSet<CodeUnit>>>,
    module_code_units: Arc<HashMap<String, CodeUnit>>,
    reverse_import_index: Arc<OnceLock<HashMap<ProjectFile, Arc<HashSet<ProjectFile>>>>>,
}

impl PythonAnalyzer {
    pub fn new(project: Arc<dyn Project>) -> Self {
        Self::new_with_config(project, AnalyzerConfig::default())
    }

    pub fn new_with_config(project: Arc<dyn Project>, config: AnalyzerConfig) -> Self {
        let memo_budget = config.memo_cache_budget_bytes();
        let inner = TreeSitterAnalyzer::new_with_config(project, PythonAdapter, config);
        Self::from_inner(inner, memo_budget)
    }

    pub fn new_with_config_and_storage(
        project: Arc<dyn Project>,
        config: AnalyzerConfig,
        storage: Arc<crate::analyzer::persistence::AnalyzerStorage>,
    ) -> Self {
        let memo_budget = config.memo_cache_budget_bytes();
        let inner = TreeSitterAnalyzer::new_with_config_and_storage(
            project,
            PythonAdapter,
            config,
            storage,
        );
        Self::from_inner(inner, memo_budget)
    }

    fn from_inner(inner: TreeSitterAnalyzer<PythonAdapter>, memo_budget: u64) -> Self {
        Self {
            module_code_units: Arc::new(build_python_module_code_units(&inner)),
            inner,
            memo_budget,
            imported_code_units: build_weighted_cache(memo_budget / 4, weight_code_unit_set),
            referencing_files: build_weighted_cache(memo_budget / 8, weight_project_file_set),
            direct_ancestors: build_weighted_cache(memo_budget / 8, weight_code_unit_vec),
            direct_descendants: build_weighted_cache(memo_budget / 8, weight_code_unit_set_by_unit),
            reverse_import_index: Arc::new(OnceLock::new()),
        }
    }

    pub fn from_project<P>(project: P) -> Self
    where
        P: Project + 'static,
    {
        Self::new(Arc::new(project))
    }

    fn extract_type_identifiers(&self, source: &str) -> BTreeSet<String> {
        let mut parser = Parser::new();
        parser
            .set_language(&tree_sitter_python::LANGUAGE.into())
            .expect("failed to load python parser");
        let Some(tree) = parser.parse(source, None) else {
            return BTreeSet::new();
        };
        let mut identifiers = HashSet::default();
        collect_python_identifiers(tree.root_node(), source, &mut identifiers);
        identifiers.into_iter().collect()
    }

    fn resolve_import_bindings(&self, file: &ProjectFile) -> HashMap<String, CodeUnit> {
        let mut bindings = HashMap::default();
        for import in self.inner.import_info_of(file) {
            for (binding, code_unit) in self.resolve_import(file, import) {
                bindings.insert(binding, code_unit);
            }
        }
        bindings
    }

    fn resolve_import(&self, file: &ProjectFile, import: &ImportInfo) -> Vec<(String, CodeUnit)> {
        if let Some(details) = parse_python_import_details(&import.raw_snippet) {
            match details {
                PythonImportDetails::Import { module, alias } => {
                    if let Some(module_code_unit) = self.resolve_module_code_unit(&module) {
                        let binding = alias.unwrap_or_else(|| {
                            module
                                .split('.')
                                .next_back()
                                .unwrap_or(module.as_str())
                                .to_string()
                        });
                        return vec![(binding, module_code_unit)];
                    }
                }
                PythonImportDetails::FromImport {
                    module,
                    name,
                    alias,
                    wildcard,
                } => {
                    let resolved_module = if module.starts_with('.') {
                        resolve_python_relative_module(file, &module)
                    } else {
                        Some(module)
                    };
                    let Some(resolved_module) = resolved_module else {
                        return Vec::new();
                    };
                    if wildcard {
                        return self
                            .public_declarations_in_module(&resolved_module)
                            .into_iter()
                            .map(|code_unit| (code_unit.identifier().to_string(), code_unit))
                            .collect();
                    }

                    let binding = alias.clone().unwrap_or_else(|| name.clone());
                    let module_candidate = format!("{resolved_module}.{name}");
                    if let Some(code_unit) = self.resolve_module_code_unit(&module_candidate) {
                        return vec![(binding, code_unit)];
                    }
                    let definitions: Vec<_> =
                        self.inner.definitions(&module_candidate).cloned().collect();
                    if !definitions.is_empty() {
                        return definitions
                            .into_iter()
                            .map(|code_unit| (binding.clone(), code_unit))
                            .collect();
                    }
                    let package_candidate: Vec<_> = self
                        .inner
                        .definitions(&format!("{resolved_module}.{name}"))
                        .cloned()
                        .collect();
                    if !package_candidate.is_empty() {
                        return package_candidate
                            .into_iter()
                            .map(|code_unit| (binding.clone(), code_unit))
                            .collect();
                    }
                }
            }
        }
        Vec::new()
    }

    fn resolve_module_code_unit(&self, module_fq: &str) -> Option<CodeUnit> {
        self.inner
            .definitions(module_fq)
            .find(|code_unit| code_unit.is_module())
            .cloned()
            .or_else(|| self.module_code_units.get(module_fq).cloned())
    }

    pub fn export_index_of(&self, file: &ProjectFile) -> ExportIndex {
        let mut index = ExportIndex::empty();

        for code_unit in self.inner.get_top_level_declarations(file) {
            let identifier = code_unit.identifier().trim();
            if identifier.is_empty() || identifier.starts_with('_') {
                continue;
            }
            index.exports_by_name.insert(
                identifier.to_string(),
                ExportEntry::Local {
                    local_name: identifier.to_string(),
                },
            );
        }

        for import in self.inner.import_info_of(file) {
            let Some(details) = parse_python_import_details(&import.raw_snippet) else {
                continue;
            };
            match details {
                PythonImportDetails::Import { .. } => {}
                PythonImportDetails::FromImport {
                    module,
                    name,
                    alias,
                    wildcard,
                } => {
                    let resolved_module = if module.starts_with('.') {
                        resolve_python_relative_module(file, &module)
                    } else {
                        Some(module.clone())
                    };
                    let Some(resolved_module) = resolved_module else {
                        continue;
                    };

                    if wildcard {
                        index.reexport_stars.push(ReexportStar {
                            module_specifier: resolved_module,
                        });
                        continue;
                    }

                    let exported_name = alias.unwrap_or(name.clone());
                    if exported_name.starts_with('_') {
                        continue;
                    }
                    let imported_name = format!("{resolved_module}.{name}");
                    if self.resolve_module_code_unit(&imported_name).is_some() {
                        index.exports_by_name.insert(
                            exported_name,
                            ExportEntry::ReexportedNamed {
                                module_specifier: imported_name,
                                imported_name: name,
                            },
                        );
                        continue;
                    }
                    index.exports_by_name.insert(
                        exported_name,
                        ExportEntry::ReexportedNamed {
                            module_specifier: resolved_module,
                            imported_name: name,
                        },
                    );
                }
            }
        }

        index
    }

    pub fn import_binder_of(&self, file: &ProjectFile) -> ImportBinder {
        let mut binder = ImportBinder::empty();

        for import in self.inner.import_info_of(file) {
            let Some(details) = parse_python_import_details(&import.raw_snippet) else {
                continue;
            };
            match details {
                PythonImportDetails::Import { module, alias } => {
                    let local_name = alias.unwrap_or_else(|| {
                        module
                            .split('.')
                            .next_back()
                            .unwrap_or(module.as_str())
                            .to_string()
                    });
                    binder.bindings.insert(
                        local_name,
                        ImportBinding {
                            module_specifier: module,
                            kind: ImportKind::Namespace,
                            imported_name: None,
                        },
                    );
                }
                PythonImportDetails::FromImport {
                    module,
                    name,
                    alias,
                    wildcard,
                } => {
                    let resolved_module = if module.starts_with('.') {
                        resolve_python_relative_module(file, &module)
                    } else {
                        Some(module.clone())
                    };
                    let Some(resolved_module) = resolved_module else {
                        continue;
                    };
                    if wildcard {
                        continue;
                    }
                    let local_name = alias.unwrap_or_else(|| name.clone());
                    let module_candidate = format!("{resolved_module}.{name}");
                    if self.resolve_module_code_unit(&module_candidate).is_some() {
                        binder.bindings.insert(
                            local_name,
                            ImportBinding {
                                module_specifier: module_candidate,
                                kind: ImportKind::Namespace,
                                imported_name: None,
                            },
                        );
                        continue;
                    }
                    binder.bindings.insert(
                        local_name,
                        ImportBinding {
                            module_specifier: resolved_module,
                            kind: ImportKind::Named,
                            imported_name: Some(name),
                        },
                    );
                }
            }
        }

        binder
    }

    fn build_reverse_import_index(&self) -> &HashMap<ProjectFile, Arc<HashSet<ProjectFile>>> {
        self.reverse_import_index.get_or_init(|| {
            let _scope = profiling::scope("PythonAnalyzer::build_reverse_import_index");
            let files: Vec<_> = self.inner.all_files().cloned().collect();
            let reverse =
                build_reverse_import_index(&files, |file| self.imported_code_units_of(file));

            if profiling::enabled() {
                profiling::note(format!(
                    "PythonAnalyzer::build_reverse_import_index files={} indexed_targets={}",
                    files.len(),
                    reverse.len()
                ));
            }

            reverse
        })
    }

    fn public_declarations_in_module(&self, module_fq: &str) -> Vec<CodeUnit> {
        let Some(module_code_unit) = self.resolve_module_code_unit(module_fq) else {
            return Vec::new();
        };
        self.inner
            .direct_children(&module_code_unit)
            .filter(|code_unit| !code_unit.identifier().starts_with('_'))
            .cloned()
            .collect()
    }

    fn resolve_base_class(&self, code_unit: &CodeUnit, raw: &str) -> Option<CodeUnit> {
        let trimmed = raw.trim();
        if trimmed.is_empty() {
            return None;
        }

        let bindings = self.resolve_import_bindings(code_unit.source());
        if let Some((head, tail)) = trimmed.split_once('.') {
            if let Some(bound) = bindings.get(head)
                && bound.is_module()
            {
                let fq_name = format!("{}.{}", bound.fq_name(), tail);
                return self.inner.definitions(&fq_name).next().cloned();
            }
            return self.inner.definitions(trimmed).next().cloned();
        }

        if let Some(bound) = bindings.get(trimmed) {
            return Some(bound.clone());
        }

        let local_fq_name = format!("{}.{}", code_unit.package_name(), trimmed);
        self.inner
            .definitions(&local_fq_name)
            .next()
            .cloned()
            .or_else(|| self.inner.definitions(trimmed).next().cloned())
    }

    fn render_skeleton_recursive(
        &self,
        code_unit: &CodeUnit,
        indent: &str,
        header_only: bool,
        out: &mut String,
    ) {
        if let Some(signature) = self.python_signature(code_unit, header_only) {
            for line in signature.lines() {
                out.push_str(indent);
                out.push_str(line);
                out.push('\n');
            }
        }

        let all_children: Vec<_> = self.inner.direct_children(code_unit).collect();
        let field_children: Vec<_> = all_children
            .iter()
            .copied()
            .filter(|child| child.is_field())
            .collect();
        let children = if header_only {
            field_children.clone()
        } else {
            all_children.clone()
        };
        if !children.is_empty() || code_unit.is_class() || code_unit.is_module() {
            let child_indent = format!("{indent}  ");
            for child in children {
                self.render_skeleton_recursive(child, &child_indent, header_only, out);
            }
            if header_only && all_children.len() > field_children.len() {
                out.push_str(&child_indent);
                out.push_str("[...]\n");
            }
        }
    }

    fn python_signature(&self, code_unit: &CodeUnit, _header_only: bool) -> Option<String> {
        if code_unit.is_module() {
            return None;
        }

        let source = self.inner.get_source(code_unit, false)?;
        let lines: Vec<_> = source
            .lines()
            .map(str::trim_end)
            .filter(|line| !line.trim().is_empty())
            .collect();
        if lines.is_empty() {
            return None;
        }

        let mut decorators = Vec::new();
        let mut header = None;
        for line in lines {
            let trimmed = line.trim_start();
            if trimmed.starts_with('@') {
                decorators.push(trimmed.to_string());
                continue;
            }
            header = Some(trimmed.to_string());
            break;
        }
        let mut rendered = String::new();
        for decorator in decorators {
            rendered.push_str(&decorator);
            rendered.push('\n');
        }

        let header = header?;
        match code_unit.kind() {
            CodeUnitType::Class => rendered.push_str(&header),
            CodeUnitType::Function => {
                rendered.push_str(header.trim_end_matches(':'));
                rendered.push_str(": ...");
            }
            CodeUnitType::Field => rendered.push_str(header.as_str()),
            CodeUnitType::Module => return None,
        }
        Some(rendered)
    }
}

impl ImportAnalysisProvider for PythonAnalyzer {
    fn imported_code_units_of(&self, file: &ProjectFile) -> HashSet<CodeUnit> {
        if let Some(cached) = self.imported_code_units.get(file) {
            return (*cached).clone();
        }

        let resolved: HashSet<_> = self.resolve_import_bindings(file).into_values().collect();
        self.imported_code_units
            .insert(file.clone(), Arc::new(resolved.clone()));
        resolved
    }

    fn referencing_files_of(&self, file: &ProjectFile) -> HashSet<ProjectFile> {
        if let Some(cached) = self.referencing_files.get(file) {
            return (*cached).clone();
        }

        let referencing = self
            .build_reverse_import_index()
            .get(file)
            .map(|files| (**files).clone())
            .unwrap_or_default();
        self.referencing_files
            .insert(file.clone(), Arc::new(referencing.clone()));
        referencing
    }

    fn import_info_of<'a>(&'a self, file: &ProjectFile) -> &'a [ImportInfo] {
        self.inner.import_info_of(file)
    }

    fn relevant_imports_for(&self, code_unit: &CodeUnit) -> HashSet<String> {
        let Some(source) = self.inner.get_source(code_unit, false) else {
            return HashSet::default();
        };

        let extracted = self.extract_type_identifiers(&source);
        if extracted.is_empty() {
            return HashSet::default();
        }

        let imports = self.inner.import_info_of(code_unit.source());
        if imports.is_empty() {
            return HashSet::default();
        }

        let mut matched = HashSet::default();
        let mut resolved = HashSet::default();
        let mut wildcard_imports = Vec::new();

        for info in imports {
            if info.is_wildcard {
                wildcard_imports.push(info.clone());
                continue;
            }

            if let Some(identifier) = info.identifier.as_deref()
                && extracted.contains(identifier)
            {
                matched.insert(info.raw_snippet.clone());
                resolved.insert(identifier.to_string());
            }

            if let Some(alias) = info.alias.as_deref()
                && extracted.contains(alias)
            {
                matched.insert(info.raw_snippet.clone());
                resolved.insert(alias.to_string());
            }
        }

        let unresolved: HashSet<_> = extracted
            .into_iter()
            .filter(|identifier| !resolved.contains(identifier))
            .collect();
        if unresolved.is_empty() || wildcard_imports.is_empty() {
            return matched;
        }

        let mut resolved_via_wildcard = HashSet::default();
        let mut used_wildcards = HashSet::default();
        for ident in &unresolved {
            for wildcard in &wildcard_imports {
                let Some(package_name) =
                    extract_package_from_python_wildcard(&wildcard.raw_snippet)
                else {
                    continue;
                };

                if self
                    .inner
                    .definitions(&format!("{package_name}.{ident}"))
                    .next()
                    .is_some()
                {
                    used_wildcards.insert(wildcard.raw_snippet.clone());
                    resolved_via_wildcard.insert(ident.clone());
                }
            }
        }

        matched.extend(used_wildcards);

        let remaining: HashSet<_> = unresolved
            .difference(&resolved_via_wildcard)
            .cloned()
            .collect();
        if !remaining.is_empty() {
            matched.extend(wildcard_imports.into_iter().map(|info| info.raw_snippet));
        }

        matched
    }

    fn could_import_file(
        &self,
        source_file: &ProjectFile,
        imports: &[ImportInfo],
        target: &ProjectFile,
    ) -> bool {
        for import in imports {
            let Some(details) = parse_python_import_details(&import.raw_snippet) else {
                continue;
            };
            match details {
                PythonImportDetails::FromImport { module, name, .. } if module.starts_with('.') => {
                    let Some(resolved_module) =
                        resolve_python_relative_module(source_file, &module)
                    else {
                        return true;
                    };
                    let candidate_module = format!("{resolved_module}.{name}");
                    if python_module_name(target) == candidate_module
                        || python_module_name(target) == resolved_module
                    {
                        return true;
                    }
                }
                _ => {
                    if self
                        .resolve_import(source_file, import)
                        .into_iter()
                        .any(|(_, code_unit)| code_unit.source() == target)
                    {
                        return true;
                    }
                }
            }
        }
        false
    }
}

impl TypeHierarchyProvider for PythonAnalyzer {
    fn get_direct_ancestors(&self, code_unit: &CodeUnit) -> Vec<CodeUnit> {
        if let Some(cached) = self.direct_ancestors.get(code_unit) {
            return (*cached).clone();
        }

        let ancestors: Vec<_> = self
            .inner
            .raw_supertypes_of(code_unit)
            .iter()
            .filter_map(|raw| self.resolve_base_class(code_unit, raw))
            .collect();
        self.direct_ancestors
            .insert(code_unit.clone(), Arc::new(ancestors.clone()));
        ancestors
    }

    fn get_direct_descendants(&self, code_unit: &CodeUnit) -> HashSet<CodeUnit> {
        if let Some(cached) = self.direct_descendants.get(code_unit) {
            return (*cached).clone();
        }

        let descendants = direct_descendants_via_ancestors(self, self, code_unit);
        self.direct_descendants
            .insert(code_unit.clone(), Arc::new(descendants.clone()));
        descendants
    }
}

impl TestDetectionProvider for PythonAnalyzer {}

impl IAnalyzer for PythonAnalyzer {
    fn top_level_declarations<'a>(
        &'a self,
        file: &ProjectFile,
    ) -> Box<dyn Iterator<Item = &'a CodeUnit> + 'a> {
        self.inner.top_level_declarations(file)
    }

    fn analyzed_files<'a>(&'a self) -> Box<dyn Iterator<Item = &'a ProjectFile> + 'a> {
        self.inner.analyzed_files()
    }

    fn all_declarations<'a>(&'a self) -> Box<dyn Iterator<Item = &'a CodeUnit> + 'a> {
        self.inner.all_declarations()
    }

    fn declarations<'a>(
        &'a self,
        file: &ProjectFile,
    ) -> Box<dyn Iterator<Item = &'a CodeUnit> + 'a> {
        self.inner.declarations(file)
    }

    fn definitions<'a>(&'a self, fq_name: &'a str) -> Box<dyn Iterator<Item = &'a CodeUnit> + 'a> {
        self.inner.definitions(fq_name)
    }

    fn direct_children<'a>(
        &'a self,
        code_unit: &CodeUnit,
    ) -> Box<dyn Iterator<Item = &'a CodeUnit> + 'a> {
        self.inner.direct_children(code_unit)
    }

    fn import_statements<'a>(&'a self, file: &ProjectFile) -> &'a [String] {
        self.inner.import_statements(file)
    }

    fn ranges<'a>(&'a self, code_unit: &CodeUnit) -> &'a [crate::analyzer::Range] {
        self.inner.ranges(code_unit)
    }

    fn compute_cognitive_complexities(&self, file: &ProjectFile) -> Vec<(CodeUnit, u32)> {
        self.inner.compute_cognitive_complexities(file)
    }

    fn signatures<'a>(&'a self, code_unit: &CodeUnit) -> &'a [String] {
        self.inner.signatures(code_unit)
    }

    fn get_top_level_declarations(&self, file: &ProjectFile) -> Vec<CodeUnit> {
        self.inner.get_top_level_declarations(file)
    }

    fn get_analyzed_files(&self) -> BTreeSet<ProjectFile> {
        self.inner.get_analyzed_files()
    }

    fn languages(&self) -> BTreeSet<Language> {
        self.inner.languages()
    }

    fn update(&self, changed_files: &BTreeSet<ProjectFile>) -> Self {
        let inner = self.inner.update(changed_files);
        Self {
            module_code_units: Arc::new(build_python_module_code_units(&inner)),
            inner,
            memo_budget: self.memo_budget,
            imported_code_units: build_weighted_cache(self.memo_budget / 4, weight_code_unit_set),
            referencing_files: build_weighted_cache(self.memo_budget / 8, weight_project_file_set),
            direct_ancestors: build_weighted_cache(self.memo_budget / 8, weight_code_unit_vec),
            direct_descendants: build_weighted_cache(
                self.memo_budget / 8,
                weight_code_unit_set_by_unit,
            ),
            reverse_import_index: Arc::new(OnceLock::new()),
        }
    }

    fn update_all(&self) -> Self {
        let inner = self.inner.update_all();
        Self {
            module_code_units: Arc::new(build_python_module_code_units(&inner)),
            inner,
            memo_budget: self.memo_budget,
            imported_code_units: build_weighted_cache(self.memo_budget / 4, weight_code_unit_set),
            referencing_files: build_weighted_cache(self.memo_budget / 8, weight_project_file_set),
            direct_ancestors: build_weighted_cache(self.memo_budget / 8, weight_code_unit_vec),
            direct_descendants: build_weighted_cache(
                self.memo_budget / 8,
                weight_code_unit_set_by_unit,
            ),
            reverse_import_index: Arc::new(OnceLock::new()),
        }
    }

    fn project(&self) -> &dyn Project {
        self.inner.project()
    }

    fn get_all_declarations(&self) -> Vec<CodeUnit> {
        self.inner.get_all_declarations()
    }

    fn get_declarations(&self, file: &ProjectFile) -> BTreeSet<CodeUnit> {
        self.inner.get_declarations(file)
    }

    fn get_definitions(&self, fq_name: &str) -> Vec<CodeUnit> {
        self.inner.get_definitions(fq_name)
    }

    fn get_direct_children(&self, code_unit: &CodeUnit) -> Vec<CodeUnit> {
        self.inner.get_direct_children(code_unit)
    }

    fn parse_errors(&self, file: &ProjectFile) -> Option<Vec<crate::analyzer::ParseError>> {
        self.inner.parse_errors(file)
    }

    fn extract_call_receiver(&self, reference: &str) -> Option<String> {
        self.inner.extract_call_receiver(reference)
    }

    fn import_statements_of(&self, file: &ProjectFile) -> Vec<String> {
        self.inner.import_statements_of(file)
    }

    fn enclosing_code_unit(
        &self,
        file: &ProjectFile,
        range: &crate::analyzer::Range,
    ) -> Option<CodeUnit> {
        self.inner.enclosing_code_unit(file, range)
    }

    fn enclosing_code_unit_for_lines(
        &self,
        file: &ProjectFile,
        start_line: usize,
        end_line: usize,
    ) -> Option<CodeUnit> {
        self.inner
            .enclosing_code_unit_for_lines(file, start_line, end_line)
    }

    fn is_access_expression(&self, file: &ProjectFile, start_byte: usize, end_byte: usize) -> bool {
        self.inner.is_access_expression(file, start_byte, end_byte)
    }

    fn find_nearest_declaration(
        &self,
        file: &ProjectFile,
        start_byte: usize,
        end_byte: usize,
        ident: &str,
    ) -> Option<crate::analyzer::DeclarationInfo> {
        self.inner
            .find_nearest_declaration(file, start_byte, end_byte, ident)
    }

    fn ranges_of(&self, code_unit: &CodeUnit) -> Vec<crate::analyzer::Range> {
        self.inner.ranges_of(code_unit)
    }

    fn get_skeleton(&self, code_unit: &CodeUnit) -> Option<String> {
        let mut rendered = String::new();
        self.render_skeleton_recursive(code_unit, "", false, &mut rendered);
        let trimmed = rendered.trim_end();
        (!trimmed.is_empty()).then(|| trimmed.to_string())
    }

    fn get_skeleton_header(&self, code_unit: &CodeUnit) -> Option<String> {
        let mut rendered = String::new();
        self.render_skeleton_recursive(code_unit, "", true, &mut rendered);
        let trimmed = rendered.trim_end();
        (!trimmed.is_empty()).then(|| trimmed.to_string())
    }

    fn get_source(&self, code_unit: &CodeUnit, include_comments: bool) -> Option<String> {
        let sources = self.get_sources(code_unit, include_comments);
        if sources.is_empty() {
            None
        } else {
            Some(sources.into_iter().collect::<Vec<_>>().join("\n\n"))
        }
    }

    fn get_sources(&self, code_unit: &CodeUnit, include_comments: bool) -> BTreeSet<String> {
        if !include_comments {
            return self.inner.get_sources(code_unit, false);
        }

        let mut ranges = if code_unit.is_function() {
            let mut grouped = Vec::new();
            for candidate in self.inner.definitions(&code_unit.fq_name()) {
                if candidate.source() == code_unit.source() {
                    grouped.extend(self.inner.ranges(candidate).iter().copied());
                }
            }
            grouped
        } else {
            self.inner.ranges(code_unit).to_vec()
        };

        let Ok(source) = self.inner.project().read_source(code_unit.source()) else {
            return BTreeSet::new();
        };

        ranges.sort_by_key(|range| range.start_byte);
        ranges
            .into_iter()
            .filter_map(|range| {
                let start_byte = python_expanded_comment_start(&source, range.start_byte);
                source.get(start_byte..range.end_byte).map(str::to_string)
            })
            .collect()
    }

    fn search_definitions(&self, pattern: &str, auto_quote: bool) -> BTreeSet<CodeUnit> {
        self.inner.search_definitions(pattern, auto_quote)
    }

    fn search_definitions_persisted(&self, pattern: &str) -> BTreeSet<CodeUnit> {
        self.inner.search_definitions_persisted(pattern)
    }

    fn signatures_of(&self, code_unit: &CodeUnit) -> Vec<String> {
        self.inner.signatures_of(code_unit).to_vec()
    }

    fn import_analysis_provider(&self) -> Option<&dyn ImportAnalysisProvider> {
        Some(self)
    }

    fn type_hierarchy_provider(&self) -> Option<&dyn TypeHierarchyProvider> {
        Some(self)
    }

    fn test_detection_provider(&self) -> Option<&dyn TestDetectionProvider> {
        Some(self)
    }

    fn contains_tests(&self, file: &ProjectFile) -> bool {
        self.inner.contains_tests(file)
    }

    fn find_test_assertion_smells(
        &self,
        file: &ProjectFile,
        weights: TestAssertionWeights,
    ) -> Vec<TestAssertionSmell> {
        if !self.contains_tests(file) || file_language(file) != Language::Python {
            return Vec::new();
        }
        let Ok(source) = self.inner.project().read_source(file) else {
            return Vec::new();
        };
        detect_python_test_assertion_smells(file, &source, &weights)
    }

    fn find_structural_clone_smells(
        &self,
        file: &ProjectFile,
        weights: CloneSmellWeights,
    ) -> Vec<CloneSmell> {
        self.find_structural_clone_smells_for_files(std::slice::from_ref(file), weights)
    }

    fn find_structural_clone_smells_for_files(
        &self,
        files: &[ProjectFile],
        weights: CloneSmellWeights,
    ) -> Vec<CloneSmell> {
        let requested_files: Vec<ProjectFile> = files
            .iter()
            .filter(|file| file_language(file) == Language::Python)
            .cloned()
            .collect();
        if requested_files.is_empty() {
            return Vec::new();
        }

        let all_candidates: Vec<CloneCandidateProfile> = self
            .get_all_declarations()
            .into_iter()
            .filter(|code_unit| {
                code_unit.is_function() && file_language(code_unit.source()) == Language::Python
            })
            .filter_map(|code_unit| self.build_clone_candidate_data(&code_unit, weights))
            .map(|candidate| CloneCandidateProfile::create(candidate, weights))
            .collect();
        if all_candidates.is_empty() {
            return Vec::new();
        }

        detect_structural_clone_smells(
            &requested_files,
            all_candidates,
            weights,
            refine_python_clone_similarity,
        )
    }
}

impl PythonAnalyzer {
    fn build_clone_candidate_data(
        &self,
        code_unit: &CodeUnit,
        weights: CloneSmellWeights,
    ) -> Option<CloneCandidateData> {
        self.get_source(code_unit, false)
            .map(|source| source.trim().to_string())
            .filter(|source| !source.is_empty())
            .and_then(|source| {
                let normalized_tokens = normalized_clone_tokens_python(&source);
                if normalized_tokens.len() < weights.min_normalized_tokens.max(0) as usize {
                    return None;
                }
                Some(CloneCandidateData {
                    unit: code_unit.clone(),
                    normalized_tokens,
                    ast_signature: build_python_clone_ast_signature(&source),
                    excerpt: compact_clone_excerpt(&source),
                })
            })
    }
}

#[derive(Clone)]
struct Scope {
    kind: ScopeKind,
    path: String,
    code_unit: Option<CodeUnit>,
}

#[derive(Clone, Copy, PartialEq, Eq)]
enum ScopeKind {
    Class,
    Function,
}

struct PythonVisitor<'a> {
    file: &'a ProjectFile,
    source: &'a str,
    package_name: &'a str,
    parsed: &'a mut crate::analyzer::tree_sitter_analyzer::ParsedFile,
    module: Option<CodeUnit>,
}

impl<'a> PythonVisitor<'a> {
    fn visit_container(&mut self, node: Node<'_>, scope: &[Scope], module_control_depth: usize) {
        let mut cursor = node.walk();
        for child in node.named_children(&mut cursor) {
            self.visit_statement(child, scope, module_control_depth);
        }
    }

    fn visit_statement(&mut self, node: Node<'_>, scope: &[Scope], module_control_depth: usize) {
        match node.kind() {
            "decorated_definition" => {
                if let Some(definition) = node.child_by_field_name("definition") {
                    self.visit_definition(definition, Some(node), scope, module_control_depth);
                }
            }
            "class_definition" | "function_definition" => {
                self.visit_definition(node, None, scope, module_control_depth)
            }
            "expression_statement" => {
                self.visit_expression_statement(node, scope, module_control_depth)
            }
            "import_statement" | "import_from_statement" => self.visit_import_statement(node),
            "if_statement" | "try_statement" | "with_statement" | "for_statement"
            | "while_statement" => {
                let next_depth = if scope.is_empty() {
                    module_control_depth + 1
                } else {
                    module_control_depth
                };
                self.visit_container(node, scope, next_depth);
            }
            "elif_clause" | "else_clause" | "except_clause" | "finally_clause" => {
                self.visit_container(node, scope, module_control_depth);
            }
            "block" | "module" => self.visit_container(node, scope, module_control_depth),
            _ => {}
        }
    }

    fn visit_definition(
        &mut self,
        definition: Node<'_>,
        wrapper: Option<Node<'_>>,
        scope: &[Scope],
        module_control_depth: usize,
    ) {
        match definition.kind() {
            "class_definition" => self.visit_class_definition(
                definition,
                wrapper.unwrap_or(definition),
                scope,
                module_control_depth,
            ),
            "function_definition" => self.visit_function_definition(
                definition,
                wrapper.unwrap_or(definition),
                scope,
                module_control_depth,
            ),
            _ => {}
        }
    }

    fn visit_class_definition(
        &mut self,
        node: Node<'_>,
        range_node: Node<'_>,
        scope: &[Scope],
        module_control_depth: usize,
    ) {
        let Some(name_node) = node.child_by_field_name("name") else {
            return;
        };
        let name = py_node_text(name_node, self.source).trim();
        if name.is_empty() {
            return;
        }

        let capture = !scope.is_empty() || module_control_depth <= 1;

        let short_name = scope
            .last()
            .map(|parent| format!("{}${name}", parent.path))
            .unwrap_or_else(|| name.to_string());
        let code_unit = CodeUnit::new(
            self.file.clone(),
            CodeUnitType::Class,
            self.package_name.to_string(),
            short_name.clone(),
        );
        if capture {
            self.parsed
                .replace_code_unit(code_unit.clone(), range_node, self.source, None, None);
            self.parsed.add_signature(
                code_unit.clone(),
                python_class_signature(range_node, self.source),
            );
            if let Some(module) = &self.module
                && scope.is_empty()
            {
                self.parsed.add_child(module.clone(), code_unit.clone());
            }
            if let Some(parent) = scope.last()
                && let Some(parent_cu) = &parent.code_unit
            {
                self.parsed.add_child(parent_cu.clone(), code_unit.clone());
            }
            self.parsed.set_raw_supertypes(
                code_unit.clone(),
                extract_python_supertypes(node, self.source),
            );
        }

        let mut next_scope = scope.to_vec();
        if capture {
            next_scope.push(Scope {
                kind: ScopeKind::Class,
                path: short_name,
                code_unit: Some(code_unit),
            });
        }
        if let Some(body) = node.child_by_field_name("body") {
            self.visit_container(body, &next_scope, module_control_depth);
        }
    }

    fn visit_function_definition(
        &mut self,
        node: Node<'_>,
        range_node: Node<'_>,
        scope: &[Scope],
        module_control_depth: usize,
    ) {
        let Some(name_node) = node.child_by_field_name("name") else {
            return;
        };
        let name = py_node_text(name_node, self.source).trim();
        if name.is_empty() {
            return;
        }

        let capture = !python_is_property_mutator(range_node, self.source)
            && ((scope.is_empty() && module_control_depth <= 1)
                || scope
                    .last()
                    .is_some_and(|parent| parent.kind == ScopeKind::Class));
        let short_name = if let Some(parent) = scope.last() {
            match parent.kind {
                ScopeKind::Class => format!("{}.{}", parent.path, name),
                ScopeKind::Function => format!("{}${name}", parent.path),
            }
        } else {
            name.to_string()
        };

        if capture {
            let signature = node
                .child_by_field_name("parameters")
                .map(|parameters| py_node_text(parameters, self.source).trim().to_string());
            let code_unit = CodeUnit::with_signature(
                self.file.clone(),
                CodeUnitType::Function,
                self.package_name.to_string(),
                short_name.clone(),
                signature,
                false,
            );
            self.parsed
                .replace_code_unit(code_unit.clone(), range_node, self.source, None, None);
            self.parsed.add_signature(
                code_unit.clone(),
                python_function_signature(range_node, self.source),
            );
            if let Some(module) = &self.module
                && scope.is_empty()
            {
                self.parsed.add_child(module.clone(), code_unit.clone());
            }
            if let Some(parent) = scope.last()
                && parent.kind == ScopeKind::Class
                && let Some(parent_cu) = &parent.code_unit
            {
                self.parsed.add_child(parent_cu.clone(), code_unit.clone());
            }
            let scope_code_unit = Some(code_unit);
            let mut next_scope = scope.to_vec();
            next_scope.push(Scope {
                kind: ScopeKind::Function,
                path: short_name,
                code_unit: scope_code_unit,
            });
            if let Some(body) = node.child_by_field_name("body") {
                self.visit_container(body, &next_scope, module_control_depth);
            }
            return;
        }

        let mut next_scope = scope.to_vec();
        next_scope.push(Scope {
            kind: ScopeKind::Function,
            path: short_name,
            code_unit: None,
        });
        if let Some(body) = node.child_by_field_name("body") {
            self.visit_container(body, &next_scope, module_control_depth);
        }
    }

    fn visit_expression_statement(
        &mut self,
        node: Node<'_>,
        scope: &[Scope],
        module_control_depth: usize,
    ) {
        let Some(assignment) = node.named_child(0) else {
            return;
        };
        if assignment.kind() != "assignment" {
            return;
        }
        let Some(left) = assignment.child_by_field_name("left") else {
            return;
        };
        let names = collect_assigned_names(left, self.source);
        for name in names {
            let short_name = if let Some(parent) = scope.last() {
                if parent.kind != ScopeKind::Class {
                    continue;
                }
                format!("{}.{}", parent.path, name)
            } else if module_control_depth <= 1 {
                name.clone()
            } else {
                continue;
            };
            let code_unit = CodeUnit::new(
                self.file.clone(),
                CodeUnitType::Field,
                self.package_name.to_string(),
                short_name,
            );
            self.parsed
                .replace_code_unit(code_unit.clone(), node, self.source, None, None);
            self.parsed.add_signature(
                code_unit.clone(),
                py_node_text(node, self.source).trim().to_string(),
            );
            if let Some(module) = &self.module
                && scope.is_empty()
            {
                self.parsed.add_child(module.clone(), code_unit.clone());
            }
            if let Some(parent) = scope.last()
                && parent.kind == ScopeKind::Class
                && let Some(parent_cu) = &parent.code_unit
            {
                self.parsed.add_child(parent_cu.clone(), code_unit);
            }
        }
    }

    fn visit_import_statement(&mut self, node: Node<'_>) {
        let raw = py_node_text(node, self.source).trim();
        for info in parse_python_import_infos(raw) {
            self.parsed.import_statements.push(info.raw_snippet.clone());
            self.parsed.imports.push(info);
        }
    }
}

fn py_node_text<'a>(node: Node<'_>, source: &'a str) -> &'a str {
    source.get(node.start_byte()..node.end_byte()).unwrap_or("")
}

struct PythonModuleInfo {
    package_name: String,
    module_name: String,
}

impl PythonModuleInfo {
    fn module_qualified_package(&self) -> String {
        if self.package_name.is_empty() {
            self.module_name.clone()
        } else {
            format!("{}.{}", self.package_name, self.module_name)
        }
    }
}

fn python_module_name(file: &ProjectFile) -> String {
    python_module_info(file).module_qualified_package()
}

fn build_python_module_code_units(
    inner: &TreeSitterAnalyzer<PythonAdapter>,
) -> HashMap<String, CodeUnit> {
    inner
        .all_files()
        .filter_map(|file| {
            let module_fq = python_module_name(file);
            module_code_unit(file, &module_fq).map(|code_unit| (module_fq, code_unit))
        })
        .collect()
}

fn python_module_info(file: &ProjectFile) -> PythonModuleInfo {
    let raw_package = python_package_name_for_file(file);
    let module_name = file
        .rel_path()
        .file_stem()
        .and_then(|stem| stem.to_str())
        .unwrap_or_default()
        .to_string();

    if module_name == "__init__" && !raw_package.is_empty() {
        if let Some((package_name, last_segment)) = raw_package.rsplit_once('.') {
            return PythonModuleInfo {
                package_name: package_name.to_string(),
                module_name: last_segment.to_string(),
            };
        }
        return PythonModuleInfo {
            package_name: String::new(),
            module_name: raw_package,
        };
    }

    PythonModuleInfo {
        package_name: raw_package,
        module_name,
    }
}

fn python_package_name_for_file(file: &ProjectFile) -> String {
    let Some(parent_rel) = file.rel_path().parent() else {
        return String::new();
    };
    if parent_rel.as_os_str().is_empty() {
        return String::new();
    }

    let mut effective_package_root_rel: Option<&Path> = None;
    let mut current_rel = Some(parent_rel);
    while let Some(path) = current_rel {
        if file.root().join(path).join("__init__.py").exists() {
            effective_package_root_rel = Some(path);
        }
        current_rel = path.parent();
    }

    let Some(package_root_rel) = effective_package_root_rel else {
        return dotted_path(parent_rel);
    };

    let Some(import_root_rel) = package_root_rel.parent() else {
        return dotted_path(parent_rel);
    };

    dotted_path(
        import_root_rel
            .strip_prefix("")
            .ok()
            .and_then(|_| parent_rel.strip_prefix(import_root_rel).ok())
            .unwrap_or(parent_rel),
    )
}

fn dotted_path(path: &Path) -> String {
    path.components()
        .map(|component| component.as_os_str().to_string_lossy().to_string())
        .filter(|component| !component.is_empty())
        .collect::<Vec<_>>()
        .join(".")
}

fn module_code_unit(file: &ProjectFile, module_fq: &str) -> Option<CodeUnit> {
    if module_fq.is_empty() {
        return None;
    }
    let mut parts = module_fq.rsplitn(2, '.');
    let short_name = parts.next().unwrap_or(module_fq);
    let package_name = parts.next().unwrap_or_default();
    Some(CodeUnit::new(
        file.clone(),
        CodeUnitType::Module,
        package_name.to_string(),
        short_name.to_string(),
    ))
}

fn python_class_signature(node: Node<'_>, source: &str) -> String {
    python_header_with_decorators(node, source)
}

fn python_function_signature(node: Node<'_>, source: &str) -> String {
    let header = python_header_with_decorators(node, source);
    if let Some((head, tail)) = header.rsplit_once('\n') {
        format!("{head}\n{} ...", tail.trim_end_matches(':'))
    } else {
        format!("{} ...", header.trim_end_matches(':'))
    }
}

fn python_is_property_mutator(node: Node<'_>, source: &str) -> bool {
    python_header_with_decorators(node, source)
        .lines()
        .map(str::trim)
        .filter(|line| line.starts_with('@'))
        .any(|decorator| decorator.ends_with(".setter") || decorator.ends_with(".deleter"))
}

fn python_expanded_comment_start(source: &str, start_byte: usize) -> usize {
    let line_starts = compute_line_starts(source);
    let line_index = find_line_index_for_offset(&line_starts, start_byte);

    let mut comment_start = start_byte;
    for line_idx in (0..line_index).rev() {
        let line_start = line_starts[line_idx];
        let line_end = line_starts
            .get(line_idx + 1)
            .copied()
            .unwrap_or(source.len());
        let line = &source[line_start..line_end];
        let trimmed = line.trim_start();

        if trimmed.trim().is_empty() {
            continue;
        }

        if trimmed.starts_with('#') {
            comment_start = line_start;
            continue;
        }

        break;
    }

    comment_start
}

fn python_header_with_decorators(node: Node<'_>, source: &str) -> String {
    let raw = py_node_text(node, source);
    let lines: Vec<_> = raw
        .lines()
        .map(str::trim_end)
        .filter(|line| !line.trim().is_empty())
        .collect();
    let mut relevant = Vec::new();
    for line in lines {
        let trimmed = line.trim_start();
        if trimmed.starts_with('@')
            || trimmed.starts_with("def ")
            || trimmed.starts_with("async def ")
            || trimmed.starts_with("class ")
        {
            relevant.push(trimmed.to_string());
            if trimmed.starts_with("def ")
                || trimmed.starts_with("async def ")
                || trimmed.starts_with("class ")
            {
                break;
            }
        }
    }
    relevant.join("\n")
}

fn extract_python_supertypes(node: Node<'_>, source: &str) -> Vec<String> {
    let Some(superclasses) = node.child_by_field_name("superclasses") else {
        return Vec::new();
    };
    let mut result = Vec::new();
    let mut cursor = superclasses.walk();
    for child in superclasses.named_children(&mut cursor) {
        match child.kind() {
            "identifier" | "attribute" => {
                let text = py_node_text(child, source).trim();
                if !text.is_empty() {
                    result.push(text.to_string());
                }
            }
            _ => {}
        }
    }
    result
}

fn collect_assigned_names(node: Node<'_>, source: &str) -> Vec<String> {
    let mut names = Vec::new();
    match node.kind() {
        "identifier" => {
            let text = py_node_text(node, source).trim();
            if !text.is_empty() {
                names.push(text.to_string());
            }
        }
        _ => {
            let mut cursor = node.walk();
            for child in node.named_children(&mut cursor) {
                names.extend(collect_assigned_names(child, source));
            }
        }
    }
    names
}

fn collect_python_identifiers(node: Node<'_>, source: &str, identifiers: &mut HashSet<String>) {
    if node.kind() == "identifier" {
        let text = py_node_text(node, source).trim();
        if !text.is_empty() {
            identifiers.insert(text.to_string());
        }
    }
    let mut cursor = node.walk();
    for child in node.named_children(&mut cursor) {
        collect_python_identifiers(child, source, identifiers);
    }
}

static PY_ASSERT_EQUALITY_RE: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(r#"self\.assert(?:Equal|Is)\s*\((?P<left>[^,\n]+?)\s*,\s*(?P<right>[^,\n\)]+)"#)
        .expect("valid regex")
});
static PY_ASSERT_TRUTH_RE: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(r#"self\.assert(?P<matcher>True|False|IsNone|IsNotNone)\s*\((?P<arg>[^,\n\)]+)"#)
        .expect("valid regex")
});
static PY_BARE_ASSERT_RE: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r#"(?m)^\s*assert\s+(?P<expr>[^\n]+)"#).expect("valid regex"));
static PY_RAISES_RE: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(r#"pytest\.raises\s*\(|self\.assertRaises\s*\("#).expect("valid regex")
});
static PY_VERIFY_RE: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(r#"\.\s*assert_(?:called|called_once|called_once_with|called_with|not_called)\s*\("#)
        .expect("valid regex")
});

#[derive(Clone)]
struct PythonTestCase {
    name: String,
    body: String,
    start_byte: usize,
}

#[derive(Clone)]
struct PythonAssertionSignal {
    kind: String,
    score: i32,
    shallow: bool,
    meaningful: bool,
    reason: String,
    excerpt: String,
    start_byte: usize,
}

fn detect_python_test_assertion_smells(
    file: &ProjectFile,
    source: &str,
    weights: &TestAssertionWeights,
) -> Vec<TestAssertionSmell> {
    let mut parser = Parser::new();
    parser
        .set_language(&tree_sitter_python::LANGUAGE.into())
        .expect("failed to load python parser");
    let Some(tree) = parser.parse(source, None) else {
        return Vec::new();
    };

    let mut test_cases = Vec::new();
    collect_python_test_cases(tree.root_node(), source, &mut test_cases);

    let mut findings = Vec::new();
    for test_case in test_cases {
        analyze_python_test_case(file, &test_case, weights, &mut findings);
    }
    findings
}

fn collect_python_test_cases(node: Node<'_>, source: &str, out: &mut Vec<PythonTestCase>) {
    match node.kind() {
        "function_definition" => {
            if python_function_has_fixture_decorator(node, source) {
                return;
            }
            if let Some(name_node) = node.child_by_field_name("name") {
                let name = py_node_text(name_node, source).trim().to_string();
                if name.starts_with("test_") {
                    let body = node
                        .child_by_field_name("body")
                        .map(|body| py_node_text(body, source).to_string())
                        .unwrap_or_else(|| py_node_text(node, source).to_string());
                    out.push(PythonTestCase {
                        name,
                        body,
                        start_byte: node.start_byte(),
                    });
                }
            }
        }
        "decorated_definition" => {
            if python_decorated_definition_has_fixture(node, source) {
                return;
            }
            let has_pytest_mark = py_node_text(node, source).contains("pytest.mark");
            if has_pytest_mark
                && let Some(definition) = node.child_by_field_name("definition")
                && definition.kind() == "function_definition"
            {
                let name = definition
                    .child_by_field_name("name")
                    .map(|name_node| py_node_text(name_node, source).trim().to_string())
                    .unwrap_or_else(|| "anonymous".to_string());
                let body = definition
                    .child_by_field_name("body")
                    .map(|body| py_node_text(body, source).to_string())
                    .unwrap_or_else(|| py_node_text(definition, source).to_string());
                out.push(PythonTestCase {
                    name,
                    body,
                    start_byte: node.start_byte(),
                });
            }
        }
        _ => {}
    }

    let mut cursor = node.walk();
    for child in node.named_children(&mut cursor) {
        collect_python_test_cases(child, source, out);
    }
}

fn python_function_has_fixture_decorator(function: Node<'_>, source: &str) -> bool {
    let Some(parent) = function.parent() else {
        return false;
    };
    parent.kind() == "decorated_definition"
        && python_decorated_definition_has_fixture(parent, source)
}

fn python_decorated_definition_has_fixture(node: Node<'_>, source: &str) -> bool {
    py_node_text(node, source).contains("@pytest.fixture")
}

fn analyze_python_test_case(
    file: &ProjectFile,
    test_case: &PythonTestCase,
    weights: &TestAssertionWeights,
    out: &mut Vec<TestAssertionSmell>,
) {
    let assertions = collect_python_assertions(&test_case.body, weights);
    let assertion_count = assertions.len() as i32;
    let symbol = format!("{}::{}", file, test_case.name);

    if assertion_count == 0 {
        out.push(TestAssertionSmell {
            file: file.clone(),
            enclosing_fq_name: symbol,
            assertion_kind: "no-assertions".to_string(),
            score: weights.no_assertion_weight,
            assertion_count: 0,
            reasons: vec!["no-assertions".to_string()],
            excerpt: compact_python_excerpt(&test_case.body),
            start_byte: test_case.start_byte,
        });
        return;
    }

    for assertion in &assertions {
        if assertion.score <= 0 {
            continue;
        }
        out.push(TestAssertionSmell {
            file: file.clone(),
            enclosing_fq_name: symbol.clone(),
            assertion_kind: assertion.kind.clone(),
            score: assertion.score,
            assertion_count,
            reasons: vec![assertion.reason.clone()],
            excerpt: assertion.excerpt.clone(),
            start_byte: test_case.start_byte + assertion.start_byte,
        });
    }

    if assertions.iter().all(|assertion| assertion.shallow) {
        let score = (weights.shallow_assertion_only_weight
            - python_meaningful_assertion_credit(assertions.iter(), weights))
        .max(0);
        if score > 0 {
            out.push(TestAssertionSmell {
                file: file.clone(),
                enclosing_fq_name: symbol,
                assertion_kind: "shallow-assertions-only".to_string(),
                score,
                assertion_count,
                reasons: vec!["shallow-assertions-only".to_string()],
                excerpt: compact_python_excerpt(&test_case.body),
                start_byte: test_case.start_byte,
            });
        }
    }
}

fn collect_python_assertions(
    body: &str,
    weights: &TestAssertionWeights,
) -> Vec<PythonAssertionSignal> {
    let mut assertions = Vec::new();

    for captures in PY_ASSERT_EQUALITY_RE.captures_iter(body) {
        let whole = captures.get(0).expect("whole match");
        let left = normalize_python_expr(captures.name("left").map(|m| m.as_str()).unwrap_or(""));
        let right = normalize_python_expr(captures.name("right").map(|m| m.as_str()).unwrap_or(""));
        if left.is_empty() || right.is_empty() {
            continue;
        }
        if left == right {
            let (kind, reason, score) = if is_python_literal(&left) {
                (
                    "constant-equality".to_string(),
                    "constant-equality".to_string(),
                    weights.constant_equality_weight,
                )
            } else {
                (
                    "self-comparison".to_string(),
                    "self-comparison".to_string(),
                    weights.tautological_assertion_weight,
                )
            };
            assertions.push(PythonAssertionSignal {
                kind,
                score,
                shallow: false,
                meaningful: false,
                reason,
                excerpt: compact_python_excerpt(whole.as_str()),
                start_byte: whole.start(),
            });
        } else if let Some(literal) = oversized_python_literal(&left, &right, weights) {
            assertions.push(PythonAssertionSignal {
                kind: "overspecified-literal".to_string(),
                score: weights.overspecified_literal_weight,
                shallow: false,
                meaningful: false,
                reason: format!("overspecified-literal:{literal}"),
                excerpt: compact_python_excerpt(whole.as_str()),
                start_byte: whole.start(),
            });
        } else {
            assertions.push(PythonAssertionSignal {
                kind: "meaningful-assertion".to_string(),
                score: 0,
                shallow: false,
                meaningful: true,
                reason: "meaningful-assertion".to_string(),
                excerpt: compact_python_excerpt(whole.as_str()),
                start_byte: whole.start(),
            });
        }
    }

    for captures in PY_ASSERT_TRUTH_RE.captures_iter(body) {
        let whole = captures.get(0).expect("whole match");
        let matcher = captures.name("matcher").map(|m| m.as_str()).unwrap_or("");
        let arg = normalize_python_expr(captures.name("arg").map(|m| m.as_str()).unwrap_or(""));
        let (kind, reason, score, shallow) = match matcher {
            "True" if arg == "True" => (
                "constant-truth".to_string(),
                "constant-truth".to_string(),
                weights.constant_truth_weight,
                true,
            ),
            "False" if arg == "False" => (
                "constant-truth".to_string(),
                "constant-truth".to_string(),
                weights.constant_truth_weight,
                true,
            ),
            "IsNone" | "IsNotNone" => (
                "nullness-only".to_string(),
                "nullness-only".to_string(),
                weights.nullness_only_weight,
                true,
            ),
            _ => (
                "meaningful-assertion".to_string(),
                "meaningful-assertion".to_string(),
                0,
                false,
            ),
        };
        assertions.push(PythonAssertionSignal {
            kind,
            score,
            shallow,
            meaningful: score == 0,
            reason,
            excerpt: compact_python_excerpt(whole.as_str()),
            start_byte: whole.start(),
        });
    }

    for captures in PY_BARE_ASSERT_RE.captures_iter(body) {
        let whole = captures.get(0).expect("whole match");
        let expr = normalize_python_expr(captures.name("expr").map(|m| m.as_str()).unwrap_or(""));
        let trimmed = expr.trim();
        let maybe_signal = if trimmed == "True" || trimmed == "False" {
            Some(("constant-truth", weights.constant_truth_weight, true))
        } else if trimmed.contains(" is not None") || trimmed.contains(" is None") {
            Some(("nullness-only", weights.nullness_only_weight, true))
        } else if let Some((left, right)) = trimmed.split_once("==") {
            let left = normalize_python_expr(left);
            let right = normalize_python_expr(right);
            if left == right {
                if is_python_literal(&left) {
                    Some(("constant-equality", weights.constant_equality_weight, false))
                } else {
                    Some((
                        "self-comparison",
                        weights.tautological_assertion_weight,
                        false,
                    ))
                }
            } else if oversized_python_literal(&left, &right, weights).is_some() {
                Some((
                    "overspecified-literal",
                    weights.overspecified_literal_weight,
                    false,
                ))
            } else {
                None
            }
        } else {
            None
        };

        if let Some((kind, score, shallow)) = maybe_signal {
            assertions.push(PythonAssertionSignal {
                kind: kind.to_string(),
                score,
                shallow,
                meaningful: false,
                reason: kind.to_string(),
                excerpt: compact_python_excerpt(whole.as_str()),
                start_byte: whole.start(),
            });
        } else {
            assertions.push(PythonAssertionSignal {
                kind: "meaningful-assertion".to_string(),
                score: 0,
                shallow: false,
                meaningful: true,
                reason: "meaningful-assertion".to_string(),
                excerpt: compact_python_excerpt(whole.as_str()),
                start_byte: whole.start(),
            });
        }
    }

    for regex in [&*PY_RAISES_RE, &*PY_VERIFY_RE] {
        for captures in regex.captures_iter(body) {
            let whole = captures.get(0).expect("whole match");
            assertions.push(PythonAssertionSignal {
                kind: "meaningful-assertion".to_string(),
                score: 0,
                shallow: false,
                meaningful: true,
                reason: "meaningful-assertion".to_string(),
                excerpt: compact_python_excerpt(whole.as_str()),
                start_byte: whole.start(),
            });
        }
    }

    assertions
}

fn python_meaningful_assertion_credit<'a>(
    assertions: impl Iterator<Item = &'a PythonAssertionSignal>,
    weights: &TestAssertionWeights,
) -> i32 {
    let count = assertions.filter(|assertion| assertion.meaningful).count() as i32;
    let creditable = count.min(weights.meaningful_assertion_credit_cap.max(0));
    weights.meaningful_assertion_credit.max(0) * creditable
}

fn normalize_python_expr(expr: &str) -> String {
    expr.trim()
        .trim_end_matches(',')
        .trim_end_matches(':')
        .trim_matches(|ch| matches!(ch, '(' | ')' | ' '))
        .split_whitespace()
        .collect::<Vec<_>>()
        .join(" ")
}

fn is_python_literal(expr: &str) -> bool {
    let trimmed = expr.trim();
    (trimmed.starts_with('"') && trimmed.ends_with('"'))
        || (trimmed.starts_with('\'') && trimmed.ends_with('\''))
        || matches!(trimmed, "True" | "False" | "None")
        || trimmed.parse::<i64>().is_ok()
        || trimmed.parse::<f64>().is_ok()
}

fn oversized_python_literal(
    left: &str,
    right: &str,
    weights: &TestAssertionWeights,
) -> Option<String> {
    [left, right].into_iter().find_map(|expr| {
        let trimmed = expr.trim();
        let unquoted = trimmed
            .strip_prefix('"')
            .and_then(|s| s.strip_suffix('"'))
            .or_else(|| {
                trimmed
                    .strip_prefix('\'')
                    .and_then(|s| s.strip_suffix('\''))
            })?;
        (unquoted.len() >= weights.large_literal_length_threshold.max(0) as usize)
            .then(|| trimmed.to_string())
    })
}

fn compact_python_excerpt(text: &str) -> String {
    text.split_whitespace().collect::<Vec<_>>().join(" ")
}

const PYTHON_CLONE_AST_IDENTIFIER_TYPES: &[&str] = &["identifier", "keyword_identifier"];
const PYTHON_CLONE_AST_STRING_TYPES: &[&str] = &["string", "string_content", "interpolation"];
const PYTHON_CLONE_AST_NUMBER_TYPES: &[&str] = &["integer", "float"];

fn normalized_clone_tokens_python(source: &str) -> Vec<String> {
    let Some(tree) = parse_python_tree(source) else {
        return Vec::new();
    };
    let mut out = Vec::new();
    collect_normalized_leaf_tokens_python(tree.root_node(), source, &mut out);
    out
}

fn collect_normalized_leaf_tokens_python(node: Node<'_>, source: &str, out: &mut Vec<String>) {
    if node.named_child_count() == 0 {
        let token = normalize_python_clone_leaf_token(node, source);
        if !token.is_empty() {
            out.push(token);
        }
    }
    let child_count = node.child_count();
    for index in 0..child_count {
        if let Some(child) = node.child(index) {
            collect_normalized_leaf_tokens_python(child, source, out);
        }
    }
}

fn normalize_python_clone_leaf_token(node: Node<'_>, source: &str) -> String {
    let kind = node.kind();
    let token = source
        .get(node.start_byte()..node.end_byte())
        .unwrap_or("")
        .trim();
    if token.is_empty() || kind == "comment" {
        return String::new();
    }
    if PYTHON_CLONE_AST_IDENTIFIER_TYPES.contains(&kind) {
        return "ID".to_string();
    }
    if PYTHON_CLONE_AST_STRING_TYPES.contains(&kind) {
        return "STR".to_string();
    }
    if PYTHON_CLONE_AST_NUMBER_TYPES.contains(&kind) {
        return "NUM".to_string();
    }
    if kind == "true" || kind == "false" || token == "True" || token == "False" {
        return "BOOL".to_string();
    }
    if token.chars().count() == 1 && token.chars().all(|ch| !ch.is_alphanumeric()) {
        return format!("OP:{token}");
    }
    format!("T:{kind}")
}

fn build_python_clone_ast_signature(source: &str) -> String {
    let Some(tree) = parse_python_tree(source) else {
        return String::new();
    };
    let mut labels = Vec::new();
    collect_python_clone_ast_labels(tree.root_node(), source, &mut labels);
    labels.join("|")
}

fn collect_python_clone_ast_labels(node: Node<'_>, source: &str, out: &mut Vec<String>) {
    out.push(normalize_python_clone_ast_label(node, source));
    let child_count = node.child_count();
    for index in 0..child_count {
        if let Some(child) = node.child(index) {
            collect_python_clone_ast_labels(child, source, out);
        }
    }
}

fn normalize_python_clone_ast_label(node: Node<'_>, source: &str) -> String {
    let kind = node.kind();
    let text = source
        .get(node.start_byte()..node.end_byte())
        .unwrap_or("")
        .trim();
    if PYTHON_CLONE_AST_IDENTIFIER_TYPES.contains(&kind) {
        return "ID".to_string();
    }
    if PYTHON_CLONE_AST_STRING_TYPES.contains(&kind) {
        return "STR".to_string();
    }
    if PYTHON_CLONE_AST_NUMBER_TYPES.contains(&kind) {
        return "NUM".to_string();
    }
    if kind == "true" || kind == "false" || text == "True" || text == "False" {
        return "BOOL".to_string();
    }
    format!("N:{kind}")
}

fn refine_python_clone_similarity(
    left: &CloneCandidateData,
    right: &CloneCandidateData,
    token_similarity: i32,
    weights: CloneSmellWeights,
) -> i32 {
    if left.ast_signature.is_empty() || right.ast_signature.is_empty() {
        return token_similarity;
    }
    let ast_similarity =
        compute_ast_refinement_similarity_percent(&left.ast_signature, &right.ast_signature);
    if ast_similarity == 0 {
        return token_similarity;
    }
    if ast_similarity < weights.ast_similarity_percent {
        return 0;
    }
    token_similarity.min(ast_similarity)
}

fn parse_python_tree(source: &str) -> Option<Tree> {
    let mut parser = Parser::new();
    parser
        .set_language(&tree_sitter_python::LANGUAGE.into())
        .expect("failed to load python parser");
    parser.parse(source, None)
}

fn python_source_contains_tests(source: &str) -> bool {
    static TEST_DEF_RE: std::sync::LazyLock<Regex> =
        std::sync::LazyLock::new(|| Regex::new(r"(?m)^\s*def\s+test_[A-Za-z0-9_]*\s*\(").unwrap());
    source.contains("@pytest.mark.") || TEST_DEF_RE.is_match(source)
}

fn extract_package_from_python_wildcard(raw: &str) -> Option<String> {
    let details = parse_python_import_details(raw)?;
    match details {
        PythonImportDetails::FromImport {
            module, wildcard, ..
        } if wildcard => Some(module),
        _ => None,
    }
}

#[derive(Debug, Clone)]
enum PythonImportDetails {
    Import {
        module: String,
        alias: Option<String>,
    },
    FromImport {
        module: String,
        name: String,
        alias: Option<String>,
        wildcard: bool,
    },
}

fn parse_python_import_infos(raw: &str) -> Vec<ImportInfo> {
    let mut infos = Vec::new();
    if let Some(body) = raw.strip_prefix("import ") {
        for part in split_top_level_commas(body) {
            let (module, alias) = split_alias(&part);
            infos.push(ImportInfo {
                raw_snippet: if let Some(alias) = &alias {
                    format!("import {module} as {alias}")
                } else {
                    format!("import {module}")
                },
                is_wildcard: false,
                identifier: Some(alias.clone().unwrap_or_else(|| {
                    module
                        .split('.')
                        .next_back()
                        .unwrap_or(module.as_str())
                        .to_string()
                })),
                alias,
            });
        }
    } else if let Some((module, names)) = raw
        .strip_prefix("from ")
        .and_then(|tail| tail.split_once(" import "))
    {
        if names.trim() == "*" {
            infos.push(ImportInfo {
                raw_snippet: format!("from {module} import *"),
                is_wildcard: true,
                identifier: None,
                alias: None,
            });
        } else {
            for part in split_top_level_commas(names) {
                let (name, alias) = split_alias(&part);
                infos.push(ImportInfo {
                    raw_snippet: if let Some(alias) = &alias {
                        format!("from {module} import {name} as {alias}")
                    } else {
                        format!("from {module} import {name}")
                    },
                    is_wildcard: false,
                    identifier: Some(alias.clone().unwrap_or_else(|| name.clone())),
                    alias,
                });
            }
        }
    }
    infos
}

fn parse_python_import_details(raw: &str) -> Option<PythonImportDetails> {
    if let Some(body) = raw.strip_prefix("import ") {
        let part = split_top_level_commas(body).into_iter().next()?;
        let (module, alias) = split_alias(&part);
        return Some(PythonImportDetails::Import { module, alias });
    }
    let (module, names) = raw.strip_prefix("from ")?.split_once(" import ")?;
    if names.trim() == "*" {
        return Some(PythonImportDetails::FromImport {
            module: module.to_string(),
            name: "*".to_string(),
            alias: None,
            wildcard: true,
        });
    }
    let part = split_top_level_commas(names).into_iter().next()?;
    let (name, alias) = split_alias(&part);
    Some(PythonImportDetails::FromImport {
        module: module.to_string(),
        name,
        alias,
        wildcard: false,
    })
}

fn split_top_level_commas(input: &str) -> Vec<String> {
    input
        .split(',')
        .map(normalize_python_import_part)
        .filter(|part| !part.is_empty())
        .collect()
}

fn normalize_python_import_part(input: &str) -> String {
    input
        .trim()
        .trim_start_matches('(')
        .trim_end_matches(')')
        .trim()
        .to_string()
}

fn split_alias(input: &str) -> (String, Option<String>) {
    input
        .rsplit_once(" as ")
        .map(|(name, alias)| (name.trim().to_string(), Some(alias.trim().to_string())))
        .unwrap_or_else(|| (input.trim().to_string(), None))
}

fn resolve_python_relative_module(source_file: &ProjectFile, module_expr: &str) -> Option<String> {
    let level = module_expr.chars().take_while(|ch| *ch == '.').count();
    let suffix = module_expr[level..].trim_matches('.');
    let current_package = python_current_package(source_file);
    let mut parts: Vec<_> = current_package
        .split('.')
        .filter(|part| !part.is_empty())
        .map(str::to_string)
        .collect();
    if level == 0 {
        return Some(module_expr.to_string());
    }
    if level > 0 {
        if level - 1 > parts.len() {
            return None;
        }
        parts.truncate(parts.len() - (level - 1));
    }
    if !suffix.is_empty() {
        parts.extend(suffix.split('.').map(str::to_string));
    }
    Some(parts.join("."))
}

fn python_current_package(source_file: &ProjectFile) -> String {
    let module = python_module_name(source_file);
    if source_file
        .rel_path()
        .file_name()
        .and_then(|name| name.to_str())
        == Some("__init__.py")
    {
        module
    } else {
        module
            .rsplit_once('.')
            .map(|(package, _)| package.to_string())
            .unwrap_or_default()
    }
}

fn weight_project_file_set(_key: &ProjectFile, value: &Arc<HashSet<ProjectFile>>) -> u32 {
    let size = value
        .iter()
        .map(|item| item.rel_path().to_string_lossy().len() + size_of::<ProjectFile>())
        .sum::<usize>()
        + size_of::<HashSet<ProjectFile>>();
    size.min(u32::MAX as usize) as u32
}

fn weight_code_unit_set(_key: &ProjectFile, value: &Arc<HashSet<CodeUnit>>) -> u32 {
    let size = value
        .iter()
        .map(|item| item.fq_name().len() + size_of::<CodeUnit>())
        .sum::<usize>()
        + size_of::<HashSet<CodeUnit>>();
    size.min(u32::MAX as usize) as u32
}

fn weight_code_unit_vec(_key: &CodeUnit, value: &Arc<Vec<CodeUnit>>) -> u32 {
    let size = value
        .iter()
        .map(|item| item.fq_name().len() + size_of::<CodeUnit>())
        .sum::<usize>()
        + size_of::<Vec<CodeUnit>>();
    size.min(u32::MAX as usize) as u32
}

fn weight_code_unit_set_by_unit(_key: &CodeUnit, value: &Arc<HashSet<CodeUnit>>) -> u32 {
    let size = value
        .iter()
        .map(|item| item.fq_name().len() + size_of::<CodeUnit>())
        .sum::<usize>()
        + size_of::<HashSet<CodeUnit>>();
    size.min(u32::MAX as usize) as u32
}
