import os
import http
import logging

import django.http
import django.template.response
import django.template.loader
import django.forms
import django.views
import django.contrib.auth
import django.utils
import django.shortcuts

# Decorator shortcuts
from django.utils.decorators import method_decorator
from django.views.decorators.http import require_GET
from django.contrib.auth.decorators import login_required, user_passes_test

import mini_buildd.api

LOG = logging.getLogger(__name__)

# We will use custom class variables on class based views instead of extra context:
#  The view object is always automatically available in context, and this is way more
#  simple, straightforward and works well with subclassing. Also, kwargs from urls.py
#  will then be automatically available in context.
# For a django view class, this needs to be set in setup(), not __init__():
# pylint: disable=attribute-defined-outside-init


def context(_request):
    """Generate generic context. Will be available in any render, see ``django_settings.py``."""
    return {
        "mbd": {
            "version": mini_buildd.__version__,
            "mdls": mini_buildd.mdls,
            "config": mini_buildd.config,
            "api": mini_buildd.api,
            "daemon": mini_buildd.get_daemon(),
        }}


class ExceptionMiddleware():
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        return self.get_response(request)

    MBD_TEMPLATES = {
        "snippet": "mini_buildd/includes/error.html",
        "page": "mini_buildd/error/index.html",
    }

    @classmethod
    def error(cls, request, exception, output="page", status=http.HTTPStatus.INTERNAL_SERVER_ERROR):
        http_exception = mini_buildd.e2http(exception, status)
        if http_exception.status != http.HTTPStatus.OK:
            mini_buildd.log_exception(LOG, "Middleware exception", exception)
        return django.template.response.TemplateResponse(request,
                                                         cls.MBD_TEMPLATES[output],
                                                         {"rfc7807": http_exception.rfc7807},
                                                         status=http_exception.status)

    @classmethod
    def process_exception(cls, request, exception):
        return cls.error(request, exception)

    # Despite having a custom middleware to handle exceptions, the following django default functions still need to be overwritten (see also: urls.py).
    @classmethod
    def bad_request(cls, request, exception):
        return cls.error(request, exception, http.HTTPStatus.BAD_REQUEST)

    @classmethod
    def permission_denied(cls, request, exception):
        return cls.error(request, exception, http.HTTPStatus.UNAUTHORIZED)

    @classmethod
    def page_not_found(cls, request, exception):
        return cls.error(request, exception, http.HTTPStatus.NOT_FOUND)

    @classmethod
    def server_error(cls, request):
        return cls.error(request, None)


class TemplateView(django.views.generic.base.TemplateView):
    """Original django class plus optional custom helpers."""

    def mbd_get_range(self):
        try:
            before_arg = self.request.GET.get("before", mini_buildd.misc.Datetime.now().isoformat())  # 'YYYY-MM-DDTHH:MM:SS' (isoformatted string). That's what we get from HTML 'datetime-local' input with no TZ meta (naive)
            before = mini_buildd.misc.Datetime.from_iso(before_arg)                                   # datetime object for internal use. Treat as UTC
            return before, int(self.request.GET.get("limit", "100"))
        except Exception as e:
            raise mini_buildd.HTTPBadRequest("Invalid range parameters 'before', or 'limit'") from e


class DefaultView(TemplateView):
    """Classic view from template; computes template name from request path (``/foo/`` -> ``foo/index.html``, ``/foo/bar.html`` -> ``foo/bar.html``)."""

    def get_template_names(self):
        template = self.request.path.lstrip("/")
        return os.path.join(template, "index.html") if template.endswith("/") else template


class HomeView(DefaultView):
    def setup(self, request, *args, **kwargs):
        super().setup(request, *args, **kwargs)
        self.mbd_events_view_max = mini_buildd.get_daemon().model.show_last_builds
        self.mbd_events, self.mbd_events_total, self.mbd_events_max, self.mbd_events_ongoing = mini_buildd.get_daemon().events.mbd_snapshot(maxlen=self.mbd_events_view_max)


class AccountRegisterView(django.views.generic.edit.FormView):
    class TokenGenerator(django.contrib.auth.tokens.PasswordResetTokenGenerator):
        def _make_hash_value(self, user, timestamp):
            return f"{user.pk}{timestamp}{user.is_active}"

    TOKEN_GENERATOR = TokenGenerator()

    class Form(django.contrib.auth.forms.UserCreationForm):
        email = django.forms.EmailField(help_text="Required. User's email address - activation link will be sent there.")

        class Meta:
            model = django.contrib.auth.models.User
            fields = ["username", "email", "password1", "password2"]

    template_name = "accounts/register/index.html"
    form_class = Form

    def form_valid(self, form):
        user = form.save(commit=False)
        user.is_active = False
        user.save()

        url = mini_buildd.http_endpoint()
        token = self.TOKEN_GENERATOR.make_token(user)
        user.email_user(f"User registration for {url}",
                        ("Dear user,\n\n"
                         f"a registration for user '{user}' has been requested to this email address.\n\n"
                         f"Use this activation link to complete the registration:\n\n"
                         f" {url}/accounts/activate/{django.utils.http.urlsafe_base64_encode(django.utils.encoding.force_bytes(user.pk))}/{token}\n\n"
                         f"Your mini-buildd at {url}\n"))
        raise mini_buildd.HTTPOk(f"Activation email sent to: {user.email}")


class AccountActivateView(DefaultView):
    def get(self, request, *args, **kwargs):
        uid = django.utils.encoding.force_str(django.utils.http.urlsafe_base64_decode(kwargs["uidb64"]))
        user = django.contrib.auth.models.User.objects.get(pk=uid)
        if not AccountRegisterView.TOKEN_GENERATOR.check_token(user, kwargs["token"]):
            raise mini_buildd.HTTPBadRequest("Invalid Activation Token")
        user.is_active = True
        user.save()
        raise mini_buildd.HTTPOk(f"User '{user}' is now activated.")


@method_decorator(login_required, "dispatch")
class AccountProfileView(DefaultView):
    def setup(self, request, *args, **kwargs):
        super().setup(request, *args, **kwargs)
        if self.request.user.is_authenticated:  # fixes error 500 when called logged out: even with login_required, setup() will still be called.
            self.mbd_subscriptions = mini_buildd.mdls().subscription.Subscription.objects.filter(subscriber=self.request.user)


@method_decorator(user_passes_test(lambda u: u.is_superuser), "dispatch")
class SetupView(DefaultView):
    pass


class BuildersView(DefaultView):
    def setup(self, request, *args, **kwargs):
        super().setup(request, *args, **kwargs)
        self.mbd_builders = mini_buildd.mdls().gnupg.Builders
        self.mbd_chroots = mini_buildd.mdls().chroot.Chroot.objects.all


class RouteView(TemplateView):
    def setup(self, request, *args, **kwargs):
        super().setup(request, *args, **kwargs)
        self.mbd_route = mini_buildd.config.ROUTES[kwargs["route"]]
        self.mbd_path = kwargs.get("path")
        self.mbd_fullpath = self.mbd_route.path.join(self.mbd_path)
        self.mbd_isfile = os.path.isfile(self.mbd_fullpath)
        self.mbd_static_uri = self.mbd_route.static_uri(self.mbd_path)

        if not os.path.exists(self.mbd_fullpath):
            raise mini_buildd.HTTPNotFound(f"No such path: {self.mbd_path}")


class DirView(RouteView):
    template_name = "mini_buildd/dir.html"

    def setup(self, request, *args, **kwargs):
        super().setup(request, *args, **kwargs)
        self.mbd_dirview = True
        if not self.mbd_isfile:
            self.mbd_scandir = os.scandir(self.mbd_fullpath)


@method_decorator(user_passes_test(lambda u: u.is_superuser), "dispatch")
class LogView(DirView):
    pass


class EventsView(RouteView):
    template_name = "mini_buildd/events/index.html"

    def setup(self, request, *args, **kwargs):
        super().setup(request, *args, **kwargs)
        self.mbd_before, self.mbd_limit = self.mbd_get_range()
        events = mini_buildd.events.load(self.mbd_path, before=self.mbd_before)
        self.mbd_events_total = len(events)
        self.mbd_events = events[0:self.mbd_limit]


class BuildsView(RouteView):
    template_name = "mini_buildd/builds/index.html"

    def setup(self, request, *args, **kwargs):
        super().setup(request, *args, **kwargs)
        self.mbd_before, self.mbd_limit = self.mbd_get_range()
        buildlogs = sorted(self.mbd_route.path.ifiles(self.mbd_path, ".buildlog", before=self.mbd_before), key=lambda e: e.stat().st_mtime, reverse=True)
        self.mbd_buildlogs_total = len(buildlogs)
        self.mbd_buildlogs = {mini_buildd.misc.Datetime.from_stamp(entry.stat().st_mtime): self.mbd_route.path.removeprefix(entry.path) for entry in buildlogs[0:self.mbd_limit]}


class RepositoriesView(RouteView):
    template_name = "mini_buildd/repositories/index.html"

    def setup(self, request, *args, **kwargs):
        super().setup(request, *args, **kwargs)
        self.mbd_repositories = mini_buildd.mdls().repository.Repository.objects.all


class APIView(TemplateView):
    class Snippets(dict):
        """Pre-computed dict of available specialized call templates (just add to or remove files from ``includes/api/``)."""

        def __init__(self):
            super().__init__()
            for call in mini_buildd.api.CALLS.keys():
                path = f"mini_buildd/includes/api/{call}.html"
                try:
                    django.template.loader.get_template(path)
                    self[call] = path
                    LOG.info(f"API call template found: {path}")
                except django.template.TemplateDoesNotExist:
                    pass

    MBD_API_SNIPPETS = Snippets()

    def setup(self, request, *args, **kwargs):
        super().setup(request, *args, **kwargs)
        self.mbd_error = None
        self.mbd_output = request.GET.get("output")
        try:
            # Get API call object from class from route
            self.mbd_call = kwargs["call"].from_sloppy_args(**dict(request.GET.items()))
            self.mbd_call.set_request(request)

            # Check authorization
            if not self.mbd_call.AUTH.is_authorized(request.user):
                raise mini_buildd.HTTPUnauthorized(f"API call needs authorization: {self.mbd_call.AUTH}")

            # Check if we need a running daemon
            if self.mbd_call.NEEDS_RUNNING_DAEMON and not mini_buildd.get_daemon().is_alive():
                raise mini_buildd.HTTPUnavailable(f"API call needs running daemon: {self.mbd_call.name()}")

            # Check confirmable calls
            if self.mbd_call.CONFIRM and request.GET.get("confirm", None) != self.mbd_call.name():
                raise mini_buildd.HTTPUnauthorized(f"API call needs to be confirmed: {self.mbd_call.name()}")

            LOG.info(f"API call '{self.mbd_call.name()}' by user '{request.user}'")
            self.mbd_call.run()

            self.mbd_api_inc = self.MBD_API_SNIPPETS.get(self.mbd_call.name(), "mini_buildd/includes/api/default.html")
            self.template_name = {
                "snippet": self.mbd_api_inc,
                "page": "mini_buildd/api/page.html",
            }.get(self.mbd_output)

        except Exception as e:
            self.mbd_error = e

    @method_decorator(require_GET)
    def get(self, request, *args, **kwargs):
        # Error handling
        if self.mbd_error is not None:
            if self.mbd_output in ["page", "snippet"]:
                return ExceptionMiddleware.error(request, self.mbd_error, output=self.mbd_output)

            mini_buildd.log_exception(LOG, "API call failed", self.mbd_error)
            http_exception = mini_buildd.e2http(self.mbd_error)
            return django.http.JsonResponse(http_exception.rfc7807.to_json(),
                                            status=http_exception.status,
                                            content_type="application/problem+json")

        # API call output
        if self.template_name is not None:
            return super().get(request, *args, **kwargs)
        return django.http.JsonResponse(self.mbd_call.result)
