
from __future__ import annotations

import importlib
import importlib.util
import json
import os
import sys
import traceback
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Type, TYPE_CHECKING

from .base import (
    Plugin, PluginMeta, PluginLoadError, PluginDependencyError, HookType,
    RegisteredButton, RegisteredPanel, RegisteredContextMenuItem,
    RegisteredKeybinding, RegisteredCommand, RegisteredWidget, RegisteredSettingsTab
)

if TYPE_CHECKING:
    from textual.app import App
    from database import DB

_loader_instance: Optional["PluginLoader"] = None

def get_plugin_loader() -> "PluginLoader":
    global _loader_instance
    if _loader_instance is None:
        _loader_instance = PluginLoader()
    return _loader_instance

class PluginLoader:

    LEGACY_CONFIG_FILE = "plugins_config.json"

    def __init__(self, plugins_dir: Optional[str] = None):
        self._plugins_dir = plugins_dir or self._get_default_plugins_dir()
        self._plugins: Dict[str, Plugin] = {}
        self._plugin_classes: Dict[str, Type[Plugin]] = {}
        self._enabled_plugins: set[str] = set()
        self._plugin_settings: Dict[str, Dict[str, Any]] = {}
        self._app: Optional[App] = None
        self._db: Optional[DB] = None
        self._hooks: Dict[HookType, List[tuple[str, Callable]]] = {}

        Path(self._plugins_dir).mkdir(parents=True, exist_ok=True)

        self._load_legacy_config()

    def _get_default_plugins_dir(self) -> str:
        home = Path.home()
        antch_dir = home / ".antch"
        plugins_dir = antch_dir / "plugins"

        antch_dir.mkdir(exist_ok=True)
        plugins_dir.mkdir(exist_ok=True)

        return str(plugins_dir)

    def _get_legacy_config_path(self) -> Path:
        return Path(self._plugins_dir).parent / self.LEGACY_CONFIG_FILE

    def _load_legacy_config(self) -> None:
        config_path = self._get_legacy_config_path()
        if config_path.exists():
            try:
                with open(config_path, "r", encoding="utf-8") as f:
                    data = json.load(f)
                    self._enabled_plugins = set(data.get("enabled", []))
                    self._plugin_settings = data.get("settings", {})
            except Exception as e:
                print(f"Error loading legacy plugin config: {e}")

    def _save_legacy_config(self) -> None:
        config_path = self._get_legacy_config_path()
        try:
            data = {
                "enabled": list(self._enabled_plugins),
                "settings": self._plugin_settings,
            }
            with open(config_path, "w", encoding="utf-8") as f:
                json.dump(data, f, indent=2, ensure_ascii=False)
        except Exception as e:
            print(f"Error saving legacy plugin config: {e}")

    def set_db(self, db: "DB") -> None:
        self._db = db

        if self._plugin_settings or self._enabled_plugins:
            self._migrate_to_db()

        self._load_from_db()

    def _migrate_to_db(self) -> None:
        if not self._db:
            return

        if self._enabled_plugins:
            enabled_json = json.dumps(list(self._enabled_plugins))
            self._db.set_setting("_plugins_enabled", enabled_json)

        for plugin_id, settings in self._plugin_settings.items():
            if settings:
                settings_json = json.dumps(settings, ensure_ascii=False)
                self._db.set_plugin_setting(plugin_id, "_config", settings_json)

        legacy_path = self._get_legacy_config_path()
        if legacy_path.exists():
            try:
                legacy_path.unlink()
                print(f"[PluginLoader] Migrated config to user DB, removed legacy file")
            except Exception:
                pass

    def _load_from_db(self) -> None:
        if not self._db:
            return

        enabled_json = self._db.get_setting("_plugins_enabled")
        if enabled_json:
            try:
                self._enabled_plugins = set(json.loads(enabled_json))
            except Exception:
                pass

        self._plugin_settings = {}

    def _save_config(self) -> None:
        if self._db:

            enabled_json = json.dumps(list(self._enabled_plugins))
            self._db.set_setting("_plugins_enabled", enabled_json)
        else:

            self._save_legacy_config()

    def set_app(self, app: App) -> None:
        self._app = app
        for plugin in self._plugins.values():
            plugin.app = app

    def discover_plugins(self) -> List[str]:
        discovered = []

        search_dirs = [
            Path(self._plugins_dir),
        ]

        try:
            local_plugins = Path(__file__).parent
            if local_plugins.exists() and local_plugins != Path(self._plugins_dir):
                search_dirs.append(local_plugins)
        except Exception:
            pass

        skip_dirs = {"__pycache__", ".git"}

        for plugins_path in search_dirs:
            if not plugins_path.exists():
                continue

            for item in plugins_path.iterdir():
                if item.is_dir() and item.name not in skip_dirs:
                    init_file = item / "__init__.py"
                    if init_file.exists():
                        try:
                            plugin_id = self._load_plugin_module(item)
                            if plugin_id:
                                if plugin_id not in discovered:
                                    discovered.append(plugin_id)
                                    print(f"[PluginLoader] Discovered: {plugin_id} from {item}")
                                else:
                                    print(f"[PluginLoader] Already discovered: {plugin_id}")
                        except Exception as e:
                            print(f"Error discovering plugin {item.name}: {e}")
                            traceback.print_exc()

        print(f"[PluginLoader] Total discovered: {len(discovered)} - {discovered}")
        return discovered

    def _load_plugin_module(self, plugin_path: Path) -> Optional[str]:

        path_hash = hash(str(plugin_path.absolute())) % 100000
        module_name = f"antch_plugin_{plugin_path.name}_{path_hash}"

        try:

            if module_name in sys.modules:

                module = sys.modules[module_name]
            else:

                plugins_parent = str(plugin_path.parent)
                if plugins_parent not in sys.path:
                    sys.path.insert(0, plugins_parent)

                spec = importlib.util.spec_from_file_location(
                    module_name,
                    plugin_path / "__init__.py"
                )
                if spec is None or spec.loader is None:
                    return None

                module = importlib.util.module_from_spec(spec)
                sys.modules[module_name] = module
                spec.loader.exec_module(module)

            plugin_class = None
            for name in dir(module):
                if name.startswith("_"):
                    continue
                obj = getattr(module, name, None)
                if obj is None:
                    continue
                if not isinstance(obj, type):
                    continue
                if obj is Plugin:
                    continue
                if not issubclass(obj, Plugin):
                    continue

                if obj.__module__ == module_name:
                    plugin_class = obj
                    break

            if plugin_class is None:

                for name in dir(module):
                    if name.startswith("_") or name == "Plugin":
                        continue
                    obj = getattr(module, name, None)
                    if obj is None:
                        continue
                    if not isinstance(obj, type):
                        continue
                    if obj is Plugin:
                        continue
                    try:
                        if issubclass(obj, Plugin) and "Plugin" in name:
                            plugin_class = obj
                            break
                    except TypeError:
                        continue

            if plugin_class is None:
                print(f"[PluginLoader] No Plugin subclass found in {module_name} (dir: {plugin_path.name})")
                return None

            print(f"[PluginLoader] Found plugin class: {plugin_class.__name__} in {plugin_path.name}")
            temp_instance = plugin_class()
            plugin_id = temp_instance.meta.id

            self._plugin_classes[plugin_id] = plugin_class

            return plugin_id

        except Exception as e:
            print(f"Error loading plugin module {plugin_path.name}: {e}")
            traceback.print_exc()
            return None

    def load_plugin(self, plugin_id: str) -> Optional[Plugin]:
        if plugin_id in self._plugins:
            return self._plugins[plugin_id]

        if plugin_id not in self._plugin_classes:
            raise PluginLoadError(f"Plugin {plugin_id} not found")

        try:
            plugin_class = self._plugin_classes[plugin_id]
            plugin = plugin_class()
            plugin.app = self._app

            if plugin_id in self._plugin_settings:
                plugin.load_settings(self._plugin_settings[plugin_id])

            plugin.on_load()

            self._plugins[plugin_id] = plugin
            return plugin

        except Exception as e:
            raise PluginLoadError(f"Error loading plugin {plugin_id}: {e}")

    def unload_plugin(self, plugin_id: str) -> None:
        if plugin_id not in self._plugins:
            return

        plugin = self._plugins[plugin_id]

        self._unregister_plugin_hooks(plugin_id)

        try:
            plugin.on_unload()
        except Exception as e:
            print(f"Error unloading plugin {plugin_id}: {e}")

        del self._plugins[plugin_id]

    def enable_plugin(self, plugin_id: str) -> bool:
        if plugin_id not in self._plugin_classes:
            return False

        try:
            plugin = self.load_plugin(plugin_id)
            if plugin:
                plugin.enabled = True
                plugin.on_enable()
                self._enabled_plugins.add(plugin_id)
                self._register_plugin_hooks(plugin)
                self._save_config()
                return True
        except Exception as e:
            print(f"Error enabling plugin {plugin_id}: {e}")
        return False

    def disable_plugin(self, plugin_id: str) -> bool:
        if plugin_id in self._plugins:
            plugin = self._plugins[plugin_id]
            plugin.enabled = False
            try:
                plugin.on_disable()
            except Exception:
                pass
            self._unregister_plugin_hooks(plugin_id)

        self._enabled_plugins.discard(plugin_id)
        self._save_config()
        return True

    def is_enabled(self, plugin_id: str) -> bool:
        return plugin_id in self._enabled_plugins

    def get_plugin(self, plugin_id: str) -> Optional[Plugin]:
        return self._plugins.get(plugin_id)

    def get_all_plugins(self) -> Dict[str, Plugin]:
        return self._plugins.copy()

    def get_all_plugin_metas(self) -> List[tuple[str, PluginMeta, bool]]:
        result = []
        print(f"[PluginLoader] get_all_plugin_metas: _plugin_classes has {len(self._plugin_classes)} entries: {list(self._plugin_classes.keys())}")
        for plugin_id, plugin_class in self._plugin_classes.items():
            try:
                temp = plugin_class()
                meta = temp.meta
                enabled = plugin_id in self._enabled_plugins
                result.append((plugin_id, meta, enabled))
            except Exception as e:
                print(f"[PluginLoader] Error getting meta for {plugin_id}: {e}")
        return result

    def get_plugin_meta(self, plugin_id: str) -> Optional[PluginMeta]:

        if plugin_id in self._plugins:
            return self._plugins[plugin_id].meta

        if plugin_id in self._plugin_classes:
            try:

                cls = self._plugin_classes[plugin_id]
                return cls().meta
            except Exception:
                pass

        return None

    def get_plugin_settings(self, plugin_id: str) -> Dict[str, Any]:

        if self._db:
            try:
                config_json = self._db.get_plugin_setting(plugin_id, "_config")
                if config_json:
                    settings = json.loads(config_json)
                    self._plugin_settings[plugin_id] = settings
                    return settings
            except Exception as e:
                print(f"Error loading plugin settings from DB: {e}")

        return self._plugin_settings.get(plugin_id, {}).copy()

    def save_plugin_settings(self, plugin_id: str, settings: Dict[str, Any]) -> None:
        self._plugin_settings[plugin_id] = settings

        if self._db:
            try:
                settings_json = json.dumps(settings, ensure_ascii=False)
                self._db.set_plugin_setting(plugin_id, "_config", settings_json)
            except Exception as e:
                print(f"Error saving plugin settings to DB: {e}")
                self._save_legacy_config()
        else:
            self._save_legacy_config()

        if plugin_id in self._plugins:
            plugin = self._plugins[plugin_id]
            plugin.load_settings(settings)

            if hasattr(plugin, 'on_settings_changed'):
                try:
                    plugin.on_settings_changed(settings)
                except Exception as e:
                    print(f"Error in plugin on_settings_changed: {e}")

    def _register_plugin_hooks(self, plugin: Plugin) -> None:
        plugin_id = plugin.meta.id

        for hook_type in HookType:
            callbacks = plugin.get_hooks(hook_type)
            for callback in callbacks:
                if hook_type not in self._hooks:
                    self._hooks[hook_type] = []
                self._hooks[hook_type].append((plugin_id, callback))

    def _unregister_plugin_hooks(self, plugin_id: str) -> None:
        for hook_type in list(self._hooks.keys()):
            self._hooks[hook_type] = [
                (pid, cb) for pid, cb in self._hooks[hook_type]
                if pid != plugin_id
            ]

    def call_hook(self, hook_type: HookType, *args, **kwargs) -> List[Any]:
        results = []

        for plugin_id, callback in self._hooks.get(hook_type, []):
            if plugin_id in self._enabled_plugins:
                try:
                    result = callback(*args, **kwargs)
                    results.append(result)
                except Exception as e:
                    print(f"Error in hook {hook_type} from {plugin_id}: {e}")

        for plugin_id, plugin in self._plugins.items():
            if plugin.enabled:
                method_name = hook_type.value
                if hasattr(plugin, method_name):
                    try:
                        method = getattr(plugin, method_name)
                        result = method(*args, **kwargs)
                        if result is not None:
                            results.append(result)
                    except Exception as e:
                        print(f"Error calling {method_name} on {plugin_id}: {e}")

        return results

    def call_hook_chain(self, hook_type: HookType, value: Any, *args, **kwargs) -> Any:
        current = value

        for plugin_id, plugin in self._plugins.items():
            if not plugin.enabled:
                continue

            method_name = hook_type.value
            if hasattr(plugin, method_name):
                try:
                    method = getattr(plugin, method_name)
                    result = method(current, *args, **kwargs)
                    if result is False:
                        return False
                    if result is not None:
                        current = result
                except Exception as e:
                    print(f"Error in hook chain {method_name} on {plugin_id}: {e}")

        return current

    def init_enabled_plugins(self) -> None:
        for plugin_id in list(self._enabled_plugins):
            try:
                self.load_plugin(plugin_id)
                plugin = self._plugins.get(plugin_id)
                if plugin:
                    plugin.enabled = True

                    try:
                        plugin.on_enable()
                    except Exception as e:
                        print(f"Error in on_enable for {plugin_id}: {e}")
                    self._register_plugin_hooks(plugin)
            except Exception as e:
                print(f"Error initializing plugin {plugin_id}: {e}")
                self._enabled_plugins.discard(plugin_id)

    def get_all_buttons(self, location: Optional[str] = None) -> List[RegisteredButton]:
        buttons = []
        for plugin in self._plugins.values():
            if plugin.enabled:
                buttons.extend(plugin.get_buttons(location))
        return sorted(buttons, key=lambda b: b.order)

    def get_all_panels(self, location: Optional[str] = None) -> List[RegisteredPanel]:
        panels = []
        for plugin in self._plugins.values():
            if plugin.enabled:
                panels.extend(plugin.get_panels(location))
        return panels

    def get_all_context_menu_items(self, target: Optional[str] = None) -> List[RegisteredContextMenuItem]:
        items = []
        for plugin in self._plugins.values():
            if plugin.enabled:
                items.extend(plugin.get_context_menu_items(target))
        return sorted(items, key=lambda i: i.order)

    def get_all_keybindings(self) -> List[RegisteredKeybinding]:
        bindings = []
        for plugin in self._plugins.values():
            if plugin.enabled:
                bindings.extend(plugin.get_keybindings())
        return bindings

    def get_all_commands(self) -> Dict[str, RegisteredCommand]:
        commands = {}
        for plugin in self._plugins.values():
            if plugin.enabled:
                commands.update(plugin.get_all_commands())
        return commands

    def get_all_widgets(self, location: Optional[str] = None) -> List[RegisteredWidget]:
        widgets = []
        for plugin in self._plugins.values():
            if plugin.enabled:
                widgets.extend(plugin.get_widgets(location))
        return sorted(widgets, key=lambda w: w.order)

    def get_all_settings_tabs(self) -> List[RegisteredSettingsTab]:
        tabs = []
        for plugin in self._plugins.values():
            if plugin.enabled:
                tabs.extend(plugin.get_settings_tabs())
        return sorted(tabs, key=lambda t: t.order)

    def handle_command(self, command: str, args: List[str]) -> bool:
        commands = self.get_all_commands()

        if command in commands:
            try:
                commands[command].callback(args)
                return True
            except Exception as e:
                print(f"Error in command {command}: {e}")
                return True

        for cmd in commands.values():
            if command in cmd.aliases:
                try:
                    cmd.callback(args)
                    return True
                except Exception as e:
                    print(f"Error in command {command}: {e}")
                    return True

        return False

    def handle_keybinding(self, key: str, modifiers: List[str]) -> bool:
        key_combo = "+".join(modifiers + [key]) if modifiers else key
        key_combo_lower = key_combo.lower()

        for binding in self.get_all_keybindings():
            if binding.key.lower() == key_combo_lower:
                try:
                    binding.callback()
                    return True
                except Exception as e:
                    print(f"Error in keybinding {binding.id}: {e}")
                    return True

        return False

    def notify_app_ready(self) -> None:
        for plugin in self._plugins.values():
            if plugin.enabled:
                try:
                    plugin.on_app_ready()
                except Exception as e:
                    print(f"Error in on_app_ready for {plugin.meta.id}: {e}")
