{-# LANGUAGE DataKinds #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}

{-# OPTIONS_HADDOCK prune #-}
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} -- needed for {#- HLINT ... #-}

module Data.UTxO.Transaction.Cardano.Byron
    (
    -- * Initialization
      mkInit
    , mainnetMagic
    , testnetMagic

    -- * Constructing Primitives
    , mkInput
    , mkOutput
    , mkSignKey

    -- Internal
    , Byron
    , encodeCoinSel
    , decodeCoinSel
    , encodeTx
    , decodeTx
    ) where

import Cardano.Binary
    ( FromCBOR (..), ToCBOR (..) )
import Cardano.Chain.Common
    ( mkAttributes, mkLovelace )
import Cardano.Chain.UTxO
    ( TxIn (..), TxInWitness (..), TxOut (..), TxSigData (..), mkTxAux )
import Cardano.Crypto.Extra
    ( xprvFromBytes )
import Cardano.Crypto.Hashing
    ( abstractHashFromDigest, serializeCborHash )
import Cardano.Crypto.ProtocolMagic
    ( ProtocolMagicId (..) )
import Cardano.Crypto.Signing
    ( SignTag (..), Signature, SigningKey (..), VerificationKey (..) )
import Cardano.Crypto.Wallet
    ( toXPub )
import Codec.CBOR.Read
    ( deserialiseFromBytes )
import Crypto.Hash
    ( Blake2b_256, digestFromByteString )
import Data.ByteString
    ( ByteString )
import Data.List.NonEmpty
    ( NonEmpty, nonEmpty )
import Data.UTxO.Transaction
    ( ErrMkPayment (..), MkPayment (..) )
import Data.Word
    ( Word32 )
import GHC.Exts
    ( IsList (fromList) )
import Numeric.Natural
    ( Natural )

import qualified Cardano.Chain.UTxO as CC
import qualified Cardano.Crypto.Signing as CC
import qualified Codec.CBOR.Decoding as CBOR
import qualified Codec.CBOR.Encoding as CBOR
import qualified Codec.CBOR.Write as CBOR
import qualified Data.ByteString.Lazy as BL
import qualified Data.List.NonEmpty as NE
import qualified Data.Text as T

-- | Construct a payment 'Init' for /Byron/ from primitive types.
--
-- __examples__:
--
-- >>> mkInit 764824073 == mainnetMagic
-- True
--
-- >>> mkInit 1097911063 == testnetMagic
-- True
--
-- @since 1.0.0
mkInit
    :: Word32
        -- ^ A protocol magic id
    -> Init Byron
mkInit =
    ProtocolMagicId

-- | Pre-defined 'Init' magic for /Byron/ MainNet.
--
-- @since 1.0.0
mainnetMagic :: Init Byron
mainnetMagic = mkInit 764824073

-- | Pre-defined 'Init' magic for /Byron/ TestNet.
--
-- @since 1.0.0
testnetMagic :: Init Byron
testnetMagic = mkInit 1097911063

-- | Construct a payment 'Input' for /Byron/ from primitive types.
--
-- __example__:
--
-- >>> mkInput 14 =<< fromBase16 "3b402651...aad1c0b7"
-- Just (Input ...)
--
-- @since 1.0.0
mkInput
    :: Word32
        -- ^ Input index.
    -> ByteString
        -- ^ Input transaction id. See also: 'fromBase16'.
    -> Maybe (Input Byron)
mkInput ix bytes =
    case digestFromByteString @Blake2b_256 bytes of
        Just txId -> Just $ TxInUtxo (abstractHashFromDigest txId) ix
        Nothing -> Nothing

-- | Construct a payment 'Output' for /Byron/ from primitive types.
--
-- __example__:
--
-- >>> mkOutput 42 =<< fromBase58 "Ae2tdPwU...DnXy319f"
-- Just (Output ...)
--
-- @since 1.0.0
mkOutput
    :: Natural
        -- ^ Output value, in Lovelace (1 Ada = 1e6 Lovelace).
    -> ByteString
        -- ^ Output Address. See also: 'fromBase58'.
    -> Maybe (Output Byron)
mkOutput n bytes =
    case (fromCBOR' bytes, mkLovelace (fromIntegral n)) of
        (Right addr, Right coin) -> Just $ TxOut addr coin
        _ -> Nothing
  where
    fromCBOR' = fmap snd . deserialiseFromBytes fromCBOR .  BL.fromStrict

-- | Construct a 'SignKey' for /Byron/ from primitive types.
--
-- __example__:
--
-- >>> mkSignKey =<< fromBase16 "3b402651...aad1c0b7"
-- Just (SignKey ...)
--
-- @since 1.0.0
mkSignKey
    :: ByteString
        -- ^ A extended address private key and its chain code.
        -- The key __must be 96 bytes__ long, internally made of two concatenated parts:
        --
        -- @
        -- BYTES = PRV | CC
        -- PRV   = 64OCTET  # a 64 bytes Ed25519 extended private key
        -- CC    = 32OCTET  # a 32 bytes chain code
        -- @
        --
        -- See also: 'fromBase16'.
    -> Maybe (SignKey Byron)
mkSignKey = fmap SigningKey . xprvFromBytes

--
-- MkPayment instance
--

-- Type-level constructor capturing types for 'Byron'.
data Byron

instance MkPayment Byron where
    type Init Byron = ProtocolMagicId

    type Input   Byron = TxIn
    type Output  Byron = TxOut
    type SignKey Byron = SigningKey

    type CoinSel Byron =
        (ProtocolMagicId, [TxIn], [TxOut])

    type Tx Byron = Either
        ErrMkPayment
        (ProtocolMagicId, NonEmpty TxIn, NonEmpty TxOut, TxSigData, [TxInWitness])

    empty :: ProtocolMagicId -> CoinSel Byron
    empty pm = (pm, mempty, mempty)

    addInput :: TxIn -> CoinSel Byron -> CoinSel Byron
    addInput inp (pm, inps, outs) = (pm, inp : inps, outs)

    addOutput :: TxOut -> CoinSel Byron -> CoinSel Byron
    addOutput out (pm, inps, outs) = (pm, inps, out : outs)

    lock :: CoinSel Byron -> Tx Byron
    lock (_pm, [], _outs) = Left MissingInput
    lock (_pm, _inps, []) = Left MissingOutput
    lock (pm, inps, outs) =
        Right (pm, neInps, neOuts, sigData, mempty)
      where
        sigData = TxSigData $ serializeCborHash $ CC.UnsafeTx neInps neOuts (mkAttributes ())
        neInps  = NE.fromList $ reverse inps
        neOuts  = NE.fromList $ reverse outs

    signWith :: SigningKey -> Tx Byron -> Tx Byron
    signWith _ (Left e) = Left e
    signWith (SigningKey prv) (Right (pm, inps, outs, sigData, wits)) =
        Right (pm, inps, outs, sigData, VKWitness vk sig : wits)
      where
        vk :: VerificationKey
        vk = VerificationKey (toXPub prv)

        sig :: Signature TxSigData
        sig = CC.sign pm SignTx (SigningKey prv) sigData

    serialize :: Tx Byron -> Either ErrMkPayment ByteString
    serialize (Left e) = Left e
    serialize (Right (_pm, inps, outs, _sigData, wits))
        | NE.length inps /= length wits = Left MissingSignature
        | otherwise = Right $ CBOR.toStrictByteString $ toCBOR $ mkTxAux
            (CC.UnsafeTx inps outs (mkAttributes ()))
            (fromList $ reverse wits)


-- Internal
--
-- For running the Payment DSL as a command-line, we need to be able to produce
-- a text output representing the internal state.
--
-- There's no point about obfuscating this more than necessary; this should
-- remain mainly invisible to users if used properly through pipes. Yet, it can
-- be useful to have a human-friendly representation that is base16 or base64
-- encoded. Three possible obvious choice:
--
-- - CBOR, since most data above can already be serialized to CBOR
-- - JSON, instances exists on most type and are derived generically from CBOR
-- - Show, although here most types don't have a corresponding 'Read' instance.
--
-- Since we also need to decode an encoded state, CBOR will be path of least
-- resistance.

-- __Internal__: Encode a 'CoinSel Byron' to CBOR.
encodeCoinSel :: CoinSel Byron -> CBOR.Encoding
encodeCoinSel (pm, inps, outs) = mconcat
    [ toCBOR pm
    , CBOR.encodeListLenIndef
    , mconcat (toCBOR <$> inps)
    , CBOR.encodeBreak
    , CBOR.encodeListLenIndef
    , mconcat (toCBOR <$> outs)
    , CBOR.encodeBreak
    ]

-- __Internal__: Decode a 'CoinSel Byron' from CBOR.
decodeCoinSel :: CBOR.Decoder s (CoinSel Byron)
decodeCoinSel = (,,)
    <$> fromCBOR
    <*> decodeListIndef fromCBOR
    <*> decodeListIndef fromCBOR

-- __Internal__: Encode a 'Tx Byron' to CBOR.
encodeTx :: Tx Byron -> CBOR.Encoding
encodeTx (Left e) = mconcat
    [ CBOR.encodeWord8 0
    , CBOR.encodeString $ T.pack $ show e
    ]
encodeTx (Right (pm, inps, outs, sigData, wits)) = mconcat
    [ CBOR.encodeWord8 1
    , encodeCoinSel (pm, NE.toList inps, NE.toList outs)
    , toCBOR sigData
    , CBOR.encodeListLenIndef
    , mconcat (toCBOR <$> wits)
    , CBOR.encodeBreak
    ]

{-# HLINT ignore decodeTx "Redundant fmap" #-}
-- __Internal__: Decode a 'Tx Byron' from CBOR.
decodeTx :: CBOR.Decoder s (Tx Byron)
decodeTx = do
    CBOR.decodeWord8 >>= \case
        0 -> fmap T.unpack CBOR.decodeString >>= \case
            str | str == show MissingInput     -> pure $ Left MissingInput
            str | str == show MissingOutput    -> pure $ Left MissingOutput
            str | str == show MissingSignature -> pure $ Left MissingSignature
            _ -> fail $
                "Invalid error constructor found in 'Tx Byron'. Constructor must \
                \be one of: " <> unwords (show <$>
                    [ MissingInput, MissingOutput, MissingSignature])
        1 -> do
            (pm, inps, outs) <- decodeCoinSel
            sigData <- fromCBOR
            wits <- decodeListIndef fromCBOR
            case (nonEmpty inps, nonEmpty outs) of
                (Nothing, _) -> fail
                    "Empty list of inputs found in 'Tx Byron'. This is impossible \
                    \unless the data has been modified by hand."
                (_, Nothing) -> fail
                    "Empty list of outputs found in 'Tx Byron'. This is impossible \
                    \unless the data has been modified by hand."
                (Just neInps, Just neOuts) ->
                    pure $ Right (pm, neInps, neOuts, sigData, wits)

        _ -> fail
            "'Tx Byron' has been modified  with and is now invalid. The first \
            \byte must be either 0 or 1, followed by respectively an error \
            \constructor or constituants of the intermediate transaction."

-- __Internal__ Decode an arbitrary long list 'sandwiched' by markers.
decodeListIndef :: forall s a. CBOR.Decoder s a -> CBOR.Decoder s [a]
decodeListIndef decodeOne = do
    _ <- CBOR.decodeListLenIndef
    CBOR.decodeSequenceLenIndef (flip (:)) [] reverse decodeOne