#!/usr/bin/env python3
# Copyright (c) 2024 Stormux
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.

"""Plugin System Manager for Cthulhu using pluggy."""

import os
import inspect
import importlib.util
import logging
from enum import IntEnum

# Import pluggy if available
try:
    import pluggy
    PLUGGY_AVAILABLE = True
except ImportError:
    PLUGGY_AVAILABLE = False
    logging.getLogger(__name__).info("Pluggy not available, plugins will be disabled")

# Set to True for more detailed plugin loading debug info
PLUGIN_DEBUG = True

logger = logging.getLogger(__name__)
if PLUGIN_DEBUG:
    logger.setLevel(logging.DEBUG)

class PluginType(IntEnum):
    """Types of plugins we support."""
    SYSTEM = 1
    USER = 2

    def get_root_dir(self):
        """Returns the directory where this type of plugins can be found."""
        if self.value == PluginType.SYSTEM:
            current_file = inspect.getfile(inspect.currentframe())
            current_dir = os.path.dirname(os.path.realpath(os.path.abspath(current_file)))
            return os.path.join(current_dir, 'plugins')
        elif self.value == PluginType.USER:
            return os.path.expanduser('~/.local/share/cthulhu/plugins')


class PluginInfo:
    """Information about a plugin."""

    def __init__(self, name, module_name, module_dir, metadata=None):
        self.name = name
        self.module_name = module_name
        self.module_dir = module_dir
        self.metadata = metadata or {}
        self.builtin = False
        self.hidden = False
        self.module = None
        self.instance = None
        self.loaded = False

    def get_module_name(self):
        return self.module_name

    def get_name(self):
        return self.metadata.get('name', self.name)

    def get_version(self):
        return self.metadata.get('version', '0.0.0')

    def get_description(self):
        return self.metadata.get('description', '')

    def get_module_dir(self):
        return self.module_dir


class PluginSystemManager:
    """Cthulhu Plugin Manager using pluggy."""

    def __init__(self, app):
        self.app = app
        logger.info("Initializing PluginSystemManager")

        # Initialize plugin manager
        if PLUGGY_AVAILABLE:
            logger.info("Pluggy is available, setting up plugin manager")
            self.plugin_manager = pluggy.PluginManager("cthulhu")

            # Define hook specifications
            hook_spec = pluggy.HookspecMarker("cthulhu")

            class CthulhuHookSpecs:
                @hook_spec
                def activate(self, plugin=None):
                    """Called when the plugin is activated."""
                    pass

                @hook_spec
                def deactivate(self, plugin=None):
                    """Called when the plugin is deactivated."""
                    pass

            logger.info("Adding hook specifications to plugin manager")
            self.plugin_manager.add_hookspecs(CthulhuHookSpecs)
        else:
            logger.warning("Pluggy is not available, plugins will be disabled")
            self.plugin_manager = None

        # Plugin storage
        self._plugins = {}  # module_name -> PluginInfo
        self._active_plugins = []

        # Create plugin directories
        self._setup_plugin_dirs()
        
        # Log available plugins directory paths
        logger.info(f"System plugins directory: {PluginType.SYSTEM.get_root_dir()}")
        logger.info(f"User plugins directory: {PluginType.USER.get_root_dir()}")

    def _setup_plugin_dirs(self):
        """Ensure plugin directories exist."""
        os.makedirs(PluginType.SYSTEM.get_root_dir(), exist_ok=True)
        os.makedirs(PluginType.USER.get_root_dir(), exist_ok=True)

    @property
    def plugins(self):
        """Get all available plugins."""
        return list(self._plugins.values())

    def getApp(self):
        return self.app

    def rescanPlugins(self):
        """Scan for plugins in the plugin directories."""
        old_plugins = self._plugins.copy()
        self._plugins = {}

        # Scan system and user plugins
        self._scan_plugins_in_directory(PluginType.SYSTEM.get_root_dir())
        self._scan_plugins_in_directory(PluginType.USER.get_root_dir())

        # Preserve state for already loaded plugins
        for name, old_info in old_plugins.items():
            if name in self._plugins and old_info.loaded:
                self._plugins[name].loaded = True
                self._plugins[name].instance = old_info.instance
                self._plugins[name].module = old_info.module

    def _scan_plugins_in_directory(self, directory):
        """Scan for plugins in a directory."""
        if not os.path.exists(directory) or not os.path.isdir(directory):
            logger.warning(f"Plugin directory not found or not a directory: {directory}")
            return

        logger.info(f"Scanning for plugins in directory: {directory}")
        for item in os.listdir(directory):
            plugin_dir = os.path.join(directory, item)
            if not os.path.isdir(plugin_dir):
                continue

            # Check for the traditional structure first (plugin.py & plugin.info)
            plugin_file = os.path.join(plugin_dir, "plugin.py")
            metadata_file = os.path.join(plugin_dir, "plugin.info")

            # Fall back to [PluginName].py if plugin.py doesn't exist
            if not os.path.isfile(plugin_file):
                alternative_plugin_file = os.path.join(plugin_dir, f"{item}.py")
                if os.path.isfile(alternative_plugin_file):
                    plugin_file = alternative_plugin_file
                    logger.info(f"Using alternative plugin file: {alternative_plugin_file}")

            # Check if we have any valid plugin file
            if os.path.isfile(plugin_file):
                # Extract plugin info
                module_name = os.path.basename(plugin_dir)
                logger.info(f"Found plugin: {module_name} in {plugin_dir}")
                metadata = self._load_plugin_metadata(metadata_file)

                plugin_info = PluginInfo(
                    metadata.get('name', module_name),
                    module_name,
                    plugin_dir,
                    metadata
                )

                # Check if it's a built-in or hidden plugin
                plugin_info.builtin = metadata.get('builtin', 'false').lower() == 'true'
                plugin_info.hidden = metadata.get('hidden', 'false').lower() == 'true'

                logger.info(f"Adding plugin to registry: {module_name}")
                self._plugins[module_name] = plugin_info
            else:
                logger.warning(f"No plugin file found in directory: {plugin_dir}")

    def _load_plugin_metadata(self, metadata_file):
        """Load plugin metadata from a file."""
        metadata = {}

        if os.path.isfile(metadata_file):
            try:
                with open(metadata_file, 'r') as f:
                    for line in f:
                        line = line.strip()
                        if not line or line.startswith('#'):
                            continue

                        if '=' in line:
                            key, value = line.split('=', 1)
                            metadata[key.strip()] = value.strip()
            except Exception as e:
                logger.error(f"Error loading plugin metadata: {e}")

        return metadata

    def getActivePlugins(self):
        """Get the list of active plugin names."""
        return self._active_plugins

    def setActivePlugins(self, activePlugins):
        """Set active plugins and sync their state."""
        logger.info(f"Setting active plugins: {activePlugins}")
        
        # Make sure we have scanned for plugins first
        if not self._plugins:
            logger.info("No plugins found, rescanning...")
            self.rescanPlugins()
            
        self._active_plugins = activePlugins
        
        # Log active vs available plugins
        available_plugins = [p.get_module_name() for p in self.plugins]
        logger.info(f"Available plugins: {available_plugins}")
        logger.info(f"Active plugins: {self._active_plugins}")
        
        # Find missing plugins
        missing_plugins = [p for p in self._active_plugins if p not in available_plugins]
        if missing_plugins:
            logger.warning(f"Active plugins not found: {missing_plugins}")
            
        self.syncAllPluginsActive()

    def setPluginActive(self, pluginInfo, active):
        """Set the active state of a plugin."""
        if pluginInfo.builtin:
            active = True

        pluginName = pluginInfo.get_module_name()

        if active:
            if pluginName not in self.getActivePlugins():
                if self.loadPlugin(pluginInfo):
                    self._active_plugins.append(pluginName)
        else:
            if pluginName in self.getActivePlugins():
                if self.unloadPlugin(pluginInfo):
                    self._active_plugins.remove(pluginName)

    def isPluginActive(self, pluginInfo):
        """Check if a plugin is active."""
        module_name = pluginInfo.get_module_name()
        
        # Builtin plugins are always active
        if pluginInfo.builtin:
            logger.debug(f"Plugin {module_name} is builtin, active by default")
            return True

        # If a plugin is already loaded, it's active
        if pluginInfo.loaded:
            logger.debug(f"Plugin {module_name} is already loaded, considered active")
            return True

        # Check case-insensitive match in active plugins list
        active_plugins = self.getActivePlugins()
        
        # Try exact match first
        if module_name in active_plugins:
            logger.debug(f"Plugin {module_name} found in active plugins list")
            return True
            
        # Try case-insensitive match
        module_name_lower = module_name.lower()
        is_active = any(plugin.lower() == module_name_lower for plugin in active_plugins)
        
        if is_active:
            logger.debug(f"Plugin {module_name} found in active plugins list (case-insensitive match)")
        else:
            logger.debug(f"Plugin {module_name} not found in active plugins list")
            
        return is_active

    def syncAllPluginsActive(self):
        """Sync the active state of all plugins."""
        logger.info("Syncing active state of all plugins")
        
        # Log plugin status before syncing
        if PLUGIN_DEBUG:
            for pluginInfo in self.plugins:
                is_active = self.isPluginActive(pluginInfo)
                is_loaded = pluginInfo.loaded
                logger.debug(f"Plugin {pluginInfo.get_module_name()}: active={is_active}, loaded={is_loaded}")
        
        # First unload inactive plugins
        for pluginInfo in self.plugins:
            if not self.isPluginActive(pluginInfo) and pluginInfo.loaded:
                logger.info(f"Unloading inactive plugin: {pluginInfo.get_module_name()}")
                self.unloadPlugin(pluginInfo)

        # Then load active plugins
        for pluginInfo in self.plugins:
            if self.isPluginActive(pluginInfo) and not pluginInfo.loaded:
                logger.info(f"Loading active plugin: {pluginInfo.get_module_name()}")
                result = self.loadPlugin(pluginInfo)
                logger.info(f"Plugin {pluginInfo.get_module_name()} load result: {result}")
        
        # Log final plugin status
        active_plugins = [p.get_module_name() for p in self.plugins if p.loaded]
        logger.info(f"Active plugins after sync: {active_plugins}")
        inactive_plugins = [p.get_module_name() for p in self.plugins if not p.loaded]
        logger.info(f"Inactive plugins after sync: {inactive_plugins}")

    def loadPlugin(self, pluginInfo):
        """Load a plugin."""
        # Skip if pluggy is not available
        if not PLUGGY_AVAILABLE:
            logger.info(f"Skipping plugin {pluginInfo.get_name()}: pluggy not available")
            return False

        module_name = pluginInfo.get_module_name()
        logger.info(f"Attempting to load plugin: {module_name}")

        try:
            # Already loaded?
            if pluginInfo.loaded:
                logger.info(f"Plugin {module_name} already loaded, skipping")
                return True

            # Try to find the plugin file
            module_name = pluginInfo.get_module_name()
            plugin_dir = pluginInfo.get_module_dir()
            
            # Check for plugin.py first (standard format)
            plugin_file = os.path.join(plugin_dir, "plugin.py")
            
            # Fall back to [PluginName].py if plugin.py doesn't exist
            if not os.path.exists(plugin_file):
                alternative_plugin_file = os.path.join(plugin_dir, f"{module_name}.py")
                if os.path.exists(alternative_plugin_file):
                    plugin_file = alternative_plugin_file
                    logger.info(f"Using alternative plugin file: {alternative_plugin_file}")
            
            if not os.path.exists(plugin_file):
                logger.error(f"Plugin file not found: {plugin_file}")
                return False
                
            logger.info(f"Loading plugin from: {plugin_file}")
            spec = importlib.util.spec_from_file_location(module_name, plugin_file)
            if spec is None:
                logger.error(f"Failed to create spec for plugin: {module_name}")
                return False
                
            module = importlib.util.module_from_spec(spec)
            spec.loader.exec_module(module)
            pluginInfo.module = module

            # Find Plugin class
            plugin_class = None
            for attr_name in dir(module):
                attr = getattr(module, attr_name)
                if (inspect.isclass(attr) and 
                    attr.__module__ == module.__name__ and
                    hasattr(attr, 'activate')):
                    plugin_class = attr
                    logger.info(f"Found plugin class: {attr.__name__} in {module_name}")
                    break

            if not plugin_class:
                logger.error(f"No plugin class found in {module_name}")
                return False

            # Create and initialize plugin instance
            logger.info(f"Creating instance of plugin class: {plugin_class.__name__}")
            plugin_instance = plugin_class()
            pluginInfo.instance = plugin_instance

            # Ensure plugins have a reference to the app
            plugin_instance.app = self.getApp()
            logger.info(f"Set app reference for plugin: {module_name}")
            
            if hasattr(plugin_instance, 'set_app'):
                plugin_instance.set_app(self.getApp())
                logger.info(f"Called set_app() for plugin: {module_name}")

            if hasattr(plugin_instance, 'set_plugin_info'):
                plugin_instance.set_plugin_info(pluginInfo)
                logger.info(f"Called set_plugin_info() for plugin: {module_name}")

            # Register with pluggy and activate
            if self.plugin_manager is None:
                logger.error(f"Plugin manager is None when loading {module_name}")
                return False
                
            logger.info(f"Registering plugin with pluggy: {module_name}")
            self.plugin_manager.register(plugin_instance)
            
            try:
                logger.info(f"Activating plugin: {module_name}")
                self.plugin_manager.hook.activate(plugin=plugin_instance)
            except Exception as e:
                logger.error(f"Error activating plugin {module_name}: {e}")
                import traceback
                logger.error(traceback.format_exc())
                return False

            pluginInfo.loaded = True
            logger.info(f"Successfully loaded plugin: {module_name}")
            return True

        except Exception as e:
            logger.error(f"Failed to load plugin {module_name}: {e}")
            import traceback
            logger.error(traceback.format_exc())
            return False

    def unloadPlugin(self, pluginInfo):
        """Unload a plugin."""
        # Skip if pluggy is not available
        if not PLUGGY_AVAILABLE:
            return False

        if pluginInfo.builtin:
            return False

        module_name = pluginInfo.get_module_name()

        try:
            # Not loaded?
            if not pluginInfo.loaded:
                return True

            # Deactivate plugin
            plugin_instance = pluginInfo.instance
            if plugin_instance:
                try:
                    self.plugin_manager.hook.deactivate(plugin=plugin_instance)
                except Exception as e:
                    logger.error(f"Error deactivating plugin {module_name}: {e}")

                # Unregister from pluggy
                self.plugin_manager.unregister(plugin_instance)

            # Clean up
            pluginInfo.instance = None
            pluginInfo.loaded = False

            logger.info(f"Unloaded plugin: {module_name}")
            return True

        except Exception as e:
            logger.error(f"Failed to unload plugin {module_name}: {e}")
            return False

    def unloadAllPlugins(self, ForceAllPlugins=False):
        """Unload all plugins."""
        if not PLUGGY_AVAILABLE:
            return

        for pluginInfo in self.plugins:
            if ForceAllPlugins or pluginInfo.loaded:
                self.unloadPlugin(pluginInfo)
