from typing import List, Optional, Dict, Any

from fastapi.routing import APIRoute, APIRouter
from loguru import logger

from fastpluggy.core.base_module_manager import BaseModuleManager
from fastpluggy.core.menu.schema import MenuItem
from fastpluggy.core.tools.fastapi import _extract_route_metadata


class MenuManager:
    def __init__(self, show_empty_menu_entries: bool = True):
        self.show_empty_menu_entries = show_empty_menu_entries
        self.menus: Dict[str, List[MenuItem]] = {}
        # O(1) lookup indexes — keyed by menu_type
        self._parent_index: Dict[str, Dict[str, MenuItem]] = {}
        self._url_index: Dict[str, Dict[str, MenuItem]] = {}
        # Cached results for get_menu() / get_flat_menu() — invalidated on mutation
        self._menu_cache: Dict[str, List[Dict[str, Any]]] = {}
        self._flat_menu_cache: Dict[str, List[Dict[str, Any]]] = {}
        self.init_menu()

    def _invalidate_cache(self):
        self._menu_cache.clear()
        self._flat_menu_cache.clear()

    def init_menu(self):
        self.menus = {
            "main": [],
            "user": [],
            "admin": [],
            "no": []
        }
        self._parent_index = {k: {} for k in self.menus}
        self._url_index = {k: {} for k in self.menus}
        self._invalidate_cache()

    def add_parent_item(self, menu_type: str, item: MenuItem):
        """
        Creates a parent entry in the specified menu group.
        This function is used in module auto-detection to create an empty submenu.
        It only adds the parent entry if one with the same parent_name does not already exist.

        :param menu_type: The menu group ("main", "user", "admin", "no").
        :param item: A MenuItem instance that represents the parent entry.
                     Its `parent_name` field should be set (and will be used as the parent's identifier).
        """
        if menu_type not in self.menus:
            raise ValueError(f"Invalid menu type: {menu_type}")

        if not item.parent_name:
            logger.info("No parent_name provided in the item; cannot add as a parent entry.")
            return

        existing_parent = self._parent_index[menu_type].get(item.parent_name)
        if existing_parent:
            logger.info(f"Parent entry '{item.parent_name}' already exists in the {menu_type} menu")
            return existing_parent

        self.menus[menu_type].append(item)
        self._parent_index[menu_type][item.parent_name] = item
        if item.url and item.url != "#":
            self._url_index[menu_type][item.url] = item
        self._invalidate_cache()
        logger.info(f"Created new parent entry '{item.parent_name}' in the {menu_type} menu")

    def add_menu_item(self, menu_type: str, item: MenuItem):
        if menu_type not in self.menus:
            raise ValueError(f"Invalid menu type: {menu_type}")

        if item.parent_name:
            parent_item = self._parent_index[menu_type].get(item.parent_name)
            if parent_item:
                parent_item.add_child(item)
                self._invalidate_cache()
                return

        if item.url not in self._url_index[menu_type]:
            self.menus[menu_type].append(item)
            if item.url and item.url != "#":
                self._url_index[menu_type][item.url] = item
            if item.parent_name:
                self._parent_index[menu_type][item.parent_name] = item
            self._invalidate_cache()

    def get_menu(self, menu_type: str, current_url: Optional[str] = None) -> List[Dict[str, Any]]:
        if menu_type not in self.menus:
            raise ValueError(f"Invalid menu type: {menu_type}")

        # Cache only when no current_url (active state highlighting is per-request)
        if current_url is None and menu_type in self._menu_cache:
            return self._menu_cache[menu_type]

        result = []
        for item in self.menus[menu_type]:
            is_valid_url = item.url and item.url != "#"
            is_non_empty_dropdown = item.is_dropdown() and any(
                child.url and child.url != "#" for child in item.children
            )

            if not self.show_empty_menu_entries and not (is_valid_url or is_non_empty_dropdown):
                continue

            result_item = item.to_dict(current_url)
            result.append(result_item)

        sorted_items = sorted(result, key=lambda x: x.get("position") if x.get("position") is not None else 9999)

        if current_url is None:
            self._menu_cache[menu_type] = sorted_items

        return sorted_items

    def get_flat_menu(self, menu_type: str, current_url: Optional[str] = None) -> List[Dict[str, Any]]:
        """Return a flat list of all menu items (including children) with group info preserved.

        Unlike get_menu() which returns a nested parent/children structure for dropdowns,
        this returns a flat list suitable for sidebar rendering with section headers.
        Items are sorted by position and grouped by their ``group`` field.
        """
        if menu_type not in self.menus:
            raise ValueError(f"Invalid menu type: {menu_type}")

        if current_url is None and menu_type in self._flat_menu_cache:
            return self._flat_menu_cache[menu_type]

        flat: List[Dict[str, Any]] = []
        for item in self.menus[menu_type]:
            if item.is_dropdown():
                for child in item.children:
                    d = child.to_dict(current_url)
                    if not d.get("group") and item.group:
                        d["group"] = item.group
                    flat.append(d)
            else:
                is_valid_url = item.url and item.url != "#"
                if self.show_empty_menu_entries or is_valid_url:
                    flat.append(item.to_dict(current_url))

        sorted_flat = sorted(flat, key=lambda x: x.get("position") if x.get("position") is not None else 9999)

        if current_url is None:
            self._flat_menu_cache[menu_type] = sorted_flat

        return sorted_flat

    def update_menu_from_routes(self, app_routes: List[APIRoute],
                                module_manager: BaseModuleManager,  # <— now pass this in
                                menu_type: str = "main", ):
        if menu_type not in self.menus:
            raise ValueError(f"Invalid menu type: {menu_type}")

        for route in app_routes:
            if not (isinstance(route, APIRoute) and hasattr(route.endpoint, "_menu_entry")):
                continue

            entry = route.endpoint._menu_entry
            target_menu = entry.get("type", menu_type)

            # infer parent from module path if not explicitly set
            parent = entry.get("parent")
            if parent is None:
                parent_module = get_plugin_state_for_route(route, module_manager.modules.values())
                print(parent_module)
                parent = parent_module.plugin.module_name if parent_module else None

            # Build a menu-friendly URL using FastAPI's url_for/url_path_for, informed by _extract_route_metadata.
            # If the route ends with an optional trailing path parameter (e.g. '/browse/{virtual_path:path}' with default),
            # we pass an empty string for that param so Starlette generates the base path (e.g. '/browse/').
            def _menu_url_from_route(r: APIRoute) -> str:
                try:
                    app = getattr(module_manager, 'app', None) or getattr(module_manager, 'fast_pluggy', None).app
                    if not app:
                        raise RuntimeError('FastAPI app not available on module_manager')

                    # Use route metadata to decide optionality of trailing path params
                    metadata = _extract_route_metadata(r)
                    path_str: str = metadata.get('path') or r.path or '#'
                    params = metadata.get('path_params') or []

                    # If no path params, simply reverse by name
                    if not params:
                        return app.url_path_for(r.name)

                    # If the route path ends with a parameter, check if the last param is optional (has default)
                    if path_str.endswith('}') and params:
                        last_param = params[-1]
                        last_param_name = last_param.get('name')
                        last_param_default = last_param.get('default')
                        # Optional if default is not None (Ellipsis mapped to None by _extract_route_metadata)
                        if last_param_name and last_param_default is not None:
                            try:
                                return app.url_path_for(r.name, **{last_param_name: ""})
                            except Exception:
                                # Fall through to other strategies if reverse fails
                                pass

                    # As a conservative fallback, return the literal route path
                    return r.path or '#'
                except Exception:
                    # Fallback to the original path on any unexpected error
                    return r.path or '#'

            item = MenuItem(
                label=entry["label"],
                url=_menu_url_from_route(route),
                icon=entry.get("icon"),
                parent_name=parent,
                permission=entry.get("permission"),
                position=entry.get("position"),
                section_title=entry.get("section_title"),
                divider_before=entry.get("divider_before", False),
                divider_after=entry.get("divider_after", False),
                group=entry.get("group"),
                badge=entry.get("badge"),
                badge_color=entry.get("badge_color"),
                badge_animate=entry.get("badge_animate", False),
            )
            self.add_menu_item(target_menu, item)
            logger.info(
                f"Added route '{route.path}' to '{target_menu}' "
                f"menu with label '{entry['label']}' under parent '{parent}'"
            )

    def create_module_menu_entries(self, module_manager):
        """
        Updates the provided MenuManager with entries for the loaded modules, to have the main entry.
        The sub link/menu will be added using update_menu_from_routes by scanning routes.
        """
        for module_info in module_manager.modules.values():
            if module_manager.is_module_loaded(module_info):
                self.add_parent_item(
                    item=MenuItem(
                        label=module_info.plugin.display_name,
                        url=module_info.url,
                        icon=module_info.plugin.module_menu_icon,
                        parent_name=module_info.plugin.module_name,
                    ),
                    menu_type=module_info.plugin.module_menu_type,
                )


def get_plugin_state_for_route(
        route_to_match: APIRoute,
        modules: List["PluginState"],
) -> Optional["PluginState"]:  # Replace Any with PluginState
    """
    Given a request path, scan through a list of modules, inspect each module's APIRouter,
    and return the module's PluginState if one of its routes matches the given path.

    :param route_path: The incoming request path (e.g., '/items/123')
    :param modules: A list of module objects, each expected to have attributes:
                    - router: an APIRouter instance or a list of APIRouter instances
                    - plugin_state: the PluginState instance for that module
    :return: The matching module's PluginState, or None if no match is found.
    """
    for module in modules:
        plugin = getattr(module, "plugin", None)
        plugin_router = getattr(plugin, "module_router", None)

        # Handle case where plugin_router is a list of routers
        if isinstance(plugin_router, list) and plugin_router is not None:
            for router in plugin_router:
                if isinstance(router, APIRouter):
                    for route in router.routes:
                        if isinstance(route, APIRoute):
                            if route_to_match.endpoint == route.endpoint:
                                return module
        # Handle case where plugin_router is a single router
        elif isinstance(plugin_router, APIRouter) and plugin_router is not None:
            for route in plugin_router.routes:
                if isinstance(route, APIRoute):
                    if route_to_match.endpoint == route.endpoint:
                        return module
    return None
