#include <xrpld/app/ledger/LedgerMaster.h>
#include <xrpld/rpc/Context.h>
#include <xrpld/rpc/detail/RPCLedgerHelpers.h>

#include <xrpl/basics/safe_cast.h>
#include <xrpl/json/json_value.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/AMMCore.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/tx/transactors/dex/AMMUtils.h>

#include <grpcpp/support/status.h>

namespace xrpl {

Expected<Issue, error_code_i>
getIssue(Json::Value const& v, beast::Journal j)
{
    try
    {
        return issueFromJson(v);
    }
    catch (std::runtime_error const& ex)
    {
        JLOG(j.debug()) << "getIssue " << ex.what();
    }
    return Unexpected(rpcISSUE_MALFORMED);
}

std::string
to_iso8601(NetClock::time_point tp)
{
    // 2000-01-01 00:00:00 UTC is 946684800s from 1970-01-01 00:00:00 UTC
    using namespace std::chrono;
    return date::format(
        "%Y-%Om-%dT%H:%M:%OS%z",
        date::sys_time<system_clock::duration>(
            system_clock::time_point{tp.time_since_epoch() + epoch_offset}));
}

Json::Value
doAMMInfo(RPC::JsonContext& context)
{
    auto const& params(context.params);
    Json::Value result;

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

    struct ValuesFromContextParams
    {
        std::optional<AccountID> accountID;
        Issue issue1;
        Issue issue2;
        std::shared_ptr<SLE const> amm;
    };

    auto getValuesFromContextParams = [&]() -> Expected<ValuesFromContextParams, error_code_i> {
        std::optional<AccountID> accountID;
        std::optional<Issue> issue1;
        std::optional<Issue> issue2;
        std::optional<uint256> ammID;

        constexpr auto invalid = [](Json::Value const& params) -> bool {
            return (params.isMember(jss::asset) != params.isMember(jss::asset2)) ||
                (params.isMember(jss::asset) == params.isMember(jss::amm_account));
        };

        // NOTE, identical check for apVersion >= 3 below
        if (context.apiVersion < 3 && invalid(params))
            return Unexpected(rpcINVALID_PARAMS);

        if (params.isMember(jss::asset))
        {
            if (auto const i = getIssue(params[jss::asset], context.j))
            {
                issue1 = *i;
            }
            else
            {
                return Unexpected(i.error());
            }
        }

        if (params.isMember(jss::asset2))
        {
            if (auto const i = getIssue(params[jss::asset2], context.j))
            {
                issue2 = *i;
            }
            else
            {
                return Unexpected(i.error());
            }
        }

        if (params.isMember(jss::amm_account))
        {
            auto const id = parseBase58<AccountID>((params[jss::amm_account].asString()));
            if (!id)
                return Unexpected(rpcACT_MALFORMED);
            auto const sle = ledger->read(keylet::account(*id));
            if (!sle)
                return Unexpected(rpcACT_MALFORMED);
            ammID = sle->getFieldH256(sfAMMID);
            if (ammID->isZero())
                return Unexpected(rpcACT_NOT_FOUND);
        }

        if (params.isMember(jss::account))
        {
            accountID = parseBase58<AccountID>(params[jss::account].asString());
            if (!accountID || !ledger->read(keylet::account(*accountID)))
                return Unexpected(rpcACT_MALFORMED);
        }

        // NOTE, identical check for apVersion < 3 above
        if (context.apiVersion >= 3 && invalid(params))
            return Unexpected(rpcINVALID_PARAMS);

        XRPL_ASSERT(
            (issue1.has_value() == issue2.has_value()) && (issue1.has_value() != ammID.has_value()),
            "xrpl::doAMMInfo : issue1 and issue2 do match");

        auto const ammKeylet = [&]() {
            if (issue1 && issue2)
                return keylet::amm(*issue1, *issue2);
            XRPL_ASSERT(ammID, "xrpl::doAMMInfo::ammKeylet : ammID is set");
            return keylet::amm(*ammID);
        }();
        auto const amm = ledger->read(ammKeylet);
        if (!amm)
            return Unexpected(rpcACT_NOT_FOUND);
        if (!issue1 && !issue2)
        {
            issue1 = (*amm)[sfAsset].get<Issue>();
            issue2 = (*amm)[sfAsset2].get<Issue>();
        }

        return ValuesFromContextParams{accountID, *issue1, *issue2, std::move(amm)};
    };

    auto const r = getValuesFromContextParams();
    if (!r)
    {
        RPC::inject_error(r.error(), result);
        return result;
    }

    auto const& [accountID, issue1, issue2, amm] = *r;

    auto const ammAccountID = amm->getAccountID(sfAccount);

    // provide funds if frozen, specify asset_frozen flag
    auto const [asset1Balance, asset2Balance] = ammPoolHolds(
        *ledger, ammAccountID, issue1, issue2, FreezeHandling::fhIGNORE_FREEZE, context.j);
    auto const lptAMMBalance =
        accountID ? ammLPHolds(*ledger, *amm, *accountID, context.j) : (*amm)[sfLPTokenBalance];

    Json::Value ammResult;
    asset1Balance.setJson(ammResult[jss::amount]);
    asset2Balance.setJson(ammResult[jss::amount2]);
    lptAMMBalance.setJson(ammResult[jss::lp_token]);
    ammResult[jss::trading_fee] = (*amm)[sfTradingFee];
    ammResult[jss::account] = to_string(ammAccountID);
    Json::Value voteSlots(Json::arrayValue);
    if (amm->isFieldPresent(sfVoteSlots))
    {
        for (auto const& voteEntry : amm->getFieldArray(sfVoteSlots))
        {
            Json::Value vote;
            vote[jss::account] = to_string(voteEntry.getAccountID(sfAccount));
            vote[jss::trading_fee] = voteEntry[sfTradingFee];
            vote[jss::vote_weight] = voteEntry[sfVoteWeight];
            voteSlots.append(std::move(vote));
        }
    }
    if (voteSlots.size() > 0)
        ammResult[jss::vote_slots] = std::move(voteSlots);
    XRPL_ASSERT(
        !ledger->rules().enabled(fixInnerObjTemplate) || amm->isFieldPresent(sfAuctionSlot),
        "xrpl::doAMMInfo : auction slot is set");
    if (amm->isFieldPresent(sfAuctionSlot))
    {
        auto const& auctionSlot = safe_downcast<STObject const&>(amm->peekAtField(sfAuctionSlot));
        if (auctionSlot.isFieldPresent(sfAccount))
        {
            Json::Value auction;
            auto const timeSlot = ammAuctionTimeSlot(
                ledger->header().parentCloseTime.time_since_epoch().count(), auctionSlot);
            auction[jss::time_interval] = timeSlot ? *timeSlot : AUCTION_SLOT_TIME_INTERVALS;
            auctionSlot[sfPrice].setJson(auction[jss::price]);
            auction[jss::discounted_fee] = auctionSlot[sfDiscountedFee];
            auction[jss::account] = to_string(auctionSlot.getAccountID(sfAccount));
            auction[jss::expiration] =
                to_iso8601(NetClock::time_point{NetClock::duration{auctionSlot[sfExpiration]}});
            if (auctionSlot.isFieldPresent(sfAuthAccounts))
            {
                Json::Value auth;
                for (auto const& acct : auctionSlot.getFieldArray(sfAuthAccounts))
                {
                    Json::Value jv;
                    jv[jss::account] = to_string(acct.getAccountID(sfAccount));
                    auth.append(jv);
                }
                auction[jss::auth_accounts] = auth;
            }
            ammResult[jss::auction_slot] = std::move(auction);
        }
    }

    if (!isXRP(asset1Balance))
    {
        ammResult[jss::asset_frozen] =
            isFrozen(*ledger, ammAccountID, issue1.currency, issue1.account);
    }
    if (!isXRP(asset2Balance))
    {
        ammResult[jss::asset2_frozen] =
            isFrozen(*ledger, ammAccountID, issue2.currency, issue2.account);
    }

    result[jss::amm] = std::move(ammResult);
    if (!result.isMember(jss::ledger_index) && !result.isMember(jss::ledger_hash))
        result[jss::ledger_current_index] = ledger->header().seq;
    result[jss::validated] = context.ledgerMaster.isValidated(*ledger);

    return result;
}

}  // namespace xrpl
