{-# OPTIONS_GHC -fno-warn-unused-foralls #-}
{-# LANGUAGE UndecidableInstances #-}

module Yesod.Servant
  ( ServantApiUnproxy, ServantApiUnproxy', ServantApiDirect
  , HasRoute(..)
  , ServantApi(..), getServantApi
  , ServantApiDispatch(..)
  , servantApiLink
  , ServantHandlerFor(..)
  , HasServantHandlerContext(..), getServantContext, getsServantContext, getYesodApproot, renderRouteAbsolute, servantApiBaseUrl
  , MonadServantHandler(..), MonadHandler(..), MonadSite(..), MonadRequest(..)
  , ServantDBFor, ServantPersist(..), defaultRunDB
  , ServantLog(..), ServantLogYesod(..)
  , mkYesodApi
  , PathPieceHttpApiData(..)
  , BearerAuth, SessionAuth
  , ServantErrorResponse, getServantErrorResponse
  , module Yesod.Servant.HttpApiDataInjective
  ) where

import ClassyPrelude hiding (Handler, fromList, link)
import Control.Lens hiding (Context)
import Control.Lens.Extras

import Foundation.Servant.Types

import Utils hiding (HasRoute)
import Model.Types.Security

import Yesod.Core ( Yesod
                  , RenderRoute(..), ParseRoute(..)
                  , YesodSubDispatch(..)
                  , PathPiece(..)
                  )
import Yesod.Core.Types ( YesodRunnerEnv(..)
                        , YesodSubRunnerEnv(..)
                        )
import qualified Yesod.Core as Yesod
import qualified Yesod.Core.Types as Yesod
import qualified Yesod.Persist.Core as Yesod

import Servant.Links
import Servant.API
import Servant.Server hiding (route)
import Servant.Server.Instances ()

import Servant.Client.Core.BaseUrl

import Data.Proxy

import Network.Wai (Request, Middleware)
import qualified Network.Wai as W

import Language.Haskell.TH hiding (Type)
import qualified Language.Haskell.TH as TH (Type)
import Language.Haskell.Meta.Parse (parseType)
import Yesod.Routes.TH.Types

import Control.Monad.Fail (MonadFail(..))

import Data.Data (Data)
import Data.Kind (Type)
import GHC.Exts (Constraint)
  
import Data.Swagger

import qualified Data.Set as Set

import Network.HTTP.Types.Status
import Network.HTTP.Types.URI

import Control.Monad.Trans.Class (MonadTrans)
import Control.Monad.Catch (MonadThrow(..), MonadCatch, MonadMask)
import Control.Monad.Base (MonadBase)
import Control.Monad.Trans.Control (MonadBaseControl)
import Control.Monad.Error.Class (MonadError)

import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import Data.Text.Lens (packed)

import Data.Typeable (eqT, typeRep)

import Network.URI
import Network.URI.Lens
import GHC.TypeLits (KnownSymbol, symbolVal, KnownNat)

import Text.Read (Read(readPrec), readP_to_Prec, readPrec_to_P)
import Text.Show (showParen, showString)
import qualified Text.ParserCombinators.ReadP as R
import qualified Data.Char as Char

import Yesod.Servant.HttpApiDataInjective
  
import qualified Data.ByteString.Base64.URL as Base64 (encode)
import qualified Data.Binary.Builder as Builder

import Database.Persist

import Data.CryptoID.Class.ImplicitNamespace

import Control.Monad.Logger


renderServantRoute :: Link -> ([Text], [(Text, Text)])
renderServantRoute link
  = ( linkSegments    link <&> pack . unEscapeString
    , linkQueryParams link <&> paramToPair
    )
  where paramToPair (FlagParam      str    ) = (pack $ unEscapeString str, Text.empty)
        paramToPair (ArrayElemParam str val) = (pack $ unEscapeString str, val       )
        paramToPair (SingleParam    str val) = (pack $ unEscapeString str, val       )


escapedSymbol :: forall sym. KnownSymbol sym => Proxy sym -> Text
escapedSymbol _ = pack . escapeURIString isUnreserved . symbolVal $ Proxy @sym

class HasLink api => HasRoute api where
  parseServantRoute :: forall proxy. ServantApiUnproxy' proxy ~ api => ([Text], [(Text, Text)]) -> Maybe (Route (ServantApi proxy))

instance HasRoute EmptyAPI where
  parseServantRoute _ = Nothing

instance (Typeable m, Typeable k) => HasRoute (NoContentVerb (m :: k)) where
  parseServantRoute ([], _) = Just $ ServantApiRoute (Proxy @(NoContentVerb m)) id mempty mempty
  parseServantRoute  _      = Nothing

instance (Typeable m, Typeable k, Typeable s, Typeable ct, Typeable a, IsSubList ct ct ~ (() :: Constraint)) => HasRoute (Verb (m :: k) s ct a) where
  parseServantRoute ([], _) = Just $ ServantApiRoute (Proxy @(Verb m s ct a)) id mempty mempty
  parseServantRoute  _      = Nothing

instance (Typeable m, Typeable k, Typeable status, Typeable fr, Typeable ct, Typeable a) => HasRoute (Stream (m :: k) status fr ct a) where
  parseServantRoute ([], _) = Just $ ServantApiRoute (Proxy @(Stream m status fr ct a)) id mempty mempty
  parseServantRoute  _      = Nothing

instance HasRoute sub => HasRoute (HttpVersion :> sub) where
  parseServantRoute args = parseServantRoute @sub @(ServantApiDirect sub) args <&> \case
    ServantApiRoute (_ :: Proxy endpoint) f ps qs -> ServantApiRoute (Proxy @(HttpVersion :> endpoint)) f ps qs
    ServantApiBaseRoute -> ServantApiBaseRoute
  
instance HasRoute sub => HasRoute (Vault :> sub) where
  parseServantRoute args = parseServantRoute @sub @(ServantApiDirect sub) args <&> \case
    ServantApiRoute (_ :: Proxy endpoint) f ps qs -> ServantApiRoute (Proxy @(Vault :> endpoint)) f ps qs
    ServantApiBaseRoute -> ServantApiBaseRoute

instance (HasRoute sub, KnownSymbol realm, Typeable a) => HasRoute (BasicAuth realm a :> sub) where
  parseServantRoute args = parseServantRoute @sub @(ServantApiDirect sub) args <&> \case
    ServantApiRoute (_ :: Proxy endpoint) f ps qs -> ServantApiRoute (Proxy @(BasicAuth realm a :> endpoint)) f ps qs
    ServantApiBaseRoute -> ServantApiBaseRoute
  
instance (HasRoute sub, KnownSymbol s) => HasRoute (Description s :> sub) where
  parseServantRoute args = parseServantRoute @sub @(ServantApiDirect sub) args <&> \case
    ServantApiRoute (_ :: Proxy endpoint) f ps qs -> ServantApiRoute (Proxy @(Description s :> endpoint)) f ps qs
    ServantApiBaseRoute -> ServantApiBaseRoute
  
instance (HasRoute sub, KnownSymbol s) => HasRoute (Summary s :> sub) where
  parseServantRoute args = parseServantRoute @sub @(ServantApiDirect sub) args <&> \case
    ServantApiRoute (_ :: Proxy endpoint) f ps qs -> ServantApiRoute (Proxy @(Summary s :> endpoint)) f ps qs
    ServantApiBaseRoute -> ServantApiBaseRoute
  
instance (HasRoute sub, Typeable tag, Typeable k) => HasRoute (AuthProtect (tag :: k) :> sub) where
  parseServantRoute args = parseServantRoute @sub @(ServantApiDirect sub) args <&> \case
    ServantApiRoute (_ :: Proxy endpoint) f ps qs -> ServantApiRoute (Proxy @(AuthProtect tag :> endpoint)) f ps qs
    ServantApiBaseRoute -> ServantApiBaseRoute

instance HasRoute sub => HasRoute (IsSecure :> sub) where
  parseServantRoute args = parseServantRoute @sub @(ServantApiDirect sub) args <&> \case
    ServantApiRoute (_ :: Proxy endpoint) f ps qs -> ServantApiRoute (Proxy @(IsSecure :> endpoint)) f ps qs
    ServantApiBaseRoute -> ServantApiBaseRoute

instance HasRoute sub => HasRoute (RemoteHost :> sub) where
  parseServantRoute args = parseServantRoute @sub @(ServantApiDirect sub) args <&> \case
    ServantApiRoute (_ :: Proxy endpoint) f ps qs -> ServantApiRoute (Proxy @(RemoteHost :> endpoint)) f ps qs
    ServantApiBaseRoute -> ServantApiBaseRoute

instance (HasRoute sub, Typeable mods, Typeable restr) => HasRoute (CaptureBearerRestriction' mods restr :> sub) where
  parseServantRoute args = parseServantRoute @sub @(ServantApiDirect sub) args <&> \case
    ServantApiRoute (_ :: Proxy endpoint) f ps qs -> ServantApiRoute (Proxy @(CaptureBearerRestriction' mods restr :> endpoint)) f ps qs
    ServantApiBaseRoute -> ServantApiBaseRoute

instance (HasRoute sub, Typeable mods) => HasRoute (CaptureBearerToken' mods :> sub) where
  parseServantRoute args = parseServantRoute @sub @(ServantApiDirect sub) args <&> \case
    ServantApiRoute (_ :: Proxy endpoint) f ps qs -> ServantApiRoute (Proxy @(CaptureBearerToken' mods :> endpoint)) f ps qs
    ServantApiBaseRoute -> ServantApiBaseRoute

instance (KnownSymbol sym, HasRoute sub, HasLink sub) => HasRoute (sym :> sub) where
  parseServantRoute (p : ps, qs)
    | p == escapedSymbol (Proxy @sym)
    = parseServantRoute @sub @(ServantApiDirect sub) (ps, qs) <&> \case
        ServantApiRoute (_ :: Proxy endpoint) f ps' qs' -> ServantApiRoute (Proxy @(sym :> endpoint)) f (escapedSymbol (Proxy @sym) : ps') qs'
        ServantApiBaseRoute -> ServantApiBaseRoute
  parseServantRoute _ = Nothing

instance (HasRoute a, HasRoute b) => HasRoute (a :<|> b) where
  parseServantRoute args = asum
    [ parseServantRoute @a @(ServantApiDirect a) args <&> \case
        ServantApiRoute (_ :: Proxy endpoint) f ps qs -> ServantApiRoute (Proxy @endpoint) f ps qs
        ServantApiBaseRoute -> ServantApiBaseRoute
    , parseServantRoute @b @(ServantApiDirect b) args <&> \case
        ServantApiRoute (_ :: Proxy endpoint) f ps qs -> ServantApiRoute (Proxy @endpoint) f ps qs
        ServantApiBaseRoute -> ServantApiBaseRoute
    ]

instance (HasRoute sub, Typeable mods, Typeable ct, Typeable a) => HasRoute (ReqBody' mods ct a :> sub) where
  parseServantRoute args = parseServantRoute @sub @(ServantApiDirect sub) args <&> \case
    ServantApiRoute (_ :: Proxy endpoint) f ps qs -> ServantApiRoute (Proxy @(ReqBody' mods ct a :> endpoint)) f ps qs
    ServantApiBaseRoute -> ServantApiBaseRoute
  
instance (HasRoute sub, Typeable mods, Typeable framing, Typeable ct, Typeable a) => HasRoute (StreamBody' mods framing ct a :> sub) where
  parseServantRoute args = parseServantRoute @sub @(ServantApiDirect sub) args <&> \case
    ServantApiRoute (_ :: Proxy endpoint) f ps qs -> ServantApiRoute (Proxy @(StreamBody' mods framing ct a :> endpoint)) f ps qs
    ServantApiBaseRoute -> ServantApiBaseRoute

instance (HasRoute sub, KnownSymbol sym, Typeable mods, Typeable a) => HasRoute (Header' mods sym (a :: Type) :> sub) where
  parseServantRoute args = parseServantRoute @sub @(ServantApiDirect sub) args <&> \case
    ServantApiRoute (_ :: Proxy endpoint) f ps qs -> ServantApiRoute (Proxy @(Header' mods sym a :> endpoint)) f ps qs
    ServantApiBaseRoute -> ServantApiBaseRoute

instance (HasRoute sub, Typeable mods, KnownSymbol sym, Typeable v, ToHttpApiDataInjective v, FromHttpApiData v) => HasRoute (Capture' mods sym (v :: Type) :> sub) where
  parseServantRoute (p : ps, qs)
    | Right v <- parseUrlPiece @v p
    = parseServantRoute @sub @(ServantApiDirect sub) (ps, qs) <&> \case
        ServantApiRoute (_ :: Proxy endpoint) f ps' qs' -> ServantApiRoute (Proxy @(Capture' mods sym v :> endpoint)) (f . ($ v)) (toUrlPieceInjective v : ps') qs'
        ServantApiBaseRoute -> ServantApiBaseRoute
  parseServantRoute _ = Nothing

instance (HasRoute sub, Typeable mods, KnownSymbol sym, Typeable plaintext, ToHttpApiDataInjective ciphertext, FromHttpApiData ciphertext, Typeable ciphertext) => HasRoute (CaptureCryptoID' mods ciphertext sym plaintext :> sub) where
  parseServantRoute (p : ps, qs)
    | Right v <- parseUrlPiece @(CryptoID ciphertext plaintext) p
    = parseServantRoute @sub @(ServantApiDirect sub) (ps, qs) <&> \case
        ServantApiRoute (_ :: Proxy endpoint) f ps' qs' -> ServantApiRoute (Proxy @(CaptureCryptoID' mods ciphertext sym plaintext :> endpoint)) (f . ($ v)) (toUrlPieceInjective v : ps') qs'
        ServantApiBaseRoute -> ServantApiBaseRoute
  parseServantRoute _ = Nothing

instance (HasRoute sub, KnownNat major, KnownNat minor, KnownNat patch) => HasRoute (ApiVersion major minor patch :> sub) where
  parseServantRoute args = parseServantRoute @sub @(ServantApiDirect sub) args <&> \case
    ServantApiRoute (_ :: Proxy endpoint) f ps qs -> ServantApiRoute (Proxy @(ApiVersion major minor patch :> endpoint)) f ps qs
    ServantApiBaseRoute -> ServantApiBaseRoute


data ServantApi (proxy :: k) = ServantApi
  deriving (Eq, Ord, Read, Show, Generic, Typeable)
  deriving anyclass (Hashable)

getServantApi :: forall master proxy. master -> ServantApi proxy
getServantApi _ = ServantApi

type family ServantApiUnproxy (proxy :: k) :: Type

type ServantApiUnproxy' :: forall k. forall (proxy :: k) -> Type
type family ServantApiUnproxy' proxy where
  ServantApiUnproxy' @Type (ServantApiDirect api) = api
  ServantApiUnproxy' @k' proxy = ServantApiUnproxy proxy

data ServantApiDirect (api :: Type)
type instance ServantApiUnproxy (ServantApiDirect api) = api
  
instance HasRoute (ServantApiUnproxy' proxy) => RenderRoute (ServantApi proxy) where
  data Route (ServantApi proxy)
    = forall endpoint.
      ( IsElem endpoint (ServantApiUnproxy' proxy) ~ (() :: Constraint)
      , HasRoute endpoint
      , Typeable endpoint
      )
   => ServantApiRoute
      (Proxy endpoint)
      (forall a. MkLink endpoint a -> a)
      [Text] (HashMap Text [Text])
    | ServantApiBaseRoute
  renderRoute (ServantApiRoute (_ :: Proxy endpoint) f _ _) = f $ safeLink' renderServantRoute (Proxy @(ServantApiUnproxy' proxy)) (Proxy @endpoint)
  renderRoute ServantApiBaseRoute = mempty

instance HasRoute (ServantApiUnproxy' proxy) => Eq (Route (ServantApi proxy)) where
  (ServantApiRoute (_ :: Proxy endpoint) _ ps qs) == (ServantApiRoute (_ :: Proxy endpoint') _ ps' qs')
    = case eqT @endpoint @endpoint' of
        Just Refl -> ps == ps' && qs == qs'
        Nothing   -> False
  ServantApiBaseRoute == ServantApiBaseRoute = True
  _ == _ = False

instance HasRoute (ServantApiUnproxy' proxy) => Ord (Route (ServantApi proxy)) where
  compare (ServantApiRoute (_ :: Proxy endpoint) _ ps qs) (ServantApiRoute (_ :: Proxy endpoint') _ ps' qs')
    = case eqT @endpoint @endpoint' of
        Just Refl -> compare ps ps' <> compare qs qs'
        Nothing   -> typeRep (Proxy @endpoint) `compare` typeRep (Proxy @endpoint')
  compare ServantApiBaseRoute ServantApiBaseRoute = EQ
  compare ServantApiBaseRoute _ = LT
  compare _ ServantApiBaseRoute = GT

instance HasRoute (ServantApiUnproxy' proxy) => Hashable (Route (ServantApi proxy)) where
  hashWithSalt salt (ServantApiRoute (_ :: Proxy endpoint) _ ps qs) = salt `hashWithSalt` (0 :: Int) `hashWithSalt` typeRep (Proxy @endpoint) `hashWithSalt` ps `hashWithSalt` qs
  hashWithSalt salt ServantApiBaseRoute = salt `hashWithSalt` (1 :: Int)

instance HasRoute (ServantApiUnproxy' proxy) => Read (Route (ServantApi proxy)) where
  readPrec = readP_to_Prec $ \d -> asum
    [ ServantApiBaseRoute <$ R.string "ServantApiBaseRoute"
    , do
        when (d > 10) . void $ R.char '('
        R.skipSpaces
        void $ R.string "ServantApiRoute "
        R.skipSpaces
        void $ R.string "_ "
        R.skipSpaces
        asum [ do
                 void $ R.char '('
                 R.skipMany . R.manyTill (R.satisfy $ const True) $ R.char ')'
                 void $ R.char ' '
             , R.skipMany . R.manyTill (R.satisfy $ not . Char.isSpace) $ R.satisfy Char.isSpace
             ]
        R.skipSpaces
        ps <- readPrec_to_P readPrec 11
        void $ R.char ' '
        R.skipSpaces
        qs <- readPrec_to_P readPrec 11 :: R.ReadP (HashMap Text [Text])
        R.skipSpaces
        when (d > 10) . void $ R.char ')'
        maybe (fail "Could not parse servant route") return $ parseServantRoute (ps, ifoldMap (fmap . (,)) qs)
    ]
instance HasRoute (ServantApiUnproxy' proxy) => Show (Route (ServantApi proxy)) where
  showsPrec d (ServantApiRoute (_ :: Proxy endpoint) _ ps qs) = showParen (d > 10)
    $ showString "ServantApiRoute "
    . showsPrec 11 (typeRep $ Proxy @endpoint)
    . showString " _ "
    . showsPrec 11 ps
    . showString " "
    . showsPrec 11 qs
  showsPrec _ ServantApiBaseRoute = showString "ServantApiBaseRoute"

instance HasRoute (ServantApiUnproxy' proxy) => ParseRoute (ServantApi proxy) where
  parseRoute = parseServantRoute

newtype ServantErrorResponse
  = ServantErrorResponse { getServantErrorResponse :: W.Response }

class (HasServer (ServantApiUnproxy' proxy) context, HasRoute (ServantApiUnproxy' proxy), HasContextEntry (context .++ DefaultErrorFormatters) ErrorFormatters) => ServantApiDispatch context m master proxy | master proxy -> context m where
  servantContext         :: ServantApi proxy -> master -> Request -> Yesod.HandlerFor master (Context context)
  servantHoist           :: ServantApi proxy -> master -> Request -> Context context -> (forall a. m a -> Handler a)
  servantMiddleware      :: ServantApi proxy -> master -> Context context -> Middleware
  servantYesodMiddleware :: ServantApi proxy -> master -> Yesod.HandlerFor master Middleware
  servantServer          :: ServantApi proxy -> master -> ServerT (ServantApiUnproxy' proxy) m

instance ServantApiDispatch context m master proxy => YesodSubDispatch (ServantApi proxy) master where
  yesodSubDispatch YesodSubRunnerEnv{..} req
    = ysreParentRunner handlerT ysreParentEnv (ysreToParentRoute <$> route) req
    where
      master :: master
      master = yreSite ysreParentEnv
      proxy :: ServantApi proxy
      proxy = ysreGetSub master
      
      route = parseRoute ( W.pathInfo req
                         , over (traverse . _2) (fromMaybe Text.empty) . queryToQueryText $ W.queryString req
                         )
  
      handlerT :: Yesod.HandlerFor master Yesod.TypedContent
      handlerT = do
        yesodMiddleware <- servantYesodMiddleware proxy master
        ctx <- servantContext proxy master req

        let server' = hoistServerWithContext (Proxy @(ServantApiUnproxy' proxy)) (Proxy @context) (servantHoist proxy master req ctx) (servantServer proxy master)
            toTypedContent = error "Servant handler did not shortcircuit"
            sendResponse res = case yesodError of
              Just err -> do
                Yesod.cacheSet $ ServantErrorResponse res
                throwM . Yesod.HCError =<< liftIO (err <$> resText)
              Nothing -> do
                when (is _Nothing route) $
                  $(Yesod.logErrorS) "Servant" "Could not parse route even though servant responded successfully"

                Yesod.sendWaiResponse res
              where
                status = W.responseStatus res
                resText = toText <$> getResBS
                  where
                    toText bs = case Text.decodeUtf8' bs of
                      Right t -> t
                      Left  _ -> Text.decodeUtf8 $ Base64.encode bs
                    
                    (_, _, resStream) = W.responseToStream res
                    getResBS = resStream $ \runStream -> do
                      resVar <- newTVarIO Builder.empty
                      runStream (\chunk -> atomically $ modifyTVar' resVar (<> chunk)) (return ())
                      toStrict . Builder.toLazyByteString <$> readTVarIO resVar
                
                yesodError :: Maybe (Text -> Yesod.ErrorResponse)
                yesodError
                  | status == notFound404
                  = Just $ const Yesod.NotFound
                  | status == internalServerError500
                  = Just   Yesod.InternalError
                  | status == badRequest400
                  = Just $ Yesod.InvalidArgs . pure
                  | status == unauthorized401
                  = Just $ const Yesod.NotAuthenticated
                  | status == forbidden403
                  = Just   Yesod.PermissionDenied
                  | status == methodNotAllowed405
                  = Just . const . Yesod.BadMethod $ W.requestMethod req
                  | otherwise = Nothing
  
        fmap toTypedContent . withUnliftIO $ \UnliftIO{..} ->
          (yesodMiddleware . servantMiddleware proxy master ctx $ serveWithContext (Proxy @(ServantApiUnproxy' proxy)) ctx server') req $ unliftIO . sendResponse

servantApiLink :: forall p1 p2 proxy endpoint.
                  ( IsElem endpoint (ServantApiUnproxy' proxy) ~ (() :: Constraint), HasRoute (ServantApiUnproxy' proxy), HasLink endpoint, Typeable endpoint )
               => p1 proxy
               -> p2 endpoint
               -> MkLink endpoint (Route (ServantApi proxy))
servantApiLink _ _ = safeLink' (fromMaybe (error "Could not parse result of safeLink'") . guardEndpoint . parseServantRoute @(ServantApiUnproxy' proxy) . renderServantRoute) (Proxy @(ServantApiUnproxy' proxy)) (Proxy @endpoint)
  where
    guardEndpoint :: Maybe (Route (ServantApi proxy)) -> Maybe (Route (ServantApi proxy))
    guardEndpoint x@(Just (ServantApiRoute (_ :: Proxy endpoint') _ _ _))
      | Just Refl <- eqT @endpoint @endpoint' = x
    guardEndpoint _ = Nothing

  
class HasServantHandlerContext site where
  data ServantHandlerContextFor site :: Type
  getSCtxSite :: ServantHandlerContextFor site -> site
  getSCtxRequest :: ServantHandlerContextFor site -> Request
  
newtype ServantHandlerFor site a = ServantHandlerFor { unServantHandlerFor :: ServantHandlerContextFor site -> Handler a }
  deriving (Generic, Typeable)
  deriving (Monad, Functor, Applicative, MonadIO, MonadThrow, MonadCatch, MonadMask, MonadBase IO, MonadBaseControl IO, MonadError ServerError) via (ReaderT (ServantHandlerContextFor site) Handler)

instance MonadUnliftIO (ServantHandlerFor site) where
  withRunInIO cont
    = ServantHandlerFor $ \app -> withRunInIO $ \unliftHandler -> cont (unliftHandler . flip unServantHandlerFor app)

getServantContext :: (site ~ site', MonadServantHandler site m) => m (ServantHandlerContextFor site')
getServantContext = liftServantHandler $ ServantHandlerFor return
  
getsServantContext :: (site ~ site', MonadServantHandler site m) => (ServantHandlerContextFor site' -> a) -> m a 
getsServantContext = liftServantHandler . ServantHandlerFor . (return .)

getYesodApproot :: (Yesod site, MonadSite site m, MonadRequest m) => m Text
getYesodApproot = Yesod.getApprootText Yesod.approot <$> getSite <*> getRequest

renderRouteAbsolute :: (Yesod site, MonadSite site m, MonadRequest m) => Route site -> m URI
renderRouteAbsolute (renderRoute -> (ps, qs)) = addRoute . unpack <$> getYesodApproot 
  where addRoute root = case parseURI root of
          Just root' -> root' & uriPathLens . packed %~ addPath
                              & uriQueryLens . packed %~ addQuery
          Nothing -> error "Could not parse approot as URI"
        addPath p = p <> "/" <> Text.intercalate "/" ps
        addQuery q | null qs = q
        addQuery "" = "?" <> Text.intercalate "&" (map (\(q, v) -> q <> "=" <> v) qs)
        addQuery "?" = addQuery ""
        addQuery q = q <> "&" <> tailEx (addQuery "")

servantApiBaseUrl :: (Yesod site, MonadSite site m, MonadRequest m, MonadThrow m) => (Route (ServantApi proxy) -> Route site) -> m BaseUrl
servantApiBaseUrl = parseBaseUrl . ($ mempty). uriToString (const "") <=< renderRouteAbsolute . ($ ServantApiBaseRoute)

class (MonadIO m, HasServantHandlerContext site) => MonadServantHandler site m | m -> site where
  liftServantHandler :: forall a. ServantHandlerFor site a -> m a

instance HasServantHandlerContext site => MonadServantHandler site (ServantHandlerFor site) where
  liftServantHandler = id

instance (MonadTrans t, MonadIO (t (ServantHandlerFor site)), HasServantHandlerContext site) => MonadServantHandler site (t (ServantHandlerFor site)) where
  liftServantHandler = lift

class MonadIO m => MonadHandler m where
  liftHandler :: forall a. Handler a -> m a

instance MonadHandler (ServantHandlerFor site) where
  liftHandler = ServantHandlerFor . const
  
instance (MonadTrans t, MonadIO (t (ServantHandlerFor site))) => MonadHandler (t (ServantHandlerFor site)) where
  liftHandler = lift . ServantHandlerFor . const

class Monad m => MonadSite site m | m -> site where
  getSite :: m site

  getsSite :: (site -> a) -> m a
  getsSite f = f <$> getSite

instance HasServantHandlerContext site => MonadSite site (ServantHandlerFor site) where
  getSite = liftServantHandler . ServantHandlerFor $ return . getSCtxSite

instance MonadSite site (Reader site) where
  getSite = ask
  getsSite = asks

instance {-# OVERLAPPABLE #-} (Yesod.MonadHandler m, site ~ Yesod.HandlerSite m) => MonadSite site m where
  getSite = Yesod.getYesod
  getsSite = Yesod.getsYesod

instance {-# OVERLAPPING #-} (MonadTrans t, Monad (t (ServantHandlerFor site)), HasServantHandlerContext site) => MonadSite site (t (ServantHandlerFor site)) where
  getSite = lift getSite
  getsSite = lift . getsSite

class Monad m => MonadRequest m where
  getRequest :: m Request

instance HasServantHandlerContext site => MonadRequest (ServantHandlerFor site) where
  getRequest = liftServantHandler . ServantHandlerFor $ return . getSCtxRequest

instance {-# OVERLAPPABLE #-} (Yesod.MonadHandler m, Monad m) => MonadRequest m where
  getRequest = Yesod.waiRequest

instance {-# OVERLAPPING #-} (MonadTrans t, Monad (t (ServantHandlerFor site)), HasServantHandlerContext site) => MonadRequest (t (ServantHandlerFor site)) where
  getRequest = lift getRequest

  
type ServantDBFor site = ReaderT (Yesod.YesodPersistBackend site) (ServantHandlerFor site)

class Yesod.YesodPersist site => ServantPersist site where
  runDB :: forall a. ServantDBFor site a -> ServantHandlerFor site a

defaultRunDB :: ( PersistConfig c
                , ServantDBFor site a ~ PersistConfigBackend c (ServantHandlerFor site) a
                , HasServantHandlerContext site
                )
             => Getting c site c
             -> Getting (PersistConfigPool c) site (PersistConfigPool c)
             -> ServantDBFor site a -> ServantHandlerFor site a
defaultRunDB confL poolL f = do
  app <- getSite
  runPool (app ^. confL) f (app ^. poolL)


class ServantLog site where
  servantLogLog :: (MonadIO m, ToLogStr msg) => site -> Loc -> LogSource -> LogLevel -> msg -> m ()

newtype ServantLogYesod site = ServantLogYesod { unServantLogYesod :: site }

instance Yesod site => ServantLog (ServantLogYesod site) where
  servantLogLog (ServantLogYesod app) a b c (toLogStr -> d) = liftIO $ do
    logger <- Yesod.makeLogger app
    Yesod.messageLoggerSource app logger a b c d

instance (ServantLog site, HasServantHandlerContext site) => MonadLogger (ServantHandlerFor site) where
  monadLoggerLog a b c d = do
    app <- getSite
    servantLogLog app a b c d

instance (ServantLog site, HasServantHandlerContext site) => MonadLoggerIO (ServantHandlerFor site) where
  askLoggerIO = servantLogLog <$> getSite


newtype PathPieceHttpApiData a = PathPieceHttpApiData { unPathPieceHttpApiData :: a }
  deriving (Eq, Ord, Read, Show, Generic, Typeable, Data)
  deriving newtype (PathPiece, ToParamSchema)
instance PathPiece a => FromHttpApiData (PathPieceHttpApiData a) where
  parseUrlPiece = maybe (Left "Could not convert from HttpApiData via PathPiece") Right . fromPathPiece
instance PathPiece a => ToHttpApiData (PathPieceHttpApiData a) where
  toUrlPiece = toPathPiece
  

mkYesodApi :: Name -> [ResourceTree String] -> DecsQ
mkYesodApi (nameBase -> masterN) ress = do
  let toPiecesApi :: [Piece String]
                  -> ResourceTree String
                  -> MaybeT Q [([Piece String], TH.Type, [Text])]
      toPiecesApi ps (ResourceLeaf Resource{..}) = do
        Subsite{..} <- pure resourceDispatch
        Just tn <- lift $ lookupTypeName subsiteType
        TyConI (TySynD _ [] (ConT conN `AppT` apiT)) <- lift $ reify tn
        guard $ conN == ''ServantApi
        return $ pure (ps <> resourcePieces, ConT ''ServantApiUnproxy' `AppT` apiT, map pack resourceAttrs)
      toPiecesApi ps (ResourceParent _ _ ps' cs)
        = lift . fmap concat $ mapMaybeM (toPiecesApi (ps <> ps')) cs
  apiRess <- concat <$> mapMaybeM (toPiecesApi []) ress

  let apiType
        | Just apiRess' <- fromNullable $ map apiEndpoint apiRess
        = ofoldr1 (\e acc -> conT ''(:<|>) `appT` e `appT` acc) apiRess'
        | otherwise
        = conT ''EmptyAPI

      apiEndpoint (pieces, apiT, attrs) = withAuth attrs $
        foldr (\p acc -> conT ''(:>) `appT` apiPiece p `appT` acc) (return apiT) pieces

      withAuth attrs typ = case authDNF of
        Left t
          -> fail $ "Invalid auth tag: " <> unpack t
        Right PredDNF{..} | (impureNonNull . Set.singleton $ PLVariable AuthFree) `Set.member` dnfTerms
          -> typ
        Right PredDNF{..} | (impureNonNull . Set.singleton $ PLVariable AuthToken) `Set.member` dnfTerms
          -> conT ''(:>) `appT` conT ''BearerAuth `appT` typ
        Right _
          -> conT ''(:>) `appT` conT ''SessionAuth `appT` typ
        where authDNF = parsePredDNF defaultAuthDNF attrs

      apiPiece (Static str) = litT $ strTyLit str
      apiPiece (Dynamic str) = conT ''PathPieceHttpApiData `appT` either fail return (parseType str)

  sequence
    [ tySynD (mkName $ masterN <> "Api") [] apiType
    ]
