import datetime
import logging
from collections.abc import Awaitable, Callable
from typing import Any
from urllib.parse import quote, urlencode

import jwt
import requests
from asgiref.sync import iscoroutinefunction, markcoroutinefunction, sync_to_async
from django.contrib.auth import logout
from django.http import (
    HttpRequest,
    HttpResponse,
    JsonResponse,
)
from django.shortcuts import render
from django.urls import reverse_lazy

from yarik_django_auth.conf.settings import (
    CLIENT_ID,
    CLIENT_SECRET,
    DEFAULT_REQUEST_TIMEOUT,
    ERROR_PAGE_DOCUMENT,
    KEYCLOAK_CERT_FILENAME,
    KEYCLOAK_SCOPES,
    KEYCLOAK_URL_BASE,
    REALM_NAME,
)

logger = logging.getLogger(__name__)


def check_session(access_token: str) -> int:
    resp_status = 401
    try:
        userinfo_endpoint = f"{KEYCLOAK_URL_BASE}realms/{REALM_NAME}/protocol/openid-connect/userinfo"
        response = requests.get(
            userinfo_endpoint,
            headers={"Authorization": f"Bearer {access_token}"},
            verify=KEYCLOAK_CERT_FILENAME,
            timeout=DEFAULT_REQUEST_TIMEOUT,
        )
        logger.debug("Выполнен запрос проверки актуальности сессии")
        resp_status = response.status_code
    except Exception as e:
        logger.debug(e)

    return resp_status


def update_token(refresh_token: str) -> tuple[Any | None, int]:
    resp_status = 401
    data = None
    try:
        token_endpoint = f"{KEYCLOAK_URL_BASE}realms/{REALM_NAME}/protocol/openid-connect/token"
        payload: dict[str, str | None] = {
            "client_id": CLIENT_ID,
            "grant_type": "refresh_token",
            "refresh_token": refresh_token,
            "scope": " ".join(KEYCLOAK_SCOPES or []),
            "client_secret": CLIENT_SECRET,
        }
        response = requests.post(
            token_endpoint,
            headers={"Content-Type": "application/x-www-form-urlencoded"},
            verify=KEYCLOAK_CERT_FILENAME,
            data=urlencode(payload, quote_via=quote),
            timeout=DEFAULT_REQUEST_TIMEOUT,
        )
        logger.debug("Выполнен запрос обновления токена")
        logger.debug(response.text)

        resp_status = response.status_code
        if resp_status == 200:
            data = response.json()
    except Exception as e:
        logger.debug(e)

    return (data, resp_status)


class AsyncMiddleware:
    async_capable = True
    sync_capable = False

    def __init__(self, get_response: Callable[[HttpRequest], Awaitable[JsonResponse | HttpResponse]]) -> None:
        self.get_response = get_response
        if iscoroutinefunction(self.get_response):
            markcoroutinefunction(self)

    async def get_403_response(self, request: HttpRequest) -> JsonResponse:
        msg = "Ошибка при проверке актуальности сессии. Возможно токен не содержит scope openid."
        logger.error(msg)
        logout(request)
        return JsonResponse({"msg": msg}, status=403)

    async def __call__(self, request: HttpRequest) -> JsonResponse | HttpResponse:
        if "oidc_auth_token" in request.session:
            oidc_auth_token = request.session["oidc_auth_token"]
            access_token = oidc_auth_token.get("access_token")
            refresh_token = oidc_auth_token.get("refresh_token")
            logger.debug("Токен извлеченный из сессии = %s", oidc_auth_token)
            logger.debug("Токен доступа = %s", access_token)

            def _get_user(r: HttpRequest) -> str:
                return r.user.username

            username = await sync_to_async(_get_user)(request)

            # 1. Проверяем актуальность токена
            logger.debug("Проверка актуальности токена доступа")
            decoded_token = jwt.decode(access_token, verify=False, options={"verify_signature": False})
            if datetime.datetime.fromtimestamp(decoded_token["exp"], tz=datetime.UTC) < datetime.datetime.now(
                tz=datetime.UTC
            ):
                # Токен просрочен, надо обновить
                logger.debug("Токен устарел: обновление токена...")

                data_token, resp_status = update_token(refresh_token)

                logger.debug("Код ответа: %s", resp_status)

                if not data_token:
                    # Токен не получен, очистка сессии
                    logger.info("Сессия очищена для пользователя %s. Причина: не удалось обновить токен", username)
                    await sync_to_async(logout)(request)
                    return await self.get_response(request)

                logger.debug("Сохранение токена в сессии...")
                request.session["oidc_auth_token"] = data_token

                access_token = data_token["access_token"]

            # 2. Проверяем актуальность сессии
            logger.debug("Выполнение запроса проверки актуальности сессии")

            resp_status = check_session(access_token)

            logger.debug("Код ответа: %s", resp_status)

            if resp_status == 403:
                # В запросе не хватает данных  # noqa: RUF003
                return await self.get_403_response(request)

            if resp_status != 200:
                # Сессия просрочена или refresh_token устарел, очистка сессии
                logger.info(
                    "Сессия очищена для пользователя %s. Причина: сессия в Keycloak была завершена",
                    username,
                )
                await sync_to_async(logout)(request)

        return await self.get_response(request)


class SyncMiddleware:
    async_capable = False
    sync_capable = True

    def __init__(self, get_response: Callable[[HttpRequest], JsonResponse | HttpResponse]) -> None:
        self.get_response = get_response

    def get_403_response(self, request: HttpRequest) -> HttpResponse:
        logger.error("Ошибка при проверке актуальности сессии. Возможно токен не содержит scope openid.")
        logout(request)
        return render(
            request,
            ERROR_PAGE_DOCUMENT or "templates/403.html",
            {
                "code": 403,
                "message": "Ошибка аутентификации в Keycloak, при проверке актуальности сессии произошла ошибка, \
                    обратитесь к администратору!",
                "header": "Ошибка аутентификации Keycloack",
                "button_text": "Вернуться к авторизации",
                "logout_url": reverse_lazy("yarik_django_auth:logout"),
            },
        )

    def __call__(self, request: HttpRequest) -> JsonResponse | HttpResponse:
        if "oidc_auth_token" in request.session:
            oidc_auth_token = request.session["oidc_auth_token"]
            access_token = oidc_auth_token.get("access_token")
            refresh_token = oidc_auth_token.get("refresh_token")
            logger.debug("Токен извлеченный из сессии = %s", oidc_auth_token)
            logger.debug("Токен доступа = %s", access_token)

            # 1. Проверяем актуальность токена
            logger.debug("Проверка актуальности токена доступа")
            decoded_token = jwt.decode(access_token, verify=False, options={"verify_signature": False})
            if datetime.datetime.fromtimestamp(decoded_token["exp"], tz=datetime.UTC) < datetime.datetime.now(
                tz=datetime.UTC
            ):
                # Токен просрочен, надо обновить
                logger.debug("Токен устарел: обновление токена...")

                data_token, resp_status = update_token(refresh_token)

                logger.debug("Код ответа: %s", resp_status)

                username = request.user.username

                if not data_token:
                    # Токен не получен, очистка сессии
                    logger.info("Сессия очищена для пользователя %s. Причина: не удалось обновить токен", username)
                    logout(request)
                    return self.get_response(request)

                logger.debug("Сохранение токена в сессии...")
                request.session["oidc_auth_token"] = data_token

                access_token = data_token["access_token"]

            # 2. Проверяем актуальность сессии
            logger.debug("Выполнение запроса проверки актуальности сессии")

            resp_status = check_session(access_token)

            logger.debug("Код ответа: %s", resp_status)

            if resp_status == 403:
                # В запросе не хватает данных  # noqa: RUF003
                return self.get_403_response(request)

            if resp_status != 200:
                # Сессия просрочена или refresh_token устарел, очистка сессии
                logger.info(
                    "Сессия очищена для пользователя %s. Причина: сессия в Keycloak была завершена",
                    request.user.username,
                )
                logout(request)

        return self.get_response(request)
