# 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.

"""debusine auth views."""

from collections import defaultdict
from functools import cached_property
from typing import Any, cast

from django.contrib.auth import views as auth_views
from django.db.models import QuerySet
from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404
from django.template.response import TemplateResponse
from django.urls import reverse

from debusine.db.context import context
from debusine.db.models import Group, Scope, Token, User
from debusine.db.models.auth import Identity, UserQuerySet
from debusine.server.signon.providers import Provider
from debusine.server.signon.signon import Signon
from debusine.server.signon.views import SignonLogoutMixin
from debusine.web.forms import TokenForm
from debusine.web.views.base import (
    BaseUIView,
    CreateViewBase,
    DeleteViewBase,
    DetailViewBase,
    FormMixinBase,
    ListViewBase,
    UpdateViewBase,
)


class LoginView(BaseUIView, auth_views.LoginView):
    """Class for the login view."""

    template_name = "account/login.html"

    def get_title(self) -> str:
        """Return the page title."""
        return "Log in"

    def get(
        self, request: HttpRequest, *args: Any, **kwargs: Any
    ) -> HttpResponse:
        """Store the ?next argument in the session."""
        request.session["sso_callback_next_url"] = self.get_success_url()
        return super().get(request, *args, **kwargs)

    def get_context_data(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
        """Tell the base template we are a login view."""
        ctx = super().get_context_data(**kwargs)
        ctx["is_login_view"] = True
        return ctx


class LogoutView(
    auth_views.LogoutView,
    SignonLogoutMixin,
    BaseUIView,
):
    """Class for the logout view."""

    template_name = "account/logged_out.html"

    def get_title(self) -> str:
        """Return the page title."""
        return "Logged out"

    def get_context_data(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
        """Tell the base template we are a logout view."""
        ctx = super().get_context_data(**kwargs)
        ctx["is_logout_view"] = True
        return ctx


class UserDetailView(BaseUIView, DetailViewBase[User]):
    """Show user details."""

    model = User
    template_name = "web/user-detail.html"
    # We need something that is not "user" not to confuse it with the
    # current user
    context_object_name = "person"

    def get_queryset(self) -> QuerySet[User]:
        """All workspaces for authenticated users or only public ones."""
        queryset = cast(UserQuerySet[Any], super().get_queryset())
        return queryset.can_display(context.user)

    def get_object(
        self, queryset: QuerySet[User] | None = None  # noqa: U100
    ) -> User:
        """Return the collection object to show."""
        if queryset is None:
            queryset = self.get_queryset()  # pragma: no cover
        return get_object_or_404(queryset, username=self.kwargs["username"])

    def get_context_data(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
        """Add user information to context data."""
        ctx = super().get_context_data(**kwargs)

        # Groups by scope
        by_group: dict[Scope, list[Group]] = defaultdict(list)
        for group in self.object.debusine_groups.all():
            by_group[group.scope].append(group)
        ctx["groups_by_scope"] = sorted(
            (
                (scope, sorted(groups, key=lambda g: g.name))
                for scope, groups in by_group.items()
            ),
            key=lambda s: s[0].name,
        )

        if self.object.can_manage(context.user):
            # SSO identities
            signon: Signon | None = getattr(self.request, "signon", None)
            if signon is not None:
                identities: list[tuple[Provider, Identity]] = []
                for identity in self.object.identities.all():
                    if (
                        provider := signon.get_provider_for_identity(identity)
                    ) is None:
                        continue
                    identities.append((provider, identity))
                ctx["identities"] = identities

        return ctx


class TokenView(BaseUIView):
    """Base view for token management."""

    @cached_property
    def user(self) -> User:
        """Return the user for which we manage tokens."""
        user = get_object_or_404(
            User.objects.can_display(context.user),
            username=self.kwargs["username"],
        )
        self.enforce(user.can_manage)
        return user

    def get_success_url(self) -> str:
        """Redirect to the view to see the created artifact."""
        return reverse(
            "user:token-list", kwargs={"username": self.user.username}
        )

    def get_context_data(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
        """Add the currently managed user to the template context."""
        ctx = super().get_context_data(**kwargs)
        ctx["person"] = self.user
        return ctx


class UserTokenListView(TokenView, ListViewBase[Token]):
    """List tokens for the user."""

    model = Token
    template_name = "web/user_token-list.html"
    context_object_name = "token_list"
    ordering = "name"

    def get_context_data(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
        """Add bits needed by the template."""
        ctx = super().get_context_data(**kwargs)
        ctx["only_one_scope"] = len(ctx["scopes"]) == 1
        return ctx

    def get_queryset(self) -> QuerySet[Token]:
        """All tokens for the authenticated user."""
        return Token.objects.filter(user=self.user).order_by('created_at')


class TokenFormView(TokenView, FormMixinBase[TokenForm]):
    """Mixin for views using TokenForm."""

    form_class = TokenForm
    action: str

    def get_title(self) -> str:
        """Get the default title for the view."""
        return f"{self.action} token"

    def get_context_data(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
        """Add action name to context data."""
        ctx = super().get_context_data(**kwargs)
        ctx["action"] = self.action
        return ctx

    def get_form_kwargs(self) -> dict[str, Any]:
        """Extend the default kwarg arguments: add "user"."""
        kwargs = super().get_form_kwargs()
        kwargs["user"] = self.user
        return kwargs


class UserTokenCreateView(TokenFormView, CreateViewBase[Token, TokenForm]):
    """Form view for creating tokens."""

    template_name = "web/user_token-form.html"
    action = "Create"

    def form_valid(self, form: TokenForm) -> HttpResponse:
        """Validate form and return token created page."""
        form.instance.user = self.user
        token = form.save()

        return TemplateResponse(
            self.request,
            "web/user_token-created.html",
            {"token": token, "person": self.user},
        )


class UserTokenUpdateView(TokenFormView, UpdateViewBase[Token, TokenForm]):
    """Form view for creating tokens."""

    model = Token
    template_name = "web/user_token-form.html"
    action = "Edit"

    def get_queryset(self) -> QuerySet[Token]:
        """Only include tokens for the current user."""
        return super().get_queryset().filter(user=self.user)


class UserTokenDeleteView(TokenView, DeleteViewBase[Token, TokenForm]):
    """View for deleting tokens."""

    object: Token  # https://github.com/typeddjango/django-stubs/issues/1227
    model = Token
    template_name = "web/user_token-confirm_delete.html"
    title = "Delete token"

    def get_queryset(self) -> QuerySet[Token]:
        """Only include tokens for the current user."""
        return super().get_queryset().filter(user=self.user)
