from pathlib import Path
import sys
from typing import Union, List 


# ============================================================
#  Internal Utility Functions
# ============================================================

def _check_pkg(pkg: str) -> Path:
	"""
	Validate the package name and return its filesystem path.

	Parameters
	----------
	pkg : str
		The name of the target package.

	Returns
	-------
	Path
		Path object pointing to the root directory of the package.

	Raises
	------
	ImportError
		If the provided package name cannot be imported.
	
	ValueError
		If the package is a built-in module without a filesystem path.
	"""
	try:
		impkg = __import__(pkg)
	except ImportError as e:
		raise ImportError(f"Cannot import package '{pkg}': {e}")

	# Ensure package has a file path
	if not hasattr(impkg, "__file__") or impkg.__file__ is None:
		raise ValueError(f"'{pkg}' is likely a built-in module without a filesystem path.")

	return Path(impkg.__file__).parent


def _get_py_files(path: Path) -> List[Path]:
	"""
	Return all Python files (.py) within a given directory (non-recursive).
	"""
	return [f for f in path.glob('*') if f.suffix == '.py']


def _check_level(level: str, path: Path):
	"""
	Validate the level argument and return the appropriate Path iterator.

	Parameters
	----------
	level : str
		Either 'local' or 'global'.
	path : Path
		Base directory to search.

	Returns
	-------
	Iterator[Path]
		A generator yielding paths according to the search depth.

	Raises
	------
	ValueError
		If level is not 'local' or 'global'.
	"""
	level = level.lower()
	if level not in ['local', 'global']:
		raise ValueError(f"level can be 'local' or 'global', not '{level}'")

	return path.glob('*') if level == 'local' else path.rglob('*')


# ============================================================
#  Main Functions
# ============================================================

def getpackages(pkg: str, level: str = 'local') -> List[str]:
	"""
	getpackages(pkg, level='local')
	---------------------------------
	Scan a given Python package and return a list of all **sub-packages**
	(those directories containing an `__init__.py` file).

	Parameters
	----------
	pkg : str
		Name of the target package to scan.
		Example: 'json', 'requests', or 'importlib'

	level : str, optional
		Determines search depth:
		- 'local'  → Only look for sub-packages in the top-level directory.
		- 'global' → Recursively search all nested directories.
		Default is 'local'.

	Returns
	-------
	List[str]
		A list of sub-package names found inside the given package.

	Raises
	------
	ImportError
		If the provided package name cannot be imported.

	ValueError
		If the level argument is invalid or package is built-in.

	Examples
	--------
	>>> getpackages("json")
	['encoder', 'decoder', 'tool']

	>>> getpackages("importlib", level="global")
	['abc', 'machinery', 'metadata', 'resources', 'util']
	"""
	path = _check_pkg(pkg)
	search = _check_level(level, path)

	pkgs = []
	for p in search:
# 		Check if directory and contains __init__.py
		if p.is_dir():
			ssearch = p.glob('*') if level == 'local' else p.rglob('*')
			if any(f.name == '__init__.py' for f in ssearch):
				pkgs.append(p.stem)

	return sorted(set(pkgs))


def getmodules(pkg: str, level: str = 'local', parents: bool = False) -> List[str]:
	"""
	getmodules(pkg, level='local', parents=False)
	---------------------------------------------
	List all `.py` module files found inside a package.

	Parameters
	----------
	pkg : str
		Name of the target package to scan.
		Example: 'json', 'importlib', 'requests'

	level : str, optional
		Determines search depth:
		- 'local'  → Only look for .py files in top-level directories.
		- 'global' → Recursively search all nested directories.
		Default is 'local'.

	parents : bool, optional
		If True → return dotted names including parent directories.
		If False → return only module filenames.
		Default is False.

	Returns
	-------
	List[str]
		List of module names (.py files) found within the package.

	Raises
	------
	ImportError
		If the provided package name cannot be imported.

	ValueError
		If the package is built-in or has no path.

	Examples
	--------
	>>> getmodules("json")
	['__init__', 'decoder', 'encoder', 'tool']

	>>> getmodules("importlib", level="global", parents=True)
	['importlib.__init__', 'importlib.abc', 'importlib.machinery', ...]
	"""
	path = _check_pkg(pkg)
	search = _check_level(level, path)
	modules = []

	for p in search:
		#  If it's a directory → collect .py files inside it
		if p.is_dir():
			for pyf in _get_py_files(p):
				name = f"{p.stem}.{pyf.stem}" if parents else pyf.stem
				modules.append(name)
		#  If it's a direct .py file
		elif p.suffix == '.py':
			name = f"{pkg}.{p.stem}" if parents else p.stem
			modules.append(name)

	return sorted(set(modules))

def getlocation(name: str) -> list[str]:
    """
    Search for all occurrences of a given module or package within the Python import path.

    This function iterates through all directories listed in `sys.path` and checks
    whether a subpath matching the provided `name` exists. If found, the full path(s)
    to the located directory or file are returned.

    Args:
        name (str): The name of the package or module to locate. This may correspond
                    to either a directory (for packages) or a .py file (for modules).

    Returns:
        list[str]: A list of `str` objects representing the full paths where
                    the given package or module name was found. Returns an empty list
                    if the name cannot be found anywhere in `sys.path`.

    Example:
        >>> getlocation("json")
        ['/usr/lib/python3.11/json']

    Notes:
        - The search uses the current runtime's `sys.path`, which can be influenced
          by the environment, virtualenvs, or user customizations.
        - The returned paths are *not* verified to contain an `__init__.py` file,
          meaning the result might include folders with matching names that aren't
          technically valid Python packages.
    """
    locations = []
    for loc in sys.path:
        loc = Path(loc) / name
        if loc.exists():
            locations.append(str(loc))
    return locations


def getsitepath() -> list:
    """
    Retrieve all Python 'site-packages' directories currently active in the interpreter.

    This function scans the interpreter's search path (`sys.path`) and identifies
    directories whose names end with 'site-packages' — the conventional storage
    location for installed third-party packages.

    Returns:
        list: A list of strings, each representing a full path to a discovered
              'site-packages' directory. Returns an empty list if none are found.

    Example:
        >>> getsitepath()
        ['/usr/lib/python3.11/site-packages', '/home/user/.local/lib/python3.11/site-packages']

    Notes:
        - Virtual environments often prepend their own site-packages directory,
          which typically appears first in the list.
        - This function performs a *simple suffix check* and does not validate
          whether the directory actually contains installed distributions.
    """
    sites = []
    for site in sys.path:
        if site.endswith("site-packages"):
            sites.append(site)
    return sites


def getmetafilepkg(name: str, path: str = None) -> Union[None, str]:
    """
    Locate the 'METADATA' file of a specific installed Python package.

    This function attempts to find the corresponding metadata directory
    (commonly ending with `.dist-info`) for a given package within the specified
    or detected site-packages path. Once found, it returns the full path to the
    'METADATA' file inside that directory.

    Args:
        name (str): The name of the package whose metadata file should be located.
        path (str, optional): The specific site-packages directory to search.
                              If omitted, the first directory returned by
                              `getsitepath()` will be used.

    Returns:
        Union[None, str]: The full filesystem path to the package's 'METADATA' file
                          if found, or `None` if no matching metadata directory exists.

    Raises:
        ImportError: If the helper module `..metadata.meta` cannot be imported.
        IndexError: If `getsitepath()` returns an empty list (no site-packages found).

    Example:
        >>> getmetafilepkg("requests")
        '/usr/lib/python3.11/site-packages/requests-2.31.0.dist-info/METADATA'

    Notes:
        - The search is case-insensitive and replaces underscores in the name with hyphens.
        - The function depends on a custom helper `version(name)` from
          `..metadata.meta`, which must return the package version string.
        - `.dist-info` directories follow the PEP 376 naming convention:
          `{distribution}-{version}.dist-info`
    """
    from ..metadata.meta import version

    path = getsitepath()[0] if not path else path
    site = Path(path)
    v = version(name)

    for p in site.iterdir():
        if (
            p.is_dir()
            and p.name.lower().startswith(f"{name.lower()}-{v}".replace("_", "-"))
            and p.name.endswith(".dist-info")
        ):
            return str(Path(p) / Path("METADATA"))


def getmetapath(name: str, path: str = None) -> Path:
    """
    Resolve the absolute directory path of a package's metadata (`.dist-info`) folder.

    This function acts as a convenience wrapper around `getmetafilepkg()`.
    It locates the `METADATA` file of the specified package, resolves its absolute
    path, and returns the directory that contains it.

    Args:
        name (str): The name of the package to locate.
        path (str, optional): The path to the site-packages directory. If not provided,
                              defaults to the first site-packages path discovered.

    Returns:
        Path: The absolute path to the `.dist-info` directory for the given package.

    Raises:
        FileNotFoundError: If the METADATA file or metadata directory cannot be found.
        TypeError: If `getmetafilepkg()` returns None.

    Example:
        >>> getmetapath("requests")
        PosixPath('/usr/lib/python3.11/site-packages/requests-2.31.0.dist-info')

    Notes:
        - This function is particularly useful for tools or scripts that need to
          inspect or extract metadata about installed packages (e.g., version,
          dependencies, license info).
    """
    try:
        return Path(getmetafilepkg(name, path)).resolve().parent
    except TypeError:
        raise FileNotFoundError(f"MetaPath not found for '{name}'") from None
