{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE OverloadedStrings  #-}

module Bitcoin.Core.Regtest
    (
    -- * Run an ephemeral regtest node
      NodeHandle (..)
    , runBitcoind
    , withBitcoind

    -- * Funding
    , oneBitcoin
    , createOutput
    , generate
    , spendPackageOutputs

    -- * Internal wallet
    --
    -- | In the following lists, entries correspond to each other e.g. the p2pkh
    -- address for @keys !! 1@ is @addrs !! 1@.
    , xprv
    , keys
    , pubKeys
    , addrs
    , textAddrs
    ) where

import           Control.Concurrent          (threadDelay)
import           Control.Exception           (bracket)
import           Control.Monad               (void)
import qualified Data.Serialize              as S
import           Data.Text                   (Text)
import           Data.Word                   (Word64)
import           Network.Haskoin.Address     (Address, addrToString,
                                              addressToOutput)
import           Network.Haskoin.Block       (blockTxns)
import           Network.Haskoin.Constants   (btcTest)
import           Network.Haskoin.Crypto      (SecKey)
import           Network.Haskoin.Keys        (PubKeyI, XPrvKey (..),
                                              deriveAddrs, derivePubKeyI,
                                              deriveXPubKey, makeXPrvKey,
                                              prvSubKeys, wrapSecKey)
import           Network.Haskoin.Script      (sigHashAll)
import           Network.Haskoin.Transaction (OutPoint (..), SigInput (..),
                                              Tx (..), TxOut (..), buildAddrTx,
                                              signTx, txHash)
import           Network.Haskoin.Util        (encodeHex, maybeToEither)
import           Network.HTTP.Client         (Manager)
import           Servant.API                 (BasicAuthData)
import           System.IO                   (Handle, IOMode (..), openFile)
import           System.IO.Temp              (withSystemTempDirectory)
import           System.Process              (CreateProcess (..), ProcessHandle,
                                              StdStream (..), createProcess,
                                              proc, terminateProcess,
                                              waitForProcess)

import           Bitcoin.Core.RPC            (BitcoindClient, BitcoindException,
                                              basicAuthFromCookie)
import qualified Bitcoin.Core.RPC            as RPC


-- | Data needed to connect to a @bitcoind-regtest@ ephemeral node
data NodeHandle = NodeHandle
    { nodePort :: Int
    , nodeAuth :: BasicAuthData
    }


-- | Run an RPC computation with an ephemeral node
runBitcoind :: Manager -> NodeHandle -> BitcoindClient r -> IO (Either BitcoindException r)
runBitcoind mgr (NodeHandle port auth) = RPC.runBitcoind mgr "127.0.0.1" port auth


-- | Provide bracketed access to a fresh ephemeral node
withBitcoind
    :: Int
    -- ^ node port
    -> (NodeHandle -> IO r)
    -> IO r
withBitcoind port k = withSystemTempDirectory "bitcoind-rpc-tests" $ \dd ->
    bracket (initBitcoind dd port) stopBitcoind . const $ do
        auth <- basicAuthFromCookie $ dd <> "/regtest/.cookie"
        k $ NodeHandle port auth


initBitcoind :: FilePath -> Int -> IO ProcessHandle
initBitcoind ddir port = do
    logH         <- openFile "/tmp/bitcoind-rpc.log" WriteMode
    (_, _, _, h) <- createProcess $ bitcoind ddir port logH
    h <$ threadDelay 1_000_000


stopBitcoind :: ProcessHandle -> IO ()
stopBitcoind h = terminateProcess h >> void (waitForProcess h)


bitcoind :: FilePath -> Int -> Handle -> CreateProcess
bitcoind ddir port output
    = (proc "bitcoind" args)
      { std_out = UseHandle output
      , std_err = UseHandle output
      }
    where
    args = ["-regtest", "-txindex", "-disablewallet", "-datadir=" <> ddir, "-rpcport=" <> show port]


oneBitcoin :: Word64
oneBitcoin = 100_000_000


-- | Funds an output with the minimum of the given amount and 100 blocks of subsidies
createOutput
    :: Address
    -- ^ address for the newly created output
    -> Word64
    -- ^ target amount
    -> BitcoindClient (OutPoint, Word64)
createOutput addr vTarget = do
    inputs <- generateEnoughBlocks vTarget
    -- Make mined outputs spendable
    RPC.generateToAddress 100 textAddr2 Nothing

    let Right (tx, vFund) = spendPackageOutputs inputs addr vTarget
    h <- RPC.sendRawTransaction (encodeHex $ S.encode tx) Nothing
    RPC.generateToAddress 6 textAddr2 Nothing

    return (OutPoint h 0, vFund)


generateEnoughBlocks :: Word64 -> BitcoindClient [(OutPoint, Word64)]
generateEnoughBlocks vTarget = go ([], 0, 0 :: Int)
    where
    go (xs, v, n)
        | n >= 100 = return xs
        | otherwise = do
            x@(_, v0) <- generate
            let xs' = x : xs
            if v + v0 >= vTarget
                then return xs'
                else go (xs', v + v0, n+1)


-- | A simplified block generator which does not require the tester to manage a
-- wallet.  Use 'spendPackageOutputs' to spend.
generate :: BitcoindClient (OutPoint, Word64)
generate
    = fmap (processCoinbase . head . blockTxns)
    $ RPC.generateToAddress 1 textAddr0 Nothing >>= RPC.getBlock . head
    where
    processCoinbase tx0 = (OutPoint (txHash tx0) 0 , outValue . head $ txOut tx0)


-- | Spend outputs created by 'generate'
spendPackageOutputs
    :: [(OutPoint, Word64)]
    -- ^ outputs produced by 'generate'
    -> Address
    -- ^ recipient address
    -> Word64
    -- ^ amount to spend
    -> Either String (Tx, Word64)
spendPackageOutputs inputs addr vTarget = do
    addrText <- maybeToEither "Addr conversion failed" $ addrToString btcTest addr

    let outSpec | vTarget + 10_000 < vAvail
                = Right ([(addrText, vTarget), (textAddr1, vAvail - vTarget - 10_000)], vTarget)

                | vAvail > 10_000
                = Right ([(addrText, vAvail - 10_000)], vAvail - 10_000)

                | otherwise
                = Left "Insufficient funds"

        vAvail          = sum $ snd <$> inputs
        sigIn (op, val) = SigInput (addressToOutput addr0) val op sigHashAll Nothing

    (outs, vFund) <- outSpec
    txSpec        <- buildAddrTx btcTest (fst <$> inputs) outs
    tx            <- signTx btcTest txSpec (sigIn <$> inputs) [key0]
    return (tx, vFund)


-- | Root key for the package wallet
xprv :: XPrvKey
xprv = makeXPrvKey "bitcoind-regtest key seed"


-- | Example secret keys
keys :: [SecKey]
keys = xPrvKey . fst <$> prvSubKeys xprv 0


-- | Example public keys
pubKeys :: [PubKeyI]
pubKeys = derivePubKeyI . wrapSecKey True <$> keys


key0 :: SecKey
key0 : _ = keys


-- | Example p2pkh addresses
addrs :: [Address]
addrs = repack <$> deriveAddrs (deriveXPubKey xprv) 0
    where repack (x, _, _) = x


addr0 :: Address
addr0 : _ = addrs


-- | Text versions of the example addresses
textAddrs :: [Text]
textAddrs = addrToString' <$> addrs
    where addrToString' a = let Just x = addrToString btcTest a in x


textAddr0, textAddr1, textAddr2 :: Text
textAddr0 : textAddr1 : textAddr2 : _ = textAddrs