//------------------------------------------------------------------------------
/*
    This file is part of rippled: https://github.com/ripple/rippled
    Copyright (c) 2012, 2013 Ripple Labs Inc.

    Permission to use, copy, modify, and/or distribute this software for any
    purpose  with  or without fee is hereby granted, provided that the above
    copyright notice and this permission notice appear in all copies.

    THE  SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
    WITH  REGARD  TO  THIS  SOFTWARE  INCLUDING  ALL  IMPLIED  WARRANTIES  OF
    MERCHANTABILITY  AND  FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
    ANY  SPECIAL ,  DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
    WHATSOEVER  RESULTING  FROM  LOSS  OF USE, DATA OR PROFITS, WHETHER IN AN
    ACTION  OF  CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
    OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================

#ifndef RIPPLE_PROTOCOL_IMPORT_H_INCLUDED
#define RIPPLE_PROTOCOL_IMPORT_H_INCLUDED

#include <xrpld/app/misc/Manifest.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/base64.h>
#include <xrpl/json/json_reader.h>
#include <charconv>

namespace ripple {

inline bool
isHex(std::string const& str)
{
    return str.find_first_not_of("0123456789abcdefABCDEF") == std::string::npos;
}

inline bool
isBase58(std::string const& str)
{
    return str.find_first_not_of(
               "rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz") ==
        std::string::npos;
}

inline bool
isBase64(std::string const& str)
{
    return str.find_first_not_of(
               "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+"
               "/=") == std::string::npos;
}

inline std::optional<uint64_t>
parse_uint64(std::string const& str)
{
    uint64_t result;
    auto [ptr, ec] =
        std::from_chars(str.data(), str.data() + str.size(), result);

    if (ec == std::errc())
        return result;
    return {};
}

inline bool
syntaxCheckProof(
    Json::Value const& proof,
    beast::Journal const& j,
    int depth = 0)
{
    if (depth > 64)
    {
        JLOG(j.warn())
            << "XPOP.transaction.proof list should be less than 64 entries";
        return false;
    }

    if (proof.isArray())
    {
        // List form
        if (proof.size() != 16)
        {
            JLOG(j.warn())
                << "XPOP.transaction.proof list should be exactly 16 entries";
            return false;
        }
        for (const auto& entry : proof)
        {
            if (entry.isString())
            {
                if (!isHex(entry.asString()) || entry.asString().size() != 64)
                {
                    JLOG(j.warn())
                        << "XPOP.transaction.proof list entry missing "
                           "or wrong format "
                        << "(should be hex string with 64 characters)";
                    return false;
                }
            }
            else if (entry.isArray())
            {
                if (!syntaxCheckProof(entry, j, depth + 1))
                    return false;
            }
            else
            {
                JLOG(j.warn())
                    << "XPOP.transaction.proof list entry has wrong format";
                return false;
            }
        }
    }
    else if (proof.isObject())
    {
        // Tree form
        if (depth == 0)  // root is special case
        {
            if (!proof["hash"].isString() ||
                proof["hash"].asString().size() != 64 ||
                !proof["key"].isString() ||
                proof["key"].asString().size() != 64 ||
                !proof["children"].isObject())
            {
                JLOG(j.warn()) << "XPOP.transaction.proof tree node has wrong "
                                  "format (root)";
                return false;
            }

            return syntaxCheckProof(proof["children"], j, depth + 1);
        }

        for (const auto& branch : proof.getMemberNames())
        {
            if (branch.size() != 1 || !isHex(branch))
            {
                JLOG(j.warn())
                    << "XPOP.transaction.proof child node was not 0-F "
                       "hex nibble";
                return false;
            }

            const auto& node = proof[branch];
            if (!node.isObject() || !node["hash"].isString() ||
                node["hash"].asString().size() != 64 ||
                !node["key"].isString() ||
                node["key"].asString().size() != 64 ||
                !node["children"].isObject())
            {
                JLOG(j.warn())
                    << "XPOP.transaction.proof tree node has wrong format";
                return false;
            }
            if (!syntaxCheckProof(node["children"], j, depth + 1))
            {
                JLOG(j.warn()) << "XPOP.transaction.proof bad children format";
                return false;
            }
        }
    }
    else
    {
        JLOG(j.warn()) << "XPOP.transaction.proof has wrong format (should be "
                          "array or object)";
        return false;
    }

    return true;
}

// does not check signature etc
inline std::optional<Json::Value>
syntaxCheckXPOP(Blob const& blob, beast::Journal const& j)
{
    if (blob.empty())
        return {};

    std::string strJson(blob.begin(), blob.end());
    if (strJson.empty())
        return {};

    try
    {
        Json::Value xpop;
        Json::Reader reader;

        if (!reader.parse(strJson, xpop))
        {
            JLOG(j.warn()) << "XPOP failed to parse string json";
            return {};
        }

        if (!xpop.isObject())
        {
            JLOG(j.warn()) << "XPOP is not a JSON object";
            return {};
        }

        if (!xpop["ledger"].isObject())
        {
            JLOG(j.warn()) << "XPOP.ledger is not a JSON object";
            return {};
        }

        if (!xpop["transaction"].isObject())
        {
            JLOG(j.warn()) << "XPOP.transaction is not a JSON object";
            return {};
        }

        if (!xpop["validation"].isObject())
        {
            JLOG(j.warn()) << "XPOP.validation is not a JSON object";
            return {};
        }

        if (!xpop["ledger"]["acroot"].isString() ||
            xpop["ledger"]["acroot"].asString().size() != 64 ||
            !isHex(xpop["ledger"]["acroot"].asString()))
        {
            JLOG(j.warn())
                << "XPOP.ledger.acroot missing or wrong format (should "
                   "be hex string)";
            return {};
        }

        if (!xpop["ledger"]["txroot"].isString() ||
            xpop["ledger"]["txroot"].asString().size() != 64 ||
            !isHex(xpop["ledger"]["txroot"].asString()))
        {
            JLOG(j.warn()) << "XPOP.ledger.txroot missing or wrong format "
                              "(should be hex string)";
            return {};
        }

        if (!xpop["ledger"]["phash"].isString() ||
            xpop["ledger"]["phash"].asString().size() != 64 ||
            !isHex(xpop["ledger"]["phash"].asString()))
        {
            JLOG(j.warn()) << "XPOP.ledger.phash missing or wrong format "
                              "(should be hex string)";
            return {};
        }

        if (!xpop["ledger"]["close"].isInt())
        {
            JLOG(j.warn()) << "XPOP.ledger.close missing or wrong format "
                              "(should be int)";
            return {};
        }

        if (xpop["ledger"]["coins"].isInt())
        {
            // pass
        }
        else if (xpop["ledger"]["coins"].isString())
        {
            if (!parse_uint64(xpop["ledger"]["coins"].asString()))
            {
                JLOG(j.warn()) << "XPOP.ledger.coins missing or wrong format "
                                  "(should be int or string)";
                return {};
            }
        }
        else
        {
            JLOG(j.warn()) << "XPOP.ledger.coins missing or wrong format "
                              "(should be int or string)";
            return {};
        }

        if (!xpop["ledger"]["cres"].isInt())
        {
            JLOG(j.warn()) << "XPOP.ledger.cres missing or wrong format "
                              "(should be int)";
            return {};
        }

        if (!xpop["ledger"]["index"].isInt())
        {
            JLOG(j.warn()) << "XPOP.ledger.index missing or wrong format "
                              "(should be int)";
            return {};
        }

        if (!xpop["ledger"]["flags"].isInt())
        {
            JLOG(j.warn()) << "XPOP.ledger.flags missing or wrong format "
                              "(should be int)";
            return {};
        }

        if (!xpop["ledger"]["pclose"].isInt())
        {
            JLOG(j.warn()) << "XPOP.ledger.pclose missing or wrong format "
                              "(should be int)";
            return {};
        }

        if (!xpop["transaction"]["blob"].isString() ||
            !isHex(xpop["transaction"]["blob"].asString()))
        {
            JLOG(j.warn()) << "XPOP.transaction.blob missing or wrong format "
                              "(should be hex string)";
            return {};
        }

        if (!xpop["transaction"]["meta"].isString() ||
            !isHex(xpop["transaction"]["meta"].asString()))
        {
            JLOG(j.warn()) << "XPOP.transaction.meta missing or wrong format "
                              "(should be hex string)";
            return {};
        }

        if (!syntaxCheckProof(xpop["transaction"]["proof"], j))
        {
            JLOG(j.warn()) << "XPOP.transaction.proof failed syntax check "
                              "(tree/list form)";
            return {};
        }

        if (!xpop["validation"]["data"].isObject())
        {
            JLOG(j.warn()) << "XPOP.validation.data missing or wrong format "
                              "(should be JSON object)";
            return {};
        }

        if (!xpop["validation"]["unl"].isObject())
        {
            JLOG(j.warn()) << "XPOP.validation.unl missing or wrong format "
                              "(should be JSON object)";
            return {};
        }

        for (const auto& key : xpop["validation"]["data"].getMemberNames())
        {
            const auto& value = xpop["validation"]["data"][key];
            if (!isBase58(key) || !value.isString() || !isHex(value.asString()))
            {
                JLOG(j.warn()) << "XPOP.validation.data entry has wrong format "
                               << "(key should be base58 string and value "
                                  "should be hex string)";
                return {};
            }
        }

        uint32_t found = 0;
        for (const auto& key : xpop["validation"]["unl"].getMemberNames())
        {
            const auto& value = xpop["validation"]["unl"][key];
            if (key == "public_key")
            {
                if (!value.isString() || !isHex(value.asString()))
                {
                    JLOG(j.warn())
                        << "XPOP.validation.unl.public_key missing or "
                           "wrong format (should be hex string)";
                    return {};
                }

                auto pk = strUnHex(value.asString());

                if (!publicKeyType(makeSlice(*pk)))
                {
                    JLOG(j.warn())
                        << "XPOP.validation.unl.public_key invalid key type.";
                    return {};
                }
                found |= 1;
            }
            else if (key == "manifest")
            {
                if (!value.isString() || !isBase64(value.asString()))
                {
                    JLOG(j.warn()) << "XPOP.validation.unl.manifest missing or "
                                      "wrong format (should be string)";
                    return {};
                }
                found |= 2;
            }
            else if (key == "blob")
            {
                if (!value.isString() || !isBase64(value.asString()))
                {
                    JLOG(j.warn())
                        << "XPOP.validation.unl.blob missing or wrong "
                           "format (should be base64 string)";
                    return {};
                }
                found |= 4;
            }
            else if (key == "signature")
            {
                if (!value.isString() || !isHex(value.asString()))
                {
                    JLOG(j.warn())
                        << "XPOP.validation.unl.signature missing or wrong "
                           "format (should be hex string)";
                    return {};
                }
                found |= 8;
            }
            else if (key == "version")
            {
                if (!value.isInt())
                {
                    JLOG(j.warn()) << "XPOP.validation.unl.version missing or "
                                      "wrong format (should be int)";
                    return {};
                }
                found |= 16;
            }
            else
            {
                if (!value.isObject() && !value.isString())
                {
                    JLOG(j.warn())
                        << "XPOP.validation.unl entry has wrong format";
                    return {};
                }
            }
        }

        if (found != 0b11111)
        {
            JLOG(j.warn()) << "XPOP.validation.unl entry has wrong format "
                              "(missing field/s)";
            return {};
        }

        return xpop;
    }
    catch (...)
    {
        JLOG(j.warn()) << "An exception occurred during XPOP validation";
    }

    return {};
}

// <sequence, master key>
inline std::optional<std::pair<uint32_t, PublicKey>>
getVLInfo(Json::Value const& xpop, beast::Journal const& j)
{
    auto const data =
        base64_decode(xpop[jss::validation][jss::unl][jss::blob].asString());
    Json::Reader r;
    Json::Value list;
    if (!r.parse(data, list))
    {
        JLOG(j.warn())
            << "Import: unl blob was not valid json (after base64 decoding)";
        return {};
    }
    auto const sequence = list[jss::sequence].asUInt();
    auto const m = deserializeManifest(base64_decode(
        xpop[jss::validation][jss::unl][jss::manifest].asString()));
    if (!m)
    {
        JLOG(j.warn()) << "Import: failed to deserialize manifest";
        return {};
    }
    return {{sequence, m->masterKey}};
}

}  // namespace ripple

#endif
