"""
Login Views overriding default ones and allowing OpenIdConnect connection
"""

import logging
import requests

from pyramid.httpexceptions import (
    HTTPFound,
    HTTPForbidden,
    HTTPUnauthorized,
    HTTPBadRequest,
)
from pyramid.security import forget, remember, NO_PERMISSION_REQUIRED
from urllib.parse import urlencode
from caerp.utils.rest import Apiv1Resp


from caerp.utils.security.auth import connect_user
from caerp.utils.session import delete_session_from_cookie_value, get_session_id

from .models import OidcSession
from .utils import check_settings, parse_id_token

logger = logging.getLogger("caerp." + __name__)

HTTP_SUCCESS_CODE = (200, 201, 202)


class AuthException(Exception):
    pass


class OidcLoginView:
    _session_nextpage_key = "oidc.redirect_afterauth"

    def __init__(self, context, request):
        self.context = context
        self.request = request
        self.pyramid_settings = request.registry.settings

    def get_configuration(self, key):
        return self.pyramid_settings[f"oidc.{key}"]

    def get_next_page(self):
        nextpage = self.request.params.get("nextpage")
        if not nextpage:
            nextpage = self.request.session.get(self._session_nextpage_key)
        logger.debug(f"Redirecting to {nextpage}")
        # avoid redirection looping or set default
        if nextpage in [
            None,
            self.request.route_path("login"),
            self.request.route_path("/oidc_callback"),
        ]:
            nextpage = self.request.route_url("index")
        return nextpage

    def success_response(self):
        """
        Return the result to send on successfull authentication
        """
        if self.request.is_xhr:
            result = Apiv1Resp(self.request)
        else:
            result = HTTPFound(
                location=self.get_next_page(),
                headers=self.request.response.headers,
            )
        return result

    def redirect_uri(self):
        return self.request.route_url("/oidc_callback")

    def auth_endpoint_url(self):
        url = self.get_configuration("auth_endpoint_url")
        params = {
            "redirect_uri": self.redirect_uri(),
            "scope": self.get_configuration("scope"),
            "response_type": "code",
            "client_id": self.get_configuration("client_id"),
        }
        url_params = urlencode(params)
        location = f"{url}?{url_params}"
        return location

    def __call__(self):
        logger.debug("Calling the login view")
        if self.request.identity is not None:
            logger.debug("Found an identity")
            return self.success_response()
        else:
            logger.debug("Redirecting to auth_endpoint")
            # First store the next page in the session
            self.request.session[self._session_nextpage_key] = self.get_next_page()
            return HTTPFound(self.auth_endpoint_url())

    def call_token_endpoint(self, code) -> dict:
        """
        Call the openid connect token endpoint

        :returns: The jsw encrypted id_token
        """
        params = {
            "grant_type": "authorization_code",
            "code": code,
            "redirect_uri": self.redirect_uri(),
            "client_secret": self.get_configuration("client_secret"),
            "client_id": self.get_configuration("client_id"),
        }
        url = self.get_configuration("token_endpoint_url")
        headers = {"Content-Type": "application/x-www-form-urlencoded"}
        req = requests.post(url, params, headers)

        if req.status_code not in HTTP_SUCCESS_CODE:
            logger.error(f"Request status {req.status_code}")
            logger.error(params)
            raise AuthException("Error code {}".format(req.status_code))

        json_result = req.json()
        return json_result

    def _get_id_token_content(self, json_result: dict) -> dict:
        """
        Parse the jsw encrypted id_token and return a dict
        """
        jwt_id_token = json_result["id_token"]
        logger.debug(json_result.keys())
        self.request.session["id_token"] = jwt_id_token

        id_token = parse_id_token(jwt_id_token)
        logger.debug(f" id_token : {id_token}")
        result = {
            "expires_in": json_result["expires_in"],
            "token_type": json_result["token_type"],
            "access_token": json_result["access_token"],
            "id_token": id_token,
        }
        return result

    def _get_user_login(self, id_token):
        """Return the user login from the id_token"""
        return id_token["preferred_username"]

    def _store_session_in_database(self, id_token):
        logger.debug("Storing session in database")
        oidc_sid = id_token.get("sid")
        oidc_sub = id_token.get("sub")
        if oidc_sid:
            oidc_session = OidcSession.get_from_oidc_sid(self.request, oidc_sid)
        elif oidc_sub:
            oidc_session = OidcSession.get_from_oidc_sub(self.request, oidc_sub)

        if oidc_session is None:
            oidc_session = OidcSession(
                local_session_id=get_session_id(self.request),
            )
            if oidc_sid:
                oidc_session.oidc_sid = oidc_sid
            if oidc_sub:
                oidc_session.oidc_sub = oidc_sub
            self.request.dbsession.add(oidc_session)
        else:
            logger.warn(
                "It's strange, we already have that remote session id in"
                " the database"
            )

    def oidc_callback_view(self):
        """
        Callback called after successfull authentication
        """
        code = self.request.GET.get("code")
        if not code:
            raise HTTPForbidden()
        else:
            try:
                json_result = self.call_token_endpoint(code)
                auth_data = self._get_id_token_content(json_result)["id_token"]
                self._store_session_in_database(auth_data)
            except Exception:
                logger.exception("Erreur")
                raise HTTPForbidden()

            login = self._get_user_login(auth_data)
            try:
                connect_user(self.request, login)
            except Exception:
                logger.exception("Erreur en connectant le user")
                raise HTTPUnauthorized()
            remember(self.request, login)
            return self.success_response()

    def logout_view(self):
        """Logout view"""
        forget(self.request)
        self.request.response.delete_cookie("remember_me")
        url = self.get_configuration("logout_endpoint_url")
        url_params = urlencode(
            {
                "post_logout_redirect_uri": self.request.route_url("login"),
                "id_token_hint": self.request.session.pop("id_token"),
            }
        )
        location = f"{url}?{url_params}"
        return HTTPFound(location)

    def _get_logout_token_from_request(self):
        """Unencrypt the logout_token that can be found in the request"""
        logger.debug("In the get_logout_token_from_request")
        # Get the jwt logout_token content
        logout_token = self.request.POST.get("logout_token")
        logger.debug(logout_token)
        if logout_token is None:
            raise AuthException("Logout token not found in request")
        parsed_logout_token = parse_id_token(logout_token)
        logger.debug(parsed_logout_token)
        return parsed_logout_token

    def backend_logout_view(self):
        """
        OpenID Connect Back channel logout view

        Receive a application/x-www-form-urlencoded request with a JWT
        logout_token in the POST params
        """
        logger.debug("+ In the backend logout view")
        # Get the jwt token content
        try:
            logout_token = self._get_logout_token_from_request()
        except Exception:
            logger.exception(" - Error parsing logout token")
            raise HTTPBadRequest()
        # Retrieve the OidcSession object
        oidc_sid = logout_token.get("sid")
        oidc_sub = logout_token.get("sub")
        if oidc_sid:
            oidc_session = OidcSession.get_from_oidc_sid(self.request, oidc_sid)
        elif oidc_sub:
            oidc_session = OidcSession.get_from_oidc_sub(self.request, oidc_sub)
        else:
            raise HTTPBadRequest()
        if oidc_session is None:
            raise HTTPBadRequest()
        delete_session_from_cookie_value(self.request, oidc_session.local_session_id)
        self.request.dbsession.delete(oidc_session)
        self.request.response.headerlist.append(("Cache-Control", "no-store"))
        return {}


def includeme(config):
    config.add_route("/oidc_callback", "/oidc_callback")
    config.add_route("/oidc_backend_logout", "/oidc_backend_logout")

    config.add_view(
        OidcLoginView,
        route_name="login",
        permission=NO_PERMISSION_REQUIRED,
        renderer="json",
        xhr=False,
    )
    config.add_view(
        OidcLoginView,
        route_name="login",
        permission=NO_PERMISSION_REQUIRED,
        renderer="json",
        xhr=True,
    )
    config.add_view(
        OidcLoginView,
        route_name="apiloginv1",
        permission=NO_PERMISSION_REQUIRED,
        renderer="json",
        request_method="GET",
    )
    config.add_view(
        OidcLoginView,
        route_name="logout",
        attr="logout_view",
        permission=NO_PERMISSION_REQUIRED,
    )

    # Custom OpenId connect views
    config.add_view(
        OidcLoginView,
        route_name="/oidc_callback",
        permission=NO_PERMISSION_REQUIRED,
        attr="oidc_callback_view",
        renderer="json",
    )
    config.add_view(
        OidcLoginView,
        route_name="/oidc_backend_logout",
        attr="backend_logout_view",
        request_method="POST",
        permission=NO_PERMISSION_REQUIRED,
        renderer="json",
    )
