from pathlib import Path
import importlib
import subprocess
import sys
import platform
import types
import traceback
import inspect
from typing import Union, Generator, Optional, List, Dict
import shutil
import re
import os
import threading
from functools import lru_cache
import asyncio

from .ImporterException import *
from .path.utils import (
    __paths__,
    __load__,
    __imports__,
    __text__,
    __typedef__,
    isdunder,
)
from ._utils._installation import __installation__
from .metadata.meta import version
from ._builtin import isbuiltin
from ._utils._size import size
from .text.tree import get_dep_tree, format_tree_text, OutputFormat
from ._cache._cache_data import DataCache
from ._Versioner import Versioner


def rmempty(ListObject):
    return [value for value in ListObject if value.strip()]


class _LaZy:

    def __init__(self, origmodule, submodule_name: str) -> None:
        self.name = submodule_name
        self.orig = origmodule
        self._lazy_cache = {}

    def subimport(self):
        """
        Lazily import a submodule or dependency when first accessed.

        Args:
                        module_name (str): Name of the module or submodule to load lazily.

        Returns:
                        module: The imported module.

        Behavior:
                        - Loads module only on first call.
                        - Caches it in self._lazy_cache.
                        - Returns cached instance on subsequent calls.
        """
        if self.name in self._lazy_cache:
            return self._lazy_cache[self.name]

        full_name = f"{self.orig}.{self.name}"
        try:
            module = importlib.import_module(full_name)
            self._lazy_cache[self.name] = module
            return module
        except ModuleNotFoundError:
            raise ModuleNotFoundError(f"Submodule '{full_name}' not found")
        except Exception as e:
            raise ImportError(f'Error lazy loading "{full_name}": {e}')

    def reload(self):
        """
        Force reload of a lazily loaded submodule.
        """
        if self.name not in self._lazy_cache:
            raise ValueError(f'Module "{self.name}" not yet loaded lazily.')
        self._lazy_cache[self.name] = importlib.reload(self._lazy_cache[self.name])
        return self._lazy_cache[self.name]

    def clear(self):
        """
        Clear all cached lazy-loaded modules.
        """
        self._lazy_cache.clear()


class _Structure:

    def __init__(self, ModuleName: str, path: str) -> None:
        self.init = importlib.import_module(ModuleName)
        self.path = path
        self.name = self.init.__name__

    @staticmethod
    def _dc(pathfile: str) -> DataCache:
        return DataCache(pathdir=".cache_paths", pathfile=pathfile)

    def defs(
        self,
        target: str = None,
        max: int = None,
        name: bool = False,
        errors: bool = True,
        dunder: bool = True,
    ) -> list[str]:
        """
                                                                        Extract all function definitions from the module's files.

                                                                        Args:
                                                                                                                                        target (str, optional): Specific
        file name to search for
                                                                                                                                        max (int, optional): Maximum number of files to process
                                                                                                                                        name (bool): If False, gives only the function names, otherwise gives the function names and the file they are in.
                                                                                                                                        errors (bool): If True, suppress errors during processing
                                                                                                                                        dunder (bool): If True, put functions with dunder methods

                                                                        Returns:
                                                                                                                                        list[str]: List of function definitions or names

                                                                        Example:
                                                                                                                                        >>> _Structure.defs()
                                                                                                                                                                                                        ['def get(url, params=None, **kwargs):', 'def post(url, data=None, **kwargs):', ...]
                                                                                                                                        >>> _Structure.defs(name=True)
                                                                                                                                                                                                        ['get', 'post', 'put', 'delete', ...]
        """
        return __typedef__(
            target=target,
            maxfiles=max,
            name=name,
            targetype="defs",
            files=self.files(".py"),
            errors=errors,
            dunder=dunder,
        )

    def classes(
        self,
        target: str = None,
        max: int = None,
        name: bool = False,
        errors: bool = True,
    ) -> list[str]:
        """
        Extract all class definitions from the module's files.

        Args:
                                                                        target (str, optional): Specific file name to search for
                                                                        max (int, optional): Maximum number of files to process
                                                                        name (bool): If False, gives only the classes names, otherwise gives the classes names and the file they are in.
                                                                        errors (bool): If True, suppress errors during processing

        Returns:
                                                                        list[str]: List of class definitions or names

        Example:
                                                                        >>> _Structure.classes()
                                                                                                                                        ['class Request:', 'class Response:', 'class Session:', ...]
                                                                        >>> _Structure.classes(name=True)
                                                                                                                                        ['Request', 'Response', 'Session', ...]
        """
        return __typedef__(
            target=target,
            maxfiles=max,
            name=name,
            targetype="classes",
            files=self.files(".py"),
            errors=errors,
        )

    def imports(self, max: Union[int, str] = None) -> dict:
        """
        Extract all import statements from the module's files.

        Args:
                                                                                                                                        max (Union[int, str], optional): Maximum number of files to process. Defaults to None.

        Returns:
                                                                                                                                        dict: {
                                                                                                                                                                                                                                                                        'import': set of modules,
                                                                                                                                                                                                                                                                        'from': dict of {'module': set(names)}
                                                                                                                                        }
        """
        result = {"import": set(), "from": {}}
        files = self.files(".py")[:max]

        for file in files:
            try:
                code = __load__(file)
                _imports_ = __imports__(code, errors=False)

                result["import"].update(_imports_.get("import", set()))

                for module, names in _imports_.get("from", {}).items():
                    if module not in result["from"]:
                        result["from"][module] = set()
                    result["from"][module].update(names)

            except (SyntaxError, FileNotFoundError):
                continue

        return result

    @lru_cache(maxsize=128)
    def files(self, suffix: str = "", names: bool = False) -> list[str]:
        """
        Get all files within the module directory.

        Args:
                                                                        names (bool): If True, It will bring only the file names, otherwise the full paths.

        Returns:
                                                                        list[str]: List of file names or paths

        Example:
            >>> _Structure.files(True)
                ['__init__.py', 'api.py', 'sessions.py', ...]
            >>> _Structure.files()
                ['/path/to/requests/__init__.py', '/path/to/requests/api.py', ...]
        """
        dc_files = _Structure._dc(f"{self.name}_cache_files.json")
        dc_files.init()
        load = dc_files.load()

        if not load:
            files = [
                str(file) for file in self.path.rglob(f"*{suffix}") if file.is_file()
            ]
            dc_files.cache(files)
        else:
            files = load
        if names:
            return [Path(file).name for file in files]
        return files

    @lru_cache(maxsize=128)
    def dirs(self, names=False) -> list[str]:
        """
        Get all subdirectories within the module directory.

        Args:
                                                                        names (bool): If True, It will bring only the file names, otherwise the full paths

        Returns:
                                                                        list[str]: List of folder names or paths

        Example:
                                                                        >>> _Structure.dirs(True)
                                                                                                                                        ['adapters', 'models', 'utils', ...]
                                                                        >>> _Structure.dirs()
                                                                                                                                        ['/path/to/requests/adapters', '/path/to/requests/models', ...]
        """
        dc_dirs = _Structure._dc(f"{self.name}_cache_dirs.json")
        dc_dirs.init()
        load = dc_dirs.load()

        if not load:
            dirs = __paths__(path=self.path, type="fold", with_path=True)
            dc_dirs.cache(dirs)
        else:
            dirs = load

        if names:
            return [Path(folder).name for folder in dirs]
        return dirs

    def update_cache(self):
        _Structure._dc(f"{self.name}_cache_dirs").clear()
        _Structure._dc(f"{self.name}_cache_files").clear()

    def examples(self, max_examples: int = 5) -> Optional[List[Dict[str, str]]]:
        """
        Extract usage examples (>>> lines) from a module's docstring or
        from functions/classes docstrings inside it.

        Args:
            max_examples (int): Maximum number of examples to return.

        Returns:
            list of dict or None: Each dict contains {location, name, example}.
        """
        mod = self.init
        pattern = re.compile(r"(^>>>.*(?:\n\.\.\..*)*)", re.MULTILINE)

        def extract_from_doc(doc: Optional[str]) -> List[str]:
            if not doc:
                return []
            return pattern.findall(doc)

        results: List[Dict[str, str]] = []

        # --- 1. try module-level docstring ---
        for ex in extract_from_doc(inspect.getdoc(mod)):
            results.append(
                {"location": "module", "name": self.name, "example": ex.strip()}
            )
            if len(results) >= max_examples:
                return results

        # --- 2. scan functions/classes ---
        for name, obj in inspect.getmembers(mod):
            if inspect.isfunction(obj) or inspect.isclass(obj):
                for ex in extract_from_doc(inspect.getdoc(obj)):
                    results.append(
                        {
                            "location": (
                                "function" if inspect.isfunction(obj) else "class"
                            ),
                            "name": name,
                            "example": ex.strip(),
                        }
                    )
                    if len(results) >= max_examples:
                        return results

        return results if results else None


class Modulex:
    """
    A comprehensive module management and analysis tool for Python packages.

    Modulex provides advanced functionality for inspecting, managing, and analyzing
    Python modules and packages. It offers features for installation management,
    dependency analysis, content exploration, and documentation extraction.

    Attributes:
                                                                                                                                                                                                                                                                    lib (Union[types.ModuleType, str]): The module object or module name to manage

    Examples:
                                                                                                                                                                                                                                                                    >>> mod = Modulex('requests')
                                                                                                                                                                                                                                                                    >>> print(mod.version)
                                                                                                                                                                                                                                                                    >>> print(mod.info())
    """

    def __init__(
        self, ModuleName: Union[types.ModuleType, str], error: bool = True
    ) -> None:  # The module name can be entered as a text string in some functions
        """
        Initialize a Modulex instance with a module or module name.

        Args:
                                                                                                                                                                                                                                                                        ModuleName (Union[types.ModuleType, str]): Module object or module name string

        Raises:
                                                                                                                                                                                                                                                                        ValueError: If lib is not a string or module type

        Example:
                                                                                                                                                                                                                                                                        >>> mod = Modulex('numpy')
                                                                                                                                                                                                                                                                        >>> mod = Modulex(requests)
        """
        if not (isinstance(ModuleName, (str, types.ModuleType))):
            raise TypeError(
                f"ModuleName most be str or module object, not <{type(ModuleName).__name__}>"
            )

        try:
            self.name = (
                ModuleName.__name__
                if isinstance(ModuleName, types.ModuleType)
                else ModuleName
            )
            self.init = importlib.import_module(self.name)
        except ImportError:
            if error:
                raise
            self.name = ModuleName
        try:
            self.file = self.init.__file__
        except AttributeError:
            self.file = None

    def __str__(self) -> str:
        """
        Return string representation of the module.

        Returns:
                                                                                                                                                                                                                                                                        str: String representation of the module object

        Example:
                                                                                                                                                                                                                                                                        >>> str(mod)
                                                                                                                                                                                                                                                                        '<module 'requests' from '/path/to/requests/__init__.py'>'
        """
        return str(self.init)

    def __iter__(self):
        """
        Make the module iterable if the underlying module supports iteration.

        Returns:
                                                                                                                                                                                                                                                                        iterator: Iterator for the module content

        Note:
                                                                                                                                                                                                                                                                        Only works if the module itself is iterable
        """
        return iter(self.name)

    def __repr__(self) -> str:
        """
        Return official string representation of the Modulex instance.

        Returns:
                                                                                                                                                                                                                                                                        str: Formal representation showing module name and type

        Example:
                                                                                                                                                                                                                                                                        >>> repr(mod)
                                                                                                                                                                                                                                                                        '<Modulex(name='requests', dtype='module')>'
        """
        return f'<Modulex(name="{self.name}", dtype="{type(self.init).__name__}")>'

    def path(self, name: Union[str, Path] = None) -> Path:
        """
        Get the directory path of the module or search for a specific subpath.

        Args:
                                                                                                                                                                                                                                                                        name (Union[str, Path], optional): Specific subpath to search for within module directory

        Returns:
                                                                                                                                                                                                                                                                        Path: Path object representing the module directory or found subpath

        Example:
                                                                                                                                                                                                                                                                        >>> mod.path()
                                                                                                                                                                                                                                                                        Path('/path/to/requests')
                                                                                                                                                                                                                                                                        >>> mod.path('utils')
                                                                                                                                                                                                                                                                        Path('/path/to/requests/utils')
        """
        path = Path(self.file).resolve().parent
        if name:
            return path / Path(name)
        return path

    @lru_cache(maxsize=128)
    def dir(self, dunder: bool = True) -> list[str]:
        """
        Get the directory listing of the module's attributes.

        Args:
                properties (bool): If True, include all attributes; if False, exclude dunder methods

        Returns:
                list[str]: List of attribute names

        Example:
                >>> mod.dir()
                ['__file__', '__name__', 'get', 'post', ...]
                >>> mod.dir(False)
                ['get', 'post', 'put', 'delete', ...]
        """
        if not dunder:
            funcs = []
            for func in dir(self.init):
                if not isdunder(func):
                    funcs.append(func)
            return funcs
        else:
            return dir(self.init)

    @property
    def version(self) -> Versioner:
        """
        Get the version of the module.

        Returns:
                Versioner: Class to know the current version, track versions in the model folder and modify the current version

        Example:
                >>> mod.version.version
                '2.28.1'
        """
        return Versioner(self.name)

    def lazy(self, submodule):
        return _LaZy(self.name, submodule)

    def isinstall(self) -> bool:
        """
        Check if the module is properly installed and importable.

        Returns:
                bool: True if module is installed and importable, False otherwise

        Example:
                >>> mod.isinstall
                True
        """
        try:
            if hasattr(self.init, "__file__") or hasattr(self.init, "__package__"):
                return True
            else:
                importlib.import_module(self.init)
                return True
        except (ImportError, AttributeError):
            return False

    def install(self, text: bool = False) -> Union[bool, str]:
        """
        Install the module using pip.

        Args:
                text (bool): If True, return pip output as text; if False, return boolean success

        Returns:
                Union[bool, str]: Success status or pip output text

        Example:
                >>> mod.install()
                True
                >>> output = mod.install(True)
        """
        return __installation__(self.name, text=text).install()

    def uninstall(self, text: bool = True) -> Union[bool, str]:
        """
        Uninstall the module using pip.

        Args:
                text (bool): If True, return pip output as text; if False, return boolean success

        Returns:
                Union[bool, str]: Success status or pip output text

        Example:
                >>> mod.uninstall()
                True
                >>> output = mod.uninstall(True)
        """
        return __installation__(self.name, text=text).uninstall()

    def update(self, text: bool = False) -> Union[bool, str]:
        """
        Update the module to the latest version using pip.

        Args:
                text (bool): If True, return pip output as text; if False, return boolean success

        Returns:
                Union[bool, str]: Success status or pip output text

        Example:
                >>> mod.update()
                True
                >>> output = mod.update(True)
        """
        return __installation__(self.name, text=text).update()

    def installation(self, text: bool = False) -> __installation__:
        """
        Function to install by different ways

        Returns:
                __installation__: A class dedicated to installation, updating, removal, and installation of a specific version of the same library, etc.

        Example:
                mod.installation()
                methods:
                        ['get_version', 'install', 'install_specific_version', 'is_installed', 'text', 'uninstall', 'update']
        """
        return __installation__(self.name, text=text)

    def doc(self, content: bool = False) -> Union[str, Path]:
        """
        Get documentation for the module or its members.

        Args:
                content (bool): If True, get docstrings for all functions and built-ins

        Returns:
                Union[str, Path]: Module docstring or dictionary of member docstrings

        Example:
                >>> mod.doc()
                'Requests: HTTP for Humans...
                >>> mod.doc(True)
                [{'get': 'Sends a GET request...'}, {'post': 'Sends a POST request...'}, ...]
        """
        results = []
        if content:
            for name, obj in inspect.getmembers(self.init):
                if inspect.isfunction(obj) or inspect.isbuiltin(obj):
                    results.append({name: obj.__doc__})
        else:
            results = f"*Note:: this {self.name} file doc.*\n\n" + self.file.__doc__

        return results

    def copy(
        self,
        path: Union[str, Path] = None,
        errors: bool = True,
        exist_ok: bool = False,
        copyroot: bool = False,
    ) -> Union[list[dict], None]:
        """
        Copy the entire module content to a specified directory.

        Args:
                path (Union[str, Path]): Destination directory path
                errors (bool): If True, raise exceptions; if False, collect errors in results

        Returns:
                Union[list[dict], None]: List of error dictionaries or None if successful

        Raises:
                DirExists: If destination directory already exists

        Example:
                >>> mod.copy('/backup/requests')
                >>> errors = mod.copy('/backup/requests', errors=False)
        """
        errors_list = []
        dircopy = Path(path if path else f"_{self.name}")
        if not dircopy.exists():
            dircopy.mkdir()
        else:
            if not exist_ok:
                raise DirExists(f"dir {dircopy} already exists")

        if copyroot:
            try:
                if self.path().name == self.name:
                    shutil.copytree(
                        str(self.path()),
                        str(dircopy / self.path().name),
                        copy_function=shutil.copy2,
                    )
            except Exception as e:
                if errors:
                    raise

        strc = self.structure()
        paths = strc.files() + strc.dirs()

        def copy_p(p):
            p = Path(p)
            if p.is_file():
                shutil.copy2(str(p), str(Path(dircopy) / p.name))
            else:
                shutil.copytree(
                    str(p), str(Path(dircopy) / p.name), copy_function=shutil.copy2
                )

        for p in paths:
            try:
                copy_p(p)
            except Exception as e:
                if errors:
                    raise
                else:
                    errors_list.append(e)

        if not errors:
            return errors_list

    def isbuiltin(self) -> bool:
        """
        Check if the module is a built-in Python module.

        Returns:
                bool: True if built-in module, False if third-party package

        Example:
                >>> mod.isbuiltin()
                False
                >>> Modulex('os').isbuiltin()
                True
        """
        return isbuiltin(self.name)

    def tree(
        self,
        maxdepth: int = 1,
        indent: int = 0,
        optional: bool = True,
        format: OutputFormat = OutputFormat.TEXT,
    ) -> str:
        """
        Get dependency tree for the module using pipdeptree.

        Args:
                maxdepth (int): Specifies the search level in dependencies
                indent (int): Specifies the length of the tree indents
                format (OutputFormat): Determines the view

        Returns:
                str: Dependency tree

        Example:
                >>> print(mod.tree())
                'requests==2.28.1\\n  - certifi [required: >=2017.4.17, installed: 2022.12.7]\\n...'
        """
        return get_dep_tree(
            self.init.__name__,
            max_depth=maxdepth,
            output_format=format,
            skip_optional=False if optional else True,
        )

    def size(self, maxworker: int = os.cpu_count() * 5):
        return size(self.name, worker=maxworker)

    def structure(self):
        return _Structure(self.name, self.path())

    def stats(self) -> dict:
        """
        Get comprehensive stats about the module.

        Returns:
                dict: Dictionary containing module metadata and properties

        Example:
                >>> mod.stats()
                {
                        'name': 'requests',
                        'file': '/path/to/requests/__init__.py',
                        'path': '/path/to/requests',
                        'version': '2.28.1',
                        'isbuiltin': False
                }
        """
        all_stats = {
            "name": self.name,
            "file": self.file,
            "path": str(self.path()),
            "version": self.version.version,
            "size": self.size().size,
            "isbuiltin": self.isbuiltin(),
        }
        return all_stats
