"""Core dead code detection logic."""

from __future__ import annotations

from collections import defaultdict
from dataclasses import dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from tree_sitter import Node

from vibelexity.dead_code.parsers.go import GoDeadCodeParser
from vibelexity.dead_code.parsers.python import PythonDeadCodeParser
from vibelexity.dead_code.parsers.typescript import TypeScriptDeadCodeParser
from vibelexity.models import DeadCodeViolation
from vibelexity.parsers.go import parse as parse_go
from vibelexity.parsers.python import parse as parse_python
from vibelexity.parsers.typescript import parse as parse_typescript


@dataclass
class SymbolInfo:
    name: str = ""
    def_type: str = ""
    definitions: dict[str, list[int]] = field(default_factory=dict)  # filepath -> lines
    references: list[tuple] = field(default_factory=list)
    definitions_count: int = 0
    exported: bool = False


def _collect_definitions(
    files: list[Path],
    parser,
    lang: str,
    parse_func,
    graph: dict[str, SymbolInfo],
) -> tuple[set, set]:
    all_abstract_classes = set()
    all_abstract_methods = set()

    for filepath in files:
        filepath_str = str(filepath)
        source = filepath.read_text()
        root = parse_func(source)

        imports = parser.extract_imports(root, source)
        definitions = parser.extract_definitions(root, source)
        abstract_classes, abstract_methods = parser.extract_abstract_info(root, source)

        all_abstract_classes.update(abstract_classes)
        all_abstract_methods.update(abstract_methods)

        for imp in imports:
            symbol_key_name = imp.alias or imp.name
            symbol_key = f"{lang}:import:{symbol_key_name}"
            if symbol_key not in graph:
                graph[symbol_key] = SymbolInfo(name=symbol_key_name, def_type="import")
            if filepath_str not in graph[symbol_key].definitions:
                graph[symbol_key].definitions[filepath_str] = []
            graph[symbol_key].definitions[filepath_str].append(imp.line)
            graph[symbol_key].definitions_count += 1

        for definition in definitions:
            symbol_key = f"{lang}:{definition.definition_type}:{definition.name}"
            if symbol_key not in graph:
                graph[symbol_key] = SymbolInfo(
                    name=definition.name,
                    def_type=definition.definition_type,
                    exported=definition.exported,
                )
            else:
                graph[symbol_key].exported = (
                    graph[symbol_key].exported or definition.exported
                )
            if filepath_str not in graph[symbol_key].definitions:
                graph[symbol_key].definitions[filepath_str] = []
            graph[symbol_key].definitions[filepath_str].append(definition.line)
            graph[symbol_key].definitions_count += 1

    return all_abstract_classes, all_abstract_methods


def _collect_references(
    files: list[Path],
    parser,
    parse_func,
    name_to_entries: dict,
    graph: dict[str, SymbolInfo],
) -> None:
    for filepath in files:
        filepath_str = str(filepath)
        source = filepath.read_text()
        root = parse_func(source)

        refs = parser.extract_references(root, source)

        for ref in refs:
            entries = name_to_entries.get(ref.name, [])
            for symbol_key, info in entries:
                def_lines = info.definitions.get(filepath_str, [])
                is_self_reference = ref.line in def_lines
                if not is_self_reference:
                    info.references.append((filepath_str, ref.line))


def build_cross_reference_graph(
    files: list[Path], lang: str = "python"
) -> tuple[dict, set, set, dict]:
    graph: dict[str, SymbolInfo] = {}

    parser_map = {
        "python": PythonDeadCodeParser(),
        "typescript": TypeScriptDeadCodeParser(),
        "go": GoDeadCodeParser(),
    }

    parser = parser_map.get(lang)
    if not parser:
        return graph, set(), set(), {}

    parse_func_map = {
        "python": lambda src: _parse_python(src),
        "typescript": lambda src: _parse_typescript(src),
        "go": lambda src: _parse_go(src),
    }

    parse_func = parse_func_map.get(lang)
    if not parse_func:
        return graph, set(), set(), {}

    all_abstract_classes, all_abstract_methods = _collect_definitions(
        files, parser, lang, parse_func, graph
    )

    name_to_entries: dict[str, list[tuple[str, SymbolInfo]]] = defaultdict(list)
    for symbol_key, info in graph.items():
        name_to_entries[info.name].append((symbol_key, info))

    _collect_references(files, parser, parse_func, name_to_entries, graph)

    return graph, all_abstract_classes, all_abstract_methods, name_to_entries


def _parse_python(source: str):
    return parse_python(source)


def _parse_typescript(source: str):
    return parse_typescript(source)


def _parse_go(source: str):
    return parse_go(source)


def find_unused_imports(graph: dict) -> list[DeadCodeViolation]:
    violations = []

    for symbol_key, info in graph.items():
        if info.def_type == "import":
            refs_count = len(info.references)
            if refs_count == 0:
                for filepath, lines in info.definitions.items():
                    for line in lines:
                        confidence = "high"
                        violations.append(
                            DeadCodeViolation(
                                type="unused_import",
                                symbol=info.name,
                                location=filepath,
                                line=line,
                                confidence=confidence,
                            )
                        )

    return violations


def _is_dunder_method(name: str) -> bool:
    return name.startswith("__") and name.endswith("__")


def _build_name_to_entries(graph: dict) -> dict:
    name_to_entries = {}
    for symbol_key, info in graph.items():
        if info.name not in name_to_entries:
            name_to_entries[info.name] = []
        name_to_entries[info.name].append((symbol_key, info))
    return name_to_entries


def _should_skip_symbol(
    info: SymbolInfo, abstract_classes: set, abstract_methods: set
) -> bool:
    if info.name in abstract_classes:
        return True
    if info.def_type == "function" and info.name in abstract_methods:
        return True
    if info.def_type == "function" and _is_dunder_method(info.name):
        return True
    if info.exported:
        return True
    return False


def _count_total_references(info: SymbolInfo, name_to_entries: dict) -> int:
    total_refs = len(info.references)
    if info.name in name_to_entries:
        for other_key, other_info in name_to_entries[info.name]:
            if other_info.def_type == "import":
                total_refs += len(other_info.references)
    return total_refs


def _add_dead_code_violations(
    info: SymbolInfo, violations: list[DeadCodeViolation]
) -> None:
    confidence = "high" if info.definitions_count == 1 else "medium"
    for filepath, lines in info.definitions.items():
        for line in lines:
            violations.append(
                DeadCodeViolation(
                    type=f"dead_{info.def_type}",
                    symbol=info.name,
                    location=filepath,
                    line=line,
                    confidence=confidence,
                )
            )


def find_dead_symbols(
    graph: dict,
    abstract_classes: set,
    abstract_methods: set,
    lang: str = "python",
    name_to_entries: dict | None = None,
) -> list[DeadCodeViolation]:
    violations = []

    if name_to_entries is None:
        name_to_entries = _build_name_to_entries(graph)

    for symbol_key, info in graph.items():
        if info.def_type in ("function", "class"):
            if _should_skip_symbol(info, abstract_classes, abstract_methods):
                continue

            total_refs = _count_total_references(info, name_to_entries)

            if total_refs == 0:
                _add_dead_code_violations(info, violations)

    return violations


def find_dead_code(
    source_files: list[Path], lang: str = "python"
) -> list[DeadCodeViolation]:
    violations = []

    graph, abstract_classes, abstract_methods, name_to_entries = (
        build_cross_reference_graph(source_files, lang)
    )
    violations.extend(find_unused_imports(graph))
    violations.extend(
        find_dead_symbols(
            graph, abstract_classes, abstract_methods, lang, name_to_entries
        )
    )

    violations.sort(key=lambda v: (v.line, v.symbol or ""))

    return violations
