import json
import requests

from django.apps import apps
from django.views import View
from django.urls import reverse
from django.conf import settings
from django.shortcuts import redirect, render, HttpResponse
from django.views.decorators.clickjacking import xframe_options_exempt
from django.utils.decorators import method_decorator

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.exceptions import APIException

import shopify

from .utils import (
    get_function_from_string,
    get_shop_model,
    shopify_app_redirect,
    webhook_request_is_valid,
    get_shop,
    get_auth_shop,
)
from .decorators import shopify_embed


class ShopAPIView(APIView):
    """Base API view for shop-authenticated endpoints."""

    pass


class AppRootView(View):
    """Root view that handles the app entry point.

    When token exchange is enabled, redirects to the dashboard path.
    When disabled, falls back to InitTokenRequestView behavior.
    """

    redirect_path_name = ""
    shop_not_provided_redirect_path_name = ""

    @method_decorator(xframe_options_exempt)
    @method_decorator(shopify_embed)
    def get(self, request, *args, **kwargs):
        config = apps.get_app_config("shopify_app")

        if config.SHOPIFY_TOKEN_EXCHANGE:
            dashboard_path = config.SHOPIFY_DASHBOARD_PATH
            query = request.META.get("QUERY_STRING", "")
            if query:
                return redirect(f"{dashboard_path}?{query}")
            return redirect(dashboard_path)

        # Fallback to OAuth authorization code grant
        shop_url = request.GET.get("shop")

        if not shop_url:
            if self.shop_not_provided_redirect_path_name:
                return redirect(self.shop_not_provided_redirect_path_name)
            else:
                return HttpResponse(
                    "No shop provided, please open this app from Shopify",
                    status=404,
                )

        auth_url = create_permission_url(
            shop_url, redirect_path_name=self.redirect_path_name
        )
        auth_shop = get_auth_shop(shop_url)
        return shopify_app_redirect(request, auth_url, auth_shop)


def create_permission_url(shop_url, redirect_path="", redirect_path_name=""):
    """Build a Shopify OAuth permission URL for the given shop."""
    auth_shop = get_auth_shop(shop_url)
    shopify.Session.setup(
        api_key=auth_shop.shopify_app_api_key,
        secret=auth_shop.shopify_app_api_secret,
    )

    api_version = "2022-07"
    state = ""

    if redirect_path_name:
        path = reverse(redirect_path_name)
    elif redirect_path:
        path = redirect_path

    redirect_uri = f"{settings.SHOPIFY_APP_HOST}{path}"
    scopes = settings.SHOPIFY_APP_SCOPES

    newSession = shopify.Session(shop_url, api_version)
    auth_url = newSession.create_permission_url(scopes, redirect_uri, state)
    return auth_url


class InitTokenRequestView(View):
    """Initiates the OAuth authorization code grant flow with Shopify."""

    redirect_path_name = ""
    shop_not_provided_redirect_path_name = ""

    @method_decorator(xframe_options_exempt)
    @method_decorator(shopify_embed)
    def get(self, request, *args, **kwargs):
        shop_url = request.GET.get("shop")

        if not shop_url:
            if self.shop_not_provided_redirect_path_name:
                return redirect(self.shop_not_provided_redirect_path_name)
            else:
                message = "No shop provided, please open this app from Shopify"
                return HttpResponse(
                    message,
                    status=404,
                )

        auth_url = create_permission_url(
            shop_url, redirect_path_name=self.redirect_path_name
        )
        auth_shop = get_auth_shop(shop_url)
        return shopify_app_redirect(request, auth_url, auth_shop)


def request_personal_access_token(shop, client_id, client_secret, authorization_code):
    """Exchange an authorization code for an access token via Shopify's OAuth endpoint."""
    response = requests.post(
        (
            f"https://{shop}/admin/oauth/access_token"
            f"?client_id={client_id}&client_secret={client_secret}&"
            f"code={authorization_code}"
        )
    )
    if response.status_code != 200:
        raise Exception("Could not request access token")

    return response.json()


class EndTokenRequestView(View):
    """Completes the OAuth flow by exchanging the code for an access token and persisting it."""

    redirect_path_name = ""

    def get(self, request, *args, **kwargs):
        shop_url = request.GET.get("shop")
        auth_shop = get_auth_shop(shop_url)

        shopify.Session.setup(
            api_key=auth_shop.shopify_app_api_key,
            secret=auth_shop.shopify_app_api_secret,
        )

        if not shopify.Session.validate_params(request.GET.dict()):
            raise Exception("Invalid HMAC: Possibly malicious login")

        data = request_personal_access_token(
            shop_url,
            auth_shop.shopify_app_api_key,
            auth_shop.shopify_app_api_secret,
            request.GET.get("code"),
        )

        Shop = get_shop_model()
        shop_record = Shop.objects.get_or_create(shopify_domain=shop_url)[0]

        if data.get("associated_user"):
            shop_record.on_user_login(data["associated_user"], request=request)

            from urllib.parse import urlencode

            host = settings.SHOPIFY_APP_HOST
            path = reverse(self.redirect_path_name)
            safe_params = {
                k: v for k, v in request.GET.items() if k in ("shop", "host", "locale")
            }
            query = urlencode(safe_params)

            return redirect(f"{host}{path}?{query}")
        else:
            access_token = data.pop("access_token")

        installed = bool(not shop_record.shopify_token and access_token)

        shop_record.shopify_token = access_token

        access_scopes = data["scope"]
        access_scopes = access_scopes[0:249]
        shop_record.access_scopes = access_scopes

        shop_record.save()

        if installed:
            shop_record.installed(request=request)

        auth_url = create_permission_url(shop_url, redirect_path=request.path)
        return redirect(f"{auth_url}&grant_options[]=per-user")


class WebhookAuthenticationFailed(APIException):
    """Raised when a webhook request fails HMAC authentication."""

    status_code = 401
    default_detail = "authentication failed"


class MandatoryWebhooksView(APIView):
    """Handles Shopify mandatory GDPR webhook topics (customer data request, erasure, shop erasure)."""

    topic = None

    def post(self, request, *args, **kwargs):
        meta = request.META
        shopify_domain = meta.get("HTTP_X_SHOPIFY_SHOP_DOMAIN")
        hmac_header = meta.get("HTTP_X_SHOPIFY_HMAC_SHA256")

        if not shopify_domain or not hmac_header:
            raise WebhookAuthenticationFailed()

        if not webhook_request_is_valid(hmac_header, request.body):
            raise WebhookAuthenticationFailed()

        func = get_function_from_string(settings.SHOPIFY_GDPR_WEBHOOK_CALLBACK)
        func(shopify_domain, self.topic, request.data, attributes={})

        return Response(status=200)


class WebhooksView(APIView):
    """Receives and dispatches incoming Shopify webhook events after HMAC validation."""

    def post(self, request, *args, **kwargs):
        meta = request.META
        shopify_domain = meta.get("HTTP_X_SHOPIFY_SHOP_DOMAIN")
        hmac = meta.get("HTTP_X_SHOPIFY_HMAC_SHA256")
        webhook_topic = meta.get("HTTP_X_SHOPIFY_TOPIC")
        api_version = meta.get("HTTP_X_SHOPIFY_API_VERSION")
        webhook_id = meta.get("HTTP_X_SHOPIFY_WEBHOOK_ID")

        if not all([shopify_domain, hmac, webhook_topic, api_version]):
            raise WebhookAuthenticationFailed()

        data = request.body

        if not webhook_request_is_valid(hmac, data):
            raise WebhookAuthenticationFailed()

        callback_path = settings.SHOPIFY_WEBHOOK_CALLBACK
        data = json.loads(data.decode("utf-8"))
        func = get_function_from_string(callback_path)
        attributes = {
            "api_version": api_version,
            "webhook_id": webhook_id,
        }
        func(shopify_domain, webhook_topic, data, attributes=attributes)

        return Response(status=200)
