module Network.Haskoin.Wallet.Settings
( SPVMode(..)
, OutputFormat(..)
, Config(..)
) where

import Control.Monad (mzero)
import Control.Exception (throw)
import Control.Monad.Logger (LogLevel(..))

import Data.Default (Default, def)
import Data.FileEmbed (embedFile)
import Data.Yaml (decodeEither')
import Data.Word (Word8, Word32, Word64)
import Data.HashMap.Strict (HashMap, unionWith)
import Data.String.Conversions (cs)
import Data.ByteString (ByteString)
import Data.Text (Text)
import Data.Text.Encoding (encodeUtf8)
import Data.Aeson
    ( Value(..), FromJSON, ToJSON
    , parseJSON, toJSON, withObject
    , (.:)
    )

import Network.Haskoin.Crypto
import Network.Haskoin.Wallet.Database
import Network.Haskoin.Wallet.Types

import Data.Restricted (Restricted, Div5)
import System.ZMQ4 (toRestricted)

data SPVMode = SPVOnline | SPVOffline
    deriving (Eq, Show, Read)

newtype LogLevelJSON = LogLevelJSON LogLevel
    deriving (Eq, Show, Read)

data OutputFormat
    = OutputNormal
    | OutputJSON
    | OutputYAML

data Config = Config
    { configCount          :: !Word32
    -- ^ Output size of commands
    , configMinConf        :: !Word32
    -- ^ Minimum number of confirmations
    , configSignTx         :: !Bool
    -- ^ Sign transactions
    , configFee            :: !Word64
    -- ^ Fee to pay per 1000 bytes when creating new transactions
    , configRcptFee        :: !Bool
    -- ^ Recipient pays fee (dangerous, no config file setting)
    , configAddrType       :: !AddressType
    -- ^ Return internal instead of external addresses
    , configDisplayPubKeys :: !Bool
    -- ^ Display public keys instead of addresses
    , configOffline        :: !Bool
    -- ^ Display the balance including offline transactions
    , configEntropy        :: !Word8
    -- ^ Entropy in bytes to use when generating a mnemonic (between 16 and 32)
    , configReversePaging  :: !Bool
    -- ^ Use reverse paging for displaying addresses and transactions
    , configDerivIndex     :: !KeyIndex
    -- ^ Derivation path when creating account
    , configFormat         :: !OutputFormat
    -- ^ How to format the command-line results
    , configConnect        :: !String
    -- ^ ZeroMQ socket to connect to (location of the server)
    , configConnectNotif   :: !String
    -- ^ ZeroMQ socket to connect for notifications
    , configDetach         :: !Bool
    -- ^ Detach server when launched from command-line
    , configFile           :: !FilePath
    -- ^ Configuration file
    , configTestnet        :: !Bool
    -- ^ Use Testnet3 network
    , configDir            :: !FilePath
    -- ^ Working directory
    , configBind           :: !String
    -- ^ Bind address for the ZeroMQ socket
    , configBindNotif      :: !String
    -- ^ Bind address for ZeroMQ notifications
    , configBTCNodes       :: !(HashMap Text [BTCNode])
    -- ^ Trusted Bitcoin full nodes to connect to
    , configMode           :: !SPVMode
    -- ^ Operation mode of the SPV node.
    , configBloomFP        :: !Double
    -- ^ False positive rate for the bloom filter.
    , configDatabase       :: !(HashMap Text DatabaseConfType)
    -- ^ Database configuration
    , configLogFile        :: !FilePath
    -- ^ Log file
    , configPidFile        :: !FilePath
    -- ^ PID File
    , configLogLevel       :: !LogLevel
    -- ^ Log level
    , configVerbose        :: !Bool
    -- ^ Verbose
    , configServerKey      :: !(Maybe (Restricted Div5 ByteString))
    -- ^ Server key for authentication and encryption (server config)
    , configServerKeyPub   :: !(Maybe (Restricted Div5 ByteString))
    -- ^ Server public key for authentication and encryption (client config)
    , configClientKey      :: !(Maybe (Restricted Div5 ByteString))
    -- ^ Client key for authentication and encryption (client config)
    , configClientKeyPub   :: !(Maybe (Restricted Div5 ByteString))
    -- ^ Client public key for authentication and encryption
    -- (client + server config)
    }

configBS :: ByteString
configBS = $(embedFile "config/config.yml")

instance ToJSON OutputFormat where
    toJSON OutputNormal = String "normal"
    toJSON OutputJSON   = String "json"
    toJSON OutputYAML   = String "yaml"

instance FromJSON OutputFormat where
    parseJSON (String "normal") = return OutputNormal
    parseJSON (String "json")   = return OutputJSON
    parseJSON (String "yaml")   = return OutputYAML
    parseJSON _ = mzero

instance ToJSON SPVMode where
    toJSON SPVOnline  = String "online"
    toJSON SPVOffline = String "offline"

instance FromJSON SPVMode where
    parseJSON (String "online")  = return SPVOnline
    parseJSON (String "offline") = return SPVOffline
    parseJSON _ = mzero

instance ToJSON LogLevelJSON where
    toJSON (LogLevelJSON LevelDebug)     = String "debug"
    toJSON (LogLevelJSON LevelInfo)      = String "info"
    toJSON (LogLevelJSON LevelWarn)      = String "warn"
    toJSON (LogLevelJSON LevelError)     = String "error"
    toJSON (LogLevelJSON (LevelOther t)) = String t

instance FromJSON LogLevelJSON where
    parseJSON (String "debug") = return $ LogLevelJSON LevelDebug
    parseJSON (String "info")  = return $ LogLevelJSON LevelInfo
    parseJSON (String "warn")  = return $ LogLevelJSON LevelWarn
    parseJSON (String "error") = return $ LogLevelJSON LevelError
    parseJSON (String x)       = return $ LogLevelJSON (LevelOther x)
    parseJSON _ = mzero

instance Default Config where
    def = either throw id $ decodeEither' "{}"

instance FromJSON Config where
    parseJSON = withObject "config" $ \o' -> do
        let defValue         = either throw id $ decodeEither' configBS
            (Object o)       = mergeValues defValue (Object o')
            configDerivIndex = 0
        configFile                  <- o .: "config-file"
        configRcptFee               <- o .: "recipient-fee"
        configCount                 <- o .: "output-size"
        configMinConf               <- o .: "minimum-confirmations"
        configSignTx                <- o .: "sign-transactions"
        configFee                   <- o .: "transaction-fee"
        configAddrType              <- o .: "address-type"
        configDisplayPubKeys        <- o .: "display-pubkeys"
        configOffline               <- o .: "offline"
        configEntropy               <- o .: "seed-entropy"
        configReversePaging         <- o .: "reverse-paging"
        configFormat                <- o .: "display-format"
        configConnect               <- o .: "connect-uri"
        configConnectNotif          <- o .: "connect-uri-notif"
        configDetach                <- o .: "detach-server"
        configTestnet               <- o .: "use-testnet"
        configDir                   <- o .: "work-dir"
        configBind                  <- o .: "bind-socket"
        configBindNotif             <- o .: "bind-socket-notif"
        configBTCNodes              <- o .: "bitcoin-full-nodes"
        configMode                  <- o .: "server-mode"
        configBloomFP               <- o .: "bloom-false-positive"
        configLogFile               <- o .: "log-file"
        configPidFile               <- o .: "pid-file"
        LogLevelJSON configLogLevel <- o .: "log-level"
        configVerbose               <- o .: "verbose"
        configDatabase              <- o .: "database"
        configServerKey             <- getKey o "server-key"
        configServerKeyPub          <- getKey o "server-key-public"
        configClientKey             <- getKey o "client-key"
        configClientKeyPub          <- getKey o "client-key-public"
        return Config {..}
      where
        getKey o i = o .: i >>= \kM ->
            case kM of
              Nothing -> return Nothing
              Just k ->
                  case toRestricted $ encodeUtf8 k of
                    Just k' -> return $ Just k'
                    Nothing -> fail $ "Invalid " ++ cs k


mergeValues :: Value -> Value -> Value
mergeValues (Object d) (Object c) = Object (unionWith mergeValues d c)
mergeValues _ c = c
