{-# LANGUAGE DataKinds             #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE LambdaCase            #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NumericUnderscores    #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE ScopedTypeVariables   #-}
{-# LANGUAGE TypeApplications      #-}
{-# LANGUAGE TypeFamilies          #-}
{-# LANGUAGE TypeOperators         #-}

module Servant.Bitcoind
    (
    -- * Bitcoind api endpoint DSL

      C
    , CX
    , F
    , I
    , O

    -- * Types related to defaulting

    , EmptyString
    , EmptyList
    , DefFalse
    , DefZero

    -- * Types related to the client

    , BitcoindClient
    , BitcoindEndpoint
    , BitcoindException (..)

    -- * Client generation mechanism

    , HasBitcoindClient (..)
    , Rewrite (..)

    -- * Utility functions
    , utcTime
    , toSatoshis
    ) where

import           Control.Exception          (Exception)
import           Control.Monad.Trans.Except (ExceptT (..))
import           Control.Monad.Trans.Reader (ReaderT (..))
import           Data.Aeson                 (FromJSON (..), ToJSON (..), Value)
import qualified Data.Aeson.Types           as Ae
import           Data.Bifunctor             (first)
import           Data.Proxy                 (Proxy (..))
import           Data.Scientific            (Scientific)
import           Data.Text                  (Text)
import           Data.Time                  (UTCTime)
import           Data.Time.Clock.POSIX      (posixSecondsToUTCTime)
import           Data.Word                  (Word32, Word64)
import           GHC.TypeLits               (KnownSymbol, Symbol)
import           Servant.API                ((:<|>) (..), (:>))
import           Servant.API.BasicAuth      (BasicAuth, BasicAuthData)
import           Servant.Client             (ClientError, ClientM, client)
import           Servant.Client.JsonRpc     (JsonRpc, JsonRpcErr (..),
                                             JsonRpcResponse (..))


-- | Exceptions resulting from interacting with bitcoind
data BitcoindException
    = RpcException String
    -- ^ The error message returned by bitcoind on failure
    | ClientException ClientError
    | DecodingError String
    deriving Show


instance Exception BitcoindException


data BitcoindEndpoint (m :: Symbol) a


-- | A client returning @Either BitcoindException r@
data C r

-- | A client returning @Either BitcoindException ()@
data CX

-- | An argument with a fixed value
data F x r

-- | An optional argument
data O r

-- | An ordinary argument
data I r


class HasDefault x a where
    getDefault :: p x -> a


data EmptyString


instance HasDefault EmptyString Text where getDefault _ = ""


data DefFalse


instance HasDefault DefFalse Bool where getDefault _ = False


data EmptyList


instance HasDefault EmptyList [a] where getDefault _ = []


data DefZero


instance Num a => HasDefault DefZero a where getDefault _ = 0


class HasBitcoindClient x where
    type TheBitcoindClient x :: *
    toBitcoindClient :: p x -> TheBitcoindClient x


instance
    (Rewrite a, RewriteFrom a ~ NakedClient, KnownSymbol m)
    => HasBitcoindClient (BitcoindEndpoint m a)
    where

    type TheBitcoindClient (BitcoindEndpoint m a) = RewriteTo a
    toBitcoindClient _
        = rewriteRpc (Proxy @a)
        . client
        $ Proxy @(BitcoindRpc m)


instance
    (HasBitcoindClient x, HasBitcoindClient y)
    => HasBitcoindClient (x :<|> y)
    where

    type TheBitcoindClient (x :<|> y) = TheBitcoindClient x :<|> TheBitcoindClient y
    toBitcoindClient _ = toBitcoindClient (Proxy @x) :<|> toBitcoindClient (Proxy @y)


type BitcoindRpc m    = BasicAuth "bitcoind" () :> JsonRpc m [Value] String Value


type BitcoindClient r = ReaderT BasicAuthData (ExceptT BitcoindException ClientM) r


type NakedClient
    =  BasicAuthData
    -> [Value]
    -> ClientM (JsonRpcResponse String Value)


-- | Bitcoind uses JSON arrays to serialize parameters.  This typeclass
-- describes a generic rewriting system, but we apply it here to transform
-- clients of the form @BasicAuthData -> [Value] -> ClientM Value@ into curried
-- functions with endpoint specific arguments.
class Rewrite a where
    type RewriteFrom a :: *
    type RewriteTo a   :: *
    rewriteRpc :: p a -> RewriteFrom a -> RewriteTo a


-- | Handle endpoints which do not have an expected return value
instance Rewrite CX where
    type RewriteFrom CX = NakedClient
    type RewriteTo   CX = BitcoindClient ()

    rewriteRpc _ f = ReaderT $ ExceptT . fmap repack . (`f` [])
        where
        repack = \case
            Ack _                       -> return ()
            Errors _ (JsonRpcErr _ e _) -> Left $ RpcException e
            Result{}                    -> Left $ RpcException "Expecting ack; got result"


-- | Endpoints which simply return a value
instance FromJSON r => Rewrite (C r) where
    type RewriteFrom (C r) = NakedClient
    type RewriteTo   (C r) = BitcoindClient r

    rewriteRpc _ f = ReaderT $ ExceptT . fmap repack . (`f` [])
        where
        repack = \case
            Result _ x                  -> first DecodingError $ Ae.parseEither parseJSON x
            Errors _ (JsonRpcErr _ e _) -> Left $ RpcException e
            Ack{}                       -> Left $ RpcException "Expecting result; got ack"


-- | Add a normal argument
instance
    (RewriteFrom b ~ NakedClient, Rewrite b, ToJSON a)
    => Rewrite (I a -> b)
    where

    type RewriteFrom (I a -> b) = NakedClient
    type RewriteTo (I a -> b)   = a -> RewriteTo b

    rewriteRpc _ f x = rewriteRpc (Proxy @b) $ \auth args -> f auth (toJSON x : args)


-- | Add an optional argument
instance
    (RewriteFrom b ~ NakedClient, Rewrite b, ToJSON a)
    => Rewrite (O a -> b)
    where

    type RewriteFrom (O a -> b) = NakedClient
    type RewriteTo (O a -> b)   = Maybe a -> RewriteTo b

    rewriteRpc _ f x = rewriteRpc (Proxy @b) $ \auth args -> f auth ((toJSON <$> x) `maybeCons` args)


-- | Add a fixed argument
instance
    (RewriteFrom b ~ NakedClient, Rewrite b, ToJSON a, HasDefault x a)
    => Rewrite (F x a -> b)
    where

    type RewriteFrom (F x a -> b) = NakedClient
    type RewriteTo   (F x a -> b) = RewriteTo b

    rewriteRpc _ f = rewriteRpc (Proxy @b) f'
        where
        f' auth args = f auth $ fixedVal : args
        fixedVal = toJSON @a . getDefault $ Proxy @x



instance (Rewrite a, Rewrite b) => Rewrite (a :<|> b) where
    type RewriteFrom (a :<|> b) = RewriteFrom a :<|> RewriteFrom b
    type RewriteTo (a :<|> b)   = RewriteTo a :<|> RewriteTo b
    rewriteRpc _ (x :<|> y) = rewriteRpc (Proxy @a) x :<|> rewriteRpc (Proxy @b) y


maybeCons :: Maybe a -> [a] -> [a]
maybeCons mx xs = maybe xs (:xs) mx


-- | Helper function for decoding POSIX timestamps
utcTime :: Word64 -> UTCTime
utcTime = posixSecondsToUTCTime . fromIntegral


-- | Convert BTC to Satoshis
toSatoshis :: Scientific -> Word32
toSatoshis = floor . (* 100_000_000)