import json
import textwrap
from io import BytesIO
from typing import final

from django import forms
from django.contrib.auth.decorators import login_required
from django.core.handlers.wsgi import WSGIRequest
from django.http import FileResponse, HttpResponse
from django.shortcuts import render
from django.views.decorators.http import require_GET
from typing_extensions import override

from vuln_demo.apps.vulnerabilities.levels.abc import LevelABC
from vuln_demo.apps.vulnerabilities.models import CompletedLevel
from vuln_demo.util.auth import cast_user
from vuln_demo.util.pw_entries import ADMIN_EMAIL, get_cached_password_entries, reset_password_entries_cache


class PasswordForm(forms.Form):
    """Form class passed to level 4 view."""

    email = forms.EmailField(label="Email", initial=ADMIN_EMAIL)
    password = forms.CharField(label="Password", widget=forms.PasswordInput(attrs={"autofocus": "autofocus"}))


@login_required
@require_GET
def db_leak(request: WSGIRequest) -> FileResponse:
    """
    Endpoint to the database leak with exposed hashes.

    This will return a response with a JSON file, which will get downloaded upon visiting this endpoint.

    Each user will be assigned a random admin password from a list of commonly used leaked passwords.
    """
    assert cast_user(request.user, cast=True)
    database, _ = get_cached_password_entries(request.user, "level4", store_hashes=True)

    json_string = json.dumps(database)
    data = BytesIO(json_string.encode("utf-8"))
    data.seek(0)  # rewind the buffer to the beginning
    return FileResponse(data, filename="leak.json", as_attachment=True, content_type="application/json")


@final
class Level4(LevelABC):
    """Scenario where an unsalted password hash of a common password is known."""

    HINT_AMOUNT = 5

    @override
    @classmethod
    def _get_hint(cls, request: WSGIRequest, hint_num: int) -> str:
        if hint_num == 1:
            return textwrap.dedent(
                """
                <p>
                    A hash is like a secret code that turns any text (like your password) into a scrambled mix of
                    letters and numbers. If you hash the same text twice, you get the exact same scrambled mix every
                    time. However, you can't easily figure out the original text from the scrambled mix. It's a one-way
                    process, meant to keep information like passwords safe.
                </p>
                <p>
                    One example of a (bad) hash function would be for example multiplying the alphabetical position of
                    each letter in your password. So, a word like <code>"CAT"</code> would be <code>C=3,A=1,T=20</code>
                    so <code>3*1*20=60</code>. As you can imagine, while it's easy to compute this hash if you know the
                    password, it's much more problematic to go back from the result of 60 to the original password of
                    "CAT".
                </p>
                <p>
                    What's interesting that it's actually possible to create a different input, like <code>"ACT"</code>
                    which would produce the same hash (<code>1*3*20=60</code>) as <code>"CAT"</code>. Indeed, this is
                    the case with real cryptographic hash functions too.
                </p
                <p>
                    However with cryptographic hashes, it's also very hard to even create another input that would give
                    you the same resulting hash. Not only that, editing just a single letter of the input to a proper
                    hash function will result in a widly different output.
                </p>
                """
            )
        if hint_num == 2:
            return textwrap.dedent(
                """
                <p>
                    CyberVault had great confidence with this mathematical impossibility to go back to the original
                    value, and so they weren't worried about this leak. However, in their certainty, they overlooked
                    a crucial detail: <strong class="story-emphasis">Salts</strong>
                </p>
                <p>
                    A salt is the sprinkle that make each hash unique. It works simply by creating some random text,
                    which will be stored in the user record, and hashing the combination of these two. We know that
                    for the same password, a hash will produce the same output. Salts are a way to avoid that.
                </p>
                <p>
                    Yet we only found the password hash next to the user email in the leak, there wasn't any mentions
                    of a salt. Can you use this to your advantage?
                </p>
                """
            )
        if hint_num == 3:
            return textwrap.dedent("""
                <p>
                    Notice that all of the hashes in the leaked file seem to be 64 character long hexadecimal strings.
                    There are only a few hashing algorithms that produce hashes like this. Can you figure out which one
                    was used for these?
                </p>
            """)
        if hint_num == 4:
            return textwrap.dedent(
                """
                <p>
                    People often use some very common passwords. These passwords were seen hundreds of times, and
                    hackers took notice. Knowing this, they managed to create tables which contain the hashes for
                    most of these commonly used passwords. That way, when there is a database leak, even though it
                    only contains hashes of the original passwords, if these hashes lack salts, they can simply
                    reference such a table, and check for a match. More often than not, there will be one.
                </p>
                """
            )
        if hint_num == 5:
            return textwrap.dedent(
                """
                <p>
                  The length and format of the leaked hashes suggests that the hash was likely produced by a <strong
                  class="story-emphasis">SHA-256</strong> hash algorithm. This algorithm on it's own is quite secure
                  and it's widely used in a bunch of modern security protocols and applications, like TLS and SSL, PGP,
                  SSH, and IPsec.
                </p>
                <p>
                    These tables are called raindbow tables, and you can find them on the internet.
                    One such online tool is: <a href="https://crackstation.net/">https://crackstation.net/</a>.
                </p>
                <p>
                    Alternatively, you can use a tool like RainbowCrack and download some pre-computed rainbow tables
                    for the SHA256 algorithm, and run them on the hash you have yourself. You can even just download
                    a list of commonly used passwords, and compute the rainbow table yourself, although this might
                    take a while.
                </p>
                """
            )
        raise

    @override
    def get(self, request: WSGIRequest) -> HttpResponse:
        assert cast_user(request.user, cast=True)
        already_completed = CompletedLevel.check_completed(request.user, 4)

        form = PasswordForm()
        return render(
            request,
            "vulnerabilities/levels/level4.html",
            {"form": form, "completed": already_completed},
        )

    def post(self, request: WSGIRequest) -> HttpResponse:
        """Handle password form submission."""
        assert cast_user(request.user, cast=True)
        already_completed = CompletedLevel.check_completed(request.user, 4)

        form = PasswordForm(request.POST)
        if not form.is_valid():
            return render(
                request,
                "vulnerabilities/levels/level4.html",
                {"form": form, "completed": already_completed},
            )

        email = form.cleaned_data["email"]
        if email != ADMIN_EMAIL:
            form.add_error("email", "You're only interested in getting into the admin account.")
            return render(
                request,
                "vulnerabilities/levels/level4.html",
                {
                    "form": form,
                    "completed": already_completed,
                    "note_msg": "You're only interested in getting into the admin account.",
                },
            )

        pw = form.cleaned_data["password"]
        _, admin_password = get_cached_password_entries(request.user, "level4", store_hashes=True)
        if pw != admin_password:
            form.add_error("password", "Incorrect password")
            return render(
                request,
                "vulnerabilities/levels/level4.html",
                {"form": form, "completed": already_completed},
            )

        reset_password_entries_cache(request.user, "level4")
        return self.completed_redirect(request, 4, skip_db=already_completed)
