import hashlib
import hmac
from datetime import timedelta

from django.core.cache import cache
from django.http import HttpRequest
from django.utils import timezone

from allauth.socialaccount.adapter import get_adapter
from allauth.socialaccount.internal import jwtkit
from allauth.socialaccount.models import SocialLogin, SocialToken
from allauth.socialaccount.providers.base import Provider
from allauth.socialaccount.providers.facebook.constants import GRAPH_API_URL


# maps fields from the Limited Login JWT to Graph API response fields
JWT_FIELD_TO_GRAPH_API_FIELD_MAP = {
    "sub": "id",
    "email": "email",
    "given_name": "first_name",
    "family_name": "last_name",
    "name": "name",
    "user_link": "link",
}


def compute_appsecret_proof(app, token) -> str:
    # Generate an appsecret_proof parameter to secure the Graph API call
    # see https://developers.facebook.com/docs/graph-api/securing-requests
    msg = token.token.encode("utf-8")
    key = app.secret.encode("utf-8")
    appsecret_proof = hmac.new(key, msg, digestmod=hashlib.sha256).hexdigest()
    return appsecret_proof


def complete_login(request, provider, token):
    with get_adapter().get_requests_session() as sess:
        params = {
            "fields": ",".join(provider.get_fields()),
            "access_token": token.token,
            "appsecret_proof": compute_appsecret_proof(provider.app, token),
        }
        resp = sess.get(f"{GRAPH_API_URL}/me", params=params)
        resp.raise_for_status()
        extra_data = resp.json()
    return provider.sociallogin_from_response(request, extra_data)


def get_app_token(provider):
    app = provider.app
    cache_key = f"allauth.facebook.app_token[{app.client_id}]"
    app_token = cache.get(cache_key)
    if not app_token:
        with get_adapter().get_requests_session() as sess:
            params = {
                "client_id": app.client_id,
                "client_secret": app.secret,
                "grant_type": "client_credentials",
            }
            resp = sess.get(f"{GRAPH_API_URL}/oauth/access_token", params=params)
            resp.raise_for_status()
            data = resp.json()
        app_token = data["access_token"]
        timeout = provider.get_settings().get("APP_TOKEN_CACHE_TIMEOUT", 300)
        cache.set(cache_key, app_token, timeout=timeout)
    return app_token


def inspect_token(provider, input_token) -> None:
    app_token = get_app_token(provider)
    with get_adapter().get_requests_session() as sess:
        resp = sess.get(
            f"{GRAPH_API_URL}/debug_token",
            params={"input_token": input_token, "access_token": app_token},
        )
        resp.raise_for_status()
        data = resp.json()["data"]
    if not data["is_valid"]:
        raise get_adapter().validation_error("invalid_token")
    if data["app_id"] != provider.app.client_id or not data["is_valid"]:
        raise get_adapter().validation_error("invalid_token")


def verify_token(
    request,
    provider: Provider,
    access_token: str,
    auth_type: str = "",
    auth_nonce: str = "",
) -> SocialLogin:
    app = provider.app
    inspect_token(provider, access_token)
    expires_at = None
    with get_adapter().get_requests_session() as sess:
        if auth_type == "reauthenticate":
            resp = sess.get(
                f"{GRAPH_API_URL}/oauth/access_token_info",
                params={
                    "client_id": app.client_id,
                    "access_token": access_token,
                },
            )
            resp.raise_for_status()
            info = resp.json()
            ok = auth_nonce and auth_nonce == info.get("auth_nonce")
            if not ok:
                raise get_adapter().validation_error("invalid_token")

        if provider.get_settings().get("EXCHANGE_TOKEN"):
            resp = sess.get(
                f"{GRAPH_API_URL}/oauth/access_token",
                params={
                    "grant_type": "fb_exchange_token",
                    "client_id": app.client_id,
                    "client_secret": app.secret,
                    "fb_exchange_token": access_token,
                },
            )
            resp.raise_for_status()
            info = resp.json()
            access_token = info["access_token"]
            expires_in = info.get("expires_in")
            if expires_in:
                expires_at = timezone.now() + timedelta(seconds=int(expires_in))

    token = SocialToken(app=app, token=access_token, expires_at=expires_at)
    login = complete_login(request, provider, token)
    login.token = token
    return login


def verify_limited_login_token(
    request: HttpRequest, provider, id_token: str
) -> SocialLogin:
    """
    Verifies a Facebook Limited Login token.
    See https://developers.facebook.com/docs/facebook-login/limited-login/token/validating.

    We validate the JWT, then convert its data/claims into
    a fake Facebook Graph API response, which is then passed to
    `provider.sociallogin_from_response` to be handled as normal.
    """

    # note: this already does replay protection internally
    jwt_data = jwtkit.verify_and_decode(
        credential=id_token,
        keys_url=provider.limited_login_jwks_url,
        issuer=provider.limited_login_expected_jwt_issuer,
        audience=provider.app.client_id,
        lookup_kid=jwtkit.lookup_kid_jwk,
    )

    fake_response = {
        graph_field: jwt_data[jwt_field]
        for jwt_field, graph_field in JWT_FIELD_TO_GRAPH_API_FIELD_MAP.items()
        if jwt_field in jwt_data
    }

    return provider.sociallogin_from_response(request, fake_response)
