"""AST-based code utilities, glob helpers, and time formatting."""

from __future__ import annotations

import ast
import logging
from pathlib import Path

from .._model import FunctionParent

log = logging.getLogger(__name__)

GLOB_PATTERN_CHARS: frozenset[str] = frozenset("*?[")


def is_glob_pattern(path_str: str) -> bool:
    """Check if a path string contains glob pattern characters."""
    return any(char in path_str for char in GLOB_PATTERN_CHARS)


def normalize_ignore_paths(
    paths: list[str],
    base_path: Path | None = None,
) -> list[Path]:
    """Resolve *paths* to absolute ``Path`` objects under *base_path*."""
    if base_path is None:
        base_path = Path.cwd()

    base_path = base_path.resolve()
    normalized: set[Path] = set()

    for path_str in paths:
        if not path_str:
            continue

        path_str = str(path_str)  # noqa: PLW2901

        if is_glob_pattern(path_str):
            path_str = path_str.removeprefix("./")  # noqa: PLW2901
            if path_str.startswith("/"):
                path_str = path_str.lstrip("/")  # noqa: PLW2901

            for matched_path in base_path.glob(path_str):
                normalized.add(matched_path.resolve())
        else:
            path_obj = Path(path_str)
            if not path_obj.is_absolute():
                path_obj = base_path / path_obj
            if path_obj.exists():
                normalized.add(path_obj.resolve())

    return list(normalized)


def validate_python_code(code: str) -> str:
    """Validate a string of Python code by attempting to compile it."""
    try:
        compile(code, "<string>", "exec")
    except SyntaxError as e:
        msg = f"Invalid Python code: {e.msg} (line {e.lineno}, column {e.offset})"
        raise ValueError(msg) from e
    return code


def is_class_defined_in_file(
    class_name: str,
    file_path: Path,
) -> bool:
    """Return *True* if *class_name* is defined in *file_path*."""
    if not file_path.exists():
        return False
    with file_path.open(encoding="utf8") as file:
        source = file.read()
    tree = ast.parse(source)
    return any(
        isinstance(node, ast.ClassDef) and node.name == class_name
        for node in ast.walk(tree)
    )


def get_all_function_names(code: str) -> tuple[bool, list[str]]:
    """Return all function names defined in *code*."""
    try:
        module = ast.parse(code)
    except SyntaxError:
        log.exception("Syntax error in code")
        return False, []

    function_names = [
        node.name
        for node in ast.walk(module)
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
    ]
    return True, function_names


def get_imports_from_file(
    file_path: Path | None = None,
    file_string: str | None = None,
    file_ast: ast.AST | None = None,
) -> list[ast.Import | ast.ImportFrom]:
    """Return all import nodes from a file, string, or AST."""
    assert (  # noqa: S101
        sum(
            [
                file_path is not None,
                file_string is not None,
                file_ast is not None,
            ],
        )
        == 1
    ), "Must provide exactly one of file_path, file_string, or file_ast"

    if file_path:
        with file_path.open(encoding="utf8") as file:
            file_string = file.read()
    if file_ast is None:
        if file_string is None:
            log.error(
                "file_string cannot be None when file_ast is not provided",
            )
            return []
        try:
            file_ast = ast.parse(file_string)
        except SyntaxError:
            log.exception("Syntax error in code")
            return []
    return [
        node
        for node in ast.walk(file_ast)
        if isinstance(node, (ast.Import, ast.ImportFrom))
    ]


def find_preexisting_objects(
    source_code: str,
) -> set[tuple[str, tuple[FunctionParent, ...]]]:
    """Find all top-level functions, classes, and methods in *source_code*."""
    preexisting_objects: set[tuple[str, tuple[FunctionParent, ...]]] = set()
    try:
        module_node: ast.Module = ast.parse(source_code)
    except SyntaxError:
        log.exception(
            "find_preexisting_objects - Syntax error while parsing code",
        )
        return preexisting_objects
    for node in module_node.body:
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
            preexisting_objects.add((node.name, ()))
        elif isinstance(node, ast.ClassDef):
            preexisting_objects.add((node.name, ()))
            for cnode in node.body:
                if isinstance(
                    cnode,
                    (ast.FunctionDef, ast.AsyncFunctionDef),
                ):
                    parent = FunctionParent(
                        node.name,
                        "ClassDef",
                    )
                    preexisting_objects.add(
                        (cnode.name, (parent,)),
                    )
    return preexisting_objects
