#include <xrpld/rpc/Context.h>
#include <xrpld/rpc/detail/RPCHelpers.h>
#include <xrpld/rpc/detail/RPCLedgerHelpers.h>
#include <xrpld/rpc/detail/TrustLine.h>
#include <xrpld/rpc/detail/Tuning.h>

#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/RPCErr.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/resource/Fees.h>

namespace xrpl {

void
addLine(Json::Value& jsonLines, RPCTrustLine const& line)
{
    STAmount const& saBalance(line.getBalance());
    STAmount const& saLimit(line.getLimit());
    STAmount const& saLimitPeer(line.getLimitPeer());
    Json::Value& jPeer(jsonLines.append(Json::objectValue));

    jPeer[jss::account] = to_string(line.getAccountIDPeer());
    // Amount reported is positive if current account holds other
    // account's IOUs.
    //
    // Amount reported is negative if other account holds current
    // account's IOUs.
    jPeer[jss::balance] = saBalance.getText();
    jPeer[jss::currency] = to_string(saBalance.issue().currency);
    jPeer[jss::limit] = saLimit.getText();
    jPeer[jss::limit_peer] = saLimitPeer.getText();
    jPeer[jss::quality_in] = line.getQualityIn().value;
    jPeer[jss::quality_out] = line.getQualityOut().value;
    if (line.getAuth())
        jPeer[jss::authorized] = true;
    if (line.getAuthPeer())
        jPeer[jss::peer_authorized] = true;
    if (line.getNoRipple())
        jPeer[jss::no_ripple] = true;
    if (line.getNoRipplePeer())
        jPeer[jss::no_ripple_peer] = true;
    if (line.getFreeze())
        jPeer[jss::freeze] = true;
    if (line.getFreezePeer())
        jPeer[jss::freeze_peer] = true;
    if (line.getDeepFreeze())
        jPeer[jss::deep_freeze] = true;
    if (line.getDeepFreezePeer())
        jPeer[jss::deep_freeze_peer] = true;
}

// {
//   account: <account>
//   ledger_hash : <ledger>
//   ledger_index : <ledger_index>
//   limit: integer                 // optional
//   marker: opaque                 // optional, resume previous query
//   ignore_default: bool           // do not return lines in default state (on
//   this account's side)
// }
Json::Value
doAccountLines(RPC::JsonContext& context)
{
    auto const& params(context.params);
    if (!params.isMember(jss::account))
        return RPC::missing_field_error(jss::account);

    if (!params[jss::account].isString())
        return RPC::invalid_field_error(jss::account);

    std::shared_ptr<ReadView const> ledger;
    auto result = RPC::lookupLedger(ledger, context);
    if (!ledger)
        return result;

    auto id = parseBase58<AccountID>(params[jss::account].asString());
    if (!id)
    {
        RPC::inject_error(rpcACT_MALFORMED, result);
        return result;
    }
    auto const accountID{std::move(id.value())};

    if (!ledger->exists(keylet::account(accountID)))
        return rpcError(rpcACT_NOT_FOUND);

    std::string strPeer;
    if (params.isMember(jss::peer))
        strPeer = params[jss::peer].asString();

    auto const raPeerAccount = [&]() -> std::optional<AccountID> {
        return strPeer.empty() ? std::nullopt : parseBase58<AccountID>(strPeer);
    }();
    if (!strPeer.empty() && !raPeerAccount)
    {
        RPC::inject_error(rpcACT_MALFORMED, result);
        return result;
    }

    unsigned int limit = 0;
    if (auto err = readLimitField(limit, RPC::Tuning::accountLines, context))
        return *err;

    // this flag allows the requester to ask incoming trustlines in default
    // state be omitted
    bool ignoreDefault =
        params.isMember(jss::ignore_default) && params[jss::ignore_default].asBool();

    Json::Value& jsonLines(result[jss::lines] = Json::arrayValue);
    struct VisitData
    {
        std::vector<RPCTrustLine> items;
        AccountID const& accountID;
        std::optional<AccountID> const& raPeerAccount;
        bool ignoreDefault;
        uint32_t foundCount;
    };
    VisitData visitData = {{}, accountID, raPeerAccount, ignoreDefault, 0};
    uint256 startAfter = beast::zero;
    std::uint64_t startHint = 0;

    if (params.isMember(jss::marker))
    {
        if (!params[jss::marker].isString())
            return RPC::expected_field_error(jss::marker, "string");

        // Marker is composed of a comma separated index and start hint. The
        // former will be read as hex, and the latter using boost lexical cast.
        std::stringstream marker(params[jss::marker].asString());
        std::string value;
        if (!std::getline(marker, value, ','))
            return rpcError(rpcINVALID_PARAMS);

        if (!startAfter.parseHex(value))
            return rpcError(rpcINVALID_PARAMS);

        if (!std::getline(marker, value, ','))
            return rpcError(rpcINVALID_PARAMS);

        try
        {
            startHint = boost::lexical_cast<std::uint64_t>(value);
        }
        catch (boost::bad_lexical_cast&)
        {
            return rpcError(rpcINVALID_PARAMS);
        }

        // We then must check if the object pointed to by the marker is actually
        // owned by the account in the request.
        auto const sle = ledger->read({ltANY, startAfter});

        if (!sle)
            return rpcError(rpcINVALID_PARAMS);

        if (!RPC::isRelatedToAccount(*ledger, sle, accountID))
            return rpcError(rpcINVALID_PARAMS);
    }

    auto count = 0;
    std::optional<uint256> marker = {};
    std::uint64_t nextHint = 0;
    {
        if (!forEachItemAfter(
                *ledger,
                accountID,
                startAfter,
                startHint,
                limit + 1,
                [&visitData, &count, &marker, &limit, &nextHint](
                    std::shared_ptr<SLE const> const& sleCur) {
                    if (!sleCur)
                    {
                        // LCOV_EXCL_START
                        UNREACHABLE("xrpl::doAccountLines : null SLE");
                        return false;
                        // LCOV_EXCL_STOP
                    }

                    if (++count == limit)
                    {
                        marker = sleCur->key();
                        nextHint = RPC::getStartHint(sleCur, visitData.accountID);
                    }

                    if (sleCur->getType() != ltRIPPLE_STATE)
                        return true;

                    bool ignore = false;
                    if (visitData.ignoreDefault)
                    {
                        if (sleCur->getFieldAmount(sfLowLimit).getIssuer() == visitData.accountID)
                        {
                            ignore = !(sleCur->getFieldU32(sfFlags) & lsfLowReserve);
                        }
                        else
                        {
                            ignore = !(sleCur->getFieldU32(sfFlags) & lsfHighReserve);
                        }
                    }

                    if (!ignore && count <= limit)
                    {
                        auto const line = RPCTrustLine::makeItem(visitData.accountID, sleCur);

                        if (line &&
                            (!visitData.raPeerAccount ||
                             *visitData.raPeerAccount == line->getAccountIDPeer()))
                        {
                            visitData.items.emplace_back(*line);
                        }
                    }

                    return true;
                }))
        {
            return rpcError(rpcINVALID_PARAMS);
        }
    }

    // Both conditions need to be checked because marker is set on the limit-th
    // item, but if there is no item on the limit + 1 iteration, then there is
    // no need to return a marker.
    if (count == limit + 1 && marker)
    {
        result[jss::limit] = limit;
        result[jss::marker] = to_string(*marker) + "," + std::to_string(nextHint);
    }

    result[jss::account] = toBase58(accountID);

    for (auto const& item : visitData.items)
        addLine(jsonLines, item);

    context.loadType = Resource::feeMediumBurdenRPC;
    return result;
}

}  // namespace xrpl
