"""Slow down password guessing at the sign-in form.

Downloads have been rate-limited since the start, but the login form was not —
and it is the more valuable target: every owner account still carries the
password it was migrated with from WordPress, and those sites were not under
our control. An unthrottled form lets someone work through a leaked password
list against a few hundred known usernames at whatever speed the server allows.

Two limits, because they stop different attacks:
  * per account+address — someone hammering one owner's account;
  * per address — someone spraying one common password across many accounts,
    which never trips a per-account limit.

Failures are already recorded in LoginEvent, so this needs no new storage and
no cache server. The window slides, so a locked-out person is let back in
without anyone having to unlock them.
"""
from django.conf import settings
from django.utils import timezone

from .models import LoginEvent

WINDOW_MINUTES = getattr(settings, "LOGIN_THROTTLE_WINDOW_MINUTES", 15)
MAX_PER_ACCOUNT = getattr(settings, "LOGIN_THROTTLE_PER_ACCOUNT", 10)
MAX_PER_ADDRESS = getattr(settings, "LOGIN_THROTTLE_PER_ADDRESS", 30)


def client_ip(request):
    if request is None:
        return None
    forwarded = request.META.get("HTTP_X_FORWARDED_FOR", "")
    if forwarded:
        return forwarded.split(",")[0].strip()
    return request.META.get("REMOTE_ADDR")


def locked_out(identifier, ip):
    """Has this account, or this address, failed too often lately?"""
    since = timezone.now() - timezone.timedelta(minutes=WINDOW_MINUTES)
    failures = LoginEvent.objects.filter(succeeded=False, occurred_at__gte=since)

    if identifier:
        by_account = failures.filter(username__iexact=identifier)
        if ip:
            by_account = by_account.filter(ip=ip)
        if by_account.count() >= MAX_PER_ACCOUNT:
            return True

    # Password spraying: many accounts, one address, few failures each.
    if ip and failures.filter(ip=ip).count() >= MAX_PER_ADDRESS:
        return True
    return False
