from collections.abc import Callable

from django.http import HttpRequest, HttpResponse

from zerver.decorator import webhook_view
from zerver.lib.exceptions import UnsupportedWebhookEventTypeError
from zerver.lib.response import json_success
from zerver.lib.typed_endpoint import JsonBodyPayload, typed_endpoint
from zerver.lib.validator import WildValue, check_int, check_none_or, check_string
from zerver.lib.webhooks.common import check_send_webhook_message, get_event_header
from zerver.models import UserProfile


# The events for this integration contain the ":" character, which is not appropriate in a
# filename and requires us to deviate from the `default_fixture_to_headers` method
# from zerver.lib.webhooks.common.
def get_custom_http_headers_from_filename(http_header_key: str) -> Callable[[str], dict[str, str]]:
    def fixture_to_headers(filename: str) -> dict[str, str]:
        event_type = filename.replace("_", ":")
        return {http_header_key: event_type}

    return fixture_to_headers


fixture_to_headers = get_custom_http_headers_from_filename("HTTP_X_PATREON_EVENT")


def get_members_create_body(payload: WildValue) -> str | None:
    last_charge_status = get_last_charge_status(payload)
    patron_status = get_patron_status(payload)
    # null values indicate the member has never pledged
    if last_charge_status is None and patron_status is None:
        template = "{user_name} has joined as a member!"
        return template.format(
            user_name=get_user_name(payload),
        ).rstrip()
    return None


def get_members_update_body(payload: WildValue) -> str | None:
    last_charge_status = get_last_charge_status(payload)
    patron_status = get_patron_status(payload)
    if last_charge_status in ("Paid", None) and patron_status in ("active_patron", "former_patron"):
        template = "{user_name}'s membership has been updated to {patron_status}."
        return template.format(
            user_name=get_user_name(payload),
            patron_status=str(patron_status).replace("_", " "),
        ).rstrip()
    return None


def get_members_delete_body(payload: WildValue) -> str | None:
    last_charge_status = get_last_charge_status(payload)
    patron_status = get_patron_status(payload)
    # null value indicates the member has never pledged
    if last_charge_status in ("Paid", None) and patron_status != "declined_patron":
        template = "{user_name}'s membership has ended."
        return template.format(
            user_name=get_user_name(payload),
        ).rstrip()
    return None


def get_members_pledge_create_body(payload: WildValue) -> str | None:
    last_charge_status = get_last_charge_status(payload)
    pledge_amount = get_pledge_amount(payload)
    # The only successful charge status is "Paid". null if not yet charged.
    if last_charge_status in ("Paid", None) and pledge_amount > 0:
        template = "{user_name} has pledged ${pledge_amount:.2f} per {pay_per_name}. :tada:\nTotal number of patrons: {patron_count}"
        return template.format(
            user_name=get_user_name(payload),
            pledge_amount=pledge_amount,
            pay_per_name=get_pay_per_name(payload),
            patron_count=get_patron_count(payload),
        ).rstrip()
    return None


def get_members_pledge_update_body(payload: WildValue) -> str | None:
    last_charge_status = get_last_charge_status(payload)
    pledge_amount = get_pledge_amount(payload)
    # The only successful charge status is "Paid". null if not yet charged.
    if last_charge_status in ("Paid", None) and pledge_amount > 0:
        template = "{user_name} has updated their pledge to ${pledge_amount:.2f} per {pay_per_name}. :gear:"
        return template.format(
            user_name=get_user_name(payload),
            pledge_amount=pledge_amount,
            pay_per_name=get_pay_per_name(payload),
        ).rstrip()
    return None


def get_members_pledge_delete_body(payload: WildValue) -> str | None:
    last_charge_status = get_last_charge_status(payload)
    if last_charge_status in ("Paid", "Deleted", None):
        template = "{user_name}'s pledge has been cancelled. :cross_mark:\nTotal number of patrons: {patron_count}"
        return template.format(
            user_name=get_user_name(payload),
            patron_count=get_patron_count(payload),
        ).rstrip()
    return None


def get_last_charge_status(payload: WildValue) -> str | None:
    return payload["data"]["attributes"]["last_charge_status"].tame(check_none_or(check_string))


def get_patron_status(payload: WildValue) -> str | None:
    return payload["data"]["attributes"]["patron_status"].tame(check_none_or(check_string))


def get_user_name(payload: WildValue) -> str:
    return payload["data"]["attributes"]["full_name"].tame(check_string)


def get_pledge_amount(payload: WildValue) -> float:
    return payload["data"]["attributes"]["currently_entitled_amount_cents"].tame(check_int) / 100


def get_patron_count(payload: WildValue) -> int:
    return payload["included"][0]["attributes"]["patron_count"].tame(check_int)


def get_pay_per_name(payload: WildValue) -> str:
    return payload["included"][0]["attributes"]["pay_per_name"].tame(check_string)


EVENT_FUNCTION_MAPPER: dict[str, Callable[[WildValue], str | None]] = {
    "members:create": get_members_create_body,
    "members:update": get_members_update_body,
    "members:delete": get_members_delete_body,
    "members:pledge:create": get_members_pledge_create_body,
    "members:pledge:update": get_members_pledge_update_body,
    "members:pledge:delete": get_members_pledge_delete_body,
}

# deprecated events
IGNORED_EVENTS = [
    "pledges:create",
    "pledges:update",
    "pledges:delete",
]

ALL_EVENT_TYPES = list(EVENT_FUNCTION_MAPPER.keys())


@webhook_view("Patreon", all_event_types=ALL_EVENT_TYPES)
@typed_endpoint
def api_patreon_webhook(
    request: HttpRequest,
    user_profile: UserProfile,
    *,
    payload: JsonBodyPayload[WildValue],
) -> HttpResponse:
    header_event = get_event_header(request, "X-Patreon-Event", "Patreon")

    event_name = get_zulip_event_name(header_event, payload)
    if event_name is None:
        # See IGNORED_EVENTS.
        return json_success(request)
    topic = "membership notifications"

    body_function = EVENT_FUNCTION_MAPPER[event_name]
    body = body_function(payload)

    if body is None:
        # None for payloads that are valid,
        # but where we intentionally do not send a message.
        return json_success(request)

    check_send_webhook_message(request, user_profile, topic, body, event_name)
    return json_success(request)


def get_zulip_event_name(
    header_event: str,
    payload: WildValue,
) -> str | None:
    """
    Usually, we return an event name that is a key in EVENT_FUNCTION_MAPPER.
    We return None for an event that we know we don't want to handle.
    """
    if header_event in EVENT_FUNCTION_MAPPER:
        return header_event
    elif header_event in IGNORED_EVENTS:
        return None
    raise UnsupportedWebhookEventTypeError(header_event)
