{-# LANGUAGE RankNTypes #-}
-- | This modules provides function that help start the Warp web
-- server using systemd's socket activation feature.
module Network.Wai.Handler.Warp.Systemd
  ( runSystemdWarp
    -- * Settings
  , SystemdSettings
  , defaultSystemdSettings

  , logInfo
  , setLogInfo

  , logWarn
  , setLogWarn

  , requireSocketActivation
  , setRequireSocketActivation

  , heartbeatInterval
  , setHeartbeatInterval

  , onBeginShutdown
  , setOnBeginShutdown

    -- * Low-level Settings
  , dontOverrideInstallShutdownHandler, setDontOverrideInstallShutdownHandler
    -- * Exceptions
  , SocketActivationException(..)
  ) where

import           Control.Concurrent       (forkIO, threadDelay)
import           Control.Exception
import           Control.Monad
import           Data.Function
import           Data.Typeable
import           Network.Socket           (withFdSocket, setNonBlockIfNeeded)
import           Network.Wai              as Wai
import           Network.Wai.Handler.Warp as Warp
import qualified Network.Wai.Handler.Warp.Internal as WarpInternal
import qualified System.Systemd.Daemon    as Systemd
import qualified System.IO as SIO
import qualified System.Posix.Signals as Signals

-- | These only occur during startup.
data SocketActivationException = SocketActivationException String
  deriving (Show, Typeable)

instance Exception SocketActivationException

-- | Warp-systemd integration settings. See the lenses in this module for details.
--
-- Note that Warp itself has some settings related to the server process lifecycle, for example 'Warp.setInstallShutdownHandler'.

data SystemdSettings =
  SystemdSettings
  { _logInfo :: String -> IO ()
  , _logWarn :: String -> IO ()
  , _requireSocketActivation :: Bool
  , _heartbeatInterval :: Maybe Int
  , _dontOverrideInstallShutdownHandler :: Bool
  , _onBeginShutdown :: IO ()
  }

-- | Default settings. See the lenses in this module for details.
defaultSystemdSettings :: SystemdSettings
defaultSystemdSettings = SystemdSettings
  { _logInfo = SIO.hPutStrLn SIO.stderr
  , _logWarn = SIO.hPutStrLn SIO.stderr . ("WARNING: " ++)
  , _requireSocketActivation = False
  , _heartbeatInterval = Nothing
  , _dontOverrideInstallShutdownHandler = False
  , _onBeginShutdown = return ()
  }

-- | How to log an info message.
--
-- Default: @hPutStrLn stderr@
logInfo :: Lens' SystemdSettings (String -> IO ())
logInfo = lens _logInfo setLogInfo

-- | How to log an info message.
--
-- Default: @hPutStrLn stderr . ("WARNING: " ++)@
logWarn :: Lens' SystemdSettings (String -> IO ())
logWarn = lens _logWarn setLogWarn

-- | If True, 'runSystemdWarp' throw a 'SocketActivationException' if
-- the server is started without socket activation.
--
-- Default: @False (continue)@
requireSocketActivation :: Lens' SystemdSettings Bool
requireSocketActivation = lens _requireSocketActivation setRequireSocketActivation

-- | If @Just n@, 'runSystemdWarp' emits a heartbeat notification to
-- systemd every @n@ seconds.
--
-- Default: @Nothing@
heartbeatInterval :: Lens' SystemdSettings (Maybe Int)
heartbeatInterval = lens _heartbeatInterval setHeartbeatInterval

-- | If @True@, do not override 'Warp.Settings'' with
-- 'setInstallShutdownHandler'. This lets you provide your own
-- shutdown handler functionality. Enabling this setting will
-- cause the default 'installShutdownHandler' to not be set,
-- with the effect of preventing the 'onBeginShutdown' action and
-- preventing the systemd ‘stopping’ notification.
-- 
--
-- Default: @Nothing@
dontOverrideInstallShutdownHandler :: Lens' SystemdSettings Bool
dontOverrideInstallShutdownHandler = lens _dontOverrideInstallShutdownHandler setDontOverrideInstallShutdownHandler

-- | Action to run on shutdown. This will be called when a shutdown
-- signal has been received from systemd and the listening socket has
-- been closed. This means that no new incoming requests will be
-- received, but previous requests are still being processed.
--
-- Control flow should return to the caller of 'runSystemdWarp' when
-- all requests have been handled.
--
-- Default: 'return ()'
onBeginShutdown :: Lens' SystemdSettings (IO ())
onBeginShutdown = lens _onBeginShutdown setOnBeginShutdown

-- | See 'logInfo'
setLogInfo :: (String -> IO ()) -> SystemdSettings -> SystemdSettings
setLogInfo x s = s { _logInfo = x }

-- | See 'logWarn'
setLogWarn :: (String -> IO ()) -> SystemdSettings -> SystemdSettings
setLogWarn x s = s { _logWarn = x }

-- | See 'requireSocketActivation'
setRequireSocketActivation :: Bool -> SystemdSettings -> SystemdSettings
setRequireSocketActivation x s = s { _requireSocketActivation = x }

-- | See 'heartbeatInterval'
setHeartbeatInterval :: Maybe Int -> SystemdSettings -> SystemdSettings
setHeartbeatInterval x s = s { _heartbeatInterval = x }

-- | See 'dontOverrideInstallShutdownHandler'
setDontOverrideInstallShutdownHandler :: Bool -> SystemdSettings -> SystemdSettings
setDontOverrideInstallShutdownHandler x s = s { _dontOverrideInstallShutdownHandler = x }

-- | See 'onBeginShutdown'
setOnBeginShutdown :: IO () -> SystemdSettings -> SystemdSettings
setOnBeginShutdown x s = s { _onBeginShutdown = x }




-- | Run a web application, see 'SystemdSettings' for details.
--
-- Note that Warp itself has some 'Warp.Settings' settings related to
-- the server process lifecycle, such as
-- 'Warp.setInstallShutdownHandler'. However, you do not have to
-- include a ready notification using 'Warp.setBeforeMainloop', because
-- 'runSystemdWarp' does this for you.
runSystemdWarp
  :: SystemdSettings
  -> Warp.Settings     -- ^ Web server settings
  -> Wai.Application   -- ^ Web application
  -> IO ()
runSystemdWarp saSettings settings app = do

  forM_ (_heartbeatInterval saSettings) $ \interval -> do
    forkIO (heartbeat (_logWarn saSettings) interval)

  socketActivationSockets <- Systemd.getActivatedSockets


  maybeSocket <- case socketActivationSockets of
    Just [socket] -> return (Just socket)

    Nothing | _requireSocketActivation saSettings ->
      throwIO (SocketActivationException "Socket activation is required to run this web application.")

    Nothing ->
      return Nothing

    Just [] ->
      throwIO (SocketActivationException "Socket activation seems active, but no sockets were passed to the process.")

    Just _ ->
      {- It is not entirely obvious how this should be implemented. When
         implementing, verify and document interaction with cleanup
         actions, notifications etc.
       -}
      throwIO (SocketActivationException "Multiple sockets were passed to the process, but only one socket was expected.")

  case maybeSocket of
    Just _ -> _logInfo saSettings "Warp is socket-activated"
    Nothing ->  _logInfo saSettings "Warp is not socket-activated"

  let
    inhibitIf :: Bool -> (a -> a) -> (a -> a)
    inhibitIf False x = x
    inhibitIf True  _ = id -- inhibited: leave unaltered

    settings' = settings
                & setBeforeMainLoop (do
                     WarpInternal.settingsBeforeMainLoop settings
                     void Systemd.notifyReady
                  )
                & inhibitIf (_dontOverrideInstallShutdownHandler saSettings) (
                     setInstallShutdownHandler $ \closeListenSocket ->
                         -- Maybe append/prepend this to the old setting?
                         -- But what about multiple sockets?
                         -- No obvious semantics to implement, sadly.
                         -- If multi-socket is needed, do the research and
                         -- probably create a bunch of new settings with
                         -- compatible defaults...
                         let handler = Signals.Catch $ do
                               void Systemd.notifyStopping
                               closeListenSocket
                               _onBeginShutdown saSettings
                         in void $ Signals.installHandler Signals.sigTERM handler Nothing
                     )

  case maybeSocket of
    Just socket -> do
      withFdSocket socket $ \fd -> do
        setNonBlockIfNeeded fd
        runSettingsSocket settings' socket app
    Nothing ->
      runSettings settings' app

heartbeat :: (String -> IO ()) -> Int -> IO ()
heartbeat flogWarn delaySeconds = loop where
  loop = do
    let delayMicroSeconds = delaySeconds * 1000 * 1000
    r <- Systemd.notifyWatchdog
    case r of
      Nothing -> do
        flogWarn "Systemd heartbeat notification does not seem to arrive. Stopping heartbeat notifications."
        return ()
      Just _ -> do
        threadDelay delayMicroSeconds
        loop

---------------- Minimal dependency-free lens ----------------

-- | Traverse a single element. The essence of getting and setting.
type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t
-- | Monomorphic 'Lens'
type Lens' s a = Lens s s a a

lens :: (s -> a) -> (b -> s -> t) -> Lens s t a b
lens sa sbt afb s = flip sbt s <$> afb (sa s)