import os
import http
import pathlib
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

from mini_buildd import config, util, api, events, models

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": {
            "VERSIONS": util.VERSIONS,
            "models": util.models,
            "config": config,
            "api": api,
            "daemon": util.daemon(),
        }}


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

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

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

    @classmethod
    def error(cls, request, exception, output="html", status=http.HTTPStatus.INTERNAL_SERVER_ERROR):
        http_exception = util.e2http(exception, status)
        if http_exception.status != http.HTTPStatus.OK:
            models.mbd_msg_exception(request, exception, "Middleware 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, status=http.HTTPStatus.BAD_REQUEST)

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

    @classmethod
    def page_not_found(cls, request, exception):
        return cls.error(request, exception, status=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", util.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 = util.Datetime.from_iso(before_arg)                                   # datetime object for internal use. Treat as UTC
            return before, int(self.request.GET.get("limit", "40"))
        except Exception as e:
            raise util.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_ongoing = util.daemon().events.ongoing.values()


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 = util.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 util.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 util.HTTPBadRequest("Invalid Activation Token")
        user.is_active = True
        user.save()
        raise util.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 = util.models().Subscription.objects.filter(subscriber=self.request.user)


@method_decorator(login_required, "dispatch")
class AccountNullView(django.views.generic.base.TemplateView):
    """For python client login support only. See :py:meth:`net.ClientEndpoint.login`"""

    def get(self, request, *args, **kwargs):
        return django.http.HttpResponse("<!DOCTYPE html>")


class BuildersView(DefaultView):
    def setup(self, request, *args, **kwargs):
        super().setup(request, *args, **kwargs)
        self.mbd_check = self.request.user.is_authenticated and self.request.user.is_active and self.request.user.is_staff
        self.mbd_builders = util.models().Builders(check=self.mbd_check)


class RouteView(TemplateView):
    def setup(self, request, *args, **kwargs):
        super().setup(request, *args, **kwargs)
        self.mbd_route = config.ROUTES[kwargs["route"]]
        # Be sure to strip leading "/" so consecutive joins won't interpret as new absolute start point (else, any system path may be delivered via an URI like ``foo//any/system/path``)
        self.mbd_path = kwargs.get("path").lstrip("/")
        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 util.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)


class EmbedView(TemplateView):
    template_name = "mini_buildd/embed.html"


class ManualView(EmbedView):
    def setup(self, request, *args, **kwargs):
        super().setup(request, *args, **kwargs)
        self.mbd_title = "Manual"
        self.mbd_uri = config.URIS["manual"]["static"]


@method_decorator(user_passes_test(lambda u: u.is_superuser), "dispatch")
class SetupView(EmbedView):
    def setup(self, request, *args, **kwargs):
        super().setup(request, *args, **kwargs)
        self.mbd_title = "Setup"
        self.mbd_uri = config.URIS["admin"]["view"]


@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 = events.load(self.mbd_path, before=self.mbd_before)
        self.mbd_events_total = len(_events)
        self.mbd_events = _events[0:self.mbd_limit]


class RepositoriesView(RouteView):
    @classmethod
    def mbd_parse_distribution(cls, repo, dist, suite):
        name = repo.mbd_get_diststr(dist, suite)
        return {"name": name,
                "uri": config.ROUTES["repositories"].uris["static"].join(repo.identity, "dists", name + "/"),
                "mandatory_version_regex": repo.mbd_get_mandatory_version_regex(dist, suite),
                "snapshots": repo.mbd_reprepro.get_snapshots(name),
                "apt_build_sources_list": util.rrpes(lambda dist, suite: repo.mbd_get_apt_build_sources_list(dist, suite).get(with_comment=True), dist, suite),
                "apt_build_preferences": repo.mbd_get_apt_build_preferences(dist, suite)}

    def setup(self, request, *args, **kwargs):
        super().setup(request, *args, **kwargs)
        try:
            self.mbd_diststr = pathlib.Path(request.path).parts[5]  # /mini_buildd/repositories/test/dists/<codename>-<repoid>-<suite>[/...]
            self.mbd_repository, self.mbd_distribution, self.mbd_suite = models.parse_diststr(self.mbd_diststr)

            self.mbd_xtra = {}
            for s in self.mbd_repository.layout.suiteoption_set.all():
                self.mbd_xtra[s.suite.name] = self.mbd_parse_distribution(self.mbd_repository, self.mbd_distribution, s)
            self.template_name = "mini_buildd/repositories/distribution.html"
        except Exception as e:
            LOG.debug(f"No distribution found, showing all repositories: {e}")
            self.mbd_repositories = util.models().Repository.objects.all
            self.template_name = "mini_buildd/repositories/index.html"


def get_client_ip(request):
    xff = request.META.get("HTTP_X_FORWARDED_FOR")
    return xff.split(",")[0] if xff else request.META.get("REMOTE_ADDR")


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 name in api.CALLS:
                path = f"mini_buildd/includes/api/{name}.html"
                try:
                    django.template.loader.get_template(path)
                    self[name] = path
                except django.template.TemplateDoesNotExist:
                    self[name] = "mini_buildd/includes/api/default.html"
                LOG.debug(f"API call '{name}' uses HTML snippet: {self[name]}")

    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 util.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 util.daemon().is_alive():
                raise util.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 util.HTTPBadRequest(f"API call needs to be confirmed: {self.mbd_call.name()}")

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

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

        except Exception as e:
            util.log_exception(LOG, "API call failed", 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 ["html", "html-snippet"]:
                return ExceptionMiddleware.error(request, self.mbd_error, output=self.mbd_output)

            http_exception = util.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)
        if self.mbd_output == "plain":
            return django.http.HttpResponse(self.mbd_call.plain(), content_type="text/plain")
        return django.http.JsonResponse(self.mbd_call.result, safe=False)
