#include <xrpl/ledger/helpers/CredentialHelpers.h>
//
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/digest.h>

#include <unordered_set>

namespace xrpl {
namespace credentials {

bool
checkExpired(std::shared_ptr<SLE const> const& sleCredential, NetClock::time_point const& closed)
{
    std::uint32_t const exp =
        (*sleCredential)[~sfExpiration].value_or(std::numeric_limits<std::uint32_t>::max());
    std::uint32_t const now = closed.time_since_epoch().count();
    return now > exp;
}

bool
removeExpired(ApplyView& view, STVector256 const& arr, beast::Journal const j)
{
    auto const closeTime = view.header().parentCloseTime;
    bool foundExpired = false;

    for (auto const& h : arr)
    {
        // Credentials already checked in preclaim. Look only for expired here.
        auto const k = keylet::credential(h);
        auto const sleCred = view.peek(k);

        if (sleCred && checkExpired(sleCred, closeTime))
        {
            JLOG(j.trace()) << "Credentials are expired. Cred: " << sleCred->getText();
            // delete expired credentials even if the transaction failed
            deleteSLE(view, sleCred, j);
            foundExpired = true;
        }
    }

    return foundExpired;
}

TER
deleteSLE(ApplyView& view, std::shared_ptr<SLE> const& sleCredential, beast::Journal j)
{
    if (!sleCredential)
        return tecNO_ENTRY;

    auto delSLE = [&view, &sleCredential, j](
                      AccountID const& account, SField const& node, bool isOwner) -> TER {
        auto const sleAccount = view.peek(keylet::account(account));
        if (!sleAccount)
        {
            // LCOV_EXCL_START
            JLOG(j.fatal()) << "Internal error: can't retrieve Owner account.";
            return tecINTERNAL;
            // LCOV_EXCL_STOP
        }

        // Remove object from owner directory
        std::uint64_t const page = sleCredential->getFieldU64(node);
        if (!view.dirRemove(keylet::ownerDir(account), page, sleCredential->key(), false))
        {
            // LCOV_EXCL_START
            JLOG(j.fatal()) << "Unable to delete Credential from owner.";
            return tefBAD_LEDGER;
            // LCOV_EXCL_STOP
        }

        if (isOwner)
            adjustOwnerCount(view, sleAccount, -1, j);

        return tesSUCCESS;
    };

    auto const issuer = sleCredential->getAccountID(sfIssuer);
    auto const subject = sleCredential->getAccountID(sfSubject);
    bool const accepted = (sleCredential->getFlags() & lsfAccepted) != 0u;

    auto err = delSLE(issuer, sfIssuerNode, !accepted || (subject == issuer));
    if (!isTesSuccess(err))
        return err;

    if (subject != issuer)
    {
        err = delSLE(subject, sfSubjectNode, accepted);
        if (!isTesSuccess(err))
            return err;
    }

    // Remove object from ledger
    view.erase(sleCredential);

    return tesSUCCESS;
}

NotTEC
checkFields(STTx const& tx, beast::Journal j)
{
    if (!tx.isFieldPresent(sfCredentialIDs))
        return tesSUCCESS;

    auto const& credentials = tx.getFieldV256(sfCredentialIDs);
    if (credentials.empty() || (credentials.size() > maxCredentialsArraySize))
    {
        JLOG(j.trace()) << "Malformed transaction: Credentials array size is invalid: "
                        << credentials.size();
        return temMALFORMED;
    }

    std::unordered_set<uint256> duplicates;
    for (auto const& cred : credentials)
    {
        auto [it, ins] = duplicates.insert(cred);
        if (!ins)
        {
            JLOG(j.trace()) << "Malformed transaction: duplicates in credentials.";
            return temMALFORMED;
        }
    }

    return tesSUCCESS;
}

TER
valid(STTx const& tx, ReadView const& view, AccountID const& src, beast::Journal j)
{
    if (!tx.isFieldPresent(sfCredentialIDs))
        return tesSUCCESS;

    auto const& credIDs(tx.getFieldV256(sfCredentialIDs));
    for (auto const& h : credIDs)
    {
        auto const sleCred = view.read(keylet::credential(h));
        if (!sleCred)
        {
            JLOG(j.trace()) << "Credential doesn't exist. Cred: " << h;
            return tecBAD_CREDENTIALS;
        }

        if (sleCred->getAccountID(sfSubject) != src)
        {
            JLOG(j.trace()) << "Credential doesn't belong to the source account. Cred: " << h;
            return tecBAD_CREDENTIALS;
        }

        if ((sleCred->getFlags() & lsfAccepted) == 0u)
        {
            JLOG(j.trace()) << "Credential isn't accepted. Cred: " << h;
            return tecBAD_CREDENTIALS;
        }

        // Expiration checks are in doApply
    }

    return tesSUCCESS;
}

TER
validDomain(ReadView const& view, uint256 domainID, AccountID const& subject)
{
    // Note, permissioned domain objects can be deleted at any time
    auto const slePD = view.read(keylet::permissionedDomain(domainID));
    if (!slePD)
        return tecOBJECT_NOT_FOUND;

    auto const closeTime = view.header().parentCloseTime;
    bool foundExpired = false;
    for (auto const& h : slePD->getFieldArray(sfAcceptedCredentials))
    {
        auto const issuer = h.getAccountID(sfIssuer);
        auto const type = h.getFieldVL(sfCredentialType);
        auto const keyletCredential = keylet::credential(subject, issuer, makeSlice(type));
        auto const sleCredential = view.read(keyletCredential);

        // We cannot delete expired credentials, that would require ApplyView&
        // However we can check if credentials are expired. Expected transaction
        // flow is to use `validDomain` in preclaim, converting tecEXPIRED to
        // tesSUCCESS, then proceed to call `verifyValidDomain` in doApply. This
        // allows expired credentials to be deleted by any transaction.
        if (sleCredential)
        {
            if (checkExpired(sleCredential, closeTime))
            {
                foundExpired = true;
                continue;
            }
            if ((sleCredential->getFlags() & lsfAccepted) != 0u)
            {
                return tesSUCCESS;
            }

            continue;
        }
    }

    return foundExpired ? tecEXPIRED : tecNO_AUTH;
}

TER
authorizedDepositPreauth(ReadView const& view, STVector256 const& credIDs, AccountID const& dst)
{
    std::set<std::pair<AccountID, Slice>> sorted;
    std::vector<std::shared_ptr<SLE const>> lifeExtender;
    lifeExtender.reserve(credIDs.size());
    for (auto const& h : credIDs)
    {
        auto sleCred = view.read(keylet::credential(h));
        if (!sleCred)            // already checked in preclaim
            return tefINTERNAL;  // LCOV_EXCL_LINE

        auto [it, ins] = sorted.emplace((*sleCred)[sfIssuer], (*sleCred)[sfCredentialType]);
        if (!ins)
            return tefINTERNAL;  // LCOV_EXCL_LINE
        lifeExtender.push_back(std::move(sleCred));
    }

    if (!view.exists(keylet::depositPreauth(dst, sorted)))
        return tecNO_PERMISSION;

    return tesSUCCESS;
}

std::set<std::pair<AccountID, Slice>>
makeSorted(STArray const& credentials)
{
    std::set<std::pair<AccountID, Slice>> out;
    for (auto const& cred : credentials)
    {
        auto [it, ins] = out.emplace(cred[sfIssuer], cred[sfCredentialType]);
        if (!ins)
            return {};
    }
    return out;
}

NotTEC
checkArray(STArray const& credentials, unsigned maxSize, beast::Journal j)
{
    if (credentials.empty() || (credentials.size() > maxSize))
    {
        JLOG(j.trace()) << "Malformed transaction: "
                           "Invalid credentials size: "
                        << credentials.size();
        return credentials.empty() ? temARRAY_EMPTY : temARRAY_TOO_LARGE;
    }

    std::unordered_set<uint256> duplicates;
    for (auto const& credential : credentials)
    {
        auto const& issuer = credential[sfIssuer];
        if (!issuer)
        {
            JLOG(j.trace()) << "Malformed transaction: "
                               "Issuer account is invalid: "
                            << to_string(issuer);
            return temINVALID_ACCOUNT_ID;
        }

        auto const ct = credential[sfCredentialType];
        if (ct.empty() || (ct.size() > maxCredentialTypeLength))
        {
            JLOG(j.trace()) << "Malformed transaction: "
                               "Invalid credentialType size: "
                            << ct.size();
            return temMALFORMED;
        }

        auto [it, ins] = duplicates.insert(sha512Half(issuer, ct));
        if (!ins)
        {
            JLOG(j.trace()) << "Malformed transaction: "
                               "duplicates in credentials.";
            return temMALFORMED;
        }
    }

    return tesSUCCESS;
}

}  // namespace credentials

TER
verifyValidDomain(ApplyView& view, AccountID const& account, uint256 domainID, beast::Journal j)
{
    auto const slePD = view.read(keylet::permissionedDomain(domainID));
    if (!slePD)
        return tecOBJECT_NOT_FOUND;

    // Collect all matching credentials on a side, so we can remove expired ones
    // We may finish the loop with this collection empty, it's fine.
    STVector256 credentials;
    for (auto const& h : slePD->getFieldArray(sfAcceptedCredentials))
    {
        auto const issuer = h.getAccountID(sfIssuer);
        auto const type = h.getFieldVL(sfCredentialType);
        auto const keyletCredential = keylet::credential(account, issuer, makeSlice(type));
        if (view.exists(keyletCredential))
            credentials.push_back(keyletCredential.key);
    }

    bool const foundExpired = credentials::removeExpired(view, credentials, j);
    for (auto const& h : credentials)
    {
        auto sleCredential = view.read(keylet::credential(h));
        if (!sleCredential)
            continue;  // expired, i.e. deleted in credentials::removeExpired

        if ((sleCredential->getFlags() & lsfAccepted) != 0u)
            return tesSUCCESS;
    }

    return foundExpired ? tecEXPIRED : tecNO_PERMISSION;
}

TER
verifyDepositPreauth(
    STTx const& tx,
    ApplyView& view,
    AccountID const& src,
    AccountID const& dst,
    std::shared_ptr<SLE const> const& sleDst,
    beast::Journal j)
{
    // If depositPreauth is enabled, then an account that requires
    // authorization has at least two ways to get a payment in:
    //  1. If src == dst, or
    //  2. If src is deposit preauthorized by dst (either by account or by
    //  credentials).

    bool const credentialsPresent = tx.isFieldPresent(sfCredentialIDs);

    if (credentialsPresent && credentials::removeExpired(view, tx.getFieldV256(sfCredentialIDs), j))
        return tecEXPIRED;

    if (sleDst && ((sleDst->getFlags() & lsfDepositAuth) != 0u))
    {
        if (src != dst)
        {
            if (!view.exists(keylet::depositPreauth(dst, src)))
            {
                return !credentialsPresent ? tecNO_PERMISSION
                                           : credentials::authorizedDepositPreauth(
                                                 view, tx.getFieldV256(sfCredentialIDs), dst);
            }
        }
    }

    return tesSUCCESS;
}

}  // namespace xrpl
