{-# LANGUAGE CPP, OverloadedStrings, UnicodeSyntax #-}

module Network.Wai.Cli where

import qualified Network.Wai.Handler.CGI as CGI
import           Network.Wai.Handler.Warp hiding (run)
#ifdef WaiCliTLS
import           Network.Wai.Handler.WarpTLS
#endif
#ifdef WaiCliFastCGI
import qualified Network.Wai.Handler.FastCGI as FCGI
#endif
import           Network.Wai.Middleware.RequestLogger (logStdoutDev)
#ifdef WaiCliUnix
import           Network.Wai (responseLBS, Application)
import           Network.HTTP.Types (serviceUnavailable503)
import           Network.Socket.Activation
import           System.Posix.Internals (setNonBlockingFD)
import           System.Posix.Signals (installHandler, sigTERM, Handler(CatchOnce))
import           Data.Streaming.Network (bindPath, bindPortTCP)
import           Control.Monad
import           Control.Monad.Trans (liftIO)
import           Control.Concurrent.STM
import           GHC.Conc (getNumCapabilities, forkIO)
#else
import           Network.Wai (Application)
import           Data.Streaming.Network (bindPortTCP)
import           GHC.Conc (getNumCapabilities)
#endif
import qualified Network.Socket as S
import           System.Console.ANSI
import           Control.Exception (bracket)
import           Options
import           Data.List (intercalate)
import           Data.String (fromString)
import           Data.IP (fromHostAddress, fromHostAddress6)

data GracefulMode = ServeNormally | Serve503

data WaiOptions = WaiOptions
  { wHttpPort                 Int
  , wHttpHost                 String
#ifdef WaiCliUnix
  , wUnixSock                 String
#endif
  , wProtocol                 String
#ifdef WaiCliTLS
  , wTlsKeyFile               String
  , wTlsCertFile              String
#endif
#ifdef WaiCliUnix
  , wGracefulMode             String
#endif
  , wDevlogging              ∷ Maybe Bool }

instance Options WaiOptions where
  defineOptions = pure WaiOptions
    <*> simpleOption "port"              3000                "The port the app should listen for connections on (for http)"
    <*> simpleOption "host"              "*4"                "Host preference (for http)"
#ifdef WaiCliUnix
    <*> simpleOption "socket"            "wai.sock"          "The UNIX domain socket path the app should listen for connections on (for unix)"
#endif
    <*> simpleOption "protocol"          "http"              ("The protocol for the server. One of: " ++ availableProtocols)
#ifdef WaiCliTLS
    <*> simpleOption "tlskey"            ""                  "Path to the TLS private key file for +tls protocols"
    <*> simpleOption "tlscert"           ""                  "Path to the TLS certificate bundle file for +tls protocols"
#endif
#ifdef WaiCliUnix
    <*> simpleOption "graceful"          "serve-normally"    "Graceful shutdown mode. One of: none, serve-normally, serve-503"
#endif
    <*> simpleOption "devlogging"        Nothing             "Whether development logging should be enabled"
    where
      availableProtocols = intercalate ", " [
          "http", "cgi"
#ifdef WaiCliUnix
        , "unix", "activate"
#endif
#ifdef WaiCliTLS
        , "http+tls"
#endif
#if defined(WaiCliTLS) && defined(WaiCliUnix)
        , "unix+tls", "activate+tls"
#endif
#ifdef WaiCliFastCGI
        , "fastcgi"
#endif
        ]


#ifdef WaiCliUnix
runActivated  (Settings  S.Socket  Application  IO ())  Settings  Application  IO ()
runActivated run warps app = do
  sockets  getActivatedSockets
  case sockets of
    Just socks 
      void $ forM socks $ \sock  do
        setNonBlockingFD (S.fdSocket sock) True
        forkIO $ run warps sock app
    Nothing  putStrLn "No sockets to activate"


runGraceful  GracefulMode  (Settings  Application  IO ())  Settings  Application  IO ()
runGraceful mode run warps app = do
  -- based on https://gist.github.com/NathanHowell/5435345
  -- XXX: an option to stop accepting (need change stuff inside Warp?)
  shutdown  newEmptyTMVarIO
  activeConnections  newTVarIO (0  Int)
  _  installHandler sigTERM (CatchOnce $ atomically $ putTMVar shutdown ()) Nothing
  let warps' = setOnOpen  (\_  atomically (modifyTVar' activeConnections (+1)) >> return True) $
               setOnClose (\_  atomically (modifyTVar' activeConnections (subtract 1)) >> return ()) warps
  let app' = case mode of
        ServeNormally  app
        Serve503  \req respond  do
          shouldRun  liftIO . atomically $ isEmptyTMVar shutdown
          if shouldRun then app req respond else respond $ responseLBS serviceUnavailable503 [] ""
  void $ forkIO $ run warps' app'
  atomically $ do
    takeTMVar shutdown
    conns  readTVar activeConnections
    when (conns /= 0) retry
#endif

-- | Adjusts 'WaiOptions' with an address assigned to a newly created
-- server socket, uses those to set a "before main loop" function in
-- Warp 'Settings', which are then used to run an application.
runWarp :: (WaiOptions -> IO ())
        -- ^ A "before main loop" function
        -> WaiOptions
        -- ^ Original options
        -> (Settings -> S.Socket -> Application -> IO ())
        -- ^ A function such as 'runSettingsSocket'
        -> Settings -> Application -> IO ()
runWarp putListening opts runSocket set app = S.withSocketsDo $
  bracket (bindPortTCP (getPort set) (getHost set)) S.close $ \s -> do
  sa <- S.getSocketName s
  S.setCloseOnExecIfNeeded $ S.fdSocket s
  runSocket (setBeforeMainLoop (putListening $ updateOptions sa opts) set) s app
  where
    updateOptions :: S.SockAddr -> WaiOptions -> WaiOptions
    updateOptions (S.SockAddrInet pn ha) opt =
      opt { wHttpPort = fromIntegral pn, wHttpHost = show (fromHostAddress ha) }
    updateOptions (S.SockAddrInet6 pn _flow ha _scope) opt =
      opt { wHttpPort = fromIntegral pn, wHttpHost = show (fromHostAddress6 ha) }
    updateOptions _ opt = opt

waiMain  (WaiOptions  IO ())  (WaiOptions  IO ())  Application  IO ()
waiMain putListening putWelcome app = runCommand $ \opts _  do
#ifdef WaiCliTLS
  let tlss = tlsSettings (wTlsCertFile opts) (wTlsKeyFile opts)
#endif
  let warps = setBeforeMainLoop (putListening opts) $ setPort (wHttpPort opts) $
              setHost (fromString $ wHttpHost opts) defaultSettings
  let app' = if wDevlogging opts == Just True then logStdoutDev app else app
  case wProtocol opts of
     "cgi"  CGI.run app'
#ifdef WaiCliFastCGI
     "fastcgi"  FCGI.run app'
#endif
     _  do
       let run = case wProtocol opts of
             "http"  runWarp putListening opts runSettingsSocket
#ifdef WaiCliUnix
             "unix"  \warps' app''  bracket (bindPath $ wUnixSock opts) S.close (\sock  runSettingsSocket warps' sock app'')
             "activate"  runActivated runSettingsSocket
#endif
#ifdef WaiCliTLS
             "http+tls"  runWarp putListening opts (runTLSSocket tlss)
#ifdef WaiCliUnix
             "unix+tls"  \warps' app''  bracket (bindPath $ wUnixSock opts) S.close (\sock  runTLSSocket tlss warps' sock app'')
             "activate+tls"  runActivated (runTLSSocket tlss)
#endif
#endif
             x  \_ _  putStrLn $ "Unsupported protocol: " ++ x
       putWelcome opts
#ifdef WaiCliUnix
       case wGracefulMode opts of
             "none"  run warps app'
             "serve-normally"  runGraceful ServeNormally run warps app'
             "serve-503"  runGraceful Serve503 run warps app'
             x   putStrLn $ "Unsupported graceful mode: " ++ x
#else
       run warps app'
#endif

defPutListening  WaiOptions  IO ()
defPutListening opts = getNumCapabilities >>= putMain
  where putMain cpus = reset "Running on " >> blue (wProtocol opts) >> putProto >> reset " with " >> green (show cpus ++ " CPUs") >> setReset >> putStrLn ""
        putProto = case wProtocol opts of
                     "http"  reset " host " >> boldMagenta (wHttpHost opts)
                              >> reset ", port " >> boldMagenta (show $ wHttpPort opts)
#ifdef WaiCliUnix
                     "unix"  reset " socket " >> boldMagenta (show $ wUnixSock opts)
                     "activate"  reset " activated socket"
#endif
#ifdef WaiCliTLS
                     "http+tls"  reset " (TLS) port "   >> boldMagenta (show $ wHttpPort opts)
#ifdef WaiCliUnix
                     "unix+tls"  reset " (TLS) socket " >> boldMagenta (show $ wUnixSock opts)
                     "activate+tls"  reset " (TLS) activated socket"
#endif
#endif
                     _       setReset
        setReset = setSGR [ Reset ]
        boldMagenta x = setReset >> setSGR [ SetConsoleIntensity BoldIntensity, SetColor Foreground Vivid Magenta ] >> putStr x
        green x = setReset >> setSGR [ SetColor Foreground Dull Green ] >> putStr x
        blue  x = setReset >> setSGR [ SetColor Foreground Dull Blue ] >> putStr x
        reset x = setReset >> putStr x

defWaiMain  Application  IO ()
defWaiMain = waiMain defPutListening (\_  return ())