from typing import Callable

from fastapi import Request, Response
from fastapi.exceptions import HTTPException
from fastapi.routing import APIRoute
from starlette import status

from src.depends.auth import (check_session_expired, get_current_session,
                              verify_session)
from src.error.exceptions import DBException, UnexpectedException
from src.logging.get_logger import get_logger
from src.system_var import constants, environment

logger = get_logger('medaca_router')


class MeDaCaRoute(APIRoute):
    """アプリケーションのカスタムルーター

    Args:
        APIRoute (APIRoute): FastAPIの標準APIRoute
    """

    def get_route_handler(self) -> Callable:
        """前後処理を付加するルートハンドラーを返す

        Raises:
            e: HTTPException
            UnexpectedException: HTTPException以外の例外をカスタム例外にする

        Returns:
            Callable: カスタムルートハンドラー
        """
        original_route_handler = super().get_route_handler()

        # 返却するルートハンドラーを定義。必ず非同期関数にする必要がある。
        async def custom_route_handler(request: Request) -> Response:
            try:
                # 事前処理
                request = await self.pre_process_route(request)
                # 本来のルーティング処理
                response = await original_route_handler(request)
                # 事後処理
                return await self.post_process_route(request, response)
            except HTTPException as e:
                raise e
            except Exception as e:
                logger.exception(e)
                if isinstance(e, DBException):
                    raise UnexpectedException(detail=constants.LOGOUT_REASON_DB_ERROR)
                else:
                    raise UnexpectedException(detail=constants.LOGOUT_REASON_UNEXPECTED)
        return custom_route_handler

    async def pre_process_route(self, request: Request) -> Request:
        """ルートハンドラーの事前処理

        Args:
            request (Request): FastAPIのリクエストクラス

        Returns:
            Request: 加工後のRequestインスタンス
        """
        return request

    async def post_process_route(self, request: Request, response: Response) -> Response:
        """ルートハンドラーの事後処理

        Args:
            request (Request): FastAPIのリクエストインスタンス
            response (Response): FastAPIのレスポンスインスタンス(original_route_handlerのレスポンス)
        Returns:
            Response: 加工後のResponseインスタンス
        """
        return response


class BeforeCheckSessionRoute(MeDaCaRoute):
    """事前処理として、セッションチェックを行うルートハンドラー

    Args:
        MeDaCaRoute (MeDaCaRoute): 共通ルートハンドラー
    """
    async def pre_process_route(self, request: Request):
        request = await super().pre_process_route(request)
        # セッションを取得
        session_key = request.cookies.get('session')
        current_session = get_current_session(session_key)
        checked_session = check_session_expired(current_session)
        verified_session = verify_session(checked_session)
        # セッションが有効でない場合、エラーにする
        if verified_session is None:
            raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
        scope = request.scope
        scope['session'] = verified_session
        session_request = Request(receive=request.receive, scope=scope)
        return session_request


class AfterSetCookieSessionRoute(MeDaCaRoute):
    """事後処理として、セッションキーをcookieに設定するカスタムルートハンドラー

    Args:
        MeDaCaRoute (MeDaCaRoute): 共通ルートハンドラー
    """
    async def post_process_route(self, request: Request, response: Response):
        response = await super().post_process_route(request, response)
        session_key = response.headers.get('session_key', None)
        # セッションキーがない場合はセットせずに返す
        if session_key is None:
            return response

        del response.headers['session_key']
        # クッキーにセッションを設定
        response.set_cookie(
            key='session',
            value=session_key,
            max_age=environment.SESSION_EXPIRE_MINUTE * 60,  # cookieの有効期限は秒数指定なので、60秒をかける
            secure=True,
            httponly=True
        )
        return response


class AuthenticatedRoute(BeforeCheckSessionRoute, AfterSetCookieSessionRoute):
    async def pre_process_route(self, request: Request):
        request = await super().pre_process_route(request)
        return request

    async def post_process_route(self, request: Request, response: Response):
        response = await super().post_process_route(request, response)
        return response
