{-# LANGUAGE DataKinds                  #-}
{-# LANGUAGE TypeOperators              #-}
{-# LANGUAGE DeriveGeneric              #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings          #-}
module Toxiproxy
    ( getVersion
    , postReset
    , getProxies
    , createProxy
    , getProxy
    , postPopulate
    , updateProxy
    , deleteProxy
    , getToxics
    , createToxic
    , getToxic
    , updateToxic
    , deleteToxic
    , Proxy(..)
    , Toxic(..)
    , Populate(..)
    , Version(..)
    , Stream(..)
    , ToxicType(..)
    , toxiproxyUrl
    , withDisabled
    , withToxic
    , withProxy
    , run
    ) where

import Servant.API
import Servant.Client
import qualified Data.Proxy as Proxy
import Data.Text (Text, pack, toLower, unpack)
import Data.List (stripPrefix)
import qualified Data.Char as Char (toLower)
import GHC.Generics
import Data.Aeson (FromJSON, parseJSON, fieldLabelModifier, defaultOptions, genericParseJSON,
                   ToJSON, genericToJSON, toJSON, FromJSONKey, Value( String ))
import Data.Map.Strict (Map)
import Network.HTTP.Client (newManager, defaultManagerSettings)
import Control.Exception (bracket)
import Control.Monad (void)
import Data.String (IsString)

type ToxiproxyAPI =
       "version"  :> Get '[PlainText] Version
  :<|> "reset"    :> Post '[] NoContent
  :<|> "proxies"  :> Get '[JSON] (Map ProxyName Proxy)
  :<|> "proxies"  :> ReqBody '[JSON] Proxy    :> Post '[JSON] Proxy
  :<|> "proxies"  :> Capture "name" ProxyName :> Get '[JSON] Proxy
  :<|> "populate" :> ReqBody '[JSON] [Proxy]  :> Post '[JSON] Populate
  :<|> "proxies"  :> Capture "name" ProxyName :> ReqBody '[JSON] Proxy :> Post '[JSON] Proxy
  :<|> "proxies"  :> Capture "name" ProxyName :> Delete '[] NoContent
  :<|> "proxies"  :> Capture "name" ProxyName :>
       "toxics"   :> Get '[JSON] [Toxic]
  :<|> "proxies"  :> Capture "name" ProxyName :>
       "toxics"   :> ReqBody '[JSON] Toxic    :> Post '[JSON] Toxic
  :<|> "proxies"  :> Capture "name" ProxyName :>
       "toxics"   :> Capture "name" ToxicName :> Get '[JSON] Toxic
  :<|> "proxies"  :> Capture "name" ProxyName :>
       "toxics"   :> Capture "name" ToxicName :> ReqBody '[JSON] Toxic :> Get '[JSON] Toxic
  :<|> "proxies"  :> Capture "name" ProxyName :>
       "toxics"   :> Capture "name" ToxicName :> Delete '[JSON] NoContent

-- | A unique string for identifying a proxy on the server.
newtype ProxyName = ProxyName Text
  deriving (Show, Eq, IsString, Ord, Generic, ToHttpApiData, FromJSONKey)

instance FromJSON ProxyName
instance ToJSON   ProxyName

-- | A unique string for identifying a toxic on a proxy.
newtype ToxicName = ToxicName Text
  deriving (Show, Eq, IsString, Generic, ToHttpApiData)

instance FromJSON ToxicName
instance ToJSON   ToxicName

-- | The version of the Toxiproxy server. This library is fully supported by any version
--   greater or equal to 2.1.3.
newtype Version = Version Text
  deriving (Show, Eq, MimeUnrender PlainText)

-- | A Toxiproxy proxy. It forwards TCP connections between a listen and upstream host.
--   Toxics can be injected into the proxy to simulate network failure.
data Proxy = Proxy
  { proxyName     :: ProxyName
  -- ^ A unique human readable name to identify a proxy.
  , proxyListen   :: Host
  -- ^ The proxy listens on this host:port.
  , proxyUpstream :: Host
  -- ^ The proxy forwards to this upstream host:port.
  , proxyEnabled  :: Bool
  -- ^ Whether a proxy is currently listening / accepting connections.
  , proxyToxics   :: [Toxic]
  -- ^ The toxics currently applied to the proxy. These should not be specified when
  --   initially creating a proxy. They must be created seperately with 'createToxic'
  --   or 'withToxic'.
  } deriving (Show, Eq, Generic)

instance FromJSON Proxy where
  parseJSON = genericParseJSON $
    defaultOptions
      { fieldLabelModifier = stripPrefixJSON "proxy" }

instance ToJSON Proxy where
  toJSON = genericToJSON $
    defaultOptions
      { fieldLabelModifier = stripPrefixJSON "proxy" }

-- | A host:port pair to represent the entrence of a proxy or the upstream the proxy
--   forwards to. For the best experience, provide 127.0.0.1 instead of localhost.
type Host = Text

-- | A toxic is applied to a proxy. It allows the user to simulate a specified kind of
--   network failure on the proxy.
data Toxic = Toxic
  { toxicName       :: ToxicName
  -- ^ A unique human readable name to identify a toxic.
  , toxicType       :: ToxicType
  -- ^ The type of toxic. For example "latency". Please refer to 'ToxicType' or the
  -- Toxiproxy documentation for more information.
  , toxicStream     :: Stream
  -- ^ The direction on which the toxic is applied. Please refer to 'Stream'.
  , toxicToxicity   :: Float
  -- ^ The strength that the toxic is applied to the proxy. Please refer to the Toxiproxy
  --   documation.
  , toxicAttributes :: Map Text Int
  -- ^ Attributes configure a toxic. They differ based on the 'ToxicType'. Please refer to
  --   the Toxiproxy documentation.
  } deriving (Show, Eq, Generic)

instance FromJSON Toxic where
  parseJSON = genericParseJSON $
    defaultOptions
      { fieldLabelModifier = stripPrefixJSON "toxic" }

instance ToJSON Toxic where
  toJSON = genericToJSON $
    defaultOptions
      { fieldLabelModifier = stripPrefixJSON "toxic" }

-- | The return value of the 'populate' endpoint.
newtype Populate = Populate { populateProxies :: [Proxy] }
  deriving (Show, Eq, Generic)

instance FromJSON Populate where
  parseJSON = genericParseJSON $
    defaultOptions
      { fieldLabelModifier = stripPrefixJSON "populate" }

-- | A toxic can be applied to the upstream or the downstream of a connection. Upstream is
--   the stream traveling from the connecting client to the upstream server. Downstream is
--   the stream traveling from the upstream server to the connecting client.
data Stream = Upstream | Downstream
  deriving (Show, Eq)

instance ToJSON Stream where
  toJSON Upstream   = String "upstream"
  toJSON Downstream = String "downstream"

instance FromJSON Stream where
  parseJSON (String stream) =
    case stream of
      "upstream"   -> return Upstream
      "downstream" -> return Downstream

-- | Different toxic types simulate different kinds of failure. Different toxics require
--   different attribute configuration. Please refer to the Toxiproxy documentation.
data ToxicType =
    Latency
  | Bandwidth
  | SlowClose
  | Timeout
  | Slicer
  | LimitData
  | Other Text
  deriving (Show, Eq)

instance ToJSON ToxicType where
  toJSON Latency       = String "latency"
  toJSON Bandwidth     = String "bandwidth"
  toJSON SlowClose     = String "slow_close"
  toJSON Timeout       = String "timeout"
  toJSON Slicer        = String "slicer"
  toJSON LimitData     = String "limit_data"
  toJSON (Other other) = String other

instance FromJSON ToxicType where
  parseJSON (String toxicType) =
    case toxicType of
      "latency"   -> return Latency
      "bandwidth" -> return Bandwidth
      "slow_clos" -> return SlowClose
      "timeout"   -> return Timeout
      "slicer"    -> return Slicer
      "limit_dat" -> return LimitData
      other       -> return . Other $ other

stripPrefixJSON :: String -> String -> String
stripPrefixJSON prefix str =
  case stripPrefix prefix str of
    Nothing             -> str
    Just (first : rest) -> Char.toLower first : rest

toxiproxyAPI :: Proxy.Proxy ToxiproxyAPI
toxiproxyAPI = Proxy.Proxy

getVersion   :: ClientM Version
postReset    :: ClientM NoContent
getProxies   :: ClientM (Map ProxyName Proxy)
createProxy  :: Proxy -> ClientM Proxy
getProxy     :: ProxyName -> ClientM Proxy
postPopulate :: [Proxy] -> ClientM Populate
updateProxy  :: ProxyName -> Proxy -> ClientM Proxy
deleteProxy  :: ProxyName -> ClientM NoContent
getToxics    :: ProxyName -> ClientM [Toxic]
createToxic  :: ProxyName -> Toxic -> ClientM Toxic
getToxic     :: ProxyName -> ToxicName -> ClientM Toxic
updateToxic  :: ProxyName -> ToxicName -> Toxic -> ClientM Toxic
deleteToxic  :: ProxyName -> ToxicName -> ClientM NoContent

(getVersion :<|> postReset :<|> getProxies :<|> createProxy :<|> getProxy :<|> postPopulate
            :<|> updateProxy :<|> deleteProxy :<|> getToxics :<|> createToxic :<|> getToxic
            :<|> updateToxic :<|> deleteToxic) = client toxiproxyAPI

-- | The default Toxiproxy service URL.
--   (127.0.0.1:8474)
toxiproxyUrl :: BaseUrl
toxiproxyUrl = BaseUrl Http "127.0.0.1" 8474 ""

-- | A helper for easily querying the Toxiproxy API. Assumes Toxiproxy is running on
--  'toxiproxyUrl'.
--
-- @
-- proxies <- run getProxies
-- @
run :: ClientM a -> IO (Either ServantError a)
run f = do
  manager <- newManager defaultManagerSettings
  runClientM f (ClientEnv manager toxiproxyUrl)

-- | Given an enabled proxy, disable the proxy, run the given action and then re-enable
--   the proxy.
--
--   This is useful for simulating a crashed server or closed connection.
--
-- @
-- connectToMyProxy       -- This will connect.
-- withDisabled myProxy $
--   connectToMyProxy     -- This will get rejected.
-- connectToMyProxy       -- This will connect again.
-- @
withDisabled :: Proxy -> IO a -> IO a
withDisabled proxy f =
  bracket disable enable $ const f
  where
    enable        = const . run $ updateProxy (proxyName proxy) proxy
    disable       = void . run $ updateProxy (proxyName proxy) disabledProxy
    disabledProxy = proxy { proxyEnabled = False }

-- | Given a proxy and a toxic, create the toxic on the proxy, run the given action and
--   then delete the toxic.
--
--   This is useful for running some action with a toxic enabled.
--
-- @
-- withToxic myProxy latencyToxic $
--   sendRequestThroughProxy -- This request will have latency applied to it.
-- @
withToxic :: Proxy -> Toxic -> IO a -> IO a
withToxic proxy toxic f =
  bracket enable disable $ const f
  where
    enable = void . run $ createToxic (proxyName proxy) toxic
    disable = const . run $ deleteToxic (proxyName proxy) (toxicName toxic)

-- | Given a proxy record, create the proxy on the server, run the given action and then
--   delete the proxy off the server.
--
--   This is useful for wrapping 'withDisabled' and 'withToxic' calls. It enures that your
--   test cleans up the Toxiproxy server so that proxies don't leak into your other tests.
withProxy :: Proxy -> (Proxy -> IO a) -> IO a
withProxy proxy =
  bracket create delete
  where
    create = run (createProxy proxy) >> return proxy
    delete = const . run $ deleteProxy (proxyName proxy)