"""Multi-language AST analysis tools using Python AST (primary) + tree-sitter (future).

Currently supports Python via ast module. Can be extended with tree-sitter
for JavaScript, TypeScript, Go, Rust, etc.
"""

from __future__ import annotations

import ast
from pathlib import Path
from typing import Any


class MultiLanguageAnalyzer:
    """Multi-language code analyzer.

    Uses Python's ast module for Python files. Designed to be extended
    with tree-sitter for other languages.
    """

    # Language detection from file extension
    LANGUAGE_MAP = {
        ".py": "python",
        ".js": "javascript",
        ".ts": "typescript",
        ".tsx": "typescript",
        ".jsx": "javascript",
        ".go": "go",
        ".rs": "rust",
        ".java": "java",
        ".c": "c",
        ".cpp": "cpp",
        ".cc": "cpp",
        ".h": "c",
        ".hpp": "cpp",
        ".kt": "kotlin",
        ".swift": "swift",
        ".rb": "ruby",
        ".php": "php",
        ".cs": "csharp",
        ".scala": "scala",
    }

    # Standard library modules (for categorizing imports)
    STDLIB_MODULES = {
        "abc", "aifc", "argparse", "array", "ast", "asynchat", "asyncio",
        "asyncore", "atexit", "audioop", "base64", "bdb", "binascii",
        "binhex", "bisect", "builtins", "bz2", "calendar", "cgi", "cgitb",
        "chunk", "cmath", "cmd", "code", "codecs", "codeop", "collections",
        "colorsys", "compileall", "concurrent", "configparser", "contextlib",
        "contextvars", "copy", "copyreg", "cProfile", "crypt", "csv", "ctypes",
        "curses", "dataclasses", "datetime", "dbm", "decimal", "difflib",
        "dis", "distutils", "doctest", "email", "encodings", "enum", "errno",
        "faulthandler", "fcntl", "filecmp", "fileinput", "fnmatch", "fractions",
        "ftplib", "functools", "gc", "getopt", "getpass", "gettext", "glob",
        "graphlib", "grp", "gzip", "hashlib", "heapq", "hmac", "html", "http",
        "idlelib", "imaplib", "imghdr", "imp", "importlib", "inspect", "io",
        "ipaddress", "itertools", "json", "keyword", "lib2to3", "linecache",
        "locale", "logging", "lzma", "mailbox", "mailcap", "marshal", "math",
        "mimetypes", "mmap", "modulefinder", "multiprocessing", "netrc",
        "nis", "nntplib", "numbers", "operator", "optparse", "os", "ossaudiodev",
        "parser", "pathlib", "pdb", "pickle", "pickletools", "pipes", "pkgutil",
        "platform", "plistlib", "poplib", "posix", "posixpath", "pprint",
        "profile", "pstats", "pty", "pwd", "py_compile", "pyclbr", "pydoc",
        "queue", "quopri", "random", "re", "readline", "reprlib", "resource",
        "rlcompleter", "runpy", "sched", "secrets", "select", "selectors",
        "shelve", "shlex", "shutil", "signal", "site", "smtpd", "smtplib",
        "sndhdr", "socket", "socketserver", "spwd", "sqlite3", "ssl", "stat",
        "statistics", "string", "stringprep", "struct", "subprocess", "sunau",
        "symtable", "sys", "sysconfig", "syslog", "tabnanny", "tarfile",
        "telnetlib", "tempfile", "termios", "test", "textwrap", "threading",
        "time", "timeit", "tkinter", "token", "tokenize", "trace", "traceback",
        "tracemalloc", "tty", "turtle", "turtledemo", "types", "typing",
        "unicodedata", "unittest", "urllib", "uu", "uuid", "venv", "warnings",
        "wave", "weakref", "webbrowser", "winreg", "winsound", "wsgiref",
        "xdrlib", "xml", "xmlrpc", "zipapp", "zipfile", "zipimport", "zlib",
        "_thread",
    }

    def __init__(self, project_root: Path):
        """Initialize analyzer.

        Args:
            project_root: Root path of the project
        """
        self.project_root = project_root.resolve()
        self._module_cache: dict[str, dict[str, Any]] = {}
        self._call_graph_cache: dict[str, dict[str, list[str]]] = {}

    def detect_language(self, file_path: Path) -> str:
        """Detect programming language from file extension.

        Args:
            file_path: Path to the file

        Returns:
            Language name
        """
        return self.LANGUAGE_MAP.get(file_path.suffix.lower(), "text")

    def list_modules(self) -> list[dict[str, Any]]:
        """List all modules (directories with source files) in the project.

        Returns:
            List of module info dictionaries
        """
        modules = []

        # Find source root
        src_root = self._find_source_root()

        # Find module directories
        for item in src_root.iterdir():
            if not item.is_dir() or item.name.startswith(("_", ".")):
                continue

            # Check if it has source files
            source_files = self._find_source_files(item)
            if not source_files:
                continue

            # Detect primary language
            languages: dict[str, int] = {}
            for f in source_files:
                lang = self.detect_language(f)
                languages[lang] = languages.get(lang, 0) + 1

            primary_lang = max(languages.items(), key=lambda x: x[1])[0] if languages else "unknown"

            rel_path = item.relative_to(self.project_root)

            modules.append({
                "name": item.name,
                "path": str(rel_path),
                "file_count": len(source_files),
                "primary_language": primary_lang,
                "languages": list(languages.keys()),
            })

        # Also check for root-level modules (single files)
        root_files = self._find_source_files(src_root, recursive=False)
        for root_file in root_files:
            if root_file.stem == "__init__":
                continue
            rel_path = root_file.relative_to(self.project_root)
            modules.append({
                "name": root_file.stem,
                "path": str(rel_path),
                "file_count": 1,
                "primary_language": self.detect_language(root_file),
                "languages": [self.detect_language(root_file)],
                "is_file": True,
            })

        return sorted(modules, key=lambda x: x["name"])

    def get_module_info(self, module_path: str) -> dict[str, Any]:
        """Get detailed information about a module.

        Args:
            module_path: Relative path to the module

        Returns:
            Module information dictionary
        """
        if module_path in self._module_cache:
            return self._module_cache[module_path]

        full_path = self.project_root / module_path

        # Handle single file module
        if full_path.is_file():
            result = self._analyze_single_file(full_path)
        else:
            result = self._analyze_directory(full_path)

        self._module_cache[module_path] = result
        return result

    def get_function_signature(
        self, file_path: str, function_name: str
    ) -> dict[str, Any] | None:
        """Get the signature of a specific function.

        Args:
            file_path: Relative path to the file
            function_name: Name of the function

        Returns:
            Function signature info or None if not found
        """
        full_path = self.project_root / file_path

        if not full_path.exists():
            return None

        if self.detect_language(full_path) != "python":
            # For non-Python, return basic info (future: use tree-sitter)
            return {
                "name": function_name,
                "file_path": file_path,
                "language": self.detect_language(full_path),
                "note": "Signature extraction not yet supported for this language",
            }

        try:
            with open(full_path, "r", encoding="utf-8") as f:
                source = f.read()

            tree = ast.parse(source, filename=str(full_path))

            for node in ast.walk(tree):
                if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                    if node.name == function_name:
                        return self._extract_function_info(node, file_path)

            return None

        except Exception as e:
            return {"error": str(e), "name": function_name, "file_path": file_path}

    def get_class_hierarchy(
        self, class_name: str, file_path: str | None = None
    ) -> dict[str, Any] | None:
        """Get class inheritance hierarchy.

        Args:
            class_name: Name of the class
            file_path: Optional file path to search in

        Returns:
            Class hierarchy info or None if not found
        """
        def search_file(fp: Path) -> dict[str, Any] | None:
            if not fp.exists() or self.detect_language(fp) != "python":
                return None

            try:
                with open(fp, "r", encoding="utf-8") as f:
                    source = f.read()

                tree = ast.parse(source, filename=str(fp))

                for node in ast.iter_child_nodes(tree):
                    if isinstance(node, ast.ClassDef) and node.name == class_name:
                        return self._extract_class_info(
                            node, str(fp.relative_to(self.project_root))
                        )

            except Exception:
                pass
            return None

        if file_path:
            full_path = self.project_root / file_path
            result = search_file(full_path)
            if result:
                return result
            return None

        # Search all Python files
        src_root = self._find_source_root()
        for py_file in src_root.rglob("*.py"):
            if "__pycache__" in str(py_file):
                continue
            result = search_file(py_file)
            if result:
                return result

        return None

    def get_imports(self, module_path: str) -> dict[str, Any]:
        """Get imports for a module.

        Args:
            module_path: Relative path to the module

        Returns:
            Import information with internal/external categorization
        """
        full_path = self.project_root / module_path

        if full_path.is_file():
            files = [full_path]
        else:
            files = self._find_source_files(full_path)

        internal = set()
        external = set()
        line_numbers: dict[str, int] = {}

        for file_path in files:
            if self.detect_language(file_path) != "python":
                continue

            try:
                with open(file_path, "r", encoding="utf-8") as f:
                    source = f.read()

                tree = ast.parse(source, filename=str(file_path))
                file_imports = self._extract_imports(tree)

                internal.update(file_imports.get("internal", []))
                external.update(file_imports.get("external", []))
                line_numbers.update(file_imports.get("line_numbers", {}))

            except Exception:
                pass

        return {
            "internal": sorted(internal),
            "external": sorted(external),
            "line_numbers": line_numbers,
        }

    def get_function_calls(
        self, file_path: str, function_name: str
    ) -> dict[str, Any]:
        """Get all functions called by a specific function.

        Args:
            file_path: Relative path to the file
            function_name: Name of the function

        Returns:
            Dictionary with called functions list
        """
        full_path = self.project_root / file_path

        if not full_path.exists():
            return {"error": f"File not found: {file_path}"}

        if self.detect_language(full_path) != "python":
            return {
                "function": function_name,
                "file_path": file_path,
                "calls": [],
                "note": "Call analysis not yet supported for this language",
            }

        try:
            with open(full_path, "r", encoding="utf-8") as f:
                source = f.read()

            tree = ast.parse(source, filename=str(full_path))

            for node in ast.walk(tree):
                if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                    if node.name == function_name:
                        calls = self._extract_calls_from_function(node)
                        return {
                            "function": function_name,
                            "file_path": file_path,
                            "line": node.lineno,
                            "calls": calls,
                        }

            return {"error": f"Function not found: {function_name}"}

        except Exception as e:
            return {"error": str(e)}

    def get_callers(self, function_name: str) -> dict[str, Any]:
        """Find all callers of a function across the project.

        Args:
            function_name: Name of the function to find callers for

        Returns:
            Dictionary with list of caller locations
        """
        callers = []

        src_root = self._find_source_root()

        for py_file in src_root.rglob("*.py"):
            if "__pycache__" in str(py_file):
                continue

            try:
                with open(py_file, "r", encoding="utf-8") as f:
                    source = f.read()

                tree = ast.parse(source, filename=str(py_file))
                rel_path = str(py_file.relative_to(self.project_root))

                # Walk all functions to find callers
                for node in ast.walk(tree):
                    if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                        # Check if this function calls the target
                        for child in ast.walk(node):
                            if isinstance(child, ast.Call):
                                func_name = self._get_call_name(child)
                                if func_name == function_name:
                                    callers.append({
                                        "file_path": rel_path,
                                        "function": node.name,
                                        "line": child.lineno,
                                    })
                                    break

            except Exception:
                pass

        return {
            "function": function_name,
            "callers": callers,
            "caller_count": len(callers),
        }

    # Private helper methods

    def _find_source_root(self) -> Path:
        """Find the source root directory."""
        src_dir = self.project_path / "src"
        if src_dir.exists() and src_dir.is_dir():
            # Look for package directory
            for item in src_dir.iterdir():
                if item.is_dir() and (item / "__init__.py").exists():
                    return item
            return src_dir
        return self.project_path

    @property
    def project_path(self) -> Path:
        """Alias for project_root for compatibility."""
        return self.project_root

    def _find_source_files(
        self, directory: Path, recursive: bool = True
    ) -> list[Path]:
        """Find all source files in a directory."""
        extensions = set(self.LANGUAGE_MAP.keys())
        files = []

        if recursive:
            for ext in extensions:
                for f in directory.rglob(f"*{ext}"):
                    if "__pycache__" not in str(f) and "node_modules" not in str(f):
                        files.append(f)
        else:
            for ext in extensions:
                for f in directory.glob(f"*{ext}"):
                    files.append(f)

        return sorted(files)

    def _analyze_single_file(self, file_path: Path) -> dict[str, Any]:
        """Analyze a single source file."""
        rel_path = str(file_path.relative_to(self.project_root))
        language = self.detect_language(file_path)

        result: dict[str, Any] = {
            "path": rel_path,
            "language": language,
            "functions": [],
            "classes": [],
            "imports": {"internal": [], "external": [], "line_numbers": {}},
            "line_count": 0,
        }

        try:
            with open(file_path, "r", encoding="utf-8") as f:
                source = f.read()
            result["line_count"] = len(source.splitlines())
        except Exception:
            return result

        if language != "python":
            return result

        try:
            tree = ast.parse(source, filename=str(file_path))
            result["functions"] = [
                self._extract_function_info(node, rel_path)
                for node in ast.walk(tree)
                if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
            ]
            result["classes"] = [
                self._extract_class_info(node, rel_path)
                for node in ast.iter_child_nodes(tree)
                if isinstance(node, ast.ClassDef)
            ]
            result["imports"] = self._extract_imports(tree)
        except Exception as e:
            result["error"] = str(e)

        return result

    def _analyze_directory(self, dir_path: Path) -> dict[str, Any]:
        """Analyze a directory of source files."""
        if not dir_path.exists():
            return {"error": f"Directory not found: {dir_path}"}

        files = self._find_source_files(dir_path)

        all_functions = []
        all_classes = []
        all_imports: dict[str, Any] = {
            "internal": set(),
            "external": set(),
            "line_numbers": {},
        }
        file_infos = []

        for file_path in files:
            rel_path = str(file_path.relative_to(self.project_path))
            file_info = self._analyze_single_file(file_path)
            file_infos.append({
                "path": rel_path,
                "language": file_info["language"],
                "line_count": file_info.get("line_count", 0),
            })

            all_functions.extend(file_info.get("functions", []))
            all_classes.extend(file_info.get("classes", []))

            imports = file_info.get("imports", {})
            all_imports["internal"].update(imports.get("internal", []))
            all_imports["external"].update(imports.get("external", []))
            all_imports["line_numbers"].update(imports.get("line_numbers", {}))

        return {
            "path": str(dir_path.relative_to(self.project_path)),
            "files": file_infos,
            "functions": all_functions,
            "classes": all_classes,
            "imports": {
                "internal": sorted(all_imports["internal"]),
                "external": sorted(all_imports["external"]),
                "line_numbers": all_imports["line_numbers"],
            },
        }

    def _extract_function_info(
        self, node: ast.FunctionDef | ast.AsyncFunctionDef, file_path: str
    ) -> dict[str, Any]:
        """Extract function information from AST node."""
        return {
            "name": node.name,
            "file_path": file_path,
            "line": node.lineno,
            "end_line": node.end_lineno,
            "parameters": [arg.arg for arg in node.args.args],
            "defaults": [
                ast.unparse(d) if isinstance(d, ast.AST) else str(d)
                for d in node.args.defaults
            ],
            "return_type": ast.unparse(node.returns) if node.returns else None,
            "is_async": isinstance(node, ast.AsyncFunctionDef),
            "is_method": (
                any(isinstance(p, ast.Name) and p.id == "self"
                    for p in [node.args.args[0]]) if node.args.args else False
            ),
            "decorators": [
                d.id if isinstance(d, ast.Name) else ast.unparse(d)
                for d in node.decorator_list
            ],
            "docstring": ast.get_docstring(node),
        }

    def _extract_class_info(
        self, node: ast.ClassDef, file_path: str
    ) -> dict[str, Any]:
        """Extract class information from AST node."""
        bases = []
        for base in node.bases:
            if isinstance(base, ast.Name):
                bases.append(base.id)
            else:
                bases.append(ast.unparse(base))

        methods = []
        for item in node.body:
            if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
                methods.append({
                    "name": item.name,
                    "line": item.lineno,
                    "is_async": isinstance(item, ast.AsyncFunctionDef),
                })

        return {
            "name": node.name,
            "file_path": file_path,
            "line": node.lineno,
            "end_line": node.end_lineno,
            "bases": bases,
            "methods": methods,
            "decorators": [
                d.id if isinstance(d, ast.Name) else ast.unparse(d)
                for d in node.decorator_list
            ],
            "docstring": ast.get_docstring(node),
        }

    def _extract_imports(self, tree: ast.AST) -> dict[str, Any]:
        """Extract import information from AST."""
        internal = []
        external = []
        line_numbers = {}

        for node in ast.walk(tree):
            if isinstance(node, ast.Import):
                for alias in node.names:
                    name = alias.name
                    line_numbers[name] = node.lineno
                    root = name.split(".")[0]
                    if root in self.STDLIB_MODULES:
                        external.append(name)
                    else:
                        internal.append(name)

            elif isinstance(node, ast.ImportFrom):
                module = node.module or ""
                line_numbers[module] = node.lineno

                if node.level > 0:  # Relative import
                    internal.append(module)
                else:
                    root = module.split(".")[0] if module else ""
                    if root in self.STDLIB_MODULES:
                        external.append(module)
                    else:
                        internal.append(module)

        return {
            "internal": list(set(internal)),
            "external": list(set(external)),
            "line_numbers": line_numbers,
        }

    def _extract_calls_from_function(
        self, node: ast.FunctionDef | ast.AsyncFunctionDef
    ) -> list[str]:
        """Extract all function calls from within a function."""
        calls = []

        for child in ast.walk(node):
            if isinstance(child, ast.Call):
                call_name = self._get_call_name(child)
                if call_name and not call_name.startswith(("(", "[")):
                    calls.append(call_name)

        return list(set(calls))

    def _get_call_name(self, call_node: ast.Call) -> str | None:
        """Get the name of a called function."""
        func = call_node.func

        if isinstance(func, ast.Name):
            return func.id
        elif isinstance(func, ast.Attribute):
            # e.g., self.method, obj.method, module.function
            parts = []
            current = func
            while isinstance(current, ast.Attribute):
                parts.append(current.attr)
                current = current.value
            if isinstance(current, ast.Name):
                parts.append(current.id)
            return ".".join(reversed(parts))

        return None
