/*
 *    Copyright (c) 2021 Project CHIP Authors
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

#include "lib/support/CHIPMem.h"
#include "lib/support/CodeUtils.h"
#include "lib/support/ScopedBuffer.h"
#include <access/AuthMode.h>
#include <lib/support/Defer.h>
#include <transport/SecureSession.h>
#include <transport/SecureSessionTable.h>

namespace chip {
namespace Transport {

Optional<SessionHandle> SecureSessionTable::CreateNewSecureSessionForTest(SecureSession::Type secureSessionType,
                                                                          uint16_t localSessionId, NodeId localNodeId,
                                                                          NodeId peerNodeId, CATValues peerCATs,
                                                                          uint16_t peerSessionId, FabricIndex fabricIndex,
                                                                          const ReliableMessageProtocolConfig & config)
{
    if (secureSessionType == SecureSession::Type::kCASE)
    {
        if ((fabricIndex == kUndefinedFabricIndex) || (localNodeId == kUndefinedNodeId) || (peerNodeId == kUndefinedNodeId))
        {
            return Optional<SessionHandle>::Missing();
        }
    }
    else if (secureSessionType == SecureSession::Type::kPASE)
    {
        if ((fabricIndex != kUndefinedFabricIndex) || (localNodeId != kUndefinedNodeId) || (peerNodeId != kUndefinedNodeId))
        {
            // TODO: This secure session type is infeasible! We must fix the tests
            if (false)
            {
                return Optional<SessionHandle>::Missing();
            }

            (void) fabricIndex;
        }
    }

    SecureSession * result = mEntries.CreateObject(*this, secureSessionType, localSessionId, localNodeId, peerNodeId, peerCATs,
                                                   peerSessionId, fabricIndex, config);
    return result != nullptr ? MakeOptional<SessionHandle>(*result) : Optional<SessionHandle>::Missing();
}

Optional<SessionHandle> SecureSessionTable::CreateNewSecureSession(SecureSession::Type secureSessionType,
                                                                   ScopedNodeId sessionEvictionHint)
{
    Optional<SessionHandle> rv = Optional<SessionHandle>::Missing();
    SecureSession * allocated  = nullptr;

    auto sessionId = FindUnusedSessionId();
    VerifyOrReturnValue(sessionId.HasValue(), Optional<SessionHandle>::Missing());

    //
    // We allocate a new session out of the pool if we have space in it. If we don't, we need
    // to run the eviction algorithm to get a free slot. We shall ALWAYS be guaranteed to evict
    // an existing session in the table in normal operating circumstances.
    //
    if (mEntries.Allocated() < GetMaxSessionTableSize())
    {
        allocated = mEntries.CreateObject(*this, secureSessionType, sessionId.Value());
    }
    else
    {
        allocated = EvictAndAllocate(sessionId.Value(), secureSessionType, sessionEvictionHint);
    }

    VerifyOrReturnValue(allocated != nullptr, Optional<SessionHandle>::Missing());

    rv             = MakeOptional<SessionHandle>(*allocated);
    mNextSessionId = sessionId.Value() == kMaxSessionID ? static_cast<uint16_t>(kUnsecuredSessionId + 1)
                                                        : static_cast<uint16_t>(sessionId.Value() + 1);

    return rv;
}

SecureSession * SecureSessionTable::EvictAndAllocate(uint16_t localSessionId, SecureSession::Type secureSessionType,
                                                     const ScopedNodeId & sessionEvictionHint)
{
    VerifyOrDieWithMsg(!mRunningEvictionLogic, SecureChannel,
                       "EvictAndAllocate isn't re-entrant, yet someone called us while we're already running");

    mRunningEvictionLogic = true;

    auto cleanup = MakeDefer([this]() { mRunningEvictionLogic = false; });

    ChipLogProgress(SecureChannel, "Evicting a slot for session with LSID: %d, type: %u", localSessionId,
                    (uint8_t) secureSessionType);

    VerifyOrDie(mEntries.Allocated() <= GetMaxSessionTableSize());

    //
    // Create a temporary list of objects each of which points to a session in the existing
    // session table, but are swappable. This allows them to then be used with a sorting algorithm
    // without affecting the sessions in the table itself.
    //
    // The size of this shouldn't place significant demands on the stack if using the default
    // configuration for CHIP_CONFIG_SECURE_SESSION_POOL_SIZE (17). Each item is
    // 8 bytes in size (on a 32-bit platform), and 16 bytes in size (on a 64-bit platform,
    // including padding).
    //
    // Total size of this stack variable = 17 * 8 = 136bytes (32-bit platform), 272 bytes (64-bit platform).
    //
    // Even if the define is set to a large value, it's likely not so bad on the sort of platform setup
    // that would have that sort of pool size.
    //
    // We need to sort (as opposed to just a linear search for the smallest/largest item)
    // since it is possible that the candidate selected for eviction may not actually be
    // released once marked for expiration (see comments below for more details).
    //
    // Consequently, we may need to walk the candidate list till we find one that is.
    // Sorting provides a better overall performance model in this scheme.
    //
    // (#19967): Investigate doing linear search instead.
    //
    //
    SortableSession sortableSessions[CHIP_CONFIG_SECURE_SESSION_POOL_SIZE];

    unsigned int index = 0;

    //
    // Compute two key stats for each session - the number of other sessions that
    // match its fabric, as well as the number of other sessions that match its peer.
    //
    // This will be used by the session eviction algorithm later.
    //
    ForEachSession([&index, &sortableSessions, this](auto * session) {
        sortableSessions[index].mSession             = session;
        sortableSessions[index].mNumMatchingOnFabric = 0;
        sortableSessions[index].mNumMatchingOnPeer   = 0;

        ForEachSession([session, index, &sortableSessions](auto * otherSession) {
            if (session != otherSession)
            {
                if (session->GetFabricIndex() == otherSession->GetFabricIndex())
                {
                    sortableSessions[index].mNumMatchingOnFabric++;

                    if (session->GetPeerNodeId() == otherSession->GetPeerNodeId())
                    {
                        sortableSessions[index].mNumMatchingOnPeer++;
                    }
                }
            }

            return Loop::Continue;
        });

        index++;
        return Loop::Continue;
    });

    auto sortableSessionSpan = Span<SortableSession>(sortableSessions, mEntries.Allocated());
    EvictionPolicyContext policyContext(sortableSessionSpan, sessionEvictionHint);

    DefaultEvictionPolicy(policyContext);
    ChipLogProgress(SecureChannel, "Sorted sessions for eviction...");

    const auto numSessions = mEntries.Allocated();

#if CHIP_DETAIL_LOGGING
    ChipLogDetail(SecureChannel, "Sorted Eviction Candidates (ranked from best candidate to worst):");
    for (auto * session = sortableSessions; session != (sortableSessions + numSessions); session++)
    {
        ChipLogDetail(SecureChannel,
                      "\t%ld: [%p] -- Peer: [%u:" ChipLogFormatX64
                      "] State: '%s', NumMatchingOnFabric: %d NumMatchingOnPeer: %d ActivityTime: %lu",
                      static_cast<long int>(session - sortableSessions), session->mSession,
                      session->mSession->GetPeer().GetFabricIndex(), ChipLogValueX64(session->mSession->GetPeer().GetNodeId()),
                      session->mSession->GetStateStr(), session->mNumMatchingOnFabric, session->mNumMatchingOnPeer,
                      static_cast<unsigned long>(session->mSession->GetLastActivityTime().count()));
    }
#endif

    for (auto * session = sortableSessions; session != (sortableSessions + numSessions); session++)
    {
        if (session->mSession->IsPendingEviction())
        {
            continue;
        }

        ChipLogProgress(SecureChannel, "Candidate Session[%p] - Attempting to evict...", session->mSession);

        auto prevCount = mEntries.Allocated();

        //
        // SessionHolders act like weak-refs on a session, but since they do still add to the ref-count of a SecureSession, we
        // cannot actually tell whether there are truly any strong-refs (SessionHandles) on this session because if we did, we'd
        // avoid evicting it since it's pointless to do so.
        //
        // However, we don't actually have SessionHolders implemented correctly as weak-refs, requiring us to go ahead and 'try' to
        // evict it, and see if it still remains in the table. If it does, we have to try the next one. If it doesn't, we know we've
        // earned a free spot.
        //
        // See #19495.
        //
        session->mSession->MarkForEviction();

        auto newCount = mEntries.Allocated();

        if (newCount < prevCount)
        {
            ChipLogProgress(SecureChannel, "Successfully evicted a session!");
            auto * retSession = mEntries.CreateObject(*this, secureSessionType, localSessionId);
            VerifyOrDie(session != nullptr);
            return retSession;
        }
    }

    VerifyOrDieWithMsg(false, SecureChannel, "We couldn't find any session to evict at all, something's wrong!");
    return nullptr;
}

void SecureSessionTable::DefaultEvictionPolicy(EvictionPolicyContext & evictionContext)
{
    //
    // This implements a spec-compliant sorting policy that ensures both guarantees for sessions per-fabric as
    // mandated by the spec as well as fairness in terms of selecting the most appropriate session to evict
    // based on multiple criteria.
    //
    // See the description of this function in the header for more details on each sorting key below.
    //
    evictionContext.Sort([&evictionContext](const SortableSession & a, const SortableSession & b) -> bool {
        //
        // Sorting on Key1
        //
        if (a.mNumMatchingOnFabric != b.mNumMatchingOnFabric)
        {
            return a.mNumMatchingOnFabric > b.mNumMatchingOnFabric;
        }

        bool doesAMatchSessionHintFabric =
            a.mSession->GetPeer().GetFabricIndex() == evictionContext.GetSessionEvictionHint().GetFabricIndex();
        bool doesBMatchSessionHintFabric =
            b.mSession->GetPeer().GetFabricIndex() == evictionContext.GetSessionEvictionHint().GetFabricIndex();

        //
        // Sorting on Key2
        //
        if (doesAMatchSessionHintFabric != doesBMatchSessionHintFabric)
        {
            return doesAMatchSessionHintFabric > doesBMatchSessionHintFabric;
        }

        //
        // Sorting on Key3
        //
        if (a.mNumMatchingOnPeer != b.mNumMatchingOnPeer)
        {
            return a.mNumMatchingOnPeer > b.mNumMatchingOnPeer;
        }

        // We have an evicton hint in two cases:
        //
        // 1) When we just established CASE as a responder, the hint is the node
        //    we just established CASE to.
        // 2) When starting to establish CASE as an initiator, the hint is the
        //    node we are going to establish CASE to.
        //
        // In case 2, we should not end up here if there is an active session to
        // the peer at all (because that session should have been used instead
        // of establishing a new one).
        //
        // In case 1, we know we have a session matching the hint, but we don't
        // want to pick that one for eviction, because we just established it.
        // So we should not consider a session as matching a hint if it's active
        // and is the only session to our peer.
        //
        // Checking for the "active" state in addition to the "only session to
        // peer" state allows us to prioritize evicting defuct sessions that
        // match the hint against other defunct sessions.
        auto sessionMatchesEvictionHint = [&evictionContext](const SortableSession & session) -> int {
            if (session.mSession->GetPeer() != evictionContext.GetSessionEvictionHint())
            {
                return false;
            }
            bool isOnlyActiveSessionToPeer = session.mSession->IsActiveSession() && session.mNumMatchingOnPeer == 0;
            return !isOnlyActiveSessionToPeer;
        };
        int doesAMatchSessionHint = sessionMatchesEvictionHint(a);
        int doesBMatchSessionHint = sessionMatchesEvictionHint(b);

        //
        // Sorting on Key4
        //
        if (doesAMatchSessionHint != doesBMatchSessionHint)
        {
            return doesAMatchSessionHint > doesBMatchSessionHint;
        }

        int aStateScore = 0, bStateScore = 0;
        auto assignStateScore = [](auto & score, const auto & session) {
            if (session.IsDefunct())
            {
                score = 2;
            }
            else if (session.IsActiveSession())
            {
                score = 1;
            }
            else
            {
                score = 0;
            }
        };

        assignStateScore(aStateScore, *a.mSession);
        assignStateScore(bStateScore, *b.mSession);

        //
        // Sorting on Key5
        //
        if (aStateScore != bStateScore)
        {
            return (aStateScore > bStateScore);
        }

        //
        // Sorting on Key6
        //
        return (a->GetLastActivityTime() < b->GetLastActivityTime());
    });
}

Optional<SessionHandle> SecureSessionTable::FindSecureSessionByLocalKey(uint16_t localSessionId)
{
    SecureSession * result = nullptr;
    mEntries.ForEachActiveObject([&](auto session) {
        if (session->GetLocalSessionId() == localSessionId)
        {
            result = session;
            return Loop::Break;
        }
        return Loop::Continue;
    });
    return result != nullptr ? MakeOptional<SessionHandle>(*result) : Optional<SessionHandle>::Missing();
}

Optional<uint16_t> SecureSessionTable::FindUnusedSessionId()
{
    uint16_t candidate_base = 0;
    uint64_t candidate_mask = 0;
    for (uint32_t i = 0; i <= kMaxSessionID; i += 64)
    {
        // candidate_base is the base session ID we are searching from.
        // We have a 64-bit mask anchored at this ID and iterate over the
        // whole session table, setting bits in the mask for in-use IDs.
        // If we can iterate through the entire session table and have
        // any bits clear in the mask, we have available session IDs.
        candidate_base = static_cast<uint16_t>(i + mNextSessionId);
        candidate_mask = 0;
        {
            uint16_t shift = static_cast<uint16_t>(kUnsecuredSessionId - candidate_base);
            if (shift <= 63)
            {
                candidate_mask |= (1ULL << shift); // kUnsecuredSessionId is never available
            }
        }
        mEntries.ForEachActiveObject([&](auto session) {
            uint16_t shift = static_cast<uint16_t>(session->GetLocalSessionId() - candidate_base);
            if (shift <= 63)
            {
                candidate_mask |= (1ULL << shift);
            }
            if (candidate_mask == UINT64_MAX)
            {
                return Loop::Break; // No bits clear means this bucket is full.
            }
            return Loop::Continue;
        });
        if (candidate_mask != UINT64_MAX)
        {
            break; // Any bit clear means we have an available ID in this bucket.
        }
    }
    if (candidate_mask != UINT64_MAX)
    {
        uint16_t offset = 0;
        while (candidate_mask & 1)
        {
            candidate_mask >>= 1;
            ++offset;
        }
        uint16_t available = static_cast<uint16_t>(candidate_base + offset);
        return MakeOptional<uint16_t>(available);
    }

    return NullOptional;
}

} // namespace Transport
} // namespace chip
