import enum
import logging
from typing import List, Optional
from urllib.parse import urlencode

from django import forms
from django.conf import settings
from django.core.paginator import Paginator
from django.db.models import F, Prefetch
from django.db.models.query import QuerySet
from django.template.loader import render_to_string

from promptgame.gameui.cooldown import accounts_with_cooldown_info
from promptgame.gameui.models import Account, AuthAttempt

log = logging.getLogger(__name__)


class ViewAs(str, enum.Enum):
    VIEW_AS_ATTACKER = "attacker"
    VIEW_AS_DEFENDER = "defender"
    VIEW_AS_SELF = "self"


class AttackListForm(forms.Form):
    attacker = forms.ModelChoiceField(
        queryset=Account.objects.all(), required=False, to_field_name="account_id"
    )
    defender = forms.ModelChoiceField(
        queryset=Account.objects.all(), required=False, to_field_name="account_id"
    )
    page = forms.IntegerField(min_value=1, initial=1, required=False)

    def __init__(self, *args, current_account: Account, **kwargs):
        super().__init__(*args, **kwargs)
        self.current_account = current_account

    # validate that at least one of attacker_id and defender_id is set
    def clean(self):
        cleaned_data = super().clean()
        attacker: Optional[Account] = cleaned_data.get("attacker")
        defender: Optional[Account] = cleaned_data.get("defender")
        if not attacker and not defender:
            raise forms.ValidationError(
                "At least one of attacker or defender must be specified"
            )
        if attacker != self.current_account and defender != self.current_account:
            raise forms.ValidationError(
                "The attacker or defender (or both) must be equal to the current user"
            )
        return cleaned_data

    def urlencode(self, page: Optional[int] = None):
        """URL-encode the form data, optionally changing the page number."""
        data = self.cleaned_data.copy()
        if page is not None:
            data["page"] = page
        return urlencode(
            {
                k: v.account_id if isinstance(v, Account) else v
                for k, v in data.items()
                if v is not None
            }
        )


def preloaded_attack_queryset(
    *, my_account: Account, attacker: Optional[Account], defender: Optional[Account]
) -> QuerySet[AuthAttempt]:
    """Return a preloaded queryset of attacks."""
    assert (
        attacker is not None or defender is not None
    ), "Either attacker or defender must be set"

    # Annotate AuthAttempts with all the metadata that we need
    with_cooldown = accounts_with_cooldown_info(my_account)
    preloaded_auth_attempts = (
        AuthAttempt.objects.all()
        .prefetch_related(Prefetch("defending_account", queryset=with_cooldown))
        .prefetch_related(Prefetch("attacking_account", queryset=with_cooldown))
        .order_by("-timestamp")
    )

    is_attacker = attacker == my_account
    is_defender = defender == my_account

    # Filter for right attacker
    if attacker is not None:
        if is_attacker:
            # my_account is the attacker, so we need to annotate the net
            # transfer amount.  We don't include attacking_net_transfer in the
            # net_transfer_amount because the attacker's transfer comes from
            # their shadow balance,n not the one shown to users.
            preloaded_auth_attempts = preloaded_auth_attempts.annotate(
                net_transfer_amount=F("defending_transfer_amount")
            )
        preloaded_auth_attempts = preloaded_auth_attempts.filter(
            attacking_account=attacker
        )

    # Filter for right defender
    if defender is not None:
        if is_defender:
            # More complex expression to annotate defending transfer amount when
            # my_account is the defender.
            preloaded_auth_attempts = preloaded_auth_attempts.annotate(
                net_transfer_amount=F("attacking_transfer_amount")
                - F("defending_transfer_amount")
            )
        preloaded_auth_attempts = preloaded_auth_attempts.filter(
            defending_account=defender
        )

    if not is_attacker:
        # if we're not explicitly listed as attacker (e.g. in "your attacks on
        # others", "attacks on yourself")
        preloaded_auth_attempts = preloaded_auth_attempts.exclude(
            attacking_account=my_account
        )
    if not is_defender:
        # if we're not explicity listed as defender (e.g. in "attacks on you",
        # "self-attacks"), then we want to hide anything where we're defending
        preloaded_auth_attempts = preloaded_auth_attempts.exclude(
            defending_account=my_account
        )

    return preloaded_auth_attempts


def generate_page_numbers(*, n_pages: int, current_page: int) -> List[Optional[int]]:
    """Returns a list of the form [1, None, i, i+1, ..., j, None, n_pages]. The aim is to satisfy these invariants:

    - The current page is always in the list.
    - The list is always of length 2 * target_pages_to_each_side + 1, unless there are fewer than that many pages.
    - The list is always symmetric around the current page, unless the current
      page is too close to the left or right of the list."""
    target_pages_to_each_side = 3
    max_list_length = 2 * target_pages_to_each_side + 1

    if current_page > n_pages or current_page < 1:
        return [1]

    if n_pages <= max_list_length:
        # no ellipsis needed
        return list(range(1, n_pages + 1))

    pages: List[Optional[int]] = []

    if current_page <= target_pages_to_each_side + 1:
        # Ellipsis only on the right side
        # (pages are [1, 2, ..., max_list_length - 2, None, n_pages])
        pages = [*range(1, max_list_length - 2 + 1), None, n_pages]
    elif current_page >= n_pages - target_pages_to_each_side:
        # Ellipsis only on the left side
        # (pages are [1, None, n_pages - max_list_length + 3, ..., n_pages])
        pages = [1, None, *range(n_pages - max_list_length + 3, n_pages + 1)]
    else:
        # Ellipsis on both sides
        pages = [
            1,
            None,
            *range(
                current_page - (target_pages_to_each_side - 2),
                current_page + (target_pages_to_each_side - 2) + 1,
            ),
            None,
            n_pages,
        ]

    return pages


def render_paginated_table(
    *,
    my_account: Account,
    attack_form: AttackListForm,
) -> str:
    """Render a paginated table of attacks."""
    assert attack_form.is_valid()  # this should be done by caller

    attacker = attack_form.cleaned_data.get("attacker")
    defender = attack_form.cleaned_data.get("defender")
    page = attack_form.cleaned_data.get("page")
    if page is None:
        page = 1

    qs = preloaded_attack_queryset(
        my_account=my_account, attacker=attacker, defender=defender
    )

    paginator = Paginator(qs, settings.ATTACKS_PER_PAGE)
    django_page = paginator.get_page(page)

    total_pages = max(1, paginator.num_pages)
    page = max(1, min(page, total_pages))
    page_numbers = generate_page_numbers(n_pages=total_pages, current_page=page)

    view_as = None
    if attacker == my_account and defender == my_account:
        view_as = ViewAs.VIEW_AS_SELF
    elif attacker == my_account:
        view_as = ViewAs.VIEW_AS_ATTACKER
    elif defender == my_account:
        view_as = ViewAs.VIEW_AS_DEFENDER
    assert view_as is not None, (
        f"Could not find view_as value for ({my_account=}, {attacker=}, "
        f"{defender=})"
    )

    context = {
        "my_account": my_account,
        "table_form": attack_form,
        "page": django_page,
        "view_as": view_as,
        "page_numbers": page_numbers,
    }

    return render_to_string("auth_attempt_table_paginated.html", context)
