// Copyright (c) 2017 The Zcash developers
// Copyright (c) 2016-2024 The Hush developers
// Distributed under the GPLv3 software license, see the accompanying
// file COPYING or https://www.gnu.org/licenses/gpl-3.0.en.html
/******************************************************************************
 * Copyright © 2014-2019 The SuperNET Developers.                             *
 *                                                                            *
 * See the AUTHORS, DEVELOPER-AGREEMENT and LICENSE files at                  *
 * the top-level directory of this distribution for the individual copyright  *
 * holder information and the developer policies on copyright and licensing.  *
 *                                                                            *
 * Unless otherwise agreed in a custom licensing agreement, no part of the    *
 * SuperNET software, including this file may be copied, modified, propagated *
 * or distributed except according to the terms contained in the LICENSE file *
 *                                                                            *
 * Removal or modification of this copyright notice is prohibited.            *
 *                                                                            *
 ******************************************************************************/
#include "asyncrpcqueue.h"
#include "amount.h"
#include "consensus/upgrades.h"
#include "core_io.h"
#include "init.h"
#include "key_io.h"
#include "main.h"
#include "net.h"
#include "netbase.h"
#include "rpc/protocol.h"
#include "rpc/server.h"
#include "timedata.h"
#include "util.h"
#include "utilmoneystr.h"
#include "wallet.h"
#include "walletdb.h"
#include "script/interpreter.h"
#include "utiltime.h"
#include "zcash/IncrementalMerkleTree.hpp"
#include "sodium.h"
#include "miner.h"
#include <array>
#include <iostream>
#include <chrono>
#include <thread>
#include <string>
#include "asyncrpcoperation_shieldcoinbase.h"

using namespace libzcash;
extern uint64_t ASSETCHAINS_TIMELOCKGTE;
extern string randomSietchZaddr();

AsyncRPCOperation_shieldcoinbase::AsyncRPCOperation_shieldcoinbase(
        TransactionBuilder builder,
        CMutableTransaction contextualTx,
        std::vector<ShieldCoinbaseUTXO> inputs,
        std::string toAddress,
        CAmount fee,
        uint8_t donation,
        UniValue contextInfo) :
        builder_(builder), tx_(contextualTx), inputs_(inputs), fee_(fee), donation_(donation), contextinfo_(contextInfo)
{
    assert(contextualTx.nVersion >= 2);  // transaction format version must support vjoinsplit

    if (fee < 0 || fee > MAX_MONEY) {
        throw JSONRPCError(RPC_INVALID_PARAMETER, "Fee is out of range");
    }

    if (inputs.size() == 0) {
        throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, "Empty inputs");
    }

    if (donation > 10 ) {
        throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid donation percentage, must be an integer between 0 and 10 inclusive");
    }

    //  Check the destination address is valid for this network i.e. not testnet being used on mainnet
    auto address = DecodePaymentAddress(toAddress);
    if (IsValidPaymentAddress(address)) {
        tozaddr_ = address;
    } else {
        throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid to address");
    }

    // Log the context info
    if (LogAcceptCategory("zrpcunsafe")) {
        LogPrint("zrpcunsafe", "%s: z_shieldcoinbase initialized (context=%s)\n", getId(), contextInfo.write());
    } else {
        LogPrint("zrpc", "%s: z_shieldcoinbase initialized\n", getId());
    }

    // Lock UTXOs
    lock_utxos();

}

AsyncRPCOperation_shieldcoinbase::~AsyncRPCOperation_shieldcoinbase() {
}

void AsyncRPCOperation_shieldcoinbase::main() {
    if (isCancelled()) {
        unlock_utxos(); // clean up
        return;
    }

    set_state(OperationStatus::EXECUTING);
    start_execution_clock();

    bool success = false;

#ifdef ENABLE_MINING
  #ifdef ENABLE_WALLET
    GenerateBitcoins(false, NULL, 0);
  #else
    GenerateBitcoins(false, 0);
  #endif
#endif

    try {
        success = main_impl();
    } catch (...) {
        set_error_from_current_exception();
    }

#ifdef ENABLE_MINING
  #ifdef ENABLE_WALLET
    GenerateBitcoins(GetBoolArg("-gen",false), pwalletMain, GetArg("-genproclimit", 1));
  #else
    GenerateBitcoins(GetBoolArg("-gen",false), GetArg("-genproclimit", 1));
  #endif
#endif

    stop_execution_clock();

    if (success) {
        set_state(OperationStatus::SUCCESS);
    } else {
        set_state(OperationStatus::FAILED);
    }

    std::string s = strprintf("%s: z_shieldcoinbase finished (status=%s", getId(), getStateAsString());
    if (success) {
        s += strprintf(", txid=%s)\n", tx_.GetHash().ToString());
    } else {
        s += strprintf(", error=%s)\n", getErrorMessage());
    }
    LogPrintf("%s",s);

    unlock_utxos(); // clean up

}

bool AsyncRPCOperation_shieldcoinbase::main_impl() {

    CAmount minersFee = fee_;
    size_t numInputs  = inputs_.size();

    CAmount targetAmount = 0;
    for (ShieldCoinbaseUTXO & utxo : inputs_) {
        targetAmount += utxo.amount;
    }

    if (targetAmount <= minersFee) {
        throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS,
            strprintf("Insufficient coinbase funds, have %s and miners fee is %s",
            FormatMoney(targetAmount), FormatMoney(minersFee)));
    }

    CAmount sendAmount = targetAmount - minersFee;
    LogPrint("zrpc", "%s: spending %s to shield %s with fee %s, donation=%d\n",
            getId(), FormatMoney(targetAmount), FormatMoney(sendAmount), FormatMoney(minersFee), donation_);

    return boost::apply_visitor(ShieldToAddress(this, sendAmount, donation_), tozaddr_);
}

extern UniValue signrawtransaction(const UniValue& params, bool fHelp, const CPubKey& mypk);
extern UniValue sendrawtransaction(const UniValue& params, bool fHelp, const CPubKey& mypk);

bool ShieldToAddress::operator()(const libzcash::SaplingPaymentAddress &zaddr) const {
    m_op->builder_.SetFee(m_op->fee_);

    // Sending from a t-address, which we don't have an ovk for. Instead,
    // generate a common one from the HD seed. This ensures the data is
    // recoverable, while keeping it logically separate from the ZIP 32
    // Sapling key hierarchy, which the user might not be using.
    HDSeed seed;
    if (!pwalletMain->GetHDSeedForDerivation(seed)) {
        throw JSONRPCError(
            RPC_WALLET_ERROR,
            "CWallet::GenerateNewSaplingZKey(): HD seed not found");
    }
    uint256 ovk = ovkForShieldingFromTaddr(seed);

    // Add transparent inputs
    for (auto t : m_op->inputs_) {
        if (t.amount >= ASSETCHAINS_TIMELOCKGTE)
        {
            m_op->builder_.SetLockTime((uint32_t)(chainActive.Height()));
            m_op->builder_.AddTransparentInput(COutPoint(t.txid, t.vout), t.scriptPubKey, t.amount, 0xfffffffe);
        }
        else
        {
            m_op->builder_.AddTransparentInput(COutPoint(t.txid, t.vout), t.scriptPubKey, t.amount);
        }
    }
    if(donation) {
        //if donation>0, send X% of value to zaddr and Y% of of value to donatezaddr where X+Y=100%

        auto dzaddr =  "";
        if(Params().NetworkIDString() == "regtest") {
            dzaddr = "zregtestsapling1y30nwg0clsu6gcyrnvht8hdyfk3vwtszlh6kc4z5hv9hmpxzg2g0nx7c60xeecggm9x9gma96t4";
        } else {
            //TODO: randomly select from a set
            dzaddr = "zs1uhmry4jjaymhh2x7x53fkahp57u60qr54ydtcd6c2nzu73lgm78a4kml2x7ylm547hu2qqtsnw5";
        }
        auto donationZaddr = DecodePaymentAddress(dzaddr);
        if (!IsValidPaymentAddress(donationZaddr)) {
            throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid donation zaddr, Unknown address format: ") + dzaddr);
        }

        // calculate donation as a double then convert to CAmount
        double amount = (static_cast<double>(donation)/100)*static_cast<double>(sendAmount);
        CAmount donationAmount = static_cast<CAmount>(amount);

        // add original recipient as first output, with sendAmount less the donation
        CAmount newAmount = sendAmount - donationAmount;
        m_op->builder_.AddSaplingOutput(ovk, zaddr, newAmount);

        auto donationZout   = boost::get<libzcash::SaplingPaymentAddress>(donationZaddr);
        m_op->builder_.AddSaplingOutput(ovk, donationZout, donationAmount);

        if(fZdebug) {
                LogPrintf("%s: donation=%ld, sendAmount=%ld, newAmount=%ld, donationAmount=%ld, newAmount+donationAmount=%ld fee=%ld\n", __func__,
                donation, sendAmount, newAmount, donationAmount, newAmount+donationAmount, m_op->fee_);
        }
    
        // zdust as third output, so donation txs are indistinguishable from
        // non-donation z_shieldcoinbase txs
        auto zdust1        = DecodePaymentAddress(randomSietchZaddr());
        auto sietchZout1   = boost::get<libzcash::SaplingPaymentAddress>(zdust1);
        m_op->builder_.AddSaplingOutput(ovk, sietchZout1, 0);

    } else {
        // Send all value to the target z-addr
        m_op->builder_.SendChangeTo(zaddr, ovk);

        // Sietchified Shielding of Coinbase Funds
        // Add Sietch zouts so it's unclear which zout contains value :)
        // This reduces metadata leakage of coinbase t=>z tx's
        CAmount amount     = 0;
        auto zdust1        = DecodePaymentAddress(randomSietchZaddr());
        auto zdust2        = DecodePaymentAddress(randomSietchZaddr());
        auto sietchZout1   = boost::get<libzcash::SaplingPaymentAddress>(zdust1);
        auto sietchZout2   = boost::get<libzcash::SaplingPaymentAddress>(zdust2);
        m_op->builder_.AddSaplingOutput(ovk, sietchZout1, amount);
        m_op->builder_.AddSaplingOutput(ovk, sietchZout2, amount);
    }


    // Build the transaction
    auto maybe_tx = m_op->builder_.Build();
    if (!maybe_tx) {
        throw JSONRPCError(RPC_WALLET_ERROR, "Failed to build transaction.");
    }
    m_op->tx_ = maybe_tx.get();

    // Send the transaction
    // TODO: Use CWallet::CommitTransaction instead of sendrawtransaction
    auto signedtxn = EncodeHexTx(m_op->tx_);
    if (!m_op->testmode) {
        UniValue params = UniValue(UniValue::VARR);
        params.push_back(signedtxn);
        UniValue sendResultValue = sendrawtransaction(params, false, CPubKey());
        if (sendResultValue.isNull()) {
            throw JSONRPCError(RPC_WALLET_ERROR, "sendrawtransaction did not return an error or a txid.");
        }

        auto txid = sendResultValue.get_str();

        UniValue o(UniValue::VOBJ);
        o.push_back(Pair("txid", txid));
        m_op->set_result(o);
    } else {
        // Test mode does not send the transaction to the network.
        UniValue o(UniValue::VOBJ);
        o.push_back(Pair("test", 1));
        o.push_back(Pair("txid", m_op->tx_.GetHash().ToString()));
        o.push_back(Pair("hex", signedtxn));
        m_op->set_result(o);
    }

    return true;
}

bool ShieldToAddress::operator()(const libzcash::InvalidEncoding& no) const {
    return false;
}


/**
 * Sign and send a raw transaction.
 * Raw transaction as hex string should be in object field "rawtxn"
 */
void AsyncRPCOperation_shieldcoinbase::sign_send_raw_transaction(UniValue obj)
{
    // Sign the raw transaction
    UniValue rawtxnValue = find_value(obj, "rawtxn");
    if (rawtxnValue.isNull()) {
        throw JSONRPCError(RPC_WALLET_ERROR, "Missing hex data for raw transaction");
    }
    std::string rawtxn = rawtxnValue.get_str();

    UniValue params = UniValue(UniValue::VARR);
    params.push_back(rawtxn);
    UniValue signResultValue = signrawtransaction(params, false, CPubKey());
    UniValue signResultObject = signResultValue.get_obj();
    UniValue completeValue = find_value(signResultObject, "complete");
    bool complete = completeValue.get_bool();
    if (!complete) {
        // TODO: #1366 Maybe get "errors" and print array vErrors into a string
        throw JSONRPCError(RPC_WALLET_ENCRYPTION_FAILED, "Failed to sign transaction");
    }

    UniValue hexValue = find_value(signResultObject, "hex");
    if (hexValue.isNull()) {
        throw JSONRPCError(RPC_WALLET_ERROR, "Missing hex data for signed transaction");
    }
    std::string signedtxn = hexValue.get_str();

    // Send the signed transaction
    if (!testmode) {
        params.clear();
        params.setArray();
        params.push_back(signedtxn);
        UniValue sendResultValue = sendrawtransaction(params, false, CPubKey());
        if (sendResultValue.isNull()) {
            throw JSONRPCError(RPC_WALLET_ERROR, "Send raw transaction did not return an error or a txid.");
        }

        std::string txid = sendResultValue.get_str();

        UniValue o(UniValue::VOBJ);
        o.push_back(Pair("txid", txid));
        set_result(o);
    } else {
        // Test mode does not send the transaction to the network.

        CDataStream stream(ParseHex(signedtxn), SER_NETWORK, PROTOCOL_VERSION);
        CTransaction tx;
        stream >> tx;

        UniValue o(UniValue::VOBJ);
        o.push_back(Pair("test", 1));
        o.push_back(Pair("txid", tx.GetHash().ToString()));
        o.push_back(Pair("hex", signedtxn));
        set_result(o);
    }

    // Keep the signed transaction so we can hash to the same txid
    CDataStream stream(ParseHex(signedtxn), SER_NETWORK, PROTOCOL_VERSION);
    CTransaction tx;
    stream >> tx;
    tx_ = tx;
}


/**
 * Override getStatus() to append the operation's context object to the default status object.
 */
UniValue AsyncRPCOperation_shieldcoinbase::getStatus() const {
    UniValue v = AsyncRPCOperation::getStatus();
    if (contextinfo_.isNull()) {
        return v;
    }

    UniValue obj = v.get_obj();
    obj.push_back(Pair("method", "z_shieldcoinbase"));
    obj.push_back(Pair("params", contextinfo_ ));
    return obj;
}

/**
 * Lock input utxos
 */
 void AsyncRPCOperation_shieldcoinbase::lock_utxos() {
    LOCK2(cs_main, pwalletMain->cs_wallet);
    for (auto utxo : inputs_) {
        COutPoint outpt(utxo.txid, utxo.vout);
        pwalletMain->LockCoin(outpt);
    }
}

/**
 * Unlock input utxos
 */
void AsyncRPCOperation_shieldcoinbase::unlock_utxos() {
    LOCK2(cs_main, pwalletMain->cs_wallet);
    for (auto utxo : inputs_) {
        COutPoint outpt(utxo.txid, utxo.vout);
        pwalletMain->UnlockCoin(outpt);
    }
}
