/*
 *    Copyright (c) 2022 Project CHIP Authors
 *    All rights reserved.
 *
 *    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 <lib/core/CHIPError.h>
#include <lib/core/DataModelTypes.h>
#include <lib/support/Span.h>

namespace chip {
namespace Credentials {

class OperationalCertificateStore
{
public:
    enum class CertChainElement : uint8_t
    {
        kRcac = 0,
        kIcac = 1,
        kNoc  = 2
    };

    virtual ~OperationalCertificateStore() {}

    // ==== API designed for commisionables to support fail-safe (although can be used by controllers) ====

    /**
     * @brief Returns true if a pending root certificate exists and is active from a previous
     *        `AddNewTrustedRootCertForFabric`.
     */
    virtual bool HasPendingRootCert() const = 0;

    /**
     * @brief Returns true if a pending operational certificate chain exists and is active from a previous
     *        `AddNewOpCertsForFabric` or `UpdateOpCertsForFabric`.
     */
    virtual bool HasPendingNocChain() const = 0;

    /**
     * @brief Returns whether a usable operational certificates chain exists for the given fabric.
     *
     * Returns true even if the certificates are not persisted yet. Only returns true if a certificate
     * is presently usable such that `GetCertificate` would succeed for the fabric.
     *
     * @param fabricIndex - FabricIndex for which availability of certificate will be checked.
     * @param element - Element of the certificate chain whose presence needs to be checked
     * @return true if there an active obtainable operational certificate of the given type for the given FabricIndex,
     *         false otherwise.
     */
    virtual bool HasCertificateForFabric(FabricIndex fabricIndex, CertChainElement element) const = 0;

    /**
     * @brief Add and temporarily activate a new Trusted Root Certificate for the given fabric
     *
     * The certificate is temporary until committed or reverted.
     * The certificate is committed to storage only on `CommitOpCertsForFabric`.
     * The certificate is destroyed if `RevertPendingOpCerts` is called before `CommitOpCertsForFabric`.
     *
     * Only one pending trusted root certificate is supported at a time and it is illegal
     * to call this method if there is already a persisted root certificate for the given
     * fabric.
     *
     * Uniqueness constraints for roots (see AddTrustedRootCertificate command in spec) are not
     * enforced by this method and must be done as a more holistic check elsewhere. Cryptographic
     * signature verification or path validation are not enforced by this method.
     *
     * If `UpdateOpCertsForFabric` had been called before this method, this method will return
     * CHIP_ERROR_INCORRECT_STATE since it is illegal to update trusted roots when updating an
     * existing NOC chain.
     *
     * @param fabricIndex - FabricIndex for which a new trusted root certificate should be added
     * @param rcac - Buffer containing the root certificate to add.
     *
     * @retval CHIP_NO_ERROR on success
     * @retval CHIP_ERROR_NO_MEMORY if there is insufficient memory to maintain the temporary root cert
     * @retval CHIP_ERROR_INVALID_ARGUMENT if the certificate is empty or too large
     * @retval CHIP_ERROR_INCORRECT_STATE if the certificate store is not properly initialized, if this method
     *                                    is called after `UpdateOpCertsForFabric`, or if there was
     *                                    already a pending or persisted root certificate for the given `fabricIndex`.
     * @retval other CHIP_ERROR value on internal errors
     */
    virtual CHIP_ERROR AddNewTrustedRootCertForFabric(FabricIndex fabricIndex, const ByteSpan & rcac) = 0;

    /**
     * @brief Add and temporarily activate an operational certificate chain for the given fabric.
     *
     * The certificate chain is temporary until committed or reverted.
     * The certificate chain is committed to storage on `CommitOpCertsForFabric`.
     * The certificate chain is destroyed if `RevertPendingOpCerts` is called before `CommitOpCertsForFabric`.
     *
     * Only one pending operational certificate chain is supported at a time and it is illegal
     * to call this method if there is already a persisted certificate chain for the given
     * fabric.
     *
     * Cryptographic signature verification or path validation are not enforced by this method.
     *
     * If `UpdateOpCertsForFabric` had been called before this method, this method will return
     * CHIP_ERROR_INCORRECT_STATE since it is illegal to add a certificate chain after
     * updating an existing NOC and before committing or reverting the update.
     *
     * If `AddNewTrustedRootCertForFabric` had not been called before this method, this method will
     * return CHIP_ERROR_INCORRECT_STATE since it is illegal in this implementation to store an
     * NOC chain without associated root.
     *
     * NOTE: The Matter spec allows AddNOC without AddTrustedRootCertificate if the NOC
     * chains to an existing root, to support root reuse. In this implementation, we expect each
     * fabric to store the root with the rest of the chain. Because of this, callers must ensure
     * that if an AddNOC command is done and no trusted root was added, that the requisite existing
     * root be "copied over" to match.
     *
     * @param fabricIndex - FabricIndex for which to add a new operational certificate chain
     * @param noc - Buffer containing the NOC certificate to add
     * @param icac - Buffer containing the ICAC certificate to add. If no ICAC is needed, `icac.empty()` must be true.
     *
     * @retval CHIP_NO_ERROR on success
     * @retval CHIP_ERROR_NO_MEMORY if there is insufficient memory to maintain the temporary `noc` and `icac` cert copies
     * @retval CHIP_ERROR_INVALID_ARGUMENT if either the noc or icac are invalid sizes
     * @retval CHIP_ERROR_INVALID_FABRIC_INDEX if `fabricIndex` mismatches the one from a previous successful
     *                                         `AddNewTrustedRootCertForFabric`.
     * @retval CHIP_ERROR_INCORRECT_STATE if the certificate store is not properly initialized, if this method
     *                                    is called after `UpdateOpCertsForFabric`, or if there was
     *                                    already a pending or persisted operational cert chain for the given `fabricIndex`,
     *                                    or if AddNewTrustedRootCertForFabric had not yet been called for the given `fabricIndex`.
     * @retval other CHIP_ERROR value on internal errors
     */
    virtual CHIP_ERROR AddNewOpCertsForFabric(FabricIndex fabricIndex, const ByteSpan & noc, const ByteSpan & icac) = 0;

    /**
     * @brief Update and temporarily activate an existing operational certificate chain for the given fabric.
     *
     * The certificate chain is temporary until committed or reverted.
     * The certificate chain is committed to storage on `CommitOpCertsForFabric`.
     * The certificate chain is reverted to prior storage if `RevertPendingOpCerts` is called
     * before `CommitOpCertsForFabric`.
     *
     * Only one pending operational certificate chain is supported at a time and it is illegal
     * to call this method if there was not already a persisted certificate chain for the given
     * fabric.
     *
     * Cryptographic signature verification or path validation are not enforced by this method.
     *
     * If `AddNewOpCertsForFabric` had been called before this method, this method will return
     * CHIP_ERROR_INCORRECT_STATE since it is illegal to update a certificate chain after
     * adding an existing NOC and before committing or reverting the addition.
     *
     * If there is no existing persisted trusted root certificate and NOC chain for the given
     * fabricIndex, this method will return CHIP_ERROR_INCORRECT_STATE since it is
     * illegal in this implementation to store an NOC chain without associated root, and it is illegal
     * to update an opcert for a fabric not already configured.
     *
     * @param fabricIndex - FabricIndex for which to update the operational certificate chain
     * @param noc - Buffer containing the new NOC certificate to use
     * @param icac - Buffer containing the ICAC certificate to use. If no ICAC is needed, `icac.empty()` must be true.
     *
     * @retval CHIP_NO_ERROR on success
     * @retval CHIP_ERROR_NO_MEMORY if there is insufficient memory to maintain the temporary `noc` and `icac` cert copies
     * @retval CHIP_ERROR_INVALID_ARGUMENT if either the noc or icac are invalid sizes
     * @retval CHIP_ERROR_INCORRECT_STATE if the certificate store is not properly initialized, if this method
     *                                    is called after `AddNewOpCertsForFabric`, or if there was
     *                                    already a pending cert chain for the given `fabricIndex`, or if there are
     *                                    no associated persisted root and NOC chain for for the given `fabricIndex`.
     * @retval other CHIP_ERROR value on internal errors
     */
    virtual CHIP_ERROR UpdateOpCertsForFabric(FabricIndex fabricIndex, const ByteSpan & noc, const ByteSpan & icac) = 0;

    /**
     * @brief Permanently commit the certificate chain last configured via successful calls to
     *        legal combinations of `AddNewTrustedRootCertForFabric`, `AddNewOpCertsForFabric` or
     *        `UpdateOpCertsForFabric`, replacing previously committed data, if any.
     *
     * This is to be used when CommissioningComplete is successfully received
     *
     * @param fabricIndex - FabricIndex for which to commit the certificate chain, used for security cross-checking
     *
     * @retval CHIP_NO_ERROR on success
     * @retval CHIP_ERROR_INCORRECT_STATE if the certificate store is not properly initialized,
     *                                    or if no valid pending state is available.
     * @retval CHIP_ERROR_INVALID_FABRIC_INDEX if there is no pending certificate chain for `fabricIndex`
     * @retval other CHIP_ERROR value on internal storage errors
     */
    virtual CHIP_ERROR CommitOpCertsForFabric(FabricIndex fabricIndex) = 0;

    /**
     * @brief Permanently remove the certificate chain associated with a fabric.
     *
     * This is to be used for RemoveFabric. Removes both the pending operational cert chain
     * elements for the fabricIndex (if any) and the committed ones (if any).
     *
     * @param fabricIndex - FabricIndex for which to remove the operational cert chain
     *
     * @retval CHIP_NO_ERROR on success
     * @retval CHIP_ERROR_INCORRECT_STATE if the certificate store is not properly initialized.
     * @retval CHIP_ERROR_INVALID_FABRIC_INDEX if there was no operational certificate data at all for `fabricIndex`
     * @retval other CHIP_ERROR value on internal storage errors
     */
    virtual CHIP_ERROR RemoveOpCertsForFabric(FabricIndex fabricIndex) = 0;

    /**
     * @brief Permanently release the operational certificate chain made via successful calls to
     *        legal combinations of `AddNewTrustedRootCertForFabric`, `AddNewOpCertsForFabric` or
     *        `UpdateOpCertsForFabric`, if any.
     *
     * This is to be used when a fail-safe expires prior to CommissioningComplete.
     *
     * This method cannot error-out and must always succeed, even on a no-op. This should
     * be safe to do given that `CommitOpCertsForFabric` must succeed to make an operation
     * certificate chain usable.
     */
    virtual void RevertPendingOpCerts() = 0;

    /**
     * @brief Same as RevertPendingOpCerts(), but leaves pending Trusted Root certs if they had
     *        been added. This is is an operation to support the complex error handling of
     *        AddNOC, where we don't want to have "sticking" ICAC/NOC after validation
     *        problems, but don't want to lose the RCAC given in an AddTrustedRootCertificate
     *        command.
     */
    virtual void RevertPendingOpCertsExceptRoot() = 0;

    /**
     * @brief Get the operational certificate element requested, giving the pending data or committed
     *        data depending on prior `AddNewTrustedRootCertForFabric`, `AddNewOpCertsForFabric` or
     *        `UpdateOpCertsForFabric` calls.
     *
     * On success, the `outCertificate` span is resized to the size of the actual certificate read-back.
     *
     * @param fabricIndex - fabricIndex for which to get the certificate
     * @param element - which element of the cerficate chain to get
     * @param outCertificate - buffer to contain the certificate obtained from persistent or temporary storage
     *
     * @retval CHIP_NO_ERROR on success.
     * @retval CHIP_ERROR_BUFFER_TOO_SMALL if `outCertificate` is too small to fit the certificate found.
     * @retval CHIP_ERROR_INCORRECT_STATE if the certificate store is not properly initialized.
     * @retval CHIP_ERROR_NOT_FOUND if the element cannot be found.
     * @retval CHIP_ERROR_INVALID_FABRIC_INDEX if the fabricIndex is invalid.
     * @retval other CHIP_ERROR value on internal storage errors.
     */
    virtual CHIP_ERROR GetCertificate(FabricIndex fabricIndex, CertChainElement element,
                                      MutableByteSpan & outCertificate) const = 0;
};

/**
 * @brief RAII class to operate on an OperationalCertificateStore with auto-revert if not committed.
 *
 * Use as:
 *
 * CHIP_ERROR FunctionWillReturnWithPendingReverted(....)
 * {
 *     OpCertStoreTransaction transaction(opCertStore);
 *
 *     ReturnErrorOnFailure(transaction->AddNewTrustedRootCertForFabric(...));
 *     ReturnErrorOnFailure(transaction->AddNewOpCertsForFabric(...));
 *     ReturnErrorOnFailure(transaction->CommitOpCertsForFabric(...));
 *
 *     return CHIP_NO_ERROR;
 * }
 */
class OpCertStoreTransaction
{
public:
    explicit OpCertStoreTransaction(OperationalCertificateStore & store) : mStore(store) {}
    ~OpCertStoreTransaction()
    {
        // This is a no-op if CommitOpCertsForFabric had been called on the store
        mStore.RevertPendingOpCerts();
    }

    // Non-copyable
    OpCertStoreTransaction(OpCertStoreTransaction const &) = delete;
    void operator=(OpCertStoreTransaction const &)         = delete;

    OperationalCertificateStore * operator->() { return &mStore; }

private:
    OperationalCertificateStore & mStore;
};

} // namespace Credentials
} // namespace chip
