import os
import sys
import time
import traceback
from pathlib import Path

from fastapi import FastAPI, Depends
from fastapi.routing import APIRoute
from fastapi.staticfiles import StaticFiles  # noqa: F401 — used by CachedStaticFiles fallback
from fastapi.templating import Jinja2Templates
from jinja2 import ChoiceLoader, PackageLoader, Environment, PrefixLoader
from loguru import logger
from starlette.middleware.authentication import AuthenticationMiddleware
from starlette.middleware.gzip import GZipMiddleware
from starlette.middleware.sessions import SessionMiddleware

from fastpluggy import __version__, __SOURCE_COMMIT_CORE__
from fastpluggy.core.auth import require_authentication, require_role
from fastpluggy.core.auth.middleware import CurrentUserMiddleware
from fastpluggy.core.base_module_manager import BaseModuleManager
from fastpluggy.core.config import FastPluggyConfig
from fastpluggy.core.error import not_found_handler, unauthorized_handler, forbidden_handler, PluginLoadFailedException, plugin_load_failed_handler
from fastpluggy.core.error.exception import custom_exception_handler
from fastpluggy.core.events import FastPluggyEventBus
from fastpluggy.core.flash import FlashMessage, FlashMessageMiddleware
from fastpluggy.core.global_registry import GlobalRegistry
from fastpluggy.core.menu.menu_manager import MenuManager
from fastpluggy.core.routers.app_static import app_static_router
from fastpluggy.core.routers.execute import execute_router
from fastpluggy.core.routers.home import home_router
from fastpluggy.core.routers.ready import ready_router
from fastpluggy.core.routers.service_worker import service_worker_router


class FastPluggy(GlobalRegistry):

    def __init__(self, app: FastAPI | None, app_root_dir=None, path_plugins=None, path_modules=None, auth_manager=None):
        self.module_types = ['plugin', 'domain']
        self.is_ready = False
        self._core_routes_registered = False
        self.path_module_types = {"plugin": path_plugins, "domain": path_modules}

        self.app = app

        # Event bus — available before load_app() so plugins can wire listeners
        # in on_load_complete and receive fp.app_ready afterwards
        self.events: FastPluggyEventBus = FastPluggyEventBus()

        # init core registry
        self.register_global('list_widget_to_inject', [])
        self.register_global('topbar_actions', [])
        self.register_global('capabilities', {})

        self.setup_database()
        self.settings = FastPluggyConfig()

        self.auth_manager = auth_manager

        # Add the flash message middleware
        if app is not None:
            app.add_middleware(FlashMessageMiddleware)
            if self.settings.gzip_enabled:
                app.add_middleware(GZipMiddleware, minimum_size=self.settings.gzip_minimum_size)
            app.add_middleware(CurrentUserMiddleware)

            if self.auth_manager:
                # app.add_middleware(CurrentUserMiddleware)
                app.add_middleware(AuthenticationMiddleware, backend=self.auth_manager)
            else:
                # TODO look if an auth plugin feature is installed
                logger.warning("No auth manager provided. Fastpluggy will not be able to authenticate users.")

            app.add_middleware(SessionMiddleware, secret_key=self.settings.session_secret_key, session_cookie="fp_session")

            # Add Exception Handlers
            app.add_exception_handler(Exception, custom_exception_handler)
            app.add_exception_handler(PluginLoadFailedException, plugin_load_failed_handler)
            app.add_exception_handler(404, not_found_handler)
            app.add_exception_handler(401, unauthorized_handler)
            app.add_exception_handler(403, forbidden_handler)

        self.app_root_dir = str(Path(app_root_dir or ".").resolve())
        sys.path.append(self.app_root_dir)

        self.base_dir = os.path.dirname(os.path.abspath(__file__))  # Get the directory of the current file
        self.menu_manager = MenuManager(show_empty_menu_entries=self.settings.show_empty_menu_entries)

        self.template_loaders = {}
        if app is not None:
            self.app.state.fastpluggy = self  # Store in app.state
            self.app.state.menu_manager = self.menu_manager  # Store in app.state
            self.app.state.events = self.events  # Shortcut: request.app.state.events

        try:
            self.load_app()
            self.is_ready = True
            logger.info("Fastpluggy initialized.")
        except Exception as e:
            logger.error(f"Initialization failed: {e}")
            traceback.print_exc()
            self.enable_degraded_mode(e)

    def set_auth_manager(self, auth_manager):
        """Set the auth manager at runtime (called by auth plugins in on_load_complete).
        Adds AuthenticationMiddleware and updates template globals."""
        self.auth_manager = auth_manager
        if self.app is not None:
            self.app.add_middleware(AuthenticationMiddleware, backend=self.auth_manager)
            if hasattr(self, 'templates'):
                self.templates.env.globals['auth_enable'] = True
        logger.info(f"Auth manager set: {type(auth_manager).__name__}")

    def enable_degraded_mode(self, exception: Exception):
        """Enable degraded mode with a fallback router."""
        # self.degraded_mode = True
        # self.degraded_exception = exception
        if self.app is not None:

            from fastpluggy.core.error.degraded_mode_handler import DegradedModeHandler
            handler = DegradedModeHandler(exception)
            fallback_router = handler.get_router()

            # Remove all existing routes and install fallback
            self.app.routes.clear()
            self.app.include_router(fallback_router)
        else:
            logger.error("No degraded mode without front app.")

    def load_app(self):
        _t0 = time.perf_counter()
        self.template_loaders = {}
        self.menu_manager.init_menu()

        if self.app is not None and not self._core_routes_registered:
            self.configure_static_files()
            self.include_core_route()
            self._core_routes_registered = True

        self.module_manager = BaseModuleManager(
            app=self.app, fast_pluggy=self
        )

        # Scan & load modules dynamically
        for module_type in self.module_types:
            folder = self.get_folder_by_module_type(module_type)

            self.module_manager.discover_plugins(folder=folder, module_type=module_type)

        self.module_manager.discover_plugins_from_entrypoints()

        # Load and initialize all plugins in the correct dependency order
        # (emits fp.plugin.loaded per plugin — see BaseModuleManager)
        self.module_manager.initialize_plugins_in_order()
        self.events.emit(
            "fp.all_plugins_loaded",
            source="fastpluggy",
            plugin_count=len(self.module_manager.modules),
        )

        if self.app is not None:

            # Update menu creates top level entries
            self.menu_manager.create_module_menu_entries(self.module_manager)

            # Combine template loaders
            self.template_loaders.update(self.module_manager.get_template_loaders())

            self.load_dynamic_route()

        self.configure_templates()
        self.events.emit("fp.templates_ready", source="fastpluggy")

        self.module_manager.execute_all_module_hook(hook_name="after_setup_templates")

        if self.app is not None:
            self.menu_manager.update_menu_from_routes(app_routes=self.app.router.routes, module_manager=self.module_manager)

        # Register core metrics provider so the metrics plugin can discover it
        from fastpluggy.core.core_metrics import CoreMetricsProvider
        self.register_global("core_metrics_provider", CoreMetricsProvider(self))

        self.module_manager.execute_all_module_hook(hook_name="on_load_complete")

        if self.app is not None:
            self.templates.env.globals['menu_main'] = self.menu_manager.get_menu(menu_type='main')
            self.templates.env.globals['menu_admin'] = self.menu_manager.get_menu(menu_type='admin')

        self.events.emit("fp.app_ready", source="fastpluggy")
        self._load_time_ms = (time.perf_counter() - _t0) * 1000
        logger.info("Fastpluggy loaded.")

    def configure_static_files(self):
        """Configure static file serving with optional cache headers"""
        static_dir = os.path.join(self.base_dir, "static")
        from fastpluggy.core.static import CachedStaticFiles
        self.app.mount(
            "/static",
            CachedStaticFiles(directory=static_dir, max_age=self.settings.static_cache_max_age),
            name="static",
        )

    def configure_templates(self):
        """Configure Jinja2 templates"""

        # Combine base loader and plugin loaders using ChoiceLoader
        loaders = ChoiceLoader(
            [
                PackageLoader("fastpluggy"),
                PrefixLoader(self.template_loaders),
            ]
        )
        jinja_env = Environment(loader=loaders)  # nosec B701
        self.templates = Jinja2Templates(env=jinja_env)
        logger.debug("List of templates : {}", lambda: jinja_env.list_templates())

        # Inject plugins as a global variable in Jinja templates
        self.templates.env.globals['version'] = __version__
        # self.templates.env.globals['fp_admin_base_url'] = self.settings.fp_admin_base_url
        self.templates.env.globals['source_commit_core'] = __SOURCE_COMMIT_CORE__
        self.templates.env.globals['app_name'] = self.settings.app_name
        self.templates.env.globals['fp_admin_base_url'] = self.settings.fp_admin_base_url
        self.templates.env.globals['auth_enable'] = bool(self.auth_manager)
        self.templates.env.globals['extra_js_files'] = self.module_manager.get_extra_js_files()
        self.templates.env.globals['extra_css_files'] = self.module_manager.get_extra_css_files()

        from fastpluggy.core.tools.jinja_tools import safe_render, b64encode_filter, nl2br_filter, from_json_filter
        self.templates.env.globals['safe_render'] = safe_render
        self.templates.env.filters['b64encode'] = b64encode_filter
        self.templates.env.filters['nl2br'] = nl2br_filter
        self.templates.env.filters['from_json'] = from_json_filter
        from fastpluggy.core.tools.fastapi import url_for_with_query
        self.templates.env.globals['url_for_with_query'] = url_for_with_query
        from fastpluggy.core.tools.jinja_tools import inject_js_global_var
        self.templates.env.globals['inject_js_global_var'] = inject_js_global_var

        self.templates.env.globals['get_flash_messages'] = FlashMessage.get_flash_messages
        self.templates.env.globals['_'] = lambda x: x  # TODO: setup babel for translate later

        from fastpluggy.core.global_registry import GlobalRegistry
        # Cache sorted topbar actions — the list is finalized after on_load_complete()
        _cached_topbar = None
        def _get_topbar_actions():
            nonlocal _cached_topbar
            if _cached_topbar is None:
                _cached_topbar = sorted(
                    GlobalRegistry.get_global('topbar_actions', []),
                    key=lambda a: a.position,
                )
            return _cached_topbar
        self.templates.env.globals['topbar_actions'] = _get_topbar_actions

        from fastpluggy.core.view_builder.builder import ViewBuilder

        self.templates.env.globals['process_topbar_widgets'] = ViewBuilder.build_widget_entries

        if self.app:
            self.app.state.jinja_templates = self.templates

    def setup_database(self, max_retries: int = 5, base_delay: float = 2.0):
        """Initialize the database, retrying on connection errors.

        Retries with exponential backoff so the app can wait for a database
        that starts after the application (e.g. Docker Compose).
        """
        database_url = os.getenv('DATABASE_URL')
        if not database_url:
            logger.warning("No Database URL provided. Using default database.")

        from fastpluggy.core.database import create_table_if_not_exist
        from fastpluggy.core.models import AppSettings, ModuleRepository

        last_exc: Exception | None = None
        for attempt in range(1, max_retries + 1):
            try:
                create_table_if_not_exist(model=AppSettings)
                create_table_if_not_exist(model=ModuleRepository)
                if attempt > 1:
                    logger.info(f"Database connected after {attempt} attempts")
                return
            except Exception as e:
                last_exc = e
                if attempt < max_retries:
                    delay = base_delay * (2 ** (attempt - 1))  # 2, 4, 8, 16s
                    logger.warning(
                        f"Database not ready (attempt {attempt}/{max_retries}): {e} "
                        f"— retrying in {delay:.0f}s"
                    )
                    time.sleep(delay)

        logger.exception(f"Failed to initialize database after {max_retries} attempts: {last_exc}")
        self.enable_degraded_mode(last_exc)

    def include_core_route(self):
        # Always include auth dependencies — they are no-ops when auth_manager is None,
        # but will enforce auth once a plugin sets auth_manager via set_auth_manager()
        auth_dependencies = [Depends(require_authentication)]

        if self.settings.admin_enabled:
            base_admin_url = "" if self.settings.fp_admin_base_url == "/" else self.settings.fp_admin_base_url
            self.app.include_router(home_router, dependencies=auth_dependencies, prefix=base_admin_url,
                                    include_in_schema=self.settings.include_in_schema_fp)

            admin_deps = auth_dependencies + [Depends(require_role("fp_admin"))]
            from fastpluggy.core.routers.admin import admin_router
            self.app.include_router(admin_router, dependencies=admin_deps, prefix=base_admin_url,
                                    include_in_schema=self.settings.include_in_schema_fp)

            from fastpluggy.core.routers.settings import app_settings_router
            self.app.include_router(app_settings_router, dependencies=admin_deps, prefix=base_admin_url,
                                    include_in_schema=self.settings.include_in_schema_fp)

            from fastpluggy.core.routers.base_module import base_module_router
            self.app.include_router(base_module_router, dependencies=admin_deps, prefix=base_admin_url,
                                    include_in_schema=self.settings.include_in_schema_fp)

        self.app.include_router(execute_router, dependencies=auth_dependencies,
                                include_in_schema=self.settings.include_in_schema_fp)

        self.app.include_router(ready_router, include_in_schema=self.settings.include_in_schema_fp)

        self.app.include_router(app_static_router, include_in_schema=self.settings.include_in_schema_fp)

        # Service worker routes — no auth (browsers fetch SW without credentials)
        self.app.include_router(service_worker_router, include_in_schema=self.settings.include_in_schema_fp)

    def load_dynamic_route(self):
        # Remove old plugin/domain routes (identified by path prefix matching module_types)
        module_prefixes = tuple(f"/{mt}/" for mt in self.module_types)  # /plugin/, /domain/
        module_prefixes += ("/plugin-pkg/",)  # entrypoint-loaded plugins

        routes_to_remove = [
            route for route in self.app.routes
            if isinstance(route, APIRoute) and route.path.startswith(module_prefixes)
        ]
        logger.debug(f"routes_to_remove : {routes_to_remove}")

        for route in routes_to_remove:
            self.app.routes.remove(route)
            logger.info(f"Removed route '{route.path}'.")

        # Register new module routes
        self.module_manager.register_all_routes()

    def get_manager(self) -> BaseModuleManager:
        """
        Helper function to retrieve the manager based on the type of module.
        """
        return self.module_manager

    def get_folder_by_module_type(self, type_module: str) -> str | None:
        """
        Helper function to retrieve the manager based on the type of module.
        """
        if type_module in self.module_types:
            if self.path_module_types[type_module] is not None:
                return self.path_module_types[type_module]
            else:
                return f'{self.app_root_dir}/{type_module}s'

        else:
            logger.error(f"Type of module '{type_module}' not supported.")
            return None

    def add_middleware(self, middleware , **kwargs):
        # todo: add a middleware manager to manage all middlewares and be able to see in module backend the middleware of pulgins and be able too dynamiclly disble it for example
        if self.app:
            self.app.add_middleware(middleware, **kwargs)
        else:
            logger.error(f"No app provided. Cannot add middleware {middleware}.")
