# Copyright © The Debusine Developers
# See the AUTHORS file at the top-level directory of this distribution
#
# This file is part of Debusine. It is subject to the license terms
# in the LICENSE file found in the top-level directory of this
# distribution. No part of Debusine, including this file, may be copied,
# modified, propagated, or distributed except according to the terms
# contained in the LICENSE file.

"""
View extension to model page descriptions.

A Place describes information about a page that can be used to generate
elements like page titles and breadcrumb elements.
"""

import abc
from enum import StrEnum
from typing import (
    NotRequired,
    TYPE_CHECKING,
    TypedDict,
    Unpack,
    overload,
    override,
)

from django.db.models import Model
from django.utils.html import format_html
from django.utils.safestring import SafeString

from debusine.web.views.widgets import widget_method

if TYPE_CHECKING:
    from debusine.web.views.ui import UI


class ValueVariants(StrEnum):
    """Well known value variants."""

    PAGE = "page"
    BREADCRUMB = "breadcrumb"
    LINK = "link"
    ACTION = "action"
    TYPE = "type"
    TOOLTIP = "tooltip"


class PlaceValue[T]:
    """Value with alternate versions."""

    def __init__(self, default_value: T, **variants: T) -> None:
        """
        Initialize a PlaceValue.

        :param default_value: the value returned when no variants are requested
          or availahle
        """
        self.default_value = default_value
        self.variants = variants

    def get(self, name: str | None = None) -> T:
        """
        Get a value or one of its variants.

        :param name: if set, it is the name of the variant to look up. If no
          variant exists for the given name, returns the default value.
        """
        if name is None:
            return self.default_value
        return self.variants.get(name, self.default_value)

    @overload
    @classmethod
    def from_arg(cls, value: "T | PlaceValue[T]") -> "PlaceValue[T]": ...

    @overload
    @classmethod
    def from_arg(cls, value: None) -> None: ...

    @classmethod
    def from_arg(
        cls, value: "T | PlaceValue[T] | None"
    ) -> "PlaceValue[T] | None":
        """Build a PlaceValue from a Place constructor argument."""
        match value:
            case None:
                return None
            case PlaceValue():
                return value
            case _:
                return cls(value)


class PlaceKwargs(TypedDict):
    """Argument typing for Place constructor."""

    #: Title, suitable for a page title
    title: str | PlaceValue[str]
    #: Description
    description: NotRequired[str | PlaceValue[str] | None]
    #: URL
    url: str
    #: Icon
    icon: NotRequired[str | PlaceValue[str] | None]
    #: Parent place (used for breadcrumbs)
    parent: NotRequired["Place | None"]
    #: True if the place view matches the current view
    current: NotRequired[bool]


class Place(abc.ABC):
    """Describe a page in Debusine's web space."""

    BREADCRUMB_FINAL_ELEMENT = "span"

    def __init__(self, **kwargs: Unpack[PlaceKwargs]) -> None:
        """Store Place attributes."""
        #: Title, suitable for a page title
        self.title = PlaceValue.from_arg(kwargs["title"])
        #: Description
        self.description: PlaceValue[str] | None = PlaceValue.from_arg(
            kwargs.get("description")
        )
        #: URL
        self.url = kwargs["url"]
        #: Icon
        self.icon: PlaceValue[str] | None = PlaceValue.from_arg(
            kwargs.get("icon")
        )
        #: Parent place (used for breadcrumbs)
        self.parent = kwargs.get("parent")
        #: True if the place view matches the current view
        self.current = kwargs.get("current", False)

    def is_page_title_visible(self) -> bool:
        """Return True if the page title should be shown."""
        return True

    def parents(self) -> list["Place"]:
        """Build the parent chain as a list, topmost parent first, self last."""
        if self.parent is None:
            res = []
        else:
            res = self.parent.parents()
        res.append(self)
        return res

    @widget_method()
    def as_icon(self, *, variant: str | None = None) -> str:
        """
        Render the icon.

        :returns: the rendered HTML, or an empty string if this Place has no
          icon
        """
        if self.icon is None:
            return SafeString()
        else:
            return format_html(
                """<span class="bi bi-{icon}"></span> """,
                icon=self.icon.get(variant),
            )

    @widget_method()
    def as_tooltip(self, *, variant: str | None = None) -> str:
        """Render as a title= attribute."""
        if self.description is not None:
            return format_html(
                ' title="{description}"',
                description=self.description.get(variant),
            )
        else:
            return SafeString()

    @widget_method()
    def as_head_title(self, *, variant: str = ValueVariants.PAGE) -> str:
        """Render as a <title> element."""
        if self.parent is None:
            prefix = ""
        else:
            prefix = " - ".join(
                p.title.get(ValueVariants.BREADCRUMB)
                for p in self.parent.parents()
            )
            prefix += " - "
        return format_html(
            "<title>{prefix}{title}</title>",
            prefix=prefix,
            title=self.title.get(variant),
        )

    @widget_method()
    def as_breadcrumb_parent(self) -> str:
        """Render as a parent in a breadcrumb list."""
        if self.parent:
            parent = self.parent.as_breadcrumb_parent()
        else:
            parent = SafeString()
        return format_html(
            """{parent}<a href="{url}" class="breadcrumb-parent"{tooltip}>"""
            """{breadcrumb}</a>"""
            """<span class="breadcrumb-separator"></span>""",
            parent=parent,
            url=self.url,
            tooltip=self.as_tooltip(variant=ValueVariants.BREADCRUMB),
            breadcrumb=self.title.get(ValueVariants.BREADCRUMB),
        )

    @widget_method()
    def as_breadcrumbs(self) -> str:
        """Render as breadcrumbs leading to this Place."""
        if self.parent:
            parent = self.parent.as_breadcrumb_parent()
        else:
            parent = SafeString()
        return format_html(
            """{parent}<{el} class="breadcrumb-current"{tooltip}>"""
            "{title}</{el}>",
            el=self.BREADCRUMB_FINAL_ELEMENT,
            parent=parent,
            tooltip=self.as_tooltip(variant=ValueVariants.BREADCRUMB),
            title=self.title.get(ValueVariants.PAGE),
        )

    @widget_method()
    def as_page_title(self, *, variant: str = ValueVariants.PAGE) -> str:
        """Render as a page title."""
        return format_html(
            """<h1 class="mb-4">{title}</h1>""", title=self.title.get(variant)
        )

    @widget_method()
    def as_button(
        self, *, variant: str = ValueVariants.ACTION, label: str | None = None
    ) -> str:
        """
        Render as a button.

        :param label: override button label
        """
        if label is not None:
            title = label
        else:
            title = self.title.get(variant)
        return format_html(
            """<a class="btn btn-primary" href="{url}"{tooltip}>"""
            """{icon}{title}</a>""",
            url=self.url,
            title=title,
            icon=self.as_icon(variant=variant),
            tooltip=self.as_tooltip(variant=variant),
        )

    @widget_method()
    def as_link(self, variant: str | None = ValueVariants.LINK) -> str:
        """Render a link to this place."""
        return format_html(
            '<a href="{url}">{text}</a>',
            url=self.url,
            text=self.title.get(variant),
        )

    @widget_method()
    def as_nav_link(self, *, variant: str = ValueVariants.ACTION) -> str:
        """Render as a <nav> link."""
        if self.current:
            a = (
                '<a class="nav-link active" aria-current="page"'
                ' href="{url}"{tooltip}>'
            )
        else:
            a = '<a class="nav-link" href="{url}"{tooltip}>'
        return format_html(
            a + "{icon}{title}</a>",
            url=self.url,
            icon=self.as_icon(variant=variant),
            tooltip=self.as_tooltip(variant=variant),
            title=self.title.get(variant),
        )

    @widget_method()
    def as_dropdown_item(self, *, variant: str = ValueVariants.ACTION) -> str:
        """Render a dropdown item linking to this place."""
        return format_html(
            '<a class="dropdown-item" href="{url}"{tooltip}>{icon}{title}</a>',
            url=self.url,
            icon=self.as_icon(variant=variant),
            title=self.title.get(variant),
            tooltip=self.as_tooltip(variant=variant),
        )


class PlaceWithoutPageTitle(Place):
    """Place which does not render a page title."""

    BREADCRUMB_FINAL_ELEMENT = "h1"

    @override
    def is_page_title_visible(self) -> bool:
        return False

    @override
    @widget_method()
    def as_page_title(self, *, variant: str = ValueVariants.PAGE) -> str:
        return SafeString()


class SitePlace(PlaceWithoutPageTitle):
    """Place that is not related to a resource."""


class ResourcePlaceKwargs[R: Model](PlaceKwargs):
    """Argument typing for ResourcePlace constructor."""

    #: Resource instance
    ui: "UI[R]"


class ResourcePlace[R: Model](Place, abc.ABC):
    """Place showing details about a resource."""

    def __init__(
        self,
        *,
        ui: "UI[R]",
        **kwargs: Unpack[PlaceKwargs],
    ) -> None:
        """Store Place attributes."""
        kwargs.setdefault("current", kwargs["url"] == ui.request.path)
        super().__init__(**kwargs)
        #: Resource instance
        self.ui = ui
        self.title.variants.setdefault(
            ValueVariants.TYPE, self.ui.instance.__class__.__name__
        )


class ResourceMainPlace[R: Model](ResourcePlace[R]):
    """Main place for showing a resource."""


class ResourceSubPlace[R: Model](ResourcePlace[R], PlaceWithoutPageTitle):
    """Place for working with a resource."""


class ResourceSettingsPlace[R: Model](ResourcePlace[R]):
    """View that is part of the settings menu for a resource."""
