from __future__ import annotations

from typing import Annotated, Any, Literal, cast

import dash_bootstrap_components as dbc
from dash import html
from pydantic import AfterValidator, BeforeValidator, Field

from vizro.managers import model_manager
from vizro.models import VizroBaseModel
from vizro.models._models_utils import _log_call
from vizro.models._navigation._navigation_utils import _NavBuildType, _validate_pages
from vizro.models._navigation.nav_link import NavLink
from vizro.models.types import ModelID


def coerce_pages_type(pages: Any) -> Any:
    if isinstance(pages, list):
        return {page: [page] for page in pages}
    else:
        return pages


class NavBar(VizroBaseModel):
    """Navigation bar to be used as a `nav_selector` for `Navigation`.

    Abstract: Usage documentation
        [How to use the navigation bar](../user-guides/navigation.md#use-a-navigation-bar-with-icons)
    """

    type: Literal["nav_bar"] = "nav_bar"
    pages: Annotated[
        dict[str, list[ModelID]],
        AfterValidator(_validate_pages),
        BeforeValidator(coerce_pages_type),
        Field(default={}, description="Mapping from name of a pages group to a list of page IDs/titles."),
    ]
    items: list[NavLink] = []
    position: Literal["left", "top"] = Field(
        default="left", description="Position of the navigation bar, either on the left sidebar or top header."
    )

    @_log_call
    def pre_build(self):
        self.items = self.items or self._create_items_from_pages()

        # Sets nav position for items
        for nav_link in self.items:
            nav_link._nav_position = self.position

        # The default icons are added only when position == "left"
        if self.position == "left":
            for position, item in enumerate(self.items, 1):
                # The default icons are named filter_1, filter_2, etc. up to filter_9.
                # If there are more than 9 items, then the 10th and all subsequent items are named filter_9+.
                icon_default = f"filter_{position}" if position <= 9 else "filter_9+"  # noqa: PLR2004
                item.icon = item.icon or icon_default

        # Since models instantiated in pre_build do not themselves have pre_build called on them, we call it manually
        # here.
        for item in self.items:
            item.pre_build()

        return self.items

    @_log_call
    def build(self, *, active_page_id=None) -> _NavBuildType:
        # We always show all the nav_links, but only show the accordion for the active page. This works because
        # item.build only returns the nav_panel Div when the item is active.
        # In future maybe we should do this by showing all navigation panels and then setting hidden=True for all but
        # one using a clientside callback?
        # Wrapping built_items into html.Div here is not for rendering purposes but so that we can look up the
        # components by id easily instead of needing to iterate through a nested list.
        built_items = html.Div([item.build(active_page_id=active_page_id) for item in self.items])
        nav_links = [built_items[item.id] for item in self.items]
        if "nav-panel" in built_items:
            nav_panel = built_items["nav-panel"]
        else:
            # Active page is not in navigation at all, so hide navigation panel.
            nav_panel = dbc.Nav(id="nav-panel", className="d-none invisible")

        # `flex-column` ensures that we return a vertical NavBar. For top position, we use `navbar-top` className.
        navbar_class = "flex-column" if self.position == "left" else "navbar-top"
        return html.Div(children=[dbc.Navbar(id="nav-bar", children=nav_links, className=navbar_class), nav_panel])

    def _create_items_from_pages(self):
        """Creates items from pages."""
        from vizro.models import Page

        return [
            NavLink(
                # If the group title is a page ID (as is the case if you do `NavBar(pages=["page_1_id", "page_2_id"])`,
                # then we prefer to have the title rather than id of that page be used
                label=(
                    cast(Page, model_manager[group_title]).title
                    if group_title in [page.id for page in model_manager._get_models(model_type=Page)]
                    else group_title
                ),
                pages=pages,
            )
            for group_title, pages in self.pages.items()
        ]
