/**
 *
 *    Copyright (c) 2023 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.
 */
#pragma once

#include <crypto/CHIPCryptoPAL.h>
#include <crypto/SessionKeystore.h>
#include <lib/core/CHIPConfig.h>
#include <lib/core/CHIPPersistentStorageDelegate.h>
#include <lib/core/ClusterEnums.h>
#include <lib/core/DataModelTypes.h>
#include <lib/support/CodeUtils.h>
#include <lib/support/PersistentData.h>
#include <stddef.h>

namespace chip {
namespace Crypto {
using SymmetricKeystore = SessionKeystore;
}
} // namespace chip

namespace chip {

static constexpr size_t MaxICDMonitoringEntrySize()
{
    // All the fields added together
    return TLV::EstimateStructOverhead(sizeof(NodeId) /*checkInNodeID*/, sizeof(uint64_t) /*monitoredSubject*/,
                                       sizeof(Crypto::Symmetric128BitsKeyByteArray) /*aes_key_handle*/,
                                       sizeof(Crypto::Symmetric128BitsKeyByteArray) /*hmac_key_handle*/,
                                       sizeof(uint8_t) /*client_type*/) *
        // Provide 50% extra space to make a firmware upgrade that starts storing
        // more data followed by a downgrade work easily and reliably.
        // The 50% number is chosen fairly randomly; storage increases larger than that are
        // possible but need to be staged carefully.
        3 / 2;
}

inline constexpr size_t kICDMonitoringBufferSize = MaxICDMonitoringEntrySize();

struct ICDMonitoringEntry : public PersistentData<kICDMonitoringBufferSize>
{
    ICDMonitoringEntry(FabricIndex fabric = kUndefinedFabricIndex, NodeId nodeId = kUndefinedNodeId)
    {
        this->fabricIndex      = fabric;
        this->checkInNodeID    = nodeId;
        this->monitoredSubject = nodeId;
    }

    ICDMonitoringEntry(Crypto::SymmetricKeystore * keyStore, FabricIndex fabric = kUndefinedFabricIndex,
                       NodeId nodeId = kUndefinedNodeId)
    {
        this->fabricIndex       = fabric;
        this->checkInNodeID     = nodeId;
        this->monitoredSubject  = nodeId;
        this->symmetricKeystore = keyStore;
    }

    CHIP_ERROR UpdateKey(StorageKeyName & key) const override;
    CHIP_ERROR Serialize(TLV::TLVWriter & writer) const override;
    CHIP_ERROR Deserialize(TLV::TLVReader & reader) override;
    void Clear() override;
    /**
     * @brief Set the Key object
     *        This method will create a new keyHandle. The key handle might contain either
     *        the raw key or a keyID depending on which Crypto implementation is used.
     *        To avoid leaking keys, API consumers must either call the DeleteKey method
     *        or save the entry within the ICDMonitoring Table before this object goes out of scope.
     *
     *        A new entry object should be used for each key when adding entries to the ICDMonitoring
     *        table.
     *
     * @param keyData A byte span containing the raw key
     * @return CHIP_ERROR CHIP_NO_ERROR     success
     *         CHIP_ERROR_INVALID_ARGUMENT  wrong size of the raw key
     *         CHIP_ERROR_INTERNAL          No KeyStore for the entry or Key Handle already present
     *         CHIP_ERROR_XXX               Crypto API related failure
     */
    CHIP_ERROR SetKey(ByteSpan keyData);
    CHIP_ERROR DeleteKey(void);
    inline bool IsValid()
    {
        return (symmetricKeystore != nullptr && keyHandleValid && fabricIndex != kUndefinedFabricIndex &&
                checkInNodeID != kUndefinedNodeId);
    }

    ICDMonitoringEntry & operator=(const ICDMonitoringEntry & icdMonitoringEntry);

    /**
     * @brief Implement the key verification needed by the ICDManagement Server.
     *        Since for some key implementations we cannot retrieve the key from the AES128KeyHandle
     *        we must implement a way to deduce whether the verification key
     *        received is the same or at least works as the same way as the one stored.
     *
     *        This method will produce a random number and then encrypt it with the keyData.
     *        It will then decrypt it with the key stored in the entry. If the resulting decrypted
     *        challenge matches the randomly generated number, then we can safely assume that both key are interchangeable.
     *        This method cannot guarantee a perfect match since the probability of two keys generating the same output in AES128 is
     *        not 0 but 1/2^128 which is small enough for our purposes.
     *
     * @param keyData
     * @return bool True if the key is equivalent to the one stored, otherwise false
     */
    bool IsKeyEquivalent(ByteSpan keyData);

    chip::FabricIndex fabricIndex                           = kUndefinedFabricIndex;
    chip::NodeId checkInNodeID                              = kUndefinedNodeId;
    uint64_t monitoredSubject                               = static_cast<uint64_t>(0);
    app::Clusters::IcdManagement::ClientTypeEnum clientType = app::Clusters::IcdManagement::ClientTypeEnum::kPermanent;
    Crypto::Aes128KeyHandle aesKeyHandle                    = Crypto::Aes128KeyHandle();
    Crypto::Hmac128KeyHandle hmacKeyHandle                  = Crypto::Hmac128KeyHandle();
    bool keyHandleValid                                     = false;
    uint16_t index                                          = 0;
    Crypto::SymmetricKeystore * symmetricKeystore           = nullptr;
};

/**
 * @brief ICDMonitoringTable exists to manage the persistence of entries in the IcdManagement Cluster.
 *        To access persisted data with the ICDMonitoringTable class, instantiate an instance of this class
 *        and call the LoadFromStorage function.
 *
 *        This class can only manage one fabric at a time. The flow is load a fabric, execute necessary operations,
 *        save it if there are any changes and load another fabric.
 *
 *        Issue to refactor the class to use one entry for the entire table
 *        https://github.com/project-chip/connectedhomeip/issues/24288
 */

struct ICDMonitoringTable
{
    ICDMonitoringTable(PersistentStorageDelegate & storage, FabricIndex fabric, uint16_t limit,
                       Crypto::SymmetricKeystore * symmetricKeystore) :
        mStorage(&storage),
        mFabric(fabric), mLimit(limit), mSymmetricKeystore(symmetricKeystore)
    {}

    /**
     * @brief Returns the MonitoringRegistrationStruct entry at the given position.
     * @param index Zero-based position within the RegisteredClients table.
     * @param entry On success, contains the MonitoringRegistrationStruct matching the given index.
     * @return CHIP_NO_ERROR on success,
     *         CHIP_ERROR_NOT_FOUND if index is greater than the index of the last entry on the table.
     */
    CHIP_ERROR Get(uint16_t index, ICDMonitoringEntry & entry) const;

    /**
     * @brief Stores the MonitoringRegistrationStruct entry at the given position,
     *        overwriting any existing entry.
     * @param index Zero-based position within the RegisteredClients table.
     * @param entry On success, contains the MonitoringRegistrationStruct matching the given index.
     * @return CHIP_NO_ERROR on success
     */
    CHIP_ERROR Set(uint16_t index, const ICDMonitoringEntry & entry);

    /**
     * @brief Search the registered clients for an entry on the fabric whose checkInNodeID matches the given id.
     * @param id    NodeId to match.
     * @param entry On success, contains the MonitoringRegistrationStruct matching the given node ID.
     *  If found, entry.index contains the position of the entry in the table.
     *  If CHIP_ERROR_NOT_FOUND is returned, entry.index contains the total number of entries in the table.
     * @return CHIP_NO_ERROR if found, CHIP_ERROR_NOT_FOUND if no checkInNodeID matches the provided id.
     */
    CHIP_ERROR Find(NodeId id, ICDMonitoringEntry & entry);

    /**
     * @brief Removes the MonitoringRegistrationStruct entry at the given position,
     *        shifting down the upper entries.
     * @param index Zero-based position within the RegisteredClients table.
     * @return CHIP_NO_ERROR on success
     */
    CHIP_ERROR Remove(uint16_t index);

    /**
     * @brief Removes all the entries for the current fabricIndex.
     * @return CHIP_NO_ERROR on success
     */
    CHIP_ERROR RemoveAll();

    /**
     * @brief Check if the table is empty
     * @return True when there is no entry in the table. False if there is at least one
     */
    bool IsEmpty();

    /**
     * @return Maximum number of entries allowed in the RegisteredClients table.
     */
    uint16_t Limit() const;

private:
    PersistentStorageDelegate * mStorage;
    FabricIndex mFabric;
    uint16_t mLimit                                = 0;
    Crypto::SymmetricKeystore * mSymmetricKeystore = nullptr;
};

} // namespace chip
