import importlib.util
import os
import pathlib
import random
import typing

import msgspec

# Logger and related types
import chitragupt.core.logger as logger
import chitragupt.ext.git_operator as git_operator  # Contains GitIO
from chitragupt.abc.misc import UserAgent

GuptLogger = logger.GuptLogger
LogType = logger.LogType


class UserAgentManager:
    def __init__(
        self,
    ):
        self.file_session = self._get_file_session()
        self.decoders = msgspec.json.Decoder(type=typing.List[UserAgent])

    @staticmethod
    def _get_file_session() -> typing.ContextManager[typing.TextIO]:
        return open(
            "chitragupt/assets/user-agents.json",
            "r",
        )

    def get_user_agents(
        self,
    ) -> typing.List[UserAgent]:
        with self.file_session as f:
            return self.decoders.decode(f.read())

    def get_random_user_agent(
        self,
    ) -> UserAgent:
        return random.choice(self.get_user_agents())

    def get_specific_device(
        self,
        device: typing.Literal[
            "mobile",
            "firefox",
            "mac",
            "chrome",
            "linux",
            "android",
            "windows",
            "explorer",
            "edge",
            "iphone",
        ],
    ) -> typing.List[UserAgent]:
        list_of_user_agents: typing.List[UserAgent] = [
            user_agent
            for user_agent in self.get_user_agents()
            if user_agent.device == device
        ]
        return list_of_user_agents


class PluginManager:
    """
    A PluginManager to handle installation, updating, removal, and dynamic loading of plugins
    from the ChitraGupt-Plugin-Garden GitHub repository. This manager uses GitIO for all GitHub
    operations and does not rely on external subprocess calls.

    Attributes
    ----------
    git_io : git_opeartor.GitIO
        Instance used for GitHub API interactions.
    plugin_install_dir : pathlib.Path
        Local directory where plugins are installed.
    available_plugins : List[str]
        List of available plugin names (without the .py extension) in the remote repository.
    logger : GuptLogger
        Logger instance for logging plugin operations.
    branch : str
        The branch of the remote repository to use.
    """

    def __init__(
        self,
        logger: GuptLogger,
        plugin_install_dir: typing.Union[
            str, pathlib.Path
        ] = "chitragupt/tmp/plugin",
        branch: str = "main",
    ) -> None:
        """
        Initialize the PluginManager.

        This constructor sets up the local plugin installation directory and
        instantiates a GitIO object (which is initialized for GitHub API operations).
        It then refreshes the list of available plugins from the remote repository.

        Parameters
        ----------
        logger : GuptLogger
            Logger instance for logging operations.
        plugin_install_dir : Union[str, pathlib.Path], optional
            Local directory where plugins are installed (default is "chitragupt/tmp/plugin").
        branch : str, optional
            The branch of the remote repository to use (default is "main").

        Returns
        -------
        None

        Examples
        --------
        >>> my_logger = GuptLogger()
        >>> pm = PluginManager(logger=my_logger)
        """
        self.logger = logger
        self.plugin_install_dir = pathlib.Path(plugin_install_dir)
        if not self.plugin_install_dir.exists():
            self.plugin_install_dir.mkdir(parents=True, exist_ok=True)
        self.branch = branch

        # Initialize GitIO and configure it.
        self.git_io = git_operator.GitIO()
        self.git_io.initialize()

        # Populate the list of available plugins.
        self.available_plugins: typing.List[str] = []
        self.refresh_available_plugins()

    def refresh_available_plugins(self) -> None:
        """
        Refresh the list of available plugins from the remote repository.

        This method uses GitIO.get_list_of_files to retrieve the file tree from the remote
        repository and updates the internal list of available plugins by filtering for files
        ending with the .py extension.

        Returns
        -------
        None

        Raises
        ------
        ValueError
            If the file tree format is not as expected.

        Examples
        --------
        >>> pm.refresh_available_plugins()
        """
        file_tree = self.git_io.get_list_of_files(recursive=1)
        if not file_tree:
            raise ValueError("Unexpected file tree format from GitIO.")
        plugins = []
        for item in file_tree.tree:
            if item and item.type == "blob" and item.path.endswith(".py"):
                plugin_name = pathlib.Path(item.path).stem
                plugins.append(plugin_name)
        self.available_plugins = plugins
        self.logger.log(
            message=f"Available plugins refreshed: {self.available_plugins}",
            log_type=LogType.DEBUG,
        )

    def list_available_plugins(self) -> typing.List[str]:
        """
        List available plugins in the remote repository.

        Returns
        -------
        List[str]
            A list of available plugin names (without the .py extension).

        Examples
        --------
        >>> plugins = pm.list_available_plugins()
        """
        return self.available_plugins

    def install_plugin(self, plugin_name: str) -> pathlib.Path:
        """
        Install a plugin from the remote repository.

        This method fetches the raw content of the specified plugin file using GitIO.get_raw_file
        and writes it to the local plugin installation directory.

        Parameters
        ----------
        plugin_name : str
            The name of the plugin to install (without the .py extension).

        Returns
        -------
        None

        Raises
        ------
        FileNotFoundError
            If the plugin is not available in the remote repository.
        ValueError
            If the plugin content could not be retrieved.

        Examples
        --------
        >>> pm.install_plugin("example_plugin")
        """
        if plugin_name not in self.available_plugins:
            raise FileNotFoundError(
                f"Plugin '{plugin_name}' not found in remote repository."
            )
        try:
            content: str = self.git_io.get_raw_file(
                file=f"{plugin_name}.py", branch=self.branch
            )
        except Exception as e:
            raise ValueError(f"Failed to retrieve plugin '{plugin_name}': {e}")
        destination_file = self.plugin_install_dir / f"{plugin_name}.py"
        with open(destination_file, "w", encoding="utf-8") as f:
            f.write(content)
        self.logger.log(
            message=f"Plugin '{plugin_name}' installed successfully.",
            log_type=LogType.INFO,
        )
        return destination_file

    def uninstall_plugin(self, plugin_name: str) -> None:
        """
        Uninstall a plugin by removing it from the local installation directory.

        Parameters
        ----------
        plugin_name : str
            The name of the plugin to uninstall (without the .py extension).

        Returns
        -------
        None

        Raises
        ------
        FileNotFoundError
            If the plugin is not found in the installation directory.

        Examples
        --------
        >>> pm.uninstall_plugin("example_plugin")
        """
        target_file = self.plugin_install_dir / f"{plugin_name}.py"
        if not target_file.exists():
            raise FileNotFoundError(
                f"Plugin '{plugin_name}' is not installed."
            )
        os.remove(str(target_file))
        self.logger.log(
            message=f"Plugin '{plugin_name}' uninstalled successfully.",
            log_type=LogType.INFO,
        )

    def update_plugin(self, plugin_name: str) -> None:
        """
        Update an installed plugin by reinstalling it from the remote repository.

        Parameters
        ----------
        plugin_name : str
            The name of the plugin to update (without the .py extension).

        Returns
        -------
        None

        Raises
        ------
        FileNotFoundError
            If the plugin is not available in the remote repository.

        Examples
        --------
        >>> pm.update_plugin("example_plugin")
        """
        self.install_plugin(plugin_name)
        self.logger.log(
            message=f"Plugin '{plugin_name}' updated successfully.",
            log_type=LogType.INFO,
        )

    def list_installed_plugins(self) -> typing.List[str]:
        """
        List all installed plugins in the local installation directory.

        Returns
        -------
        List[str]
            A list of installed plugin names (without the .py extension).

        Examples
        --------
        >>> installed = pm.list_installed_plugins()
        """
        plugin_files = list(self.plugin_install_dir.glob("*.py"))
        installed = [file.stem for file in plugin_files]
        self.logger.log(
            message=f"Installed plugins: {installed}",
            log_type=LogType.DEBUG,
        )
        return installed

    def search_plugins(self, query: str) -> typing.List[str]:
        """
        Search available plugins in the remote repository by name.

        Parameters
        ----------
        query : str
            The search query string.

        Returns
        -------
        List[str]
            A list of plugin names that contain the query (case-insensitive).

        Examples
        --------
        >>> results = pm.search_plugins("audio")
        """
        results = [
            name
            for name in self.available_plugins
            if query.lower() in name.lower()
        ]
        self.logger.log(
            message=f"Search for '{query}' yielded: {results}",
            log_type=LogType.DEBUG,
        )
        return results

    def get_plugin_details(self, plugin_name: str) -> str:
        """
        Retrieve details of an installed plugin.

        Reads the plugin file and returns a snippet (first 300 characters) which
        typically includes the plugin's docstring or description.

        Parameters
        ----------
        plugin_name : str
            The name of the plugin (without the .py extension).

        Returns
        -------
        str
            A snippet of the plugin file's content.

        Raises
        ------
        FileNotFoundError
            If the plugin is not installed.

        Examples
        --------
        >>> details = pm.get_plugin_details("example_plugin")
        """
        plugin_file = self.plugin_install_dir / f"{plugin_name}.py"
        if not plugin_file.exists():
            raise FileNotFoundError(
                f"Plugin '{plugin_name}' is not installed."
            )
        with open(plugin_file, "r", encoding="utf-8") as f:
            content = f.read()
        snippet = content[:300]
        self.logger.log(
            message=f"Retrieved details for plugin '{plugin_name}'.",
            log_type=LogType.DEBUG,
        )
        return snippet

    def load_plugin(self, plugin_name: str) -> typing.Any:
        """
        Dynamically load an installed plugin as a Python module.

        Parameters
        ----------
        plugin_name : str
            The name of the plugin (without the .py extension).

        Returns
        -------
        Any
            The loaded plugin module.

        Raises
        ------
        FileNotFoundError
            If the plugin is not installed.
        ImportError
            If the plugin module cannot be imported.

        Examples
        --------
        >>> module = pm.load_plugin("example_plugin")
        """
        plugin_file = self.plugin_install_dir / f"{plugin_name}.py"
        if not plugin_file.exists():
            raise FileNotFoundError(
                f"Plugin '{plugin_name}' is not installed."
            )
        spec = importlib.util.spec_from_file_location(
            plugin_name, str(plugin_file)
        )
        if spec is None or spec.loader is None:
            raise ImportError(f"Could not load plugin module '{plugin_name}'.")
        module = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(module)
        self.logger.log(
            message=f"Plugin '{plugin_name}' loaded successfully.",
            log_type=LogType.INFO,
        )
        return module

    def update_all_plugins(self) -> None:
        """
        Update all installed plugins by reinstalling them from the remote repository.

        Returns
        -------
        None

        Examples
        --------
        >>> pm.update_all_plugins()
        """
        installed = self.list_installed_plugins()
        for plugin in installed:
            self.update_plugin(plugin)
        self.logger.log(
            message="All installed plugins updated successfully.",
            log_type=LogType.INFO,
        )
