Implement Joint Fabric Node management commands (#40998)

diff --git a/src/app/clusters/joint-fabric-datastore-server/joint-fabric-datastore-server.cpp b/src/app/clusters/joint-fabric-datastore-server/joint-fabric-datastore-server.cpp
index 434c152..c1e55d2 100644
--- a/src/app/clusters/joint-fabric-datastore-server/joint-fabric-datastore-server.cpp
+++ b/src/app/clusters/joint-fabric-datastore-server/joint-fabric-datastore-server.cpp
@@ -24,6 +24,7 @@
 #include <app/AttributeAccessInterface.h>
 #include <app/AttributeAccessInterfaceRegistry.h>
 #include <app/ConcreteCommandPath.h>
+#include <app/InteractionModelEngine.h>
 #include <app/reporting/reporting.h>
 #include <app/server/Server.h>
 #include <app/util/attribute-storage.h>
@@ -298,8 +299,7 @@
     app::JointFabricDatastore & jointFabricDatastore = Server::GetInstance().GetJointFabricDatastore();
 
     VerifyOrExit(jointFabricDatastore.IsGroupKeySetEntryPresent(groupKeySet.groupKeySetID) == false,
-                 // TODO: make sure this maps to Protocols::InteractionModel::ClusterStatusCode::ConstraintError
-                 err = CHIP_ERROR_INVALID_ARGUMENT);
+                 err = CHIP_IM_GLOBAL_STATUS(ConstraintError));
     SuccessOrExit(err = jointFabricDatastore.AddGroupKeySetEntry(groupKeySet));
 
 exit:
@@ -550,7 +550,10 @@
 
     app::JointFabricDatastore & jointFabricDatastore = Server::GetInstance().GetJointFabricDatastore();
 
-    SuccessOrExit(err = jointFabricDatastore.RefreshNode(nodeId));
+    ReadOnlyBufferBuilder<DataModel::EndpointEntry> endpointsList;
+    // TODO: Get Endpoints List from connected device with <nodeId>
+
+    SuccessOrExit(err = jointFabricDatastore.RefreshNode(nodeId, endpointsList.TakeBuffer()));
 
 exit:
     if (err == CHIP_NO_ERROR)
diff --git a/src/app/server/JointFabricDatastore.cpp b/src/app/server/JointFabricDatastore.cpp
index 5b57095..4e26790 100644
--- a/src/app/server/JointFabricDatastore.cpp
+++ b/src/app/server/JointFabricDatastore.cpp
@@ -17,11 +17,11 @@
 
 #include <app/server/JointFabricDatastore.h>
 
+#include <algorithm>
+
 namespace chip {
 namespace app {
 
-#define CHIP_ERROR_IM_STATUS_CODE_CONSTRAINT_ERROR CHIP_CORE_ERROR(0x87)
-
 void JointFabricDatastore::AddListener(Listener & listener)
 {
     if (mListeners == nullptr)
@@ -122,16 +122,178 @@
     return CHIP_ERROR_NOT_FOUND;
 }
 
-CHIP_ERROR JointFabricDatastore::RefreshNode(NodeId nodeId)
+CHIP_ERROR JointFabricDatastore::RefreshNode(NodeId nodeId, ReadOnlyBuffer<DataModel::EndpointEntry> endpointsList)
 {
     // 1. && 2.
     ReturnErrorOnFailure(SetNode(nodeId, Clusters::JointFabricDatastore::DatastoreStateEnum::kPending));
 
-    // 3. TODO: Read the PartsList of the Descriptor cluster from the Node.
+    // 3.
 
-    // 4. TODO
+    // cycle through endpointsList and add them to the endpoint entries
+    for (const auto & endpoint : endpointsList)
+    {
+        auto it = std::find_if(
+            mEndpointEntries.begin(), mEndpointEntries.end(),
+            [&endpoint, &nodeId](const Clusters::JointFabricDatastore::Structs::DatastoreEndpointEntryStruct::Type & entry) {
+                return entry.nodeID == nodeId && entry.endpointID == endpoint.id;
+            });
+        if (it == mEndpointEntries.end())
+        {
+            Clusters::JointFabricDatastore::Structs::DatastoreEndpointEntryStruct::Type newEntry;
+            newEntry.endpointID = endpoint.id;
+            newEntry.nodeID     = nodeId;
+            mEndpointEntries.push_back(newEntry);
+        }
+    }
 
-    // 5. TODO
+    // Remove EndpointEntries that are not in the endpointsList
+    mEndpointEntries.erase(std::remove_if(mEndpointEntries.begin(), mEndpointEntries.end(),
+                                          [&](const auto & entry) {
+                                              if (entry.nodeID != nodeId)
+                                              {
+                                                  return false; // Not for this node, don't remove.
+                                              }
+                                              // Remove if not found in endpointsList.
+                                              return !std::any_of(
+                                                  endpointsList.begin(), endpointsList.end(),
+                                                  [&](const auto & endpoint) { return entry.endpointID == endpoint.id; });
+                                          }),
+                           mEndpointEntries.end());
+
+    // TODO: read the Endpoint Group ID List from the actual device
+
+    for (auto it = mEndpointGroupIDEntries.begin(); it != mEndpointGroupIDEntries.end();)
+    {
+        if (it->nodeID == nodeId)
+        {
+            if (it->statusEntry.state == Clusters::JointFabricDatastore::DatastoreStateEnum::kPending)
+            {
+                // TODO: add this entry to the actual device. Only mark as committed if successful.
+                // it->statusEntry.state = Clusters::JointFabricDatastore::DatastoreStateEnum::kCommitted;
+            }
+            else if (it->statusEntry.state == Clusters::JointFabricDatastore::DatastoreStateEnum::kDeletePending)
+            {
+                // TODO: Remove the binding from the actual device. If successful, then remove the entry.
+                // otherwise update status to CommitFailed and return error
+                it = mEndpointGroupIDEntries.erase(it);
+                continue;
+            }
+            else if (it->statusEntry.state == Clusters::JointFabricDatastore::DatastoreStateEnum::kCommitFailed)
+            {
+                CHIP_ERROR failureCode(it->statusEntry.failureCode);
+
+                if (failureCode == CHIP_IM_GLOBAL_STATUS(ConstraintError) ||
+                    failureCode == CHIP_IM_GLOBAL_STATUS(ResourceExhausted))
+                {
+                    // remove entry from the list
+                    it = mEndpointGroupIDEntries.erase(it);
+                    continue;
+                }
+                else
+                {
+                    // TODO: update binding on actual device.
+                    // TODO: check if change has been made, retry if not. Mark as committed upon success.
+                    // TODO: retry delete, remove entry if successful.
+                }
+            }
+        }
+
+        ++it;
+    }
+
+    // TODO: read the Endpoint Binding List from the actual device
+
+    for (auto it = mEndpointBindingEntries.begin(); it != mEndpointBindingEntries.end();)
+    {
+        if (it->nodeID == nodeId)
+        {
+            if (it->statusEntry.state == Clusters::JointFabricDatastore::DatastoreStateEnum::kPending)
+            {
+                // TODO: add this entry to the actual device. Only mark as committed if successful.
+                // it->statusEntry.state = Clusters::JointFabricDatastore::DatastoreStateEnum::kCommitted;
+            }
+            else if (it->statusEntry.state == Clusters::JointFabricDatastore::DatastoreStateEnum::kDeletePending)
+            {
+                // TODO: Remove the binding from the actual device. If successful, then remove the entry.
+                // otherwise update status to CommitFailed and return error
+                it = mEndpointBindingEntries.erase(it);
+                continue;
+            }
+            else if (it->statusEntry.state == Clusters::JointFabricDatastore::DatastoreStateEnum::kCommitFailed)
+            {
+                CHIP_ERROR failureCode(it->statusEntry.failureCode);
+
+                if (failureCode == CHIP_IM_GLOBAL_STATUS(ConstraintError) ||
+                    failureCode == CHIP_IM_GLOBAL_STATUS(ResourceExhausted))
+                {
+                    // remove entry from the list
+                    it = mEndpointBindingEntries.erase(it);
+                    continue;
+                }
+                else
+                {
+                    // TODO: update binding on actual device.
+                    // TODO: check if change has been made, retry if not. Mark as committed upon success.
+                    // TODO: retry delete, remove entry if successful.
+                }
+            }
+        }
+
+        ++it;
+    }
+
+    // TODO: read the Group Key Set List from the actual device
+
+    // 4.
+    for (auto it = mGroupKeySetList.begin(); it != mGroupKeySetList.end();)
+    {
+        // TODO: Apply any pending updates from datastore, upon success mark as committed
+        // Retry any CommitFailure entries from datastore, upon success can be removed.
+        // Ensure list from device matches list in datastore
+        ++it;
+    }
+
+    // TODO: read the ACL List from the actual device
+
+    // 5.
+    for (auto it = mACLEntries.begin(); it != mACLEntries.end();)
+    {
+        if (it->nodeID == nodeId)
+        {
+            if (it->statusEntry.state == Clusters::JointFabricDatastore::DatastoreStateEnum::kPending)
+            {
+                // TODO: add this entry to the actual device. Only mark as committed if successful.
+                // it->statusEntry.state = Clusters::JointFabricDatastore::DatastoreStateEnum::kCommitted;
+            }
+            else if (it->statusEntry.state == Clusters::JointFabricDatastore::DatastoreStateEnum::kDeletePending)
+            {
+                // TODO: Remove the binding from the actual device. If successful, then remove the entry.
+                // otherwise update status to CommitFailed and return error
+                it = mACLEntries.erase(it);
+                continue;
+            }
+            else if (it->statusEntry.state == Clusters::JointFabricDatastore::DatastoreStateEnum::kCommitFailed)
+            {
+                CHIP_ERROR failureCode(it->statusEntry.failureCode);
+
+                if (failureCode == CHIP_IM_GLOBAL_STATUS(ConstraintError) ||
+                    failureCode == CHIP_IM_GLOBAL_STATUS(ResourceExhausted))
+                {
+                    // remove entry from the list
+                    it = mACLEntries.erase(it);
+                    continue;
+                }
+                else
+                {
+                    // TODO: update binding on actual device.
+                    // TODO: check if change has been made, retry if not. Mark as committed upon success.
+                    // TODO: retry delete, remove entry if successful.
+                }
+            }
+        }
+
+        ++it;
+    }
 
     // 6.
     ReturnErrorOnFailure(SetNode(nodeId, Clusters::JointFabricDatastore::DatastoreStateEnum::kCommitted));
@@ -284,7 +446,7 @@
     {
         if (entry.groupKeySetID == groupKeySet.groupKeySetID)
         {
-            // TODO: update device
+            // TODO: Need to update the keySetList on the actual device
             entry.statusEntry.state = Clusters::JointFabricDatastore::DatastoreStateEnum::kCommitted;
             return CHIP_NO_ERROR;
         }
@@ -301,7 +463,7 @@
         {
             if (it->statusEntry.state != Clusters::JointFabricDatastore::DatastoreStateEnum::kDeletePending)
             {
-                return CHIP_ERROR_IM_STATUS_CODE_CONSTRAINT_ERROR; // Cannot remove a key set that is not pending
+                return CHIP_IM_GLOBAL_STATUS(ConstraintError); // Cannot remove a key set that is not pending
             }
 
             ReturnErrorOnFailure(RemoveGroupKeySetEntry(groupKeySetId));
@@ -318,12 +480,12 @@
     size_t index = 0;
     // Check if the group ID already exists in the datastore
     VerifyOrReturnError(IsGroupIDInDatastore(commandData.groupID, index) == CHIP_ERROR_NOT_FOUND,
-                        CHIP_ERROR_IM_STATUS_CODE_CONSTRAINT_ERROR);
+                        CHIP_IM_GLOBAL_STATUS(ConstraintError));
 
     if (commandData.groupCAT.ValueOr(0) == kAdminCATIdentifier || commandData.groupCAT.ValueOr(0) == kAnchorCATIdentifier)
     {
         // If the group is an AdminCAT or AnchorCAT, we cannot add it
-        return CHIP_ERROR_IM_STATUS_CODE_CONSTRAINT_ERROR;
+        return CHIP_IM_GLOBAL_STATUS(ConstraintError);
     }
 
     Clusters::JointFabricDatastore::Structs::DatastoreGroupInformationEntryStruct::Type groupEntry;
@@ -345,40 +507,73 @@
 {
     size_t index = 0;
     // Check if the group ID exists in the datastore
-    VerifyOrReturnError(IsGroupIDInDatastore(commandData.groupID, index) == CHIP_NO_ERROR,
-                        CHIP_ERROR_IM_STATUS_CODE_CONSTRAINT_ERROR);
+    VerifyOrReturnError(IsGroupIDInDatastore(commandData.groupID, index) == CHIP_NO_ERROR, CHIP_IM_GLOBAL_STATUS(ConstraintError));
 
     if (commandData.groupCAT.ValueOr(0) == kAdminCATIdentifier || commandData.groupCAT.ValueOr(0) == kAnchorCATIdentifier)
     {
         // If the group is an AdminCAT or AnchorCAT, we cannot update it
-        return CHIP_ERROR_IM_STATUS_CODE_CONSTRAINT_ERROR;
+        return CHIP_IM_GLOBAL_STATUS(ConstraintError);
     }
 
     // Update the group entry with the new data
     if (commandData.friendlyName.IsNull() == false)
     {
+        if (mGroupInformationEntries[index].friendlyName.data_equal(commandData.friendlyName.Value()) == false)
+        {
+            // TODO: Iterate through each Endpoint Information Entry:
+            // If the GroupIDList contains an entry with the given GroupID:
+            // Update the GroupIDList Entry in the Datastore with the new values and Status Pending
+            // Update the Groups on the given Node with the new values....
+        }
+        // Update the friendly name
         mGroupInformationEntries[index].friendlyName = commandData.friendlyName.Value();
     }
     if (commandData.groupKeySetID.IsNull() == false)
     {
+        if (mGroupInformationEntries[index].groupKeySetID != commandData.groupKeySetID.Value())
+        {
+            // If the groupKeySetID is being updated, we need to ensure that the new key set exists
+            ReturnErrorOnFailure(AddNodeKeySetEntry(commandData.groupID, commandData.groupKeySetID.Value()));
+            ReturnErrorOnFailure(RemoveNodeKeySetEntry(
+                commandData.groupID, mGroupInformationEntries[index].groupKeySetID.Value())); // Remove the old key set
+        }
         mGroupInformationEntries[index].groupKeySetID = commandData.groupKeySetID.Value();
     }
+
+    bool anyGroupCATFieldUpdated = false;
+
     if (commandData.groupCAT.IsNull() == false)
     {
+        if (mGroupInformationEntries[index].groupCAT != commandData.groupCAT.Value())
+        {
+            anyGroupCATFieldUpdated = true;
+        }
+        // Update the groupCAT
         mGroupInformationEntries[index].groupCAT = commandData.groupCAT.Value();
     }
     if (commandData.groupCATVersion.IsNull() == false)
     {
+        if (mGroupInformationEntries[index].groupCATVersion != commandData.groupCATVersion.Value())
+        {
+            anyGroupCATFieldUpdated = true;
+        }
         mGroupInformationEntries[index].groupCATVersion = commandData.groupCATVersion.Value();
     }
     if (commandData.groupPermission != Clusters::JointFabricDatastore::DatastoreAccessControlEntryPrivilegeEnum::kUnknownEnumValue)
     {
+        if (mGroupInformationEntries[index].groupPermission != commandData.groupPermission)
+        {
+            anyGroupCATFieldUpdated = true;
+        }
         // If the groupPermission is not set to kUnknownEnumValue, update it
         mGroupInformationEntries[index].groupPermission = commandData.groupPermission;
     }
 
-    // TODO: iterate through each Node Information Entry to check for membership, and when found, set to pending, update device, set
-    // to committed, etc.
+    if (anyGroupCATFieldUpdated)
+    {
+        // TODO: iterate through NodeACLList for entries referencing this group.
+        // if found, set status to pending, update corresponding ACL on device, and then mark as committed.
+    }
 
     return CHIP_NO_ERROR;
 }
@@ -388,8 +583,7 @@
 {
     size_t index = 0;
     // Check if the group ID exists in the datastore
-    VerifyOrReturnError(IsGroupIDInDatastore(commandData.groupID, index) == CHIP_NO_ERROR,
-                        CHIP_ERROR_IM_STATUS_CODE_CONSTRAINT_ERROR);
+    VerifyOrReturnError(IsGroupIDInDatastore(commandData.groupID, index) == CHIP_NO_ERROR, CHIP_IM_GLOBAL_STATUS(ConstraintError));
 
     // Remove the group entry from the datastore
     auto it = mGroupInformationEntries.begin();
@@ -398,7 +592,7 @@
     if (it->groupCAT == kAdminCATIdentifier || it->groupCAT == kAnchorCATIdentifier)
     {
         // If the group is an AdminCAT or AnchorCAT, we cannot remove it
-        return CHIP_ERROR_IM_STATUS_CODE_CONSTRAINT_ERROR;
+        return CHIP_IM_GLOBAL_STATUS(ConstraintError);
     }
 
     mGroupInformationEntries.erase(it);
@@ -468,7 +662,7 @@
     size_t index = 0;
     ReturnErrorOnFailure(IsNodeIdAndEndpointInEndpointInformationEntries(nodeId, endpointId, index));
 
-    VerifyOrReturnError(IsGroupIDInDatastore(groupId, index) == CHIP_ERROR_NOT_FOUND, CHIP_ERROR_IM_STATUS_CODE_CONSTRAINT_ERROR);
+    VerifyOrReturnError(IsGroupIDInDatastore(groupId, index) == CHIP_ERROR_NOT_FOUND, CHIP_IM_GLOBAL_STATUS(ConstraintError));
 
     if (mGroupInformationEntries[index].groupKeySetID.IsNull() == false)
     {
@@ -894,5 +1088,55 @@
     return CHIP_ERROR_NOT_FOUND;
 }
 
+CHIP_ERROR JointFabricDatastore::AddNodeKeySetEntry(GroupId groupId, uint16_t groupKeySetId)
+{
+    // TODO: Iterate through all nodes that are part of this group and add an entry for each node
+
+    // If the key set does not exist, create a new entry
+    Clusters::JointFabricDatastore::Structs::DatastoreNodeKeySetEntryStruct::Type newEntry;
+    newEntry.nodeID            = groupId; // Using groupId as nodeId temporarily, will be updated when iterating through nodes
+    newEntry.groupKeySetID     = groupKeySetId;
+    newEntry.statusEntry.state = Clusters::JointFabricDatastore::DatastoreStateEnum::kPending;
+
+    mNodeKeySetEntries.push_back(newEntry);
+
+    // TODO: (1) Add keyset to device, (2) Update group entry on device to point to this keyset
+
+    // After adding the new entry, we can set it to committed
+    mNodeKeySetEntries.back().statusEntry.state = Clusters::JointFabricDatastore::DatastoreStateEnum::kCommitted;
+
+    return CHIP_NO_ERROR;
+}
+
+CHIP_ERROR JointFabricDatastore::RemoveNodeKeySetEntry(GroupId groupId, uint16_t groupKeySetId)
+{
+    // NOTE: this method assumes its ok to remove the keyset from each node (its not in use by any group)
+
+    // TODO: Iterate through all nodes that are part of this group and add an entry for each node
+
+    bool any_node_removed = false;
+
+    for (auto it = mNodeKeySetEntries.begin(); it != mNodeKeySetEntries.end();)
+    {
+        // TODO: Iterate trough nodes in the group
+        if (it->groupKeySetID == groupKeySetId)
+        {
+            any_node_removed = true;
+
+            it->statusEntry.state = Clusters::JointFabricDatastore::DatastoreStateEnum::kDeletePending;
+
+            // TODO: Update device: send command to it->nodeID to remove this keyset
+
+            it = mNodeKeySetEntries.erase(it);
+        }
+        else
+        {
+            ++it;
+        }
+    }
+
+    return any_node_removed ? CHIP_NO_ERROR : CHIP_ERROR_NOT_FOUND;
+}
+
 } // namespace app
 } // namespace chip
diff --git a/src/app/server/JointFabricDatastore.h b/src/app/server/JointFabricDatastore.h
index ed3b9fa..6847342 100644
--- a/src/app/server/JointFabricDatastore.h
+++ b/src/app/server/JointFabricDatastore.h
@@ -18,10 +18,12 @@
 #pragma once
 
 #include <app-common/zap-generated/cluster-objects.h>
+#include <app/data-model-provider/MetadataTypes.h>
 #include <credentials/CHIPCert.h>
 #include <lib/core/CHIPPersistentStorageDelegate.h>
 #include <lib/core/CHIPVendorIdentifiers.hpp>
 #include <lib/core/NodeId.h>
+#include <lib/support/ReadOnlyBuffer.h>
 #include <vector>
 
 namespace chip {
@@ -176,7 +178,7 @@
     CHIP_ERROR AddPendingNode(NodeId nodeId, const CharSpan & friendlyName);
     CHIP_ERROR UpdateNode(NodeId nodeId, const CharSpan & friendlyName);
     CHIP_ERROR RemoveNode(NodeId nodeId);
-    CHIP_ERROR RefreshNode(NodeId nodeId);
+    CHIP_ERROR RefreshNode(NodeId nodeId, ReadOnlyBuffer<DataModel::EndpointEntry> endpointsList);
 
     CHIP_ERROR SetNode(NodeId nodeId, Clusters::JointFabricDatastore::DatastoreStateEnum state);
 
@@ -299,6 +301,9 @@
                     const Clusters::JointFabricDatastore::Structs::DatastoreAccessControlEntryStruct::DecodableType & acl2);
     bool ACLTargetMatches(const Clusters::JointFabricDatastore::Structs::DatastoreAccessControlTargetStruct::Type & target1,
                           const Clusters::JointFabricDatastore::Structs::DatastoreAccessControlTargetStruct::Type & target2);
+
+    CHIP_ERROR AddNodeKeySetEntry(GroupId groupId, uint16_t groupKeySetId);
+    CHIP_ERROR RemoveNodeKeySetEntry(GroupId groupId, uint16_t groupKeySetId);
 };
 
 } // namespace app