content: {-# LANGUAGE ConstraintKinds            #-}
  {-# LANGUAGE DataKinds                  #-}
  {-# LANGUAGE DeriveAnyClass             #-}
  {-# LANGUAGE DeriveGeneric              #-}
  {-# LANGUAGE DuplicateRecordFields      #-}
  {-# LANGUAGE FlexibleInstances          #-}
  {-# LANGUAGE GeneralizedNewtypeDeriving #-}
  {-# LANGUAGE MultiParamTypeClasses      #-}
  {-# LANGUAGE OverloadedStrings          #-}
  {-# LANGUAGE ScopedTypeVariables        #-}
  {-# LANGUAGE TemplateHaskell            #-}
  {-# LANGUAGE TupleSections              #-}
  {-# LANGUAGE TypeFamilies               #-}
  
  module Cardano.Faucet.Types.Config (
     FaucetConfig(..)
   , HasFaucetConfig(..)
   , PaymentDistribution(..), mean, scale
   , FaucetEnv(..)
   , HasFaucetEnv(..)
   , SourceWalletConfig(..)
   , cfgToPaymentSource
   , SourceWallet(..), srcWalletId, srcAccountIndex, srcSpendingPassword
   , InitializedWallet(..), walletBalance, walletConfig, walletReturnAddress
   , CreatedWallet(..)
   , ProcessorPayload(..), ppQueue, ppResult
   , InitFaucetError(..)
    ) where
  
  import           Control.Concurrent.STM.TBQueue (TBQueue)
  import           Control.Concurrent.STM.TMVar (TMVar)
  import           Control.Exception.Safe (Exception)
  import           Control.Lens hiding ((.=))
  import           Data.Aeson (FromJSON (..), ToJSON (..), object, withObject,
                       (.:), (.:?), (.=))
  import           Data.Int (Int64)
  import           Data.Text (Text)
  import           Data.Typeable (Typeable)
  import           GHC.Generics (Generic)
  import           System.Metrics (Store)
  import           System.Metrics.Counter (Counter)
  import           System.Metrics.Gauge (Gauge)
  import           System.Remote.Monitoring.Statsd (StatsdOptions (..))
  
  import           Cardano.Mnemonic (Mnemonic)
  import           Cardano.Wallet.API.V1.Types (AccountIndex, Payment,
                       PaymentSource (..), V1, WalletId (..))
  import           Cardano.Wallet.Client (ClientError (..), WalletClient (..))
  import           Pos.Core (Address (..))
  import           Test.QuickCheck (Arbitrary (..), choose)
  import           Test.QuickCheck.Arbitrary.Generic
  import           Universum
  
  import           Cardano.Faucet.Types.API
  import           Cardano.Faucet.Types.Recaptcha
  
  --------------------------------------------------------------------------------
  -- | Newtype for 'StatsdOptions' for the 'FromJSON' instance
  newtype FaucetStatsdOpts = FaucetStatsdOpts StatsdOptions deriving (Generic)
  
  makeWrapped ''FaucetStatsdOpts
  
  instance FromJSON FaucetStatsdOpts where
      parseJSON = fmap FaucetStatsdOpts . (withObject "StatsdOptions" $ \v ->
          StatsdOptions
            <$> v .: "host"
            <*> v .: "port"
            <*> v .: "flush-interval"
            <*> pure False
            <*> pure "faucet"
            <*> pure "")
  
  --------------------------------------------------------------------------------
  -- | Configuration for making the faucet use an existing wallet
  --
  -- /This wallet may have just been created by the faucet starting up/
  data SourceWalletConfig = SourceWalletConfig {
      -- | An ID for an existing wallet
      _srcWalletId         :: !WalletId
      -- | The index of an existing account in the wallet
    , _srcAccountIndex     :: !AccountIndex
      -- | Optional spending password for the account ('Nothing' if no password)
    , _srcSpendingPassword :: !(Maybe Text)
    } deriving (Generic)
  
  makeLenses ''SourceWalletConfig
  
  instance FromJSON SourceWalletConfig where
      parseJSON = withObject "SourceWalletConfig" $ \v -> SourceWalletConfig
        <$> v .: "wallet-id"
        <*> v .: "account-index"
        <*> v .:? "spending-password"
  
  -- | Turns a 'SourceWalletConfig' into a 'PaymentSource' needed to construct a 'Payment'
  --
  -- See 'Cardano.WalletClient.withdraw'
  cfgToPaymentSource :: SourceWalletConfig -> PaymentSource
  cfgToPaymentSource (SourceWalletConfig wId aIdx _) = PaymentSource wId aIdx
  
  --------------------------------------------------------------------------------
  -- | Config for the payment amount distribution
  --
  -- The amount of ADA (units in lovelace) to send from the faucet is calculated
  -- by
  --
  -- @
  --   'paymentMean' + randomFloat(-1, 1) * 'paymentScale'
  -- @
  data PaymentDistribution = PaymentDistribution {
      _mean  :: Int
    , _scale :: Int
    } deriving (Show)
  
  makeLenses ''PaymentDistribution
  
  instance FromJSON PaymentDistribution where
      parseJSON = withObject "PaymentDistibution" $ \v -> PaymentDistribution
        <$> (v .: "mean")
        <*> (v .: "scale")
  
  instance Arbitrary PaymentDistribution where
      arbitrary = do
          m <- choose (1000, 10000)
          s <- choose (0, m)
          return $ PaymentDistribution m s
  
  
  --------------------------------------------------------------------------------
  -- | Config for the wallet used by the faucet as a source of ADA
  data SourceWallet
      -- | Tells the faucet to generate its own wallet at start up
      --
      -- After 'CreatedWallet' will be written to 'FilePath'
      = Generate !FilePath
      -- | Tells the faucet to read a 'SourceWalletConfig' from the 'FilePath'
      | Provided !FilePath
  
  instance FromJSON SourceWallet where
      parseJSON = withObject "SourceWallet" $ \v ->
          (Generate <$> v .: "generate-to") <|> (Provided <$> v .: "read-from")
  
  --------------------------------------------------------------------------------
  -- | Once a wallet is created or read 'FaucetEnv' gets one of these
  data InitializedWallet = InitializedWallet {
      -- | The details of the wallet
      _walletConfig        :: !SourceWalletConfig
      -- | The wallet's balance (0 if just created otherwise queried)
    , _walletBalance       :: !Int64
      -- | Return address to send ADA back to
    , _walletReturnAddress :: !(V1 Address)
    } deriving (Generic)
  
  makeLenses ''InitializedWallet
  
  --------------------------------------------------------------------------------
  -- | Static config provided to the faucet
  data FaucetConfig = FaucetConfig {
      -- | Host the wallet API is running on
      _fcWalletApiHost       :: !String
      -- | Port the wallet API is running on
    , _fcWalletApiPort       :: !Int
      -- | Port to serve the faucet on
    , _fcPort                :: !Int
      -- | Distribution for withdrawals (default to 1000 and 500)
    , _fcPaymentDistribution :: !PaymentDistribution
      -- | Statsd server details
    , _fcStatsdOpts          :: !FaucetStatsdOpts
      -- | Config for wallet to use for funds
    , _fcSourceWallet        :: !SourceWallet
      -- | Logging config file
    , _fcLoggerConfigFile    :: !FilePath
      -- | TLS public certificate
    , _fcPubCertFile         :: !FilePath
      -- | TLS private key
    , _fcPrivKeyFile         :: !FilePath
      -- | File path containing recapctch sectret key.
      --
      -- Absence indicates not to use recaptcha
    , _fcRecaptchaSecretFile :: !(Maybe FilePath)
      -- | Optional path to HTML to serve from /
    , _fcHomePage            :: !(Maybe FilePath)
    }
  
  makeClassy ''FaucetConfig
  
  instance FromJSON FaucetConfig where
      parseJSON = withObject "FaucetConfig" $ \v ->
          FaucetConfig
            <$> v .: "wallet-host"
            <*> v .: "wallet-port"
            <*> v .: "port"
            <*> v .: "payment-distribution"
            <*> v .: "statsd"
            <*> v .: "source-wallet"
            <*> v .: "logging-config"
            <*> v .: "public-certificate"
            <*> v .: "private-key"
            <*> v .:? "recaptcha-secret-file"
            <*> v .:? "home-page"
  
  --------------------------------------------------------------------------------
  -- | Details of a wallet created by the faucet at run time if 'Generate' is used
  data CreatedWallet = CreatedWallet {
      -- | ID of the created wallet
      _createdWalletId :: WalletId
      -- | 12 word recovery mnemonic
    , _createdPhrase   :: Mnemonic 12
      -- | Index of the account present in the created wallet
    , _createdAcctIdx  :: AccountIndex
      -- | Sending address within the account in the created wallet
    , _createdAddress  :: Address
    } deriving (Eq, Show, Generic)
  
  instance ToJSON CreatedWallet where
      toJSON (CreatedWallet wId phrase acctIdx addr) =
          object [ "wallet-id" .= wId
                 , "recovery-words" .= phrase
                 , "account-index" .= acctIdx
                 , "address" .= addr
                 ]
  
  instance FromJSON CreatedWallet where
      parseJSON = withObject "CreatedWallet" $ \v -> do CreatedWallet
        <$> v .: "wallet-id"
        <*> v .: "recovery-words"
        <*> v .: "account-index"
        <*> v .: "address"
  
  instance Arbitrary CreatedWallet where
      arbitrary = genericArbitrary
      shrink = genericShrink
  
  --------------------------------------------------------------------------------
  -- | Sum type for possible errors encountered at faucet startup time
  data InitFaucetError =
      -- | Bad parse on the file suplying existing wallet details
      SourceWalletParseError String
      -- | Error reading sync state from wallet API
    | CouldntReadSyncState ClientError
      -- | Error creating a new wallet
    | WalletCreationError ClientError
      -- | Error reading a previously created wallet
    | CreatedWalletReadError String
      -- | Error reading the balance of an existing wallet
    | CouldntReadBalance ClientError
      -- | Error thrown if created wallet doesn't have an account (shouldn't happen)
    | NoWalletAccounts WalletId
      -- | Error thrown if exactly one address isn't found (shouln't happen)
    | BadAddress WalletId AccountIndex
    deriving (Typeable, Show)
  
  instance Exception InitFaucetError
  
  --------------------------------------------------------------------------------
  data ProcessorPayload = ProcessorPayload {
      _ppQueue  :: !Payment
    , _ppResult :: !(TMVar WithdrawalResult)
    }
  
  makeLenses ''ProcessorPayload
  --------------------------------------------------------------------------------
  -- | Run time environment for faucet's reader Monad
  data FaucetEnv = FaucetEnv {
      -- | Counter for total amount withdawn from a wallet while faucet is running
      _feWithdrawn       :: !Counter
      -- | Counter for number of withdrawals made
    , _feNumWithdrawn    :: !Counter
      -- | Gauge for wallet balance
    , _feWalletBalance   :: !Gauge
      -- | Metrics store
    , _feStore           :: !Store
      -- | Config for source of funds
    , _feSourceWallet    :: !SourceWalletConfig
      -- | Return address for sending ADA back to the faucet
    , _feReturnAddress   :: !(V1 Address)
      -- | Original static config object
    , _feFaucetConfig    :: !FaucetConfig
      -- | Client for communicating with wallet API
    , _feWalletClient    :: !(WalletClient IO)
      -- | Lock to ensure only one withdrawal at a time
    , _feWithdrawalQ     :: !(TBQueue ProcessorPayload)
      -- | Recaptcha secret read from 'fcRecaptchaSecretFile'
    , _feRecaptchaSecret :: !(Maybe CaptchaSecret)
    }
  
  makeClassy ''FaucetEnv
  