"""
The ``discover_imports`` module provides utilities for discovering project imports
for Snowflake stored procedure deployment.
"""
import ast
import importlib.util
import inspect
import os
import warnings
from typing import Dict, List, Tuple, Set, Optional

from relationalai.util.docutils import include_in_docs

__include_in_docs__ = True


def _to_forward_slashes(path: str) -> str:
    """Normalize a path to use forward slashes."""
    return path.replace(os.sep, '/')


def _get_all_imports_from_file(file_path: str, project_root: str) -> Set[str]:
    """Get all import module names from a file (full module paths).

    Parameters
    ----------
    file_path : str
        Path to the file to analyze.
    project_root : str
        Root directory of the project.

    Returns
    -------
    set of str
        Full module names (e.g., ``'tests.fixtures.discover_imports.transitive_level2'``).
    """
    try:
        with open(file_path, 'r') as f:
            content = f.read()
    except (FileNotFoundError, PermissionError, IsADirectoryError, UnicodeDecodeError):
        return set()

    try:
        tree = ast.parse(content, filename=file_path)
    except SyntaxError:
        return set()

    imports = set()

    # Convert file path to module path for resolving relative imports
    abs_file_path = os.path.abspath(file_path)
    rel_file_path = os.path.relpath(abs_file_path, project_root)
    module_path = _to_forward_slashes(rel_file_path).replace('.py', '').replace('/', '.')
    module_parts = module_path.split('.')

    for node in ast.walk(tree):
        if isinstance(node, ast.Import):
            for alias in node.names:
                imports.add(alias.name)
        elif isinstance(node, ast.ImportFrom):
            if node.level > 0:
                # Relative import
                if node.level <= len(module_parts) - 1:
                    base = '.'.join(module_parts[:-node.level])
                    if node.module:
                        base += '.' + node.module
                    for name in node.names:
                        if name.name != '*':
                            imports.add(base + '.' + name.name)
                        else:
                            imports.add(base)
            elif node.module:
                for name in node.names:
                    if name.name != '*':
                        imports.add(node.module + '.' + name.name)
                    else:
                        imports.add(node.module)

    return imports


def _find_module_origin(module_name: str) -> Optional[str]:
    """Find the source file for a module, trying parent modules as fallback.

    For ``from X import Y``, Y might be a submodule (with a file) or a name
    inside X (function/class). This tries the full name first, then strips
    components until a real module file is found.

    Returns
    -------
    str or None
        Path to the module's source file, or ``None`` if not found.
    """
    name = module_name
    while name:
        try:
            spec = importlib.util.find_spec(name)
            if spec and spec.origin and os.path.isfile(spec.origin):
                return spec.origin
        except Exception:
            pass
        parts = name.rsplit('.', 1)
        if len(parts) == 1:
            return None
        name = parts[0]
    return None


def _is_project_file(file_path: str, project_root: str) -> bool:
    """Check if a file belongs to the project (not a dependency)."""
    abs_path = os.path.abspath(file_path)
    return (
        abs_path.startswith(project_root)
        and 'site-packages' not in abs_path
        and 'dist-packages' not in abs_path
    )


def _is_layout_directory(module_name: str, project_root: str) -> bool:
    """Check if a namespace package is just a layout directory (e.g. ``src/``).

    A layout directory is a namespace package whose child modules are
    independently importable as top-level packages.
    """
    parts = module_name.split('.')
    if len(parts) <= 1:
        return False
    try:
        child_spec = importlib.util.find_spec(parts[1])
        if child_spec and child_spec.origin and child_spec.origin != 'built-in':
            abs_child = os.path.abspath(child_spec.origin)
            return abs_child.startswith(project_root) and 'site-packages' not in abs_child
    except Exception:
        pass
    return False


def _get_top_level_package(module_name: str, project_root: str) -> Optional[Tuple[str, str]]:
    """Get the top-level package (path, name) for a given module.

    Handles regular packages (with ``__init__.py``) and namespace packages,
    skipping layout directories like ``src/``.

    Returns
    -------
    tuple of (str, str) or None
        ``(relative_path, package_name)`` or ``None``.
    """
    top_level_name = module_name.split('.')[0]
    try:
        spec = importlib.util.find_spec(top_level_name)
    except Exception:
        return None

    if not spec:
        return None

    # Regular package with __init__.py
    if spec.origin and spec.origin != 'built-in':
        rel_path = _to_forward_slashes(os.path.relpath(os.path.dirname(spec.origin)))
        if not rel_path.endswith('/'):
            rel_path += '/'
        return (rel_path, top_level_name)

    # Namespace package — skip if it's just a layout directory
    if spec.submodule_search_locations:
        if _is_layout_directory(module_name, project_root):
            return None
        for location in spec.submodule_search_locations:
            abs_location = os.path.abspath(location)
            if abs_location.startswith(project_root):
                rel_path = _to_forward_slashes(os.path.relpath(abs_location))
                if not rel_path.endswith('/'):
                    rel_path += '/'
                return (rel_path, top_level_name)

    return None


def _discover_imports_recursive(
    file_path: str, project_root: str, visited: Set[str]
) -> Dict[str, Tuple[str, str]]:
    """Recursively discover imports from a file and its dependencies.

    Returns
    -------
    dict of str to tuple of (str, str)
        Mapping of ``module_name`` to ``(module_path, module_name)`` for
        discovered user modules.
    """
    abs_file_path = os.path.abspath(file_path)
    if abs_file_path in visited:
        return {}
    visited.add(abs_file_path)

    results = {}
    for module_name in _get_all_imports_from_file(file_path, project_root):
        origin = _find_module_origin(module_name)
        if not origin or not _is_project_file(origin, project_root):
            continue

        try:
            package = _get_top_level_package(module_name, project_root)
            if package:
                results[package[1]] = package
            results.update(_discover_imports_recursive(origin, project_root, visited))
        except Exception:
            continue

    return results


@include_in_docs
def discover_imports(file_path: Optional[str] = None) -> List[Tuple[str, str]]:
    """Discover all user (non-dependency) imports from the given file and its dependencies.

    Recursively discovers imports from imported modules (transitive dependencies).
    Only includes top-level custom modules (not standard library or dependencies).
    Always returns an empty list on errors, with warnings logged.

    Output is intended to be passed directly to the `imports` parameter of
    the snowflake.snowpark.Session.sproc.register().

    Parameters
    ----------
    file_path : str, optional
        Path to a file to analyze. If ``None``, uses the calling file.

    Returns
    -------
    list of tuple of (str, str)
        List of ``(module_path, module_name)`` tuples for discovered user modules.

    Examples
    --------

    >>> imports = discover_imports()
    >>> imports
    [('src/myproject/', 'myproject')]
    """
    try:
        if file_path is None:
            current_frame = inspect.currentframe()
            if current_frame is None:
                warnings.warn("Could not determine current frame for auto-detection", RuntimeWarning)
                return []
            caller_frame = current_frame.f_back
            if caller_frame is None:
                warnings.warn("Could not determine caller frame for auto-detection", RuntimeWarning)
                return []
            file_path = caller_frame.f_code.co_filename

        # Validate file exists and is readable
        try:
            with open(file_path, 'r') as f:
                content = f.read()  # noqa: F841
        except FileNotFoundError:
            warnings.warn(f"File not found: {file_path}", RuntimeWarning)
            return []
        except PermissionError:
            warnings.warn(f"Permission denied reading file: {file_path}", RuntimeWarning)
            return []
        except IsADirectoryError:
            warnings.warn(f"Path is a directory, not a file: {file_path}", RuntimeWarning)
            return []
        except UnicodeDecodeError as e:
            warnings.warn(f"Unicode decode error reading file {file_path}: {e}", RuntimeWarning)
            return []

        # Check for syntax errors
        try:
            ast.parse(content, filename=file_path)
        except SyntaxError as e:
            warnings.warn(f"Syntax error parsing {file_path}: {e}", RuntimeWarning)
            return []

        project_root = os.getcwd()
        results = _discover_imports_recursive(file_path, project_root, set())
        return sorted(results.values(), key=lambda x: x[1])

    except Exception as e:
        warnings.warn(f"Unexpected error in discover_imports: {type(e).__name__}: {e}", RuntimeWarning)
        return []
