from pathlib import Path
from typing import Any, Annotated, Optional, List, Dict
from typing import Callable

from fastapi import APIRouter
from loguru import logger
from pydantic import BaseModel, ConfigDict, Field, computed_field

from fastpluggy.core.tools.inspect_tools import InjectDependency

# todo we are in python 3.11 minimal this is required
try:
    import tomllib
except ImportError:
    try:
        import tomli as tomllib
    except ImportError:
        tomllib = None

ModuleRouterType = Any | APIRouter | Callable[[], APIRouter] | List[Any]


class PluginLoadError(Exception):
    """Raised by plugins in lifecycle hooks for expected load failures.

    Use this instead of ``RuntimeError`` when a plugin cannot load due to a
    known condition (e.g. wrong database backend, missing external service).
    The ``user_message`` is displayed on the admin UI without a traceback.

    Example::

        def on_load_complete(self, fast_pluggy):
            if not check_postgres():
                raise PluginLoadError("PostgreSQL required — SQLite is not supported")
    """

    def __init__(self, user_message: str):
        self.user_message = user_message
        super().__init__(user_message)


class FastPluggyBaseModule(BaseModel):
    """
    Base class for FastPluggy plugins.

    Plugins must inherit from this class and implement lifecycle hooks like `on_load_complete()` 
    and `after_setup_templates()` as needed.

    They can optionally define metadata fields and attributes for integration (e.g., router, settings).
    """

    # --- Identity / version ---
    module_name: Optional[str] = ""
    module_version: Optional[str] = "0.0.0"
    module_settings: Optional[Any] = None
    depends_on: Optional[Dict[str, str]] = Field(default_factory=dict)
    optional_depends_on: Dict[str, str] = Field(default_factory=dict)

    # --- Menu Metadata ---
    module_menu_name: str = ""
    module_menu_icon: str = "fas fa-cube"
    module_menu_type: Optional[str] = "main"

    # --- Capabilities ---
    capabilities: List[str] = Field(default_factory=list)

    # --- Event documentation ---
    emits: List[Any] = Field(default_factory=list)
    """
    List of :class:`~fastpluggy.core.events.EventSpec` objects declaring the events
    this plugin emits.  Shown in the admin **Events** page as a discoverable catalog.

    Example::

        from fastpluggy.core.events import EventSpec

        class MyPlugin(FastPluggyBaseModule):
            emits = [
                EventSpec(
                    name="my_plugin.done",
                    description="Fired when processing completes.",
                    payload={"job_id": "int", "status": "str"},
                ),
            ]
    """

    # --- JS & CSS ---
    extra_js_files: List[str] = Field(default_factory=list)
    extra_css_files: List[str] = Field(default_factory=list)

    # --- Service Worker ---
    sw_handler_files: List[str] = Field(default_factory=list)
    """Paths relative to the plugin's static dir (e.g. ``"js/sw-handlers.js"``).

    Each file is appended to the composed service worker served by core.
    Handler files execute in SW scope and can register message handlers via
    ``self.swHandlers['MY_TYPE'] = function(event) { ... }``.
    """

    # --- Optional Router ---
    module_router: ModuleRouterType = None
    module_mount_url: Optional[str] = None

    # --- Path ---
    module_path: Optional[Path] = None

    model_config = ConfigDict(arbitrary_types_allowed=True)

    # --- Computed Properties ---
    @computed_field
    @property
    def display_name(self) -> Optional[str]:
        return self.module_menu_name or self.module_name

    @computed_field
    @property
    def templates_dir(self) -> Optional[Path]:
        if self.module_path:
            path = self.module_path / "templates"
            return path if path.exists() else None
        return None

    @computed_field
    @property
    def requirements_path(self) -> Optional[Path]:
        return self.module_path / "requirements.txt" if self.module_path else None

    @computed_field
    @property
    def requirements_exists(self) -> bool:
        req = self.requirements_path
        return req.exists() if req else False

    @computed_field
    @property
    def pyproject_path(self) -> Optional[Path]:
        return self.module_path / "pyproject.toml" if self.module_path else None

    @computed_field
    @property
    def pyproject_exists(self) -> bool:
        pyproject = self.pyproject_path
        return pyproject.exists() if pyproject else False

    @computed_field
    @property
    def has_dependencies(self) -> bool:
        """Check if the module has any dependencies (requirements.txt or pyproject.toml)"""
        return self.requirements_exists or self.pyproject_exists

    # --- Lifecycle Hooks ---
    def on_load_complete(self, fast_pluggy: Annotated["FastPluggy", InjectDependency]) -> None:
        """
        Called once, after all plugins are loaded and templates are configured.

        This is the recommended place to:
        - Subscribe to events via ``fast_pluggy.events.on(...)``
        - Register capabilities via ``GlobalRegistry.register_capability(...)``
        - Set up cross-plugin wiring that depends on other plugins being present

        After ``on_load_complete`` runs for all plugins, ``fast_pluggy.events.emit("fp.app_ready")``
        fires — the first event that all subscribers can receive.

        Example::

            def on_load_complete(self, fast_pluggy):
                fast_pluggy.events.on("user.login", self._handle_login)

                @fast_pluggy.events.on("fp.app_ready")
                def ready(event):
                    logger.info("All plugins loaded, app is ready")

        Args:
            fast_pluggy: The FastPluggy instance, automatically injected.
        """
        pass

    def after_setup_templates(self, fast_pluggy: Annotated["FastPluggy", InjectDependency]) -> None:
        """
        This method will be called when the Jinja2 environment is set up.

        Use this hook to add custom template filters, globals, or extensions
        to the Jinja2 environment. This is useful for adding custom rendering
        functionality that can be used in your templates.
        You should also create the parent_item of menu here.

        Args:
            fast_pluggy: The FastPluggy instance, automatically injected
        """
        pass

    # --- Static Dependency Management Methods ---
    def get_requirements_list(self) -> list[str]:
        """
        Get a list of dependencies from requirements.txt file.
        Returns a list of dependency strings.
        """
        if not self.requirements_exists:
            return []

        dependencies = []
        with open(self.requirements_path) as f:
            lines = [line.strip() for line in f if line.strip() and not line.startswith("#")]

        for line in lines:
            dependencies.append(line)

        return dependencies

    def get_pyproject_dependencies_list(self) -> dict:
        """
        Get dependencies from pyproject.toml, split by group.

        Returns::

            {
                'required': ['dep1', 'dep2', ...],
                'optional': {
                    'all':  ['pympler', ...],
                    'dev':  ['pytest', ...],
                    ...
                }
            }
        """
        empty: dict = {'required': [], 'optional': {}}

        if not self.pyproject_exists:
            return empty

        if tomllib is None:
            logger.warning("tomllib/tomli not available, cannot parse pyproject.toml files")
            return empty

        try:
            with open(self.pyproject_path, 'rb') as f:
                data = tomllib.load(f)

            project = data.get('project', {})
            return {
                'required': list(project.get('dependencies', [])),
                'optional': {
                    group: list(pkgs)
                    for group, pkgs in project.get('optional-dependencies', {}).items()
                },
            }

        except Exception as e:
            logger.error(f"Error reading pyproject.toml file '{self.pyproject_path}': {e}")
            return empty

    def get_dependency(self) -> dict:
        """
        Returns dependency information from all dependency files.

        Format::

            {
                'requirements.txt': ['dep1', 'dep2'],
                'pyproject.toml': {
                    'required': ['dep1', ...],
                    'optional': {'all': ['pympler'], ...},
                },
            }
        """
        result: dict = {
            'requirements.txt': [],
            'pyproject.toml': {'required': [], 'optional': {}},
        }

        if self.requirements_exists:
            result['requirements.txt'] = self.get_requirements_list()

        if self.pyproject_exists:
            result['pyproject.toml'] = self.get_pyproject_dependencies_list()

        return result
