{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}

module Yesod.Auth.OAuth2.DispatchError
    ( DispatchError(..)
    , handleDispatchError
    , onDispatchError
    ) where

import Control.Monad.Except
import Data.Text (Text, pack)
import Network.OAuth.OAuth2
import Network.OAuth.OAuth2.TokenRequest (Errors)
import UnliftIO.Except ()
import UnliftIO.Exception
import Yesod.Auth hiding (ServerError)
import Yesod.Auth.OAuth2.ErrorResponse
import Yesod.Auth.OAuth2.Exception
import Yesod.Auth.OAuth2.Random
import Yesod.Core hiding (ErrorResponse)

data DispatchError
    = MissingParameter Text
    | InvalidStateToken (Maybe Text) Text
    | InvalidCallbackUri Text
    | OAuth2HandshakeError ErrorResponse
    | OAuth2ResultError (OAuth2Error Errors)
    | FetchCredsIOException IOException
    | FetchCredsYesodOAuth2Exception YesodOAuth2Exception
    | OtherDispatchError Text
    deriving stock Show
    deriving anyclass Exception

-- | User-friendly message for any given 'DispatchError'
--
-- Most of these are opaque to the user. The exception details are present for
-- the server logs.
--
dispatchErrorMessage :: DispatchError -> Text
dispatchErrorMessage = \case
    MissingParameter name ->
        "Parameter '" <> name <> "' is required, but not present in the URL"
    InvalidStateToken{} -> "State token is invalid, please try again"
    InvalidCallbackUri{}
        -> "Callback URI was not valid, this server may be misconfigured (no approot)"
    OAuth2HandshakeError er -> "OAuth2 handshake failure: " <> erUserMessage er
    OAuth2ResultError{} -> "Login failed, please try again"
    FetchCredsIOException{} -> "Login failed, please try again"
    FetchCredsYesodOAuth2Exception{} -> "Login failed, please try again"
    OtherDispatchError{} -> "Login failed, please try again"

handleDispatchError
    :: MonadAuthHandler site m
    => ExceptT DispatchError m TypedContent
    -> m TypedContent
handleDispatchError f = do
    result <- runExceptT f
    either onDispatchError pure result

onDispatchError :: MonadAuthHandler site m => DispatchError -> m TypedContent
onDispatchError err = do
    errorId <- liftIO $ randomText 16
    let suffix = " [errorId=" <> errorId <> "]"
    $(logError) $ pack (displayException err) <> suffix

    let message = dispatchErrorMessage err <> suffix
        messageValue =
            object ["error" .= object ["id" .= errorId, "message" .= message]]

    loginR <- ($ LoginR) <$> getRouteToParent

    selectRep $ do
        provideRep @_ @Html $ onErrorHtml loginR message
        provideRep @_ @Value $ pure messageValue
