{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NumericUnderscores #-}

{- |
Module: Language.Bitcoin.Script.Descriptors.Utils
Stability: experimental
-}
module Language.Bitcoin.Script.Descriptors.Utils (
    -- * Conversions
    descriptorAddresses,
    compile,

    -- * Transaction pieces
    TransactionScripts (..),
    outputDescriptorScripts,

    -- * Script families
    keyAtIndex,
    keyDescriptorAtIndex,
    scriptDescriptorAtIndex,
    outputDescriptorAtIndex,

    -- * Pub keys
    outputDescriptorPubKeys,
    scriptDescriptorPubKeys,

    -- * PSBT
    toPsbtInput,
    PsbtInputError (..),
) where

import Control.Applicative ((<|>))
import Control.Exception (Exception)
import Data.Functor ((<&>))
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HM
import Data.List (sortOn)
import Data.Maybe (fromMaybe, mapMaybe, maybeToList)
import Data.Serialize (decode, encode)
import qualified Data.Serialize as S
import Data.Word (Word32)
import Haskoin (
    Address,
    DerivPath,
    DerivPathI ((:/), (:|)),
    Fingerprint,
    Input,
    KeyIndex,
    PubKeyI (..),
    Script,
    ScriptOutput (..),
    Tx,
    addressHash,
    eitherToMaybe,
    emptyInput,
    encodeOutput,
    inputHDKeypaths,
    inputRedeemScript,
    inputWitnessScript,
    nonWitnessUtxo,
    pathToList,
    payToNestedScriptAddress,
    payToScriptAddress,
    payToWitnessScriptAddress,
    pubKeyAddr,
    pubKeyCompatWitnessAddr,
    pubKeyCompressed,
    pubKeyWitnessAddr,
    sortMulSig,
    toP2SH,
    toP2WSH,
    txOut,
    witnessUtxo,
    xPubFP,
    (++/),
 )

import qualified Language.Bitcoin.Miniscript.Compiler as M (
    compile,
 )
import qualified Language.Bitcoin.Miniscript.Syntax as M (
    key,
    keyH,
    multi,
 )
import Language.Bitcoin.Script.Descriptors.Syntax (
    Key (XPub),
    KeyCollection (..),
    KeyDescriptor (KeyDescriptor, keyDef),
    OutputDescriptor (..),
    ScriptDescriptor (..),
    derivation,
    fingerprint,
    keyBytes,
    keyDescPubKey,
 )

{- | Get the set of addresses associated with an output descriptor.  The list will be empty if:

     * any keys are indefinite
     * the output is p2pk
     * the output has a non-standard script

     The list can contain more than one address in the case of the "combo" construct.
-}
descriptorAddresses :: OutputDescriptor -> [Address]
descriptorAddresses = \case
    ScriptPubKey Pk{} -> mempty
    ScriptPubKey (Pkh key) -> foldMap (pure . pubKeyAddr) $ keyDescPubKey key
    P2SH descriptor -> maybeToList $ payToScriptAddress <$> scriptDescriptorOutput descriptor
    P2WPKH key -> foldMap (pure . pubKeyWitnessAddr) $ keyDescPubKey key
    P2WSH descriptor -> maybeToList $ payToWitnessScriptAddress <$> scriptDescriptorOutput descriptor
    WrappedWPkh key -> foldMap (pure . pubKeyCompatWitnessAddr) $ keyDescPubKey key
    WrappedWSh descriptor -> maybeToList $ payToNestedScriptAddress <$> scriptDescriptorOutput descriptor
    Combo key
        | Just pk <- keyDescPubKey key ->
            [pubKeyAddr pk]
                <> if pubKeyCompressed pk
                    then [pubKeyWitnessAddr pk, pubKeyCompatWitnessAddr pk]
                    else mempty
    Addr addr -> [addr]
    _ -> mempty

scriptDescriptorOutput :: ScriptDescriptor -> Maybe ScriptOutput
scriptDescriptorOutput = \case
    Pk key -> PayPK <$> keyDescPubKey key
    Pkh key -> PayPKHash . addressHash . encode <$> keyDescPubKey key
    Multi k ks -> PayMulSig <$> traverse keyDescPubKey ks <*> pure k
    SortedMulti k ks -> sortMulSig <$> (PayMulSig <$> traverse keyDescPubKey ks <*> pure k)
    _ -> Nothing

-- | Produce the script described by the descriptor.  Fails when any keys in the descriptor are indeterminate.
compile :: ScriptDescriptor -> Maybe Script
compile = \case
    Pk key -> compileMaybe $ M.key key
    Pkh key -> compileMaybe $ M.keyH key
    Multi k ks -> compileMaybe $ M.multi k ks
    SortedMulti k ks -> compileMaybe $ M.multi k (sortOn keyBytes ks)
    Raw bs -> eitherToMaybe (decode bs)
  where
    compileMaybe = eitherToMaybe . M.compile

data TransactionScripts = TransactionScripts
    { txScriptPubKey :: Script
    , txRedeemScript :: Maybe Script
    , txWitnessScript :: Maybe Script
    }
    deriving (Eq, Show)

outputDescriptorScripts :: OutputDescriptor -> Maybe TransactionScripts
outputDescriptorScripts =
    \case
        ScriptPubKey sd ->
            compile sd <&> \theScriptPubKey ->
                TransactionScripts
                    { txScriptPubKey = theScriptPubKey
                    , txRedeemScript = Nothing
                    , txWitnessScript = Nothing
                    }
        P2SH sd ->
            compile sd <&> \theScript ->
                TransactionScripts
                    { txScriptPubKey = encodeOutput $ toP2SH theScript
                    , txRedeemScript = Just theScript
                    , txWitnessScript = Nothing
                    }
        P2WPKH kd -> do
            theScriptPubKey <- encodeOutput . PayWitnessPKHash . addressHash . S.encode <$> keyDescPubKey kd
            pure
                TransactionScripts
                    { txScriptPubKey = theScriptPubKey
                    , txRedeemScript = Nothing
                    , txWitnessScript = Nothing
                    }
        P2WSH sd ->
            compile sd <&> \theScript ->
                TransactionScripts
                    { txScriptPubKey = encodeOutput $ toP2WSH theScript
                    , txRedeemScript = Nothing
                    , txWitnessScript = Just theScript
                    }
        WrappedWPkh kd -> do
            theRedeemScript <- encodeOutput . PayWitnessPKHash . addressHash . S.encode <$> keyDescPubKey kd
            pure
                TransactionScripts
                    { txScriptPubKey = encodeOutput $ toP2SH theRedeemScript
                    , txRedeemScript = Just theRedeemScript
                    , txWitnessScript = Nothing
                    }
        WrappedWSh sd ->
            compile sd <&> \theScript ->
                let theRedeemScript = encodeOutput $ toP2WSH theScript
                 in TransactionScripts
                        { txScriptPubKey = encodeOutput $ toP2SH theRedeemScript
                        , txRedeemScript = Just theRedeemScript
                        , txWitnessScript = Just theScript
                        }
        Combo _kd -> Nothing
        Addr _ad -> Nothing

{- | For key families, get the key at the given index.  Otherwise, return the input key.

  @since 0.2.1
-}
keyAtIndex :: Word32 -> Key -> Key
keyAtIndex ix = \case
    XPub xpub path HardKeys -> XPub xpub (path :| ix) Single
    XPub xpub path SoftKeys -> XPub xpub (path :/ ix) Single
    key -> key

{- | Specialize key families occurring in the descriptor to the given index

 @since 0.2.1
-}
outputDescriptorAtIndex :: KeyIndex -> OutputDescriptor -> OutputDescriptor
outputDescriptorAtIndex ix = \case
    o@ScriptPubKey{} -> o
    P2SH sd -> P2SH $ scriptDescriptorAtIndex ix sd
    P2WPKH kd -> P2WPKH $ keyDescriptorAtIndex ix kd
    P2WSH sd -> P2WSH $ scriptDescriptorAtIndex ix sd
    WrappedWPkh kd -> WrappedWPkh $ keyDescriptorAtIndex ix kd
    WrappedWSh sd -> WrappedWSh $ scriptDescriptorAtIndex ix sd
    Combo kd -> Combo $ keyDescriptorAtIndex ix kd
    a@Addr{} -> a

{- | Specialize key families occurring in the descriptor to the given index

 @since 0.2.1
-}
scriptDescriptorAtIndex :: KeyIndex -> ScriptDescriptor -> ScriptDescriptor
scriptDescriptorAtIndex ix = \case
    Pk kd -> Pk $ specialize kd
    Pkh kd -> Pkh $ specialize kd
    Multi k ks -> Multi k $ specialize <$> ks
    SortedMulti k ks -> SortedMulti k $ specialize <$> ks
    r@Raw{} -> r
  where
    specialize = keyDescriptorAtIndex ix

{- | Specialize key families occurring in the descriptor to the given index

 @since 0.2.1
-}
keyDescriptorAtIndex :: KeyIndex -> KeyDescriptor -> KeyDescriptor
keyDescriptorAtIndex ix keyDescriptor = keyDescriptor{keyDef = keyAtIndex ix $ keyDef keyDescriptor}

{- | Produce the psbt input parameters needed to spend an output from the
descriptor.  Caveat: This construction fails on `Combo` and `Addr` outputs.

 @since 0.2.1
-}
toPsbtInput ::
    -- | Transaction being spent
    Tx ->
    -- | Output being spent
    Int ->
    -- | Descriptor for output being spent
    OutputDescriptor ->
    Either PsbtInputError Input
toPsbtInput tx ix descriptor = case descriptor of
    ScriptPubKey sd ->
        pure
            emptyInput
                { nonWitnessUtxo = Just tx
                , inputHDKeypaths = hdPaths sd
                }
    P2SH sd -> do
        script <- compileForInput sd
        pure
            emptyInput
                { nonWitnessUtxo = Just tx
                , inputRedeemScript = Just script
                , inputHDKeypaths = hdPaths sd
                }
    P2WPKH kd -> do
        output <- txOut tx `safeIndex` ix
        pure
            emptyInput
                { witnessUtxo = Just output
                , inputHDKeypaths = hdPath kd
                }
    P2WSH sd -> do
        output <- txOut tx `safeIndex` ix
        script <- compileForInput sd
        pure
            emptyInput
                { witnessUtxo = Just output
                , inputWitnessScript = Just script
                , inputHDKeypaths = hdPaths sd
                }
    WrappedWPkh kd -> do
        output <- txOut tx `safeIndex` ix
        k <- maybe (Left $ KeyNotAvailable kd) pure $ keyDescPubKey kd
        pure
            emptyInput
                { witnessUtxo = Just output
                , inputRedeemScript =
                    Just
                        . encodeOutput
                        . PayWitnessPKHash
                        . addressHash
                        $ encode k
                , inputHDKeypaths = hdPath kd
                }
    WrappedWSh sd -> do
        output <- txOut tx `safeIndex` ix
        script <- compileForInput sd
        pure
            emptyInput
                { witnessUtxo = Just output
                , inputRedeemScript = Just . encodeOutput $ toP2WSH script
                , inputWitnessScript = Just script
                , inputHDKeypaths = hdPaths sd
                }
    o@Combo{} -> Left $ InvalidOutput o
    o@Addr{} -> Left $ InvalidOutput o
  where
    hdPaths = foldMap hdPath . scriptKeys
    compileForInput sd = maybe (Left $ CompileError sd) pure $ compile sd

    safeIndex (x : xs) n
        | n == 0 = pure x
        | n > 0 = safeIndex xs (n - 1)
    safeIndex _ _ = Left $ OutputIndexOOB tx ix

data PsbtInputError
    = OutputIndexOOB Tx Int
    | CompileError ScriptDescriptor
    | KeyNotAvailable KeyDescriptor
    | InvalidOutput OutputDescriptor
    deriving (Eq, Show)

instance Exception PsbtInputError

hdPath :: KeyDescriptor -> HashMap PubKeyI (Fingerprint, [KeyIndex])
hdPath k@(KeyDescriptor origin theKeyDef) = fromMaybe mempty $ do
    pubKey <- keyDescPubKey k
    fromOrigin pubKey <|> fromKey pubKey
  where
    fromOrigin pubKey = do
        theOrigin <- origin
        theKeyPath <- keyPath theKeyDef
        pure $
            HM.singleton
                pubKey
                ( fingerprint theOrigin
                , pathToList $ derivation theOrigin ++/ theKeyPath
                )
    fromKey pubKey =
        case theKeyDef of
            XPub xpub path Single ->
                pure $
                    HM.singleton
                        pubKey
                        ( xPubFP xpub
                        , pathToList path
                        )
            _ -> Nothing

keyPath :: Key -> Maybe DerivPath
keyPath = \case
    XPub _ path Single -> Just path
    _ -> Nothing

scriptKeys :: ScriptDescriptor -> [KeyDescriptor]
scriptKeys = \case
    Pk k -> [k]
    Pkh k -> [k]
    Multi _ ks -> ks
    SortedMulti _ ks -> ks
    Raw{} -> mempty

-- | Extract pubkeys from an 'OutputDescriptor' where possible
outputDescriptorPubKeys :: OutputDescriptor -> [PubKeyI]
outputDescriptorPubKeys = \case
    ScriptPubKey sd -> scriptDescriptorPubKeys sd
    P2SH sd -> scriptDescriptorPubKeys sd
    P2WPKH kd -> foldMap pure $ keyDescPubKey kd
    P2WSH sd -> scriptDescriptorPubKeys sd
    WrappedWPkh kd -> foldMap pure $ keyDescPubKey kd
    WrappedWSh sd -> scriptDescriptorPubKeys sd
    Combo kd -> foldMap pure $ keyDescPubKey kd
    Addr _ad -> mempty

-- | Extract pubkeys from a 'ScriptDescriptor' where possible
scriptDescriptorPubKeys :: ScriptDescriptor -> [PubKeyI]
scriptDescriptorPubKeys = mapMaybe keyDescPubKey . scriptKeys
