import html
import logging
import random
from typing import Any, Dict, Iterable, Optional
from urllib.parse import ParseResult, urlencode, urlparse, urlunparse

import humanize
from django import template
from django.conf import settings
from django.urls import reverse
from django.utils.safestring import SafeString, mark_safe
from django.utils.timezone import timedelta

from promptgame.gameui import tiers
from promptgame.gameui.attack import (
    DenialReason,
    PermissionDenied,
    PermissionGranted,
    StateEmoji,
    user_attack_permission,
)
from promptgame.gameui.attack_lists import (
    AttackListForm,
    ViewAs,
    render_paginated_table,
)
from promptgame.gameui.models import Account, AuthAttempt, Defense

log = logging.getLogger(__name__)

register = template.Library()


@register.simple_tag
def settings_value(name):
    return getattr(settings, name, "")


@register.simple_tag
def random_choice(options):
    return random.choice(options)


@register.simple_tag
def tier(balance, key=None):
    return tiers.tier(int(balance), key)


@register.simple_tag
def valid_tier(balance, proposed_balance):
    return tiers.valid_tier(balance, proposed_balance)


@register.filter(name="abs")
def abs_filter(value):
    # Most useful for converting negative numbers to positive when displaying.
    # Taken from: https://stackoverflow.com/a/26841129
    return abs(value)


@register.inclusion_tag("pretty_account_link.html")
def pretty_account_link(my_account: Account, target_account: Account) -> Dict[str, str]:
    """Pretty link to an account for the matchmaking and leaderboard table."""
    permission = user_attack_permission(attacker=my_account, defender=target_account)

    match my_account == target_account, permission:
        case True, _:
            # self-attack
            badge = StateEmoji.SELF_ATTACK
            title_text = "This is you! Click to test your defenses."
        case _, PermissionGranted():
            badge = StateEmoji.UNLOCKED
            title_text = "You can attack this account"
        case _, PermissionDenied(reason=reason, user_message=user_message):
            # This defines the precedence that different lock reasons take over
            # one another in the matchmaking table and leaderboard.
            if reason & DenialReason.WRONG_TIERS:
                title_text = (
                    "This is account is protected because it's more than "
                    "one tier below you"
                )
                badge = StateEmoji.PROTECTED
            elif reason & DenialReason.COOLDOWN:
                title_text = (
                    "You broke into this account recently, "
                    "come back later to try again."
                )
                badge = StateEmoji.COOLDOWN
            elif reason & DenialReason.DEFENDER_LOCKED:
                title_text = "This account is locked"
                badge = StateEmoji.LOCKED
            elif reason & DenialReason.ATTACKER_LOCKED:
                title_text = "You can attack this account if you unlock your acccount"
                badge = StateEmoji.UNLOCKED
            else:
                # This is actually an error case, but we will fail with just a
                # log message (don't want to bring down the view just because an
                # emoji doesn't render)
                log.error(
                    f"Unknown denial reason {reason} for {my_account} attacking {target_account}"
                )
                title_text = ""
                badge = ""
        case _:
            # This is also an error case, but we fail silently again
            log.error(
                f"Unknown permission {permission} for {my_account} attacking {target_account}"
            )
            title_text = ""
            badge = ""

    return {
        "target_name": target_account.account_name,
        "target_url": reverse("subaccount_login", args=[target_account.account_id]),
        "target_badge": badge,
        "target_title_text": title_text,
    }


@register.inclusion_tag("auth_attempt_table.html")
def auth_attempt_table(
    my_account: Account, auth_attempts: Iterable[AuthAttempt], view_as: ViewAs
) -> Dict[str, Any]:
    view_as_attacker = view_as == ViewAs.VIEW_AS_ATTACKER
    view_as_defender = view_as == ViewAs.VIEW_AS_DEFENDER
    view_as_self = view_as == ViewAs.VIEW_AS_SELF
    if not (view_as_attacker or view_as_defender or view_as_self):
        raise ValueError(f"Unknown value for {view_as=}")
    return {
        "my_account": my_account,
        "auth_attempts": auth_attempts,
        "view_as_attacker": view_as_attacker,
        "view_as_defender": view_as_defender,
        "view_as_self": view_as_self,
    }


@register.inclusion_tag("auth_attempt_table_paginated_link.html")
def auth_attempt_table_page_link(
    form: AttackListForm, page: int, content: Any, classes: str = ""
) -> Dict[str, str]:
    params = form.urlencode(page=page)
    replace_url = reverse("auth_attempt_table") + "?" + params
    return {
        "classes": classes,
        "replace_url": replace_url,
        "content": str(content),
    }


@register.simple_tag
def paginated_auth_attempt_table(
    my_account: Account,
    attacker: Optional[Account] = None,
    defender: Optional[Account] = None,
    page: int = 1,
) -> str:
    form = AttackListForm(
        {
            "attacker": attacker,
            "defender": defender,
            "page": page,
        },
        current_account=my_account,
    )
    if not form.is_valid():
        raise ValueError(f"Invalid arguments: {form.errors}")
    return render_paginated_table(attack_form=form, my_account=my_account)


@register.filter
def abbreviate(value):
    value = float(value)
    if value >= 1000000:
        return f"{value / 1000000:.1f}M"
    elif value >= 1000:
        return f"{value / 1000:.0f}K"
    return value


@register.filter
def naturaldelta(timedelta: timedelta) -> str:
    return humanize.naturaldelta(timedelta)


@register.inclusion_tag("llm_choice_selector.html")
def llm_choice_selector(account: Account) -> Dict[str, Any]:
    active_defense: Any = account.active_defense
    if not isinstance(active_defense, Defense):
        # should never happen
        raise ValueError(
            f"Account {account} has invalid {active_defense=} ({type(active_defense)=})"
        )
    return {
        "llm_choices": [
            {
                "machine_readable_name": key,
                "human_readable_name": value,
                "selected": key == active_defense.llm_choice,
            }
            for key, value in active_defense.llm_choice_to_name.items()
        ],
    }


@register.simple_tag
def canonical_link(view_name: str, **kwargs: Dict[str, Any]) -> SafeString:
    """Create a <link rel="canonical"> that points to the right domain, if
    necessary (i.e. if settings.NEW_DOMAIN is defined). Useful for SEO. Put it
    in the <head> tag. Noop when settings.NEW_DOMAIN is not defined."""
    # treat kwargs as GET params (but always escape)
    if kwargs:
        query = "?" + urlencode(kwargs)
    else:
        query = ""
    view_url = reverse(view_name) + query
    new_domain: Optional[str] = settings.NEW_DOMAIN
    if new_domain is not None:
        new_domain_url = urlparse(new_domain)
        if not new_domain_url.scheme or not new_domain_url.hostname:
            log.warning(f"{settings.NEW_DOMAIN=} is missing a scheme or hostname")
        parsed_url = urlparse(view_url)
        new_parsed_url: ParseResult = parsed_url._replace(
            netloc=new_domain_url.hostname, scheme=new_domain_url.scheme
        )
        canon_url = urlunparse(new_parsed_url)
        escaped_canon_url = html.escape(canon_url)
        return mark_safe(f'<link rel="canonical" href="{escaped_canon_url}"/>')
    # no NEW_DOMAIN defined, cannot make any canonical <link>, so this template
    # tag is a noop (this happens in a dev environment, for instance)
    return mark_safe("")
