{-|
Module      : PostgREST.Client
Description : PostgREST HTTP client
-}
{-# LANGUAGE NamedFieldPuns #-}
module PostgREST.Client
  ( ready
  ) where

import qualified Data.Text                 as T
import qualified Network.HTTP.Client       as HC
import qualified Network.HTTP.Types.Status as HTTP

import Network.HTTP.Client (HttpException (..))
import System.IO           (hFlush)

import PostgREST.Config  (AppConfig (..))
import PostgREST.Network (isSpecialHostName)

import Protolude

data PgrstClientError
  = NoAdminServer
  | NoSpecialHostNamesAllowed Text
  | PostgRESTNotReady         Text
  | HTTPConnectionRefused     Text
  | HTTPExceptionInvalidURL   Text

-- | This is invoked by the CLI "--ready" flag.
--   The http-client sends and a request to /ready endpoint
--   and exits with success or failure.
ready :: AppConfig -> IO ()
ready AppConfig{configAdminServerHost, configAdminServerPort} = do

  client <- HC.newManager HC.defaultManagerSettings
  readyURL <- getURL
  req <- HC.parseRequest (T.unpack readyURL) `catch` handleHttpException
  resp <- HC.httpLbs req client `catch` handleHttpException

  let status = HC.responseStatus resp

  if status >= HTTP.status200 && status < HTTP.status300
    then printAndExitWithSuccess $ "OK: " <> readyURL
    else printAndExitWithFailure $ clientErrorMsg (PostgRESTNotReady readyURL)
        where
          getURL :: IO Text
          getURL =
            -- Here, we have three cases:
            -- 1. If the admin port config is not defined, we exit
            --      with "no admin server error"
            -- 2. Otherwise, if admin server is running, then we check if
            --      postgrest server-host is configured with special hostname like "*4",
            --      if it is, we fail with "no special hostname allowed with "--ready".
            --      The reason for this is that we can't know the actual address.
            -- 3. Finally, if we know the "actual" hostname and the port, then we
            --      construct the URL and return it.
            case configAdminServerPort of
              Nothing   -> printAndExitWithFailure $ clientErrorMsg NoAdminServer
              Just port ->
                if isSpecialHostName configAdminServerHost
                  then printAndExitWithFailure $ clientErrorMsg (NoSpecialHostNamesAllowed configAdminServerHost)
                  else return $ makeReadyUrl port

          -- NOTE: http-client automatically resolves hostnames
          makeReadyUrl :: Int -> Text
          makeReadyUrl p = "http://" <> wrapIfIpv6 configAdminServerHost <> ":" <> (T.pack . show) p <> "/ready"
            where
              -- IPv6 needs to wrapped in [], it has ':' as separator
              wrapIfIpv6 :: Text -> Text
              wrapIfIpv6 s
                | T.any (== ':') s = "[" <> s <> "]"
                | otherwise = s

-- | Handle HTTP exception for "http-client" requests
handleHttpException :: HttpException -> IO a
handleHttpException (HttpExceptionRequest req _) = do
  let url = show (HC.getUri req)
  printAndExitWithFailure $ clientErrorMsg (HTTPConnectionRefused $ T.pack url)
handleHttpException (InvalidUrlException url _) = do
  printAndExitWithFailure $ clientErrorMsg (HTTPExceptionInvalidURL $ T.pack url)

-- | Print the message on stdout and exit with success
printAndExitWithSuccess :: Text -> IO a
printAndExitWithSuccess msg = putStrLn (T.unpack msg) >> hFlush stdout >> exitSuccess

-- | Print the message on stderr and exit with failure
printAndExitWithFailure :: Text -> IO a
printAndExitWithFailure msg = hPutStrLn stderr (T.unpack msg) >> hFlush stderr >> exitWith (ExitFailure 1)

-- | Pgrst client error to error message
clientErrorMsg :: PgrstClientError -> Text
clientErrorMsg err = "ERROR: " <>
  case err of
    NoAdminServer -> "Admin server is not running. Please check admin-server-port config."
    NoSpecialHostNamesAllowed host ->
      "The `--ready` flag cannot be used when server-host is configured as \"" <> host <> "\". "
      <> "Please update your server-host config to \"localhost\"."
    PostgRESTNotReady url -> url
    HTTPConnectionRefused url -> "connection refused to " <> url
    HTTPExceptionInvalidURL url -> "invalid url - " <> url
