"""Import validation and static analysis utilities."""

from __future__ import annotations

import ast
from enum import Enum
from pathlib import Path
from typing import TYPE_CHECKING, TypeVar

import attrs

if TYPE_CHECKING:
    from .._model import FunctionParent

ObjectDefT = TypeVar(
    "ObjectDefT", ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef
)


def _validate_identifier(
    instance: object,
    attribute: attrs.Attribute[str],
    value: str,
) -> None:
    """Validate that *value* is a Python identifier."""
    if not value.isidentifier():
        msg = "must be an identifier"
        raise ValueError(msg)


def _validate_dotted_identifier(
    instance: object,
    attribute: attrs.Attribute[str],
    value: str,
) -> None:
    """Validate that *value* is a dotted Python identifier."""
    if any(not s or not s.isidentifier() for s in value.split(".")):
        msg = "must be a dotted identifier"
        raise ValueError(msg)


def _validate_file_path_exists(
    instance: object,
    attribute: attrs.Attribute[Path],
    value: Path,
) -> None:
    """Validate that *value* is an existing path."""
    if not value.exists():
        msg = "must be an existing path"
        raise ValueError(msg)


@attrs.frozen
class ImportedInternalModuleAnalysis:
    """Analysis result for a single imported internal module."""

    name: str = attrs.field(validator=_validate_identifier)
    full_name: str = attrs.field(validator=_validate_dotted_identifier)
    file_path: Path = attrs.field(
        converter=Path, validator=_validate_file_path_exists
    )


class FunctionKind(Enum):
    """Classification of a function definition."""

    FUNCTION = 0
    STATIC_METHOD = 1
    CLASS_METHOD = 2
    INSTANCE_METHOD = 3


def parse_imports(
    code: str,
) -> list[ast.Import | ast.ImportFrom]:
    """Parse import statements from *code*."""
    return [
        node
        for node in ast.walk(ast.parse(code))
        if isinstance(node, (ast.Import, ast.ImportFrom))
    ]


def resolve_relative_name(
    module: str | None, level: int, current_module: str
) -> str | None:
    """Resolve a relative import name to its absolute form."""
    if level == 0:
        return module
    current_parts = current_module.split(".")
    if level > len(current_parts):
        return None
    base_parts = current_parts[:-level]
    if module:
        base_parts.extend(module.split("."))
    return ".".join(base_parts)


def get_module_full_name(
    node: ast.Import | ast.ImportFrom, current_module: str
) -> list[str]:
    """Get full module names from an import node."""
    if isinstance(node, ast.Import):
        return [alias.name for alias in node.names]
    base_module = resolve_relative_name(
        node.module, node.level, current_module
    )
    if base_module is None:
        return []
    if node.module is None and node.level > 0:
        return [f"{base_module}.{alias.name}" for alias in node.names]
    return [base_module]


def is_internal_module(module_name: str, project_root: Path) -> bool:
    """Check if *module_name* refers to a module inside *project_root*."""
    module_path = module_name.replace(".", "/")
    possible_paths = [
        project_root / f"{module_path}.py",
        project_root / module_path / "__init__.py",
    ]
    return any(path.exists() for path in possible_paths)


def get_module_file_path(module_name: str, project_root: Path) -> Path | None:
    """Find the file path for *module_name* under *project_root*."""
    module_path = module_name.replace(".", "/")
    possible_paths = [
        project_root / f"{module_path}.py",
        project_root / module_path / "__init__.py",
    ]
    for path in possible_paths:
        if path.exists():
            return path.resolve()
    return None


def analyze_imported_modules(
    code_str: str, module_file_path: Path, project_root: Path
) -> list[ImportedInternalModuleAnalysis]:
    """Statically find and analyze all imported internal modules."""
    module_rel_path = module_file_path.relative_to(project_root).with_suffix(
        ""
    )
    current_module = ".".join(module_rel_path.parts)
    imports = parse_imports(code_str)
    module_names: set[str] = set()
    for imp_node in imports:
        module_names.update(get_module_full_name(imp_node, current_module))
    internal_modules = {
        mod_name
        for mod_name in module_names
        if is_internal_module(mod_name, project_root)
    }
    return [
        ImportedInternalModuleAnalysis(
            name=str(mod_name).split(".")[-1],
            full_name=mod_name,
            file_path=file_path,
        )
        for mod_name in internal_modules
        if (file_path := get_module_file_path(mod_name, project_root))
        is not None
    ]


def get_first_top_level_object_def_ast(
    object_name: str, object_type: type[ObjectDefT], node: ast.AST
) -> ObjectDefT | None:
    """Find the first top-level definition of *object_name* with *object_type*."""
    for child in ast.iter_child_nodes(node):
        if isinstance(child, object_type) and child.name == object_name:
            return child
        if isinstance(
            child, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)
        ):
            continue
        descendant: ObjectDefT | None = get_first_top_level_object_def_ast(
            object_name, object_type, child
        )
        if descendant is not None:
            return descendant
    return None


def get_first_top_level_function_or_method_ast(
    function_name: str,
    parents: list[FunctionParent],
    node: ast.AST,
) -> ast.FunctionDef | ast.AsyncFunctionDef | None:
    """Find a function or method definition in *node* by name and parent chain."""
    if not parents:
        result = get_first_top_level_object_def_ast(
            function_name, ast.FunctionDef, node
        )
        if result is not None:
            return result
        return get_first_top_level_object_def_ast(
            function_name, ast.AsyncFunctionDef, node
        )
    if parents[0].type == "ClassDef":
        class_node = get_first_top_level_object_def_ast(
            parents[0].name, ast.ClassDef, node
        )
        if class_node is not None:
            func_result = get_first_top_level_object_def_ast(
                function_name, ast.FunctionDef, class_node
            )
            if func_result is not None:
                return func_result
            return get_first_top_level_object_def_ast(
                function_name, ast.AsyncFunctionDef, class_node
            )
    return None


def function_kind(
    node: ast.FunctionDef | ast.AsyncFunctionDef,
    parents: list[FunctionParent],
) -> FunctionKind | None:
    """Classify a function as plain function, static/class/instance method."""
    if not parents or parents[0].type in [
        "FunctionDef",
        "AsyncFunctionDef",
    ]:
        return FunctionKind.FUNCTION
    if parents[0].type == "ClassDef":
        for decorator in node.decorator_list:
            if isinstance(decorator, ast.Name):
                if decorator.id == "classmethod":
                    return FunctionKind.CLASS_METHOD
                if decorator.id == "staticmethod":
                    return FunctionKind.STATIC_METHOD
        return FunctionKind.INSTANCE_METHOD
    return None


def has_typed_parameters(
    node: ast.FunctionDef | ast.AsyncFunctionDef,
    parents: list[FunctionParent],
) -> bool:
    """Check if all parameters of *node* have type annotations."""
    kind = function_kind(node, parents)
    if kind in [FunctionKind.FUNCTION, FunctionKind.STATIC_METHOD]:
        return all(arg.annotation for arg in node.args.args)
    if kind in [FunctionKind.CLASS_METHOD, FunctionKind.INSTANCE_METHOD]:
        return all(arg.annotation for arg in node.args.args[1:])
    return False
