from django.conf import settings
from django.http import FileResponse, Http404, HttpResponse
from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from django.urls import reverse
from django.utils.http import url_has_allowed_host_and_scheme
from django.views.decorators.http import etag

from wagtail import hooks
from wagtail.documents import get_document_model
from wagtail.documents.models import document_served
from wagtail.forms import PasswordViewRestrictionForm
from wagtail.models import CollectionViewRestriction
from wagtail.utils import sendfile_streaming_backend
from wagtail.utils.sendfile import sendfile


def document_etag(request, document_id, document_filename):
    Document = get_document_model()
    if hasattr(Document, "file_hash"):
        return (
            Document.objects.filter(id=document_id)
            .values_list("file_hash", flat=True)
            .first()
        )


@etag(document_etag)
def serve(request, document_id, document_filename):
    Document = get_document_model()
    doc = get_object_or_404(Document, id=document_id)

    # We want to ensure that the document filename provided in the URL matches the one associated with the considered
    # document_id. If not we can't be sure that the document the user wants to access is the one corresponding to the
    # <document_id, document_filename> pair.
    if doc.filename != document_filename:
        raise Http404("This document does not match the given filename.")

    for fn in hooks.get_hooks("before_serve_document"):
        result = fn(doc, request)
        if isinstance(result, HttpResponse):
            return result

    # Send document_served signal
    document_served.send(sender=Document, instance=doc, request=request)

    try:
        local_path = doc.file.path
    except NotImplementedError:
        local_path = None

    try:
        direct_url = doc.file.url
    except NotImplementedError:
        direct_url = None

    serve_method = getattr(settings, "WAGTAILDOCS_SERVE_METHOD", None)

    # If no serve method has been specified, select an appropriate default for the storage backend:
    # redirect for remote storages (i.e. ones that provide a url but not a local path) and
    # serve_view for all other cases
    if serve_method is None:
        if direct_url and not local_path:
            serve_method = "redirect"
        else:
            serve_method = "serve_view"

    if serve_method in ("redirect", "direct") and direct_url:
        # Serve the file by redirecting to the URL provided by the underlying storage;
        # this saves the cost of delivering the file via Python.
        # For serve_method == 'direct', this view should not normally be reached
        # (the document URL as used in links should point directly to the storage URL instead)
        # but we handle it as a redirect to provide sensible fallback /
        # backwards compatibility behaviour.
        return redirect(direct_url)

    if local_path:
        # Use wagtail.utils.sendfile to serve the file;
        # this provides support for mimetypes, if-modified-since and django-sendfile backends

        sendfile_opts = {
            "attachment": (doc.content_disposition != "inline"),
            "attachment_filename": doc.filename,
            "mimetype": doc.content_type,
        }
        if not hasattr(settings, "SENDFILE_BACKEND"):
            # Fallback to streaming backend if user hasn't specified SENDFILE_BACKEND
            sendfile_opts["backend"] = sendfile_streaming_backend.sendfile

        response = sendfile(request, local_path, **sendfile_opts)

    else:
        # We are using a storage backend which does not expose filesystem paths
        # (e.g. storages.backends.s3boto.S3BotoStorage) AND the developer has not allowed
        # redirecting to the file url directly.
        # Fall back on pre-sendfile behaviour of reading the file content and serving it
        # as a FileResponse
        response = FileResponse(doc.file, doc.content_type)

        # set filename and filename* to handle non-ascii characters in filename
        # see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition
        response["Content-Disposition"] = doc.content_disposition

        # FIXME: storage backends are not guaranteed to implement 'size'
        response["Content-Length"] = doc.file.size

    # Add a CSP header to prevent inline execution
    if getattr(settings, "WAGTAILDOCS_BLOCK_EMBEDDED_CONTENT", True):
        response["Content-Security-Policy"] = "default-src 'none'"

    # Prevent browsers from auto-detecting the content-type of a document
    response["X-Content-Type-Options"] = "nosniff"

    return response


def authenticate_with_password(request, restriction_id):
    """
    Handle a submission of PasswordViewRestrictionForm to grant view access over a
    subtree that is protected by a PageViewRestriction
    """
    restriction = get_object_or_404(CollectionViewRestriction, id=restriction_id)

    if request.method == "POST":
        form = PasswordViewRestrictionForm(request.POST, instance=restriction)
        if form.is_valid():
            return_url = form.cleaned_data["return_url"]

            if not url_has_allowed_host_and_scheme(
                return_url, request.get_host(), request.is_secure()
            ):
                return_url = settings.LOGIN_REDIRECT_URL

            restriction.mark_as_passed(request)
            return redirect(return_url)
    else:
        form = PasswordViewRestrictionForm(instance=restriction)

    action_url = reverse(
        "wagtaildocs_authenticate_with_password", args=[restriction.id]
    )

    password_required_template = getattr(
        settings,
        "WAGTAILDOCS_PASSWORD_REQUIRED_TEMPLATE",
        "wagtaildocs/password_required.html",
    )

    context = {"form": form, "action_url": action_url}
    return TemplateResponse(request, password_required_template, context)
