{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE NoPolyKinds         #-}
-- |
-- Module:      Yam
-- Copyright:   (c) 2019 Daniel YU
-- License:     BSD3
-- Maintainer:  leptonyu@gmail.com
-- Stability:   experimental
-- Portability: portable
--
-- A out-of-the-box wrapper of [servant](https://hackage.haskell.org/package/servant-server),
-- providing configuration loader [salak](https://hackage.haskell.org/package/salak) and flexible extension with 'AppMiddleware'.
--
module Yam(
  -- * How to use this library
  -- $use

  -- * Yam Server
    start
  , start'
  , serveWarp
  -- ** Application Configuration
  , AppConfig(..)
  -- ** Application Context
  , AppT
  , AppV
  , AppIO
  , AppSimple
  , Simple
  , runAppT
  , runVault
  , throwS
  -- ** Application Middleware
  , AppMiddleware(..)
  , emptyAM
  , simpleContext
  , simpleConfig
  , simpleConfig'
  , simpleMiddleware
  -- *** Health
  , HealthStatus(..)
  , HealthResult(..)
  , mergeHealth
  -- * Modules
  -- ** Logger
  , LogConfig(..)
  , HasLogger
  , LogFuncHolder
  , VaultHolder
  -- ** Context
  , Context(..)
  , HasContextEntry(..)
  , TryContextEntry(..)
  , getEntry
  , tryEntry
  -- ** Swagger
  , SwaggerConfig(..)
  , serveWithContextAndSwagger
  , baseInfo
  , SwaggerTag
  -- * Reexport
  , spanNoNotifier
  , Span(..)
  , SpanContext(..)
  , SpanTag(..)
  , SpanReference(..)
  , showText
  , randomString
  , randomCode
  , decodeUtf8
  , encodeUtf8
  , pack
  , liftIO
  , fromMaybe
  , throw
  , logInfo
  , logError
  , logWarn
  , logDebug
  , RunSalak(..)
  ) where

import qualified Control.Category               as C
import           Control.Monad.IO.Unlift
import           Control.Monad.Logger.CallStack
import           Data.Opentracing
import           Data.Text                      (pack)
import           Network.Wai
import           Salak
import           Servant
import           Servant.Swagger
import           Yam.App
import           Yam.Config
import           Yam.Logger
import           Yam.Middleware
import           Yam.Middleware.Error
import           Yam.Middleware.Trace
import           Yam.Prelude
import           Yam.Server
import           Yam.Server.Health
import           Yam.Swagger

-- | Standard Starter of Yam.
start
  :: forall file api cxt
  . ( HasLoad file
    , HasLogger cxt
    , HasServer api  cxt
    , HasSwagger api)
  => String -- ^ File config name
  -> file -- ^ Config file format
  -> Version -- ^ Version
  -> RunSalak (AppMiddleware Simple cxt) -- ^ Application Middleware
  -> Proxy api -- ^ Application API Proxy
  -> RunSalak (ServerT api (AppV cxt IO)) -- ^ Application API Server
  -> IO ()
start cfg file = start' (loadSalakFile cfg file)

-- | Standard Starter of Yam.
start'
  :: forall api cxt
  .(HasLogger cxt
  , HasServer api  cxt
  , HasSwagger api)
  => LoadSalakT IO ()
  -> Version
  -> RunSalak (AppMiddleware Simple cxt)
  -> Proxy api
  -> RunSalak (ServerT api (AppV cxt IO))
  -> IO ()
start' load ver mkAMD pSer mkSer = loadAndRunSalak load $ do
  c                    <- requireD "logging"
  app@AppConfig{..}    <- require  "application"
  sw@SwaggerConfig{..} <- require  "swagger"
  withLogger name c $ \logger -> unSalak $ do
    let portText = showText port
        baseCxt = (LF logger :. EmptyContext)
    logInfo $ "Start Service [" <> name <> "] ..."
    amd <- mkAMD
    ser <- mkSer
    f   <- askUnliftIO
    liftX $ runAM amd baseCxt id emptyHealth $ \cxt middleware hr -> lift $ unliftIO f $ do
      (en, aep) <- actuatorEndpoint hr
      readLogs >>= mapM_ (logInfo . ("Parsing " <>))
      when enabled $
        logInfo    $ "Swagger enabled: http://localhost:" <> portText <> "/" <> pack urlDir
      logInfo      $ "Servant started on port(s): "       <> portText
      let go :: forall a. (HasSwagger a, HasServer a cxt) => Proxy a -> ServerT a (AppV cxt IO) -> RunSalak ()
          go x y = liftIO
            $ serveWarp app
            $ traceMiddleware (\v -> runAppT (VH v :. cxt) . spanNoNotifier)
            $ middleware
            $ errorMiddleware baseCxt
            $ serveWithContextAndSwagger sw (baseInfo hostname name ver port) (Proxy @(Vault :> a)) cxt
            $ \v -> hoistServerWithContext x (Proxy @cxt) (nt cxt v) y
      if en
        then go (Proxy @(api :<|> ActuatorEndpoint)) (ser :<|> aep)
        else go pSer ser

-- | default http server by warp.
serveWarp :: AppConfig -> Application -> IO ()
serveWarp AppConfig{..} = runSettings
  $ defaultSettings
  & setPort port
  & setOnException (\_ _ -> return ())
  & setOnExceptionResponse whenException
  & setSlowlorisSize slowlorisSize

-- | Empty span notifier.
spanNoNotifier :: Span -> AppV cxt IO ()
spanNoNotifier _ = return ()

-- | Empty Application Middleware.
emptyAM :: AppMiddleware cxt cxt
emptyAM = C.id

-- | Simple Application context
type Simple = '[LogFuncHolder]

-- | Simple Application with logger context.
type AppSimple = AppV Simple IO


-- $use
--
-- > import           Salak.Yaml
-- > import           Servant
-- > import           Yam
-- > import           Data.Version
-- >
-- > type API = "hello" :> Get '[PlainText] Text
-- >
-- > service :: ServerT API AppSimple
-- > service = return "world"
-- >
-- > main = start "app" YAML (makeVersion []) (return emptyAM) (Proxy @API) (return service)