{-# LANGUAGE KindSignatures        #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RankNTypes            #-}
module Network.Haskoin.Node.HeaderTree
( BlockChainAction(..)
, BlockHeight
, NodeBlock
, Timestamp
, initHeaderTree
, migrateHeaderTree
, getBestBlock
, getHeads
, getBlockByHash
, getParentBlock
, getBlockWindow
, getBlockAfterTime
, getChildBlocks
, getBlockByHeight
, getBlocksByHeight
, getBlocksFromHeight
, getBlocksAtHeight
, putBlock
, putBlocks
, genesisBlock
, splitBlock
, splitChains
, nodeBlock
, nodeBlockHeight
, nodeHash
, nodeHeader
, nodePrev
, nodeTimestamp
, isBestChain
, isChainReorg
, isSideChain
, isKnownChain
, connectHeader
, connectHeaders
, blockLocator
) where

import           Control.Monad                         (foldM, forM, unless,
                                                        when, (<=<))
import           Control.Monad.State                   (evalStateT, get, put)
import           Control.Monad.Trans                   (MonadIO, lift)
import           Control.Monad.Trans.Either            (EitherT, left,
                                                        runEitherT)
import           Data.Bits                             (shiftL)
import qualified Data.ByteString                       as BS (reverse, take)
import           Data.Function                         (on)
import           Data.List                             (find, maximumBy, sort)
import           Data.Maybe                            (fromMaybe, isNothing,
                                                        listToMaybe, mapMaybe)
import           Data.String.Conversions               (cs)
import           Data.Word                             (Word32)
import           Database.Esqueleto                    (Esqueleto, Value, asc,
                                                        delete, from, groupBy,
                                                        in_, insertMany_, limit,
                                                        max_, not_, orderBy,
                                                        select, set, unValue,
                                                        update, val, valList,
                                                        where_, (!=.), (&&.),
                                                        (<=.), (=.), (==.),
                                                        (>.), (>=.), (^.),
                                                        (||.))
import           Database.Persist                      (Entity (..), insert_)
import           Database.Persist.Sql                  (SqlPersistT)
import           Network.Haskoin.Block
import           Network.Haskoin.Constants
import           Network.Haskoin.Crypto
import           Network.Haskoin.Node.Checkpoints
import           Network.Haskoin.Node.HeaderTree.Model
import           Network.Haskoin.Node.HeaderTree.Types
import           Network.Haskoin.Util

data BlockChainAction
    = BestChain  { actionNodes :: ![NodeBlock] }
    | ChainReorg { actionSplitNode :: !NodeBlock
                 , actionOldNodes  :: ![NodeBlock]
                 , actionNodes     :: ![NodeBlock]
                 }
    | SideChain  { actionNodes :: ![NodeBlock] }
    | KnownChain { actionNodes :: ![NodeBlock] }
    deriving (Show, Eq)

type MinWork = Word32

shortHash :: BlockHash -> ShortHash
shortHash = decode' . BS.take 8 . getHash256 . getBlockHash

nodeHeader :: NodeBlock -> BlockHeader
nodeHeader = getNodeHeader . nodeBlockHeader

nodeHash :: NodeBlock -> BlockHash
nodeHash = headerHash . nodeHeader

nodePrev :: NodeBlock -> BlockHash
nodePrev = prevBlock . nodeHeader

nodeTimestamp :: NodeBlock -> Timestamp
nodeTimestamp = blockTimestamp . nodeHeader

-- | Number of blocks on average between difficulty cycles (2016 blocks).
diffInterval :: Word32
diffInterval = targetTimespan `div` targetSpacing

-- | Genesis block.
genesisBlock :: NodeBlock
genesisBlock = NodeBlock
    { nodeBlockHash          = shortHash $ headerHash genesisHeader
    , nodeBlockHeader        = NodeHeader genesisHeader
    , nodeBlockWork          = 1.0
    , nodeBlockHeight        = 0
    , nodeBlockChain         = 0
    }

-- | Initialize the block header chain by inserting the genesis block if it
-- doesn't already exist.
initHeaderTree :: MonadIO m => SqlPersistT m ()
initHeaderTree = do
    nodeM <- getBlockByHash $ headerHash genesisHeader
    when (isNothing nodeM) $ putBlock genesisBlock

getVerifyParams
    :: MonadIO m
    => BlockHeader
    -> EitherT String (SqlPersistT m)
               (NodeBlock, [Timestamp], Timestamp, Word32, Maybe Word32)
getVerifyParams bh = do
    parentM <- lift $ getBlockByHash $ prevBlock bh
    parent <- maybe (left "Could not get parent node") return parentM
    checkPointM <- fmap nodeBlockHeight <$> lift lastSeenCheckpoint
    diffBlockM <- lift $ getBlockByHeight parent $
        nodeBlockHeight parent `div` diffInterval * diffInterval
    diffTime <- maybe (left "Could not get difficulty change block")
        (return . nodeTimestamp)
        diffBlockM
    medianBlocks <- lift $ map nodeTimestamp <$>
        getBlocksFromHeight parent 11 (min 0 $ nodeBlockHeight parent - 10)
    minWork <- lift $ findMinWork parent
    return (parent, medianBlocks, diffTime, minWork, checkPointM)

findMinWork :: MonadIO m => NodeBlock -> SqlPersistT m MinWork
findMinWork bn
    | isMinWork bn = return $ blockBits $ nodeHeader bn
    | otherwise = getParentBlock bn >>=
        maybe (return $ blockBits $ nodeHeader bn) findMinWork

isMinWork :: NodeBlock -> Bool
isMinWork bn
    | not allowMinDifficultyBlocks = True
    | nodeBlockHeight bn `mod` diffInterval == 0 = True
    | blockBits (nodeHeader bn) /= encodeCompact powLimit = True
    | otherwise = False

splitKnown :: MonadIO m
           => [BlockHeader]
           -> SqlPersistT m ([NodeBlock], [BlockHeader])
splitKnown hs = do
    (kno, unk) <- foldM f ([], []) hs
    return (reverse kno, reverse unk)
  where
    f (kno, []) n = do
        bnM <- getBlockByHash (headerHash n)
        case bnM of
            Nothing -> return (kno, [n])
            Just bn -> return (bn:kno, [])
    f (kno, unk) n = return (kno, n:unk)

-- | Connect a block header to this block header chain. Corresponds to bitcoind
-- function ProcessBlockHeader and AcceptBlockHeader in main.cpp.
connectHeader :: MonadIO m
              => NodeBlock
              -> BlockHeader
              -> Timestamp
              -> SqlPersistT m (Either String BlockChainAction)
connectHeader best bh ts = runEitherT $ do
    (kno, _) <- lift $ splitKnown [bh]
    case kno of
        [] -> do
            (parent, medians, diffTime, minWork, cpM) <- getVerifyParams bh
            chain <- lift $ getChain parent
            let bn = nodeBlock parent chain bh
            liftEither $
                verifyBlockHeader parent medians diffTime cpM minWork ts bh
            lift $ putBlock bn
            lift $ evalNewChain best [bn]
        _ -> return $ KnownChain kno

-- | A more efficient way of connecting a list of block headers than connecting
-- them individually. The list of block headers have must form a valid chain.
connectHeaders :: MonadIO m
               => NodeBlock
               -> [BlockHeader]
               -> Timestamp
               -> SqlPersistT m (Either String BlockChainAction)
connectHeaders _ [] _ = runEitherT $ left "Nothing to connect"
connectHeaders best bhs ts = runEitherT $ do
    unless (validChain bhs) $ left "Block headers do not form a valid chain"
    (kno, unk) <- lift $ splitKnown bhs
    case unk of
        [] -> return $ KnownChain kno
        (bh:_) -> do
            (parent, medians, diffTime, minWork, cpM) <- getVerifyParams bh
            chain <- lift $ getChain parent
            nodes <- (`evalStateT` (parent, diffTime, medians, minWork)) $
                forM unk $ \b -> do
                    (p, d, ms, mw) <- get
                    lift $ liftEither $ verifyBlockHeader p ms d cpM mw ts b
                    let bn = nodeBlock p chain b
                        d' = if nodeBlockHeight bn `mod` diffInterval == 0
                             then blockTimestamp b
                             else d
                        ms' = blockTimestamp b : if length ms == 11
                                                 then tail ms
                                                 else ms
                        mw' = if isMinWork bn then blockBits b else mw
                    put (bn, d', ms', mw')
                    return bn
            lift $ putBlocks nodes
            lift $ evalNewChain best nodes
  where
    validChain (a:b:xs) = prevBlock b == headerHash a && validChain (b:xs)
    validChain [_] = True
    validChain _ = False

-- | Returns True if the action is a best chain.
isBestChain :: BlockChainAction -> Bool
isBestChain (BestChain _) = True
isBestChain _             = False

-- | Returns True if the action is a chain reorg.
isChainReorg :: BlockChainAction -> Bool
isChainReorg ChainReorg{} = True
isChainReorg _            = False

-- | Returns True if the action is a side chain.
isSideChain :: BlockChainAction -> Bool
isSideChain (SideChain _) = True
isSideChain _             = False

-- | Returns True if the action is a known chain.
isKnownChain :: BlockChainAction -> Bool
isKnownChain (KnownChain _) = True
isKnownChain _              = False

-- | Returns a BlockLocator object for a given block hash.
blockLocator :: MonadIO m => NodeBlock -> SqlPersistT m BlockLocator
blockLocator node = do
    nodes <- getBlocksByHeight node bs
    return $ map nodeHash nodes
  where
    h = nodeBlockHeight node
    f x s = (fst x - s, fst x > s)
    bs = (++ [0]) $ map fst $ takeWhile snd $
        [(h - x, x < h) | x <- [0..9]] ++
        scanl f (h - 10, h > 10) [2 ^ (x :: Word32) | x <- [1..]]

-- | Verify block header conforms to protocol.
verifyBlockHeader :: NodeBlock        -- ^ Parent block header
                  -> [Timestamp]      -- ^ Timestamps of previous 11 blocks
                  -> Timestamp        -- ^ Previous difficulty change
                  -> Maybe Word32     -- ^ Height of most recent checkpoint
                  -> MinWork          -- ^ Last MinWork (e.g. Testnet3)
                  -> Timestamp        -- ^ Current time
                  -> BlockHeader      -- ^ Block header to validate
                  -> Either String ()
-- TODO: Add DOS return values
verifyBlockHeader par mts dt cp mw ts bh = do
    unless (isValidPOW bh) $
        Left "Invalid proof of work"

    unless (blockTimestamp bh <= ts + 2 * 60 * 60) $
        Left "Invalid header timestamp"

    let nextWork = nextWorkRequired par dt mw bh
    unless (blockBits bh == nextWork) $
        Left "Incorrect work transition (bits)"

    let sortedMedians = sort mts
        medianTime    = sortedMedians !! (length sortedMedians `div` 2)
    when (blockTimestamp bh <= medianTime) $
        Left "Block timestamp is too early"

    let newHeight = nodeBlockHeight par + 1
    unless (maybe True (fromIntegral newHeight >) cp) $
        Left "Rewriting pre-checkpoint chain"

    unless (verifyCheckpoint (fromIntegral newHeight) (headerHash bh)) $
        Left "Rejected by checkpoint lock-in"

    -- All block of height 227836 or more use version 2 in prodnet
    -- TODO: Find out the value here for testnet
    when (networkName == "prodnet"
          && blockVersion bh == 1
          && nodeBlockHeight par + 1 >= 227836) $
        Left "Rejected version 1 block"

-- | Create a block node data structure from a block header.
nodeBlock :: NodeBlock    -- ^ Parent block node
          -> Word32       -- ^ Chain number for new node
          -> BlockHeader
          -> NodeBlock
nodeBlock parent chain bh = NodeBlock
    { nodeBlockHash              = shortHash $ headerHash bh
    , nodeBlockHeader            = NodeHeader bh
    , nodeBlockWork              = newWork
    , nodeBlockHeight            = height
    , nodeBlockChain             = chain
    }
  where
    newWork = nodeBlockWork parent + fromIntegral
        (headerWork bh `div` headerWork genesisHeader)
    height = nodeBlockHeight parent + 1

-- | Return blockchain action to connect given block with best block. Count will
-- limit the amount of blocks building up from split point towards the best
-- block.
getBlockWindow :: MonadIO m
               => NodeBlock  -- ^ Best block
               -> NodeBlock  -- ^ Start of window
               -> Word32     -- ^ Window count
               -> SqlPersistT m BlockChainAction
getBlockWindow best node cnt = do
    (_, old, new) <- splitChains (node, 0) (best, cnt)
    return $ if null old then BestChain new else ChainReorg node old new

-- | Find the split point between two nodes. It also returns the two partial
-- chains leading from the split point to the respective nodes. Tuples must
-- contain a block node and the count of nodes that should be returned from the
-- split towards that block. 0 means all.
splitChains :: MonadIO m
            => (NodeBlock, Word32)
            -> (NodeBlock, Word32)
            -> SqlPersistT m (NodeBlock, [NodeBlock], [NodeBlock])
splitChains (l, ln) (r, rn) = do
    sn <- splitBlock l r
    (split:ls) <- getBlocksFromHeight l ln (nodeBlockHeight sn)
    rs         <- getBlocksFromHeight r rn (nodeBlockHeight sn + 1)
    return (split, ls, rs)

-- | Finds the parent of a block.
getParentBlock :: MonadIO m
               => NodeBlock
               -> SqlPersistT m (Maybe NodeBlock)
getParentBlock node
    | nodeBlockHeight node == 0 = return Nothing
    | otherwise = getBlockByHash p
  where
    p = nodePrev node

-- | Get all children for a block
getChildBlocks :: MonadIO m
              => BlockHash
              -> SqlPersistT m [NodeBlock]
getChildBlocks h = do
    ch <- (+1) . nodeBlockHeight . fromMaybe e <$> getBlockByHash h
    filter ((==h) . nodePrev) <$> getBlocksAtHeight ch
  where
    e = error $ "Cannot find block hash " ++ cs (blockHashToHex h)


-- | Get the last checkpoint that we have seen.
lastSeenCheckpoint :: MonadIO m
                   => SqlPersistT m (Maybe NodeBlock)
lastSeenCheckpoint =
    fmap listToMaybe $ getBlocksByHash $ map snd $ reverse checkpointList

-- | Returns the work required for a block header given the previous block. This
-- coresponds to bitcoind function GetNextWorkRequired in main.cpp.
nextWorkRequired :: NodeBlock
                 -> Timestamp
                 -> MinWork
                 -> BlockHeader
                 -> Word32
nextWorkRequired par ts mw bh
    -- Genesis block
    | nodeBlockHeight par == 0 = encodeCompact powLimit
    -- Only change the difficulty once per interval
    | (nodeBlockHeight par + 1) `mod` diffInterval /= 0 =
        if allowMinDifficultyBlocks
            then minPOW
            else blockBits $ nodeHeader par
    | otherwise = workFromInterval ts (nodeHeader par)
  where
    delta = targetSpacing * 2
    minPOW
        | blockTimestamp bh > nodeTimestamp par + delta = encodeCompact powLimit
        | otherwise = mw

-- | Computes the work required for the next block given a timestamp and the
-- current block. The timestamp should come from the block that matched the
-- last jump in difficulty (spaced out by 2016 blocks in prodnet).
workFromInterval :: Timestamp -> BlockHeader -> Word32
workFromInterval ts lastB
    | newDiff > powLimit = encodeCompact powLimit
    | otherwise          = encodeCompact newDiff
  where
    t = fromIntegral $ blockTimestamp lastB - ts
    actualTime
        | t < targetTimespan `div` 4 = targetTimespan `div` 4
        | t > targetTimespan * 4     = targetTimespan * 4
        | otherwise                  = t
    lastDiff = decodeCompact $ blockBits lastB
    newDiff = lastDiff * toInteger actualTime `div` toInteger targetTimespan

-- | Returns True if the difficulty target (bits) of the header is valid and the
-- proof of work of the header matches the advertised difficulty target. This
-- function corresponds to the function CheckProofOfWork from bitcoind in
-- main.cpp.
isValidPOW :: BlockHeader -> Bool
isValidPOW bh
    | target <= 0 || target > powLimit = False
    | otherwise = headerPOW bh <= fromIntegral target
  where
    target = decodeCompact $ blockBits bh

-- | Returns the proof of work of a block header as an Integer number.
headerPOW :: BlockHeader -> Integer
headerPOW =  bsToInteger . BS.reverse . encode' . headerHash

-- | Returns the work represented by this block. Work is defined as the number
-- of tries needed to solve a block in the average case with respect to the
-- target.
headerWork :: BlockHeader -> Integer
headerWork bh =
    fromIntegral $ largestHash `div` (target + 1)
  where
    target      = decodeCompact (blockBits bh)
    largestHash = 1 `shiftL` 256

{- Persistent backend -}

chainPathQuery :: forall (expr :: * -> *) (query :: * -> *) backend.
                  Esqueleto query expr backend
               => expr (Entity NodeBlock)
               -> [NodeBlock]
               -> expr (Value Bool)
chainPathQuery _ [] = error "Monsters, monsters everywhere"

chainPathQuery t [NodeBlock{..}] =
    t ^. NodeBlockHeight <=. val nodeBlockHeight &&.
    t ^. NodeBlockChain ==. val nodeBlockChain

chainPathQuery t (n1:bs@(n2:_)) = chainPathQuery t bs ||.
    (   t ^. NodeBlockHeight <=. val (nodeBlockHeight n1)
    &&. t ^. NodeBlockHeight >.  val (nodeBlockHeight n2)
    &&. t ^. NodeBlockChain  ==. val (nodeBlockChain  n1)
    )

getHeads :: MonadIO m => SqlPersistT m [NodeBlock]
getHeads = fmap (map (entityVal . snd)) $ select $ from $ \t -> do
    groupBy $ t ^. NodeBlockChain
    return (max_ (t ^. NodeBlockHeight), t)

-- | Chain for new block building on a parent node
getChain :: MonadIO m
         => NodeBlock    -- ^ Parent node
         -> SqlPersistT m Word32
getChain parent = do
    maxHeightM <- fmap (unValue <=< listToMaybe) $ select $ from $ \t -> do
        where_ $ t ^. NodeBlockChain ==. val (nodeBlockChain parent)
        return $ max_ $ t ^. NodeBlockHeight
    let maxHeight = fromMaybe (error "That chain does not exist") maxHeightM
    if maxHeight == nodeBlockHeight parent
        then return $ nodeBlockChain parent
        else do
            maxChainM <- fmap (unValue <=< listToMaybe) $ select $ from $ \t ->
                return $ max_ $ t ^. NodeBlockChain
            let maxChain = fromMaybe (error "Ran out of chains") maxChainM
            return $ maxChain + 1

getPivots :: MonadIO m => NodeBlock -> SqlPersistT m [NodeBlock]
getPivots = go []
  where
    go acc b
        | nodeBlockChain b == 0 = return $ genesisBlock : b : acc
        | otherwise = do
            l <- fromMaybe (error "Houston, we have a problem") <$>
                getChainLowest b
            c <- fromMaybe (error "Ground Control to Major Tom") <$>
                getParentBlock l
            go (b:acc) c

getChainLowest :: MonadIO m => NodeBlock -> SqlPersistT m (Maybe NodeBlock)
getChainLowest nb = fmap (listToMaybe . map entityVal) $
    select $ from $ \t -> do
        where_ $ t ^. NodeBlockChain ==. val (nodeBlockChain nb)
        orderBy [ asc $ t ^. NodeBlockHeight ]
        limit 1
        return t

-- | Get node height and chain common to both given.
splitBlock :: MonadIO m
           => NodeBlock
           -> NodeBlock
           -> SqlPersistT m NodeBlock
splitBlock l r = if nodeBlockChain l == nodeBlockChain r
    then if nodeBlockHeight l < nodeBlockHeight r
        then return l
        else return r
    else do
        pivotsL <- getPivots l
        pivotsR <- getPivots r
        let ns = zip pivotsL pivotsR
            f (x,y) = nodeBlockChain x == nodeBlockChain y
            (one, two) = last $ takeWhile f ns
        if nodeBlockHeight one < nodeBlockHeight two
            then return one
            else return two

-- | Put single block in database.
putBlock :: MonadIO m => NodeBlock -> SqlPersistT m ()
putBlock = insert_

-- | Put multiple blocks in database.
putBlocks :: MonadIO m => [NodeBlock] -> SqlPersistT m ()
putBlocks = mapM_ insertMany_ . f
  where
    f [] = []
    f xs = let (xs',xxs) = splitAt 50 xs in xs' : f xxs

getBestBlock :: MonadIO m => SqlPersistT m NodeBlock
getBestBlock =
    maximumBy (compare `on` nodeBlockWork) <$> getHeads

getBlockByHash :: MonadIO m => BlockHash -> SqlPersistT m (Maybe NodeBlock)
getBlockByHash h =
    fmap (listToMaybe . map entityVal) $ select $ from $ \t -> do
        where_ $ t ^. NodeBlockHash ==. val (shortHash h)
        return t

-- | Get multiple blocks corresponding to given hashes
getBlocksByHash :: MonadIO m
                => [BlockHash]
                -> SqlPersistT m [NodeBlock]
getBlocksByHash hashes = do
    nodes <- fmap (map entityVal) $ select $ from $ \t -> do
        where_ $ t ^. NodeBlockHash `in_` valList (map shortHash hashes)
        return t
    return $ mapMaybe
        (\h -> find ((== shortHash h) . nodeBlockHash) nodes)
        hashes

-- | Get ancestor of specified block at given height.
getBlockByHeight :: MonadIO m
                  => NodeBlock     -- ^ Best block
                  -> BlockHeight
                  -> SqlPersistT m (Maybe NodeBlock)
getBlockByHeight block height = do
    forks <- reverse <$> getPivots block
    fmap (listToMaybe . map entityVal) $ select $ from $ \t -> do
        where_ $ chainPathQuery t forks &&.
            t ^. NodeBlockHeight ==. val height
        return t

-- | Get ancestors for specified block at given heights.
getBlocksByHeight :: MonadIO m
                  => NodeBlock       -- ^ Best block
                  -> [BlockHeight]
                  -> SqlPersistT m [NodeBlock]
getBlocksByHeight best heights = do
    forks <- reverse <$> getPivots best
    nodes <- fmap (map entityVal) $ select $ from $ \t -> do
        where_ $ chainPathQuery t forks &&.
            t ^. NodeBlockHeight `in_` valList heights
        return t
    return $ mapMaybe (\h -> find ((==h) . nodeBlockHeight) nodes) heights

-- | Get a range of block headers building up to specified block. If
-- specified height is too large, an empty list will be returned.
getBlocksFromHeight :: MonadIO m
                    => NodeBlock     -- ^ Best block
                    -> Word32        -- ^ Count (0 for all)
                    -> BlockHeight   -- ^ Height from (including)
                    -> SqlPersistT m [NodeBlock]
getBlocksFromHeight block cnt height = do
    forks <- reverse <$> getPivots block
    fmap (map entityVal) $ select $ from $ \t -> do
        where_ $ chainPathQuery t forks &&.
            t ^. NodeBlockHeight >=. val height
        when (cnt > 0) $ limit $ fromIntegral cnt
        return t

-- | Get node immediately at or after timestamp in main chain.
getBlockAfterTime :: MonadIO m => Timestamp -> SqlPersistT m (Maybe NodeBlock)
getBlockAfterTime ts = do
    n@NodeBlock{..} <- getBestBlock
    f genesisBlock n
  where
    f l r | nodeTimestamp r < ts =
              return Nothing
          | nodeTimestamp l >= ts =
              return $ Just l
          | (nodeBlockHeight r - nodeBlockHeight l) `div` 2 == 0 =
              return $ Just r
          | otherwise = do
              let rh = nodeBlockHeight r
                  lh = nodeBlockHeight l
                  mh = rh - (rh - lh) `div` 2
              m <- fromMaybe (error "My God, it’s full of stars!") <$>
                  getBlockByHeight r mh
              if nodeTimestamp m > ts then f l m else f m r

-- | Get blocks at specified height in all chains.
getBlocksAtHeight :: MonadIO m => BlockHeight -> SqlPersistT m [NodeBlock]
getBlocksAtHeight height = fmap (map entityVal) $ select $ from $ \t -> do
      where_ $ t ^. NodeBlockHeight ==. val height
      return t

-- | Evaluate block action for provided best block and chain of new blocks.
evalNewChain :: MonadIO m
             => NodeBlock
             -> [NodeBlock]
             -> SqlPersistT m BlockChainAction
evalNewChain _ [] = error "You find yourself in the dungeon of missing blocks"
evalNewChain best newNodes
    | buildsOnBest = do
        pruneChain best
        return $ BestChain $ map (\n -> n{ nodeBlockChain = 0 }) newNodes
    | nodeBlockWork (last newNodes) > nodeBlockWork best = do
        (split, old, new) <- splitChains (best, 0) (head newNodes, 0)
        return $ ChainReorg split old (new ++ tail newNodes)
    | otherwise = do
        (split, _, new) <- splitChains (best, 0) (head newNodes, 0)
        case new of
            [] -> return $ KnownChain newNodes
            _  -> return $ SideChain $ split : new ++ tail newNodes
  where
    buildsOnBest = nodePrev (head newNodes) == nodeHash best

pruneChain :: MonadIO m
           => NodeBlock
           -> SqlPersistT m ()
pruneChain best = do
    when (nodeBlockChain best /= 0) $ do
        forks <- reverse <$> getPivots best
        delete $ from $ \t -> do
            where_ $ not_ (chainPathQuery t forks)
                 &&. t ^. NodeBlockHeight <=. val (nodeBlockHeight best)
        update $ \t -> do
            set t [ NodeBlockChain =. val 0 ]
            where_ $ t ^. NodeBlockHeight <=. val (nodeBlockHeight best)
                 &&. t ^. NodeBlockChain  !=. val 0