/*
 *  Copyright (c) 2023, The OpenThread Authors.
 *  All rights reserved.
 *
 *  Redistribution and use in source and binary forms, with or without
 *  modification, are permitted provided that the following conditions are met:
 *  1. Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *  2. Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *  3. Neither the name of the copyright holder nor the
 *     names of its contributors may be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 *  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 *  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 *  POSSIBILITY OF SUCH DAMAGE.
 */

/**
 * @file
 *   This file implements the TCAT Agent service.
 */

#include "tcat_agent.hpp"

#if OPENTHREAD_CONFIG_BLE_TCAT_ENABLE

#include "common/code_utils.hpp"
#include "common/error.hpp"
#include "crypto/storage.hpp"
#include "instance/instance.hpp"
#include "thread/net_diag.hpp"

namespace ot {
namespace MeshCoP {

RegisterLogModule("TcatAgent");

bool TcatAgent::VendorInfo::IsValid(void) const
{
    // Note: mProvisioningUrl can be nullptr, if not present
    return mPskdString != nullptr && Tlv::ValidateStringValue<ProvisioningUrlTlv>(mProvisioningUrl) == kErrorNone;
}

TcatAgent::TcatAgent(Instance &aInstance)
    : InstanceLocator(aInstance)
    , mVendorInfo(nullptr)
    , mState(kStateDisabled)
    , mNextState(kStateDisabled)
    , mTimerSetsToActive(false)
    , mActiveOrStandbyTimer(aInstance)
    , mTcatActiveDurationMs(0)
    , mHashVerificationAttempts(1)
    , mLastDeviceRole(Mle::kRoleDisabled)
{
    ClearCommissionerState();
    mLastHashVerificationTimestamp = Get<UptimeTracker>().GetUptimeInSeconds();
}

void TcatAgent::ClearCommissionerState(void)
{
    mCommissionerAuthorizationField = {};
    mCommissionerExtendedPanId.Clear();
    mCommissionerHasNetworkName    = false;
    mCommissionerHasDomainName     = false;
    mCommissionerHasExtendedPanId  = false;
    mCommissionerNetworkName.m8[0] = kNullChar;
    mCommissionerDomainName.m8[0]  = kNullChar;
    mRandomChallenge               = 0;
    mPskdVerified                  = false;
    mPskcVerified                  = false;
    mInstallCodeVerified           = false;
    mIsCommissioned                = false;
    mApplicationResponsePending    = false;
}

Error TcatAgent::Start(AppDataReceiveCallback aAppDataReceiveCallback, JoinCallback aJoinHandler, void *aContext)
{
    Error error = kErrorNone;

    VerifyOrExit(!IsStarted(), error = kErrorAlready);
    VerifyOrExit(mVendorInfo != nullptr, error = kErrorFailed);

    mAppDataReceiveCallback.Set(aAppDataReceiveCallback, aContext);
    mJoinCallback.Set(aJoinHandler, aContext);
    mState                = kStateActive;
    mNextState            = kStateActive;
    mTcatActiveDurationMs = 0;
    mLastDeviceRole       = Get<Mle::Mle>().GetRole();

    LogInfo("Start");

    if (!mVendorInfo->mKeepActiveAfterJoining && mLastDeviceRole != Mle::kRoleDisabled &&
        Get<ActiveDatasetManager>().IsCommissioned())
    {
        IgnoreError(Standby());
    }

exit:
    LogWarnOnError(error, "Start");
    return error;
}

// Note: closing the TLS is handled by the transport class like #BleSecure
void TcatAgent::Stop(void)
{
    mAppDataReceiveCallback.Clear();
    mJoinCallback.Clear();
    mState = kStateDisabled;
    mActiveOrStandbyTimer.Stop();
    ClearCommissionerState();
    LogInfo("Stop");
}

Error TcatAgent::Standby(void)
{
    Error error = kErrorNone;

    VerifyOrExit(IsStarted(), error = kErrorInvalidState);

    mTcatActiveDurationMs = 0;
    mActiveOrStandbyTimer.Stop();
    mNextState = kStateStandby;
    if (!IsConnected() && mState != kStateStandby)
    {
        // if already TLS-connected, only move to 'standby' once the connection is done.
        // if not yet fully connected, go to 'standby' immediately (ignoring a TLS handshake that may be ongoing)
        mState = kStateStandby;
        NotifyStateChange();
        LogInfo("Standby");
    }

exit:
    LogWarnOnError(error, "Standby");
    return error;
}

Error TcatAgent::Activate(const uint32_t aDelayMs, const uint32_t aDurationMs)
{
    Error error = kErrorNone;

    VerifyOrExit(IsStarted(), error = kErrorInvalidState);
    VerifyOrExit(mState != kStateActive);

    mTcatActiveDurationMs = aDurationMs;
    mTimerSetsToActive    = true;
    if (aDelayMs > 0)
    {
        mActiveOrStandbyTimer.Start(aDelayMs);
    }
    else
    {
        mActiveOrStandbyTimer.Stop();
        HandleTimer();
    }

exit:
    LogWarnOnError(error, "Activate");
    return error;
}

Error TcatAgent::SetTcatVendorInfo(const VendorInfo &aVendorInfo)
{
    Error      error = kErrorNone;
    JoinerPskd pskd;

    VerifyOrExit(aVendorInfo.IsValid(), error = kErrorInvalidArgs);
    SuccessOrExit(error = pskd.SetFrom(aVendorInfo.mPskdString));
    mVendorInfo = &aVendorInfo;

exit:
    return error;
}

Error TcatAgent::Connected(MeshCoP::Tls::Extension &aTls)
{
    size_t len;
    Error  error;

    VerifyOrExit(IsStarted() && !IsConnected() && mState != kStateStandby && mState != kStateStandbyTemporary,
                 error = kErrorInvalidState);
    ClearCommissionerState();

    len = sizeof(mCommissionerAuthorizationField);
    SuccessOrExit(
        error = aTls.GetThreadAttributeFromPeerCertificate(
            kCertificateAuthorizationField, reinterpret_cast<uint8_t *>(&mCommissionerAuthorizationField), &len));
    VerifyOrExit(len == sizeof(mCommissionerAuthorizationField), error = kErrorParse);
    VerifyOrExit((mCommissionerAuthorizationField.mHeader & kCommissionerFlag) == 1, error = kErrorParse);

    len = sizeof(mDeviceAuthorizationField);
    SuccessOrExit(error = aTls.GetThreadAttributeFromOwnCertificate(
                      kCertificateAuthorizationField, reinterpret_cast<uint8_t *>(&mDeviceAuthorizationField), &len));
    VerifyOrExit(len == sizeof(mDeviceAuthorizationField), error = kErrorParse);
    VerifyOrExit((mDeviceAuthorizationField.mHeader & kCommissionerFlag) == 0, error = kErrorParse);

    len = sizeof(mCommissionerDomainName) - 1;
    if (aTls.GetThreadAttributeFromPeerCertificate(
            kCertificateDomainName, reinterpret_cast<uint8_t *>(&mCommissionerDomainName), &len) == kErrorNone)
    {
        mCommissionerDomainName.m8[len] = kNullChar;
        mCommissionerHasDomainName      = true;
    }

    len = sizeof(mCommissionerNetworkName) - 1;
    if (aTls.GetThreadAttributeFromPeerCertificate(
            kCertificateNetworkName, reinterpret_cast<uint8_t *>(&mCommissionerNetworkName), &len) == kErrorNone)
    {
        mCommissionerNetworkName.m8[len] = kNullChar;
        mCommissionerHasNetworkName      = true;
    }

    len = sizeof(mCommissionerExtendedPanId);
    if (aTls.GetThreadAttributeFromPeerCertificate(
            kCertificateExtendedPanId, reinterpret_cast<uint8_t *>(&mCommissionerExtendedPanId), &len) == kErrorNone)
    {
        if (len == sizeof(mCommissionerExtendedPanId))
        {
            mCommissionerHasExtendedPanId = true;
        }
    }

    // A temporary enablement stops after disconnect: to standby.
    mNextState = (mState == kStateActiveTemporary) ? kStateStandby : kStateActive;
    mState     = kStateConnected;
    NotifyStateChange();
    LogInfo("Connected");

    // This specifically stores the state IsCommissioned at _start_ of session:
    mIsCommissioned = Get<ActiveDatasetManager>().IsCommissioned();

exit:
    return error;
}

void TcatAgent::Disconnected(void)
{
    if (mState != kStateDisabled)
    {
        mState = mNextState;
        LogInfo("Disconnected");
        NotifyStateChange();
        ClearCommissionerState();
    }
}

uint8_t TcatAgent::CheckAuthorizationRequirements(CommandClassFlags aFlagsRequired, Dataset::Info *aDatasetInfo) const
{
    uint8_t res = kAccessFlag;

    for (uint16_t flag = kPskdFlag; flag < kMaxFlag; flag <<= 1)
    {
        if (aFlagsRequired & flag)
        {
            bool authorized = false;

            switch (flag)
            {
            case kPskdFlag:
                authorized = mPskdVerified;
                break;

            case kNetworkNameFlag:
                authorized = mCommissionerHasNetworkName && aDatasetInfo != nullptr &&
                             aDatasetInfo->IsPresent<Dataset::kNetworkName>() &&
                             aDatasetInfo->Get<Dataset::kNetworkName>() == mCommissionerNetworkName;
                break;

            case kExtendedPanIdFlag:
                authorized = mCommissionerHasExtendedPanId && aDatasetInfo != nullptr &&
                             aDatasetInfo->IsPresent<Dataset::kExtendedPanId>() &&
                             aDatasetInfo->Get<Dataset::kExtendedPanId>() == mCommissionerExtendedPanId;
                break;

            case kThreadDomainFlag:
                if (mCommissionerHasDomainName)
                {
#if (OPENTHREAD_CONFIG_THREAD_VERSION >= OT_THREAD_VERSION_1_4)
                    authorized = Get<MeshCoP::NetworkIdentity>().GetDomainName() == mCommissionerDomainName;
#else
                    authorized =
                        StringMatch(mCommissionerDomainName.GetAsCString(), NetworkIdentity::kDefaultDomainName);
#endif
                }
                break;

            case kPskcFlag:
                authorized = mPskcVerified;
                break;

            default:
                // A requirement for an unknown/future flag will always fail (see spec).
                break;
            }

            if (authorized)
            {
                res |= flag;
            }
        }
    }

    return res;
}

bool TcatAgent::IsCommandClassAuthorizedWithFlags(CommandClassFlags aCommissionerCommandClassFlags,
                                                  CommandClassFlags aDeviceCommandClassFlags,
                                                  const Dataset    *aCommSuppliedDataset) const
{
    bool           authorized = false;
    uint8_t        deviceRequirementMet;
    uint8_t        commissionerRequirementMet;
    Dataset::Info  datasetInfo;
    Dataset::Info *datasetInfoPtr = nullptr;

    VerifyOrExit(IsConnected());

    datasetInfo.Clear();
    if (aCommSuppliedDataset != nullptr)
    {
        aCommSuppliedDataset->ConvertTo(datasetInfo);
        datasetInfoPtr = &datasetInfo;
    }
    else if (Get<ActiveDatasetManager>().Read(datasetInfo) == kErrorNone)
    {
        datasetInfoPtr = &datasetInfo;
    }

    deviceRequirementMet       = CheckAuthorizationRequirements(aDeviceCommandClassFlags, datasetInfoPtr);
    commissionerRequirementMet = CheckAuthorizationRequirements(aCommissionerCommandClassFlags, datasetInfoPtr);

    if (aCommSuppliedDataset != nullptr)
    {
        // If Commissioner supplies a dataset (only for Set Active Operational Dataset TLV commands),
        // then the PSKc check is always considered successful. Table 13-9, bit 5.
        deviceRequirementMet |= kPskcFlag;
        commissionerRequirementMet |= (aCommissionerCommandClassFlags & kPskcFlag);
    }

    authorized = (commissionerRequirementMet == aCommissionerCommandClassFlags) &&
                 (deviceRequirementMet & aDeviceCommandClassFlags);

exit:
    return authorized;
}

bool TcatAgent::IsCommandClassAuthorized(CommandClass aCommandClass) const
{
    bool authorized = false;

    VerifyOrExit(IsConnected());

    switch (aCommandClass)
    {
    case kGeneral:
        authorized = true;
        break;

    case kCommissioning:
        authorized = IsCommandClassAuthorizedWithFlags(mCommissionerAuthorizationField.mCommissioningFlags,
                                                       mDeviceAuthorizationField.mCommissioningFlags, nullptr);
        break;

    case kExtraction:
        authorized = IsCommandClassAuthorizedWithFlags(mCommissionerAuthorizationField.mExtractionFlags,
                                                       mDeviceAuthorizationField.mExtractionFlags, nullptr);
        break;

    case kDecommissioning:
        authorized = IsCommandClassAuthorizedWithFlags(mCommissionerAuthorizationField.mDecommissioningFlags,
                                                       mDeviceAuthorizationField.mDecommissioningFlags, nullptr);
        break;

    case kApplication:
        authorized = IsCommandClassAuthorizedWithFlags(mCommissionerAuthorizationField.mApplicationFlags,
                                                       mDeviceAuthorizationField.mApplicationFlags, nullptr);
        break;

    default:
        break;
    }

exit:
    return authorized;
}

bool TcatAgent::IsSetActiveDatasetAuthorized(const Dataset *aDataset) const
{
    return !mIsCommissioned &&
           IsCommandClassAuthorizedWithFlags(mCommissionerAuthorizationField.mCommissioningFlags,
                                             mDeviceAuthorizationField.mCommissioningFlags, aDataset);
}

Error TcatAgent::HandleSingleTlv(const Message &aIncomingMessage, Message &aOutgoingMessage)
{
    Error          error;
    StatusCode     statusCode = kStatusGeneralError;
    Tlv::Info      tlvInfo;
    const uint16_t initialOutgoingMsgLength = aOutgoingMessage.GetLength();
    bool           response                 = false;

    VerifyOrExit(mVendorInfo != nullptr, error = kErrorInvalidState);
    VerifyOrExit(IsConnected(), error = kErrorInvalidState);

    SuccessOrExit(error = tlvInfo.ParseFrom(aIncomingMessage, aIncomingMessage.GetOffset()));

    if (mVendorInfo->mTlvSupportHandler != nullptr &&
        !mVendorInfo->mTlvSupportHandler(&GetInstance(), static_cast<otTcatCommandTlvType>(tlvInfo.GetType()),
                                         mJoinCallback.GetContext()))
    {
        statusCode = kStatusUnsupported;
        ExitNow();
    }

    switch (tlvInfo.GetType())
    {
    case kTlvDisconnect:
        error    = kErrorAbort;
        response = true; // true - to avoid response-with-status being sent.
        break;

    case kTlvSetActiveOperationalDataset:
        error = HandleSetActiveOperationalDataset(aIncomingMessage, tlvInfo.GetValueOffsetRange());
        break;

    case kTlvGetActiveOperationalDataset:
        error = HandleGetActiveOperationalDataset(aOutgoingMessage, response);
        break;

    case kTlvGetDiagnosticTlvs:
        error = HandleGetDiagnosticTlvs(aIncomingMessage, aOutgoingMessage, tlvInfo.GetValueOffsetRange(), response);
        break;

    case kTlvStartThreadInterface:
        error = HandleStartThreadInterface();
        break;

    case kTlvStopThreadInterface:
        error = HandleStopThreadInterface();
        break;

    case kTlvGetApplicationLayers:
        error = HandleGetApplicationLayers(aOutgoingMessage, response);
        break;

    case kTlvSendApplicationData1:
    case kTlvSendApplicationData2:
    case kTlvSendApplicationData3:
    case kTlvSendApplicationData4:
    case kTlvSendVendorSpecificData:
        error = HandleApplicationData(aIncomingMessage, tlvInfo.GetValueOffset(),
                                      static_cast<TcatApplicationProtocol>(tlvInfo.GetType()), response);
        break;

    case kTlvDecommission:
        error = HandleDecommission();
        break;

    case kTlvPing:
        error = HandlePing(aIncomingMessage, aOutgoingMessage, tlvInfo.GetValueOffsetRange(), response);
        break;

    case kTlvGetNetworkName:
        error = HandleGetNetworkName(aOutgoingMessage, response);
        break;

    case kTlvGetDeviceId:
        error = HandleGetDeviceId(aOutgoingMessage, response);
        break;

    case kTlvGetExtendedPanID:
        error = HandleGetExtPanId(aOutgoingMessage, response);
        break;

    case kTlvGetProvisioningURL:
        error = HandleGetProvisioningUrl(aOutgoingMessage, response);
        break;

    case kTlvPresentPskdHash:
        error = HandlePresentPskdHash(aIncomingMessage, tlvInfo.GetValueOffsetRange());
        break;

    case kTlvPresentPskcHash:
        error = HandlePresentPskcHash(aIncomingMessage, tlvInfo.GetValueOffsetRange());
        break;

    case kTlvPresentInstallCodeHash:
        error = HandlePresentInstallCodeHash(aIncomingMessage, tlvInfo.GetValueOffsetRange());
        break;

    case kTlvRequestRandomNumChallenge:
        error = HandleRequestRandomNumberChallenge(aOutgoingMessage, response);
        break;

    case kTlvGetCommissionerCertificate:
        error = HandleGetCommissionerCertificate(aOutgoingMessage, response);
        break;

    default:
        error = kErrorInvalidCommand;
    }

    if (!response)
    {
        switch (error)
        {
        case kErrorNone:
            statusCode = kStatusSuccess;
            break;

        case kErrorNotImplemented:
        case kErrorInvalidCommand:
            statusCode = kStatusUnsupported;
            break;

        case kErrorParse:
            statusCode = kStatusParseError;
            break;

        case kErrorInvalidArgs:
            statusCode = kStatusValueError;
            break;

        case kErrorBusy:
            statusCode = kStatusBusy;
            break;

        case kErrorNotFound:
            statusCode = kStatusUndefined;
            break;

        case kErrorSecurity:
            statusCode = kStatusHashError;
            break;

        case kErrorInvalidState:
        case kErrorAlready:
            statusCode = kStatusInvalidState;
            break;

        case kErrorRejected:
            statusCode = kStatusUnauthorized;
            break;

        default:
            // remains kStatusGeneralError
            break;
        }
        error = kErrorNone; // reset the error - as it's now converted to statusCode
    }

exit:
    if (!response && error == kErrorNone) // skip initial-check error cases, where no TLV response must be sent.
    {
        // reset any partial TLV content that may have been appended already by failed Handle...() methods.
        IgnoreError(aOutgoingMessage.SetLength(initialOutgoingMsgLength));
        // Append a single Response with Status TLV to the response message; and ensure to only
        // return error != kErrorNone if there was an issue appending this TLV.
        error = ot::Tlv::Append<ResponseWithStatusTlv>(aOutgoingMessage, statusCode);
    }
    return error;
}

Error TcatAgent::HandleSetActiveOperationalDataset(const Message &aIncomingMessage, const OffsetRange &aOffsetRange)
{
    Dataset dataset;
    Error   error;
    uint8_t buf[kCommissionerCertMaxLength];
    size_t  bufLen = sizeof(buf);

    VerifyOrExit(!mIsCommissioned, error = kErrorAlready);

    SuccessOrExit(error = dataset.SetFrom(aIncomingMessage, aOffsetRange));
    SuccessOrExit(error = dataset.ValidateTlvs());
    VerifyOrExit(dataset.ContainsTlv(Tlv::kNetworkKey), error = kErrorInvalidArgs);

    VerifyOrExit(IsSetActiveDatasetAuthorized(&dataset), error = kErrorRejected);

    SuccessOrExit(error = Get<Ble::BleSecure>().GetPeerCertificateDer(buf, &bufLen, bufLen));
    Get<Settings>().SaveTcatCommissionerCertificate(buf, static_cast<uint16_t>(bufLen));

    Get<ActiveDatasetManager>().SaveLocal(dataset);

exit:
    return error;
}

Error TcatAgent::HandleGetActiveOperationalDataset(Message &aOutgoingMessage, bool &aResponse)
{
    Error         error = kErrorNone;
    Dataset::Tlvs datasetTlvs;

    VerifyOrExit(IsCommandClassAuthorized(kExtraction), error = kErrorRejected);
    SuccessOrExit(error = Get<ActiveDatasetManager>().Read(datasetTlvs));
    SuccessOrExit(
        error = Tlv::AppendTlv(aOutgoingMessage, kTlvResponseWithPayload, datasetTlvs.mTlvs, datasetTlvs.mLength));
    aResponse = true;

exit:
    return error;
}

Error TcatAgent::HandleGetCommissionerCertificate(Message &aOutgoingMessage, bool &aResponse)
{
    Error         error = kErrorNone;
    unsigned char buf[kCommissionerCertMaxLength];
    uint16_t      bufLen = sizeof(buf);

    VerifyOrExit(IsCommandClassAuthorized(kCommissioning), error = kErrorRejected);
    VerifyOrExit(kErrorNone == Get<Settings>().ReadTcatCommissionerCertificate(buf, bufLen), error = kErrorNotFound);
    SuccessOrExit(error = Tlv::AppendTlv(aOutgoingMessage, kTlvResponseWithPayload, buf, bufLen));
    aResponse = true;

exit:
    return error;
}

Error TcatAgent::HandleGetDiagnosticTlvs(const Message     &aIncomingMessage,
                                         Message           &aOutgoingMessage,
                                         const OffsetRange &aOffsetRange,
                                         bool              &aResponse)
{
    Error           error = kErrorNone;
    ot::ExtendedTlv extTlv;
    uint16_t        initialLength;
    uint16_t        length;

    VerifyOrExit(IsCommandClassAuthorized(kCommissioning), error = kErrorRejected);

    initialLength = aOutgoingMessage.GetLength();

    // Start with extTlv to avoid the need for a temporary message buffer to calculate reply length
    extTlv.SetType(kTlvResponseWithPayload);
    extTlv.SetLength(0);
    SuccessOrExit(error = aOutgoingMessage.Append(extTlv));

    error = Get<NetDiag::Server>().AppendRequestedTlvsForTcat(aIncomingMessage, aOutgoingMessage, aOffsetRange);

    // Ensure enough message buffers are left for transmission of the result. Report error otherwise.
    if (Get<MessagePool>().GetFreeBufferCount() < kBufferReserve)
    {
        error = kErrorNoBufs;
    }

    if (error != kErrorNone)
    {
        IgnoreError(aOutgoingMessage.SetLength(initialLength));
        ExitNow();
    }

    length = aOutgoingMessage.GetLength() - initialLength - sizeof(extTlv);

    if (length > 0)
    {
        extTlv.SetLength(length);
        aOutgoingMessage.WriteBytes(initialLength, &extTlv, sizeof(extTlv));
        aResponse = true;
    }
    else
    {
        IgnoreError(aOutgoingMessage.SetLength(initialLength));
    }

exit:
    return error;
}

Error TcatAgent::HandleDecommission(void)
{
    Error         error = kErrorNone;
    unsigned char buf[kCommissionerCertMaxLength];
    size_t        bufLen = sizeof(buf);

    VerifyOrExit(IsCommandClassAuthorized(kDecommissioning), error = kErrorRejected);
    SuccessOrExit(error = Get<Ble::BleSecure>().GetPeerCertificateDer(buf, &bufLen, bufLen));

    Get<Mle::Mle>().Stop();

    if (!mVendorInfo->mDoNotActivateAfterLeaving)
    {
        IgnoreError(Activate(0, 0));
    }

    Get<ActiveDatasetManager>().Clear();
    Get<PendingDatasetManager>().Clear();

    IgnoreReturnValue(Get<Instance>().ErasePersistentInfo());
    Get<Settings>().SaveTcatCommissionerCertificate(buf, static_cast<uint16_t>(bufLen));

#if !OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE
    {
        NetworkKey networkKey;
        networkKey.Clear();
        Get<KeyManager>().SetNetworkKey(networkKey);
    }
#endif

    mJoinCallback.InvokeIfSet(&GetInstance(), /* aIsJoin */ false, error);
    mIsCommissioned = false; // enable repeated commissioning/decommissioning in a session

exit:
    return error;
}

Error TcatAgent::HandlePing(const Message     &aIncomingMessage,
                            Message           &aOutgoingMessage,
                            const OffsetRange &aOffsetRange,
                            bool              &aResponse)
{
    Error error;

    VerifyOrExit(aOffsetRange.GetLength() <= kPingPayloadMaxLength, error = kErrorParse);

    SuccessOrExit(error = Tlv::AppendTlvWithValueFromMessage(aOutgoingMessage, kTlvResponseWithPayload,
                                                             aIncomingMessage, aOffsetRange));
    aResponse = true;

exit:
    return error;
}

Error TcatAgent::HandleGetNetworkName(Message &aOutgoingMessage, bool &aResponse)
{
    Error             error    = kErrorNone;
    MeshCoP::NameData nameData = Get<MeshCoP::NetworkIdentity>().GetNetworkName().GetAsData();

    VerifyOrExit(Get<ActiveDatasetManager>().IsCommissioned(), error = kErrorNotFound);
#if !OPENTHREAD_CONFIG_ALLOW_EMPTY_NETWORK_NAME
    VerifyOrExit(nameData.GetLength() > 0, error = kErrorNotFound);
#endif

    SuccessOrExit(
        error = Tlv::AppendTlv(aOutgoingMessage, kTlvResponseWithPayload, nameData.GetBuffer(), nameData.GetLength()));
    aResponse = true;

exit:
    return error;
}

Error TcatAgent::HandleGetDeviceId(Message &aOutgoingMessage, bool &aResponse)
{
    const uint8_t  *deviceId;
    uint16_t        length = 0;
    Mac::ExtAddress eui64;
    Error           error = kErrorNone;

    VerifyOrExit(mVendorInfo != nullptr, error = kErrorInvalidState);

    if (mVendorInfo->mGeneralDeviceId != nullptr)
    {
        length   = mVendorInfo->mGeneralDeviceId->mDeviceIdLen;
        deviceId = mVendorInfo->mGeneralDeviceId->mDeviceId;
    }

    if (length == 0)
    {
        Get<Radio>().GetIeeeEui64(eui64);

        length   = sizeof(Mac::ExtAddress);
        deviceId = eui64.m8;
    }

    SuccessOrExit(error = Tlv::AppendTlv(aOutgoingMessage, kTlvResponseWithPayload, deviceId, length));

    aResponse = true;

exit:
    return error;
}

Error TcatAgent::HandleGetExtPanId(Message &aOutgoingMessage, bool &aResponse)
{
    Error error;

    VerifyOrExit(Get<ActiveDatasetManager>().IsCommissioned(), error = kErrorNotFound);

    SuccessOrExit(error = Tlv::AppendTlv(aOutgoingMessage, kTlvResponseWithPayload,
                                         &Get<MeshCoP::NetworkIdentity>().GetExtPanId(), sizeof(ExtendedPanId)));
    aResponse = true;

exit:
    return error;
}

Error TcatAgent::HandleGetProvisioningUrl(Message &aOutgoingMessage, bool &aResponse)
{
    Error    error = kErrorNone;
    uint16_t length;

    VerifyOrExit(mVendorInfo != nullptr, error = kErrorInvalidState);
    VerifyOrExit(mVendorInfo->mProvisioningUrl != nullptr, error = kErrorNotImplemented);

    length = StringLength(mVendorInfo->mProvisioningUrl, kProvisioningUrlMaxLength);
    VerifyOrExit(length > 0, error = kErrorNotImplemented);

    SuccessOrExit(error =
                      Tlv::AppendTlv(aOutgoingMessage, kTlvResponseWithPayload, mVendorInfo->mProvisioningUrl, length));
    aResponse = true;

exit:
    return error;
}

Error TcatAgent::HandlePresentPskdHash(const Message &aIncomingMessage, const OffsetRange &aOffsetRange)
{
    Error error = kErrorNone;

    VerifyOrExit(mVendorInfo != nullptr, error = kErrorInvalidState);
    VerifyOrExit(mVendorInfo->mPskdString != nullptr, error = kErrorSecurity);

    SuccessOrExit(error = VerifyHash(aIncomingMessage, aOffsetRange, mVendorInfo->mPskdString,
                                     StringLength(mVendorInfo->mPskdString, kMaxPskdLength)));
    mPskdVerified = true;

exit:
    return error;
}

Error TcatAgent::HandlePresentPskcHash(const Message &aIncomingMessage, const OffsetRange &aOffsetRange)
{
    Error         error = kErrorNone;
    Dataset::Info datasetInfo;
    Pskc          pskc;

    VerifyOrExit(Get<ActiveDatasetManager>().Read(datasetInfo) == kErrorNone, error = kErrorSecurity);
    VerifyOrExit(datasetInfo.IsPresent<Dataset::kPskc>(), error = kErrorSecurity);
    pskc = datasetInfo.Get<Dataset::kPskc>();

    SuccessOrExit(error = VerifyHash(aIncomingMessage, aOffsetRange, pskc.m8, Pskc::kSize));
    mPskcVerified = true;

exit:
    return error;
}

Error TcatAgent::HandlePresentInstallCodeHash(const Message &aIncomingMessage, const OffsetRange &aOffsetRange)
{
    Error error = kErrorNone;

    VerifyOrExit(mVendorInfo != nullptr, error = kErrorInvalidState);
    VerifyOrExit(mVendorInfo->mInstallCode != nullptr, error = kErrorSecurity);

    SuccessOrExit(error = VerifyHash(aIncomingMessage, aOffsetRange, mVendorInfo->mInstallCode,
                                     StringLength(mVendorInfo->mInstallCode, kInstallCodeMaxSize)));
    mInstallCodeVerified = true;

exit:
    return error;
}

Error TcatAgent::HandleRequestRandomNumberChallenge(Message &aOutgoingMessage, bool &aResponse)
{
    Error error = kErrorNone;

    SuccessOrExit(error = Random::Crypto::Fill(mRandomChallenge));

    SuccessOrExit(
        error = Tlv::AppendTlv(aOutgoingMessage, kTlvResponseWithPayload, &mRandomChallenge, sizeof(mRandomChallenge)));
    aResponse = true;

exit:
    return error;
}

Error TcatAgent::VerifyHash(const Message     &aIncomingMessage,
                            const OffsetRange &aOffsetRange,
                            const void        *aBuf,
                            size_t             aBufLen)
{
    Error                    error = kErrorNone;
    Crypto::HmacSha256::Hash hash;
    UptimeSec                currentTime = Get<UptimeTracker>().GetUptimeInSeconds();
    uint32_t newAttempts   = (currentTime - mLastHashVerificationTimestamp) / kHashVerificationAttemptTime;
    uint32_t totalAttempts = newAttempts + mHashVerificationAttempts;

    VerifyOrExit(aOffsetRange.GetLength() == Crypto::HmacSha256::Hash::kSize, error = kErrorSecurity);
    VerifyOrExit(mRandomChallenge != 0, error = kErrorSecurity);

    // In case uptime has overflowed (will never happen in practical functional operational life), up to
    // kHashVerificationMaxAttempts additional attempts can be tolerated.
    mHashVerificationAttempts = static_cast<uint8_t>(Min<uint32_t>(totalAttempts, kHashVerificationMaxAttempts));

    VerifyOrExit(mHashVerificationAttempts > 0, error = kErrorBusy);
    mLastHashVerificationTimestamp = currentTime;
    mHashVerificationAttempts--;

    SuccessOrExit(error = CalculateHash(mRandomChallenge, reinterpret_cast<const char *>(aBuf), aBufLen, hash));
    DumpDebg("Hash", &hash, sizeof(hash));

    VerifyOrExit(aIncomingMessage.Compare(aOffsetRange.GetOffset(), hash), error = kErrorSecurity);
    mHashVerificationAttempts++;

exit:
    return error;
}

Error TcatAgent::CalculateHash(uint64_t aChallenge, const char *aBuf, size_t aBufLen, Crypto::HmacSha256::Hash &aHash)
{
    const mbedtls_asn1_buf &rawKey = Get<Ble::BleSecure>().GetOwnPublicKey();
    Crypto::Key             cryptoKey;
    Crypto::HmacSha256      hmac;
    Error                   error = kErrorNone;

#if OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE
    Crypto::Storage::KeyRef keyRef;
    SuccessOrExit(error = Crypto::Storage::ImportKey(keyRef, Crypto::Storage::kKeyTypeHmac,
                                                     Crypto::Storage::kKeyAlgorithmHmacSha256,
                                                     Crypto::Storage::kUsageSignHash, Crypto::Storage::kTypeVolatile,
                                                     reinterpret_cast<const uint8_t *>(aBuf), aBufLen));
    cryptoKey.SetAsKeyRef(keyRef);
#else
    cryptoKey.Set(reinterpret_cast<const uint8_t *>(aBuf), static_cast<uint16_t>(aBufLen));
#endif

    hmac.Start(cryptoKey);
    hmac.Update(aChallenge);
    hmac.Update(rawKey.p, static_cast<uint16_t>(rawKey.len));
    hmac.Finish(aHash);

#if OPENTHREAD_CONFIG_PLATFORM_KEY_REFERENCES_ENABLE
    Crypto::Storage::DestroyKey(keyRef);
exit:
#endif
    return error;
}

Error TcatAgent::HandleGetApplicationLayers(Message &aOutgoingMessage, bool &aResponse)
{
    Error         error = kErrorNone;
    Tlv::Bookmark tlvBookmark;

    static_assert((kApplicationLayerMaxCount * (kServiceNameMaxLength + 2)) <= 250,
                  "Unsupported TCAT application layers configuration");

    VerifyOrExit(mVendorInfo != nullptr, error = kErrorInvalidState);
    VerifyOrExit(IsCommandClassAuthorized(kApplication), error = kErrorRejected);

    SuccessOrExit(error = Tlv::StartTlv(aOutgoingMessage, kTlvResponseWithPayload, tlvBookmark));

    for (uint8_t i = 0; i < kApplicationLayerMaxCount && mVendorInfo->mApplicationServiceName[i] != nullptr; i++)
    {
        uint16_t length = StringLength(mVendorInfo->mApplicationServiceName[i], kServiceNameMaxLength);
        uint8_t  type   = mVendorInfo->mApplicationServiceIsTcp[i] ? kTlvServiceNameTcp : kTlvServiceNameUdp;

        SuccessOrExit(error = Tlv::AppendTlv(aOutgoingMessage, type, mVendorInfo->mApplicationServiceName[i], length));
    }

    SuccessOrExit(error = Tlv::EndTlv(aOutgoingMessage, tlvBookmark));

    aResponse = true;

exit:
    return error;
}

Error TcatAgent::HandleApplicationData(const Message          &aIncomingMessage,
                                       uint16_t                aOffset,
                                       TcatApplicationProtocol aApplicationProtocol,
                                       bool                   &aResponse)
{
    Error error = kErrorNone;

    VerifyOrExit(IsCommandClassAuthorized(kApplication), error = kErrorRejected);

    mApplicationResponsePending = true;
    mAppDataReceiveCallback.InvokeIfSet(&GetInstance(), &aIncomingMessage, aOffset,
                                        static_cast<otTcatApplicationProtocol>(aApplicationProtocol));

    if (mApplicationResponsePending)
    {
        mApplicationResponsePending = false;
        error                       = kErrorNotImplemented; // Application unsupported
    }
    else
    {
        aResponse = true;
    }

exit:
    return error;
}

Error TcatAgent::HandleStartThreadInterface(void)
{
    Error         error;
    Dataset::Info datasetInfo;

    VerifyOrExit(IsCommandClassAuthorized(kCommissioning), error = kErrorRejected);
    VerifyOrExit(Get<ActiveDatasetManager>().Read(datasetInfo) == kErrorNone, error = kErrorInvalidState);
    VerifyOrExit(datasetInfo.IsPresent<Dataset::kNetworkKey>(), error = kErrorInvalidState);

#if OPENTHREAD_CONFIG_LINK_RAW_ENABLE
    VerifyOrExit(!Get<Mac::LinkRaw>().IsEnabled(), error = kErrorInvalidState);
#endif

    Get<ThreadNetif>().Up();
    SuccessOrExit(error = Get<Mle::Mle>().Start());

exit:
    // error values for callback MUST be limited to the allowed set, see #JoinCallback
    mJoinCallback.InvokeIfSet(&GetInstance(), /* aIsJoin */ true, error);
    return error;
}

Error TcatAgent::HandleStopThreadInterface(void)
{
    Error error = kErrorNone;

    VerifyOrExit(IsCommandClassAuthorized(kCommissioning), error = kErrorRejected);

    Get<Mle::Mle>().Stop();

    if (!mVendorInfo->mDoNotActivateAfterLeaving)
    {
        IgnoreError(Activate(0, 0));
    }

exit:
    mJoinCallback.InvokeIfSet(&GetInstance(), /* aIsJoin */ false, error);
    return error;
}

// called when TCAT active-or-standby timer expires
void TcatAgent::HandleTimer(void)
{
    switch (mState)
    {
    case kStateStandby:
    case kStateStandbyTemporary:
    case kStateActiveTemporary:
        if (mTimerSetsToActive)
        {
            mTimerSetsToActive = false;
            if (mTcatActiveDurationMs > 0)
            {
                mState = kStateActiveTemporary;
                mActiveOrStandbyTimer.Start(mTcatActiveDurationMs);
            }
            else
            {
                mState = kStateActive;
            }
            NotifyStateChange();
            LogInfo("Active");
        }
        else
        {
            IgnoreError(Standby());
        }
        break;

    // kStateActive: will not go to standby, based on timer. Application has forced it to 'active'.
    // kStateConnected: no change here, mNextState already set.
    default:
        break;
    }
}

// internally called when TcatAgent state changes: perform any required actions.
void TcatAgent::NotifyStateChange(void)
{
    Get<Ble::BleSecure>().NotifySendAdvertisements(mState == kStateActive || mState == kStateActiveTemporary ||
                                                   mState == kStateConnected);
}

void TcatAgent::HandleNotifierEvents(Events aEvents)
{
    VerifyOrExit(IsStarted());
    VerifyOrExit(mVendorInfo != nullptr);

    if (aEvents.ContainsAny(kEventThreadRoleChanged))
    {
        if (!mVendorInfo->mKeepActiveAfterJoining && Get<Mle::Mle>().IsAttached() &&
            (mLastDeviceRole == Mle::kRoleDisabled || mLastDeviceRole == Mle::kRoleDetached))
        {
            IgnoreError(Standby());
        }

        mLastDeviceRole = Get<Mle::Mle>().GetRole();
    }

exit:
    return;
}

template <> void TcatAgent::HandleTmf<kUriTcatEnable>(Coap::Msg &aMsg)
{
    Error          error        = kErrorNone;
    Coap::Message *message      = nullptr;
    uint32_t       delayTimerMs = 0;
    uint16_t       durationSec  = 0;
    uint32_t       durationMs;

    VerifyOrExit(aMsg.IsConfirmable());
    LogInfo("Received %s from %s", UriToString<kUriTcatEnable>(),
            aMsg.mMessageInfo.GetPeerAddr().ToString().AsCString());
    message = Get<Tmf::Agent>().AllocateAndInitResponseFor(aMsg.mMessage);
    VerifyOrExit(message != nullptr, error = kErrorNoBufs);

    SuccessOrExit(error = Tlv::Find<DelayTimerTlv>(aMsg.mMessage, delayTimerMs));
    switch (Tlv::Find<DurationTlv>(aMsg.mMessage, durationSec))
    {
    case kErrorNone:
        break;
    case kErrorNotFound:
        durationSec = kTcatTmfEnableDefaultSec; // If Duration TLV absent: use default duration
        break;
    default:
        ExitNow(error = kErrorParse);
    }
    durationMs = Time::SecToMsec(durationSec);

    // if an existing activation is ongoing, adapt the requested one to be compatible.
    AdaptToExistingActivePeriod(delayTimerMs, durationMs);

    error = Activate(delayTimerMs, durationMs);

exit:
    if (message != nullptr)
    {
        error =
            Tlv::Append<StateTlv>(*message, error == kErrorNone ? StateTlv::State::kAccept : StateTlv::State::kReject);
        if (error == kErrorNone)
        {
            error = Get<Tmf::Agent>().SendMessage(*message, aMsg.mMessageInfo);
        }
        FreeMessageOnError(message, error);
    }
    LogWarnOnError(error, "send TCAT_ENABLE.rsp");
}

// Adapts delay/duration parameters of the given TCAT temporary activation period to the
// already-ongoing temporary TCAT activation (if any). The goals of the adaptation are:
// - not shorten the duration of an existing (scheduled/ongoing) activation
// - not set the start of activation later than the existing scheduled activation time
void TcatAgent::AdaptToExistingActivePeriod(uint32_t &aPeriodDelayMs, uint32_t &aPeriodDurationMs)
{
    VerifyOrExit(mState != kStateActive);

    if (mActiveOrStandbyTimer.IsRunning())
    {
        TimeMilli now = TimerMilli::GetNow();
        uint32_t  remainingMs;

        remainingMs = mActiveOrStandbyTimer.GetFireTime().DetermineRemainingDurationFrom(now);

        if (mTimerSetsToActive)
        {
            aPeriodDelayMs    = Min(aPeriodDelayMs, remainingMs);
            aPeriodDurationMs = Max(aPeriodDurationMs, remainingMs + mTcatActiveDurationMs - aPeriodDelayMs);
        }
        else
        {
            aPeriodDelayMs    = 0;
            aPeriodDurationMs = Max(aPeriodDurationMs, remainingMs);
        }
    }

exit:
    return;
}

void SerializeTcatAdvertisementTlv(uint8_t                 *aBuffer,
                                   uint16_t                &aOffset,
                                   TcatAdvertisementTlvType aType,
                                   uint16_t                 aLength,
                                   const uint8_t           *aValue)
{
    aBuffer[aOffset++] = static_cast<uint8_t>(aType << 4 | (aLength & 0xf));
    memcpy(aBuffer + aOffset, aValue, aLength);
    aOffset += aLength;
}

Error TcatAgent::GetAdvertisementData(uint16_t &aLen, uint8_t *aAdvertisementData) const
{
    Error                 error = kErrorNone;
    DeviceTypeAndStatus   tas;
    otBleLinkCapabilities caps;

    VerifyOrExit(mVendorInfo != nullptr && aAdvertisementData != nullptr, error = kErrorInvalidArgs);

    aLen = 0;

    LittleEndian::WriteUint16(OT_TOBLE_SERVICE_UUID, aAdvertisementData);
    aLen += sizeof(uint16_t);
    aAdvertisementData[2] = OPENTHREAD_CONFIG_THREAD_VERSION << 4 | OT_TCAT_OPCODE;
    aLen++;

    if (mVendorInfo->mAdvertisedDeviceIds != nullptr)
    {
        for (uint8_t i = 0; mVendorInfo->mAdvertisedDeviceIds[i].mDeviceIdType != OT_TCAT_DEVICE_ID_EMPTY; i++)
        {
            switch (MapEnum(mVendorInfo->mAdvertisedDeviceIds[i].mDeviceIdType))
            {
            case kTcatDeviceIdOui24:
                SerializeTcatAdvertisementTlv(aAdvertisementData, aLen, kTlvVendorOui24,
                                              mVendorInfo->mAdvertisedDeviceIds[i].mDeviceIdLen,
                                              mVendorInfo->mAdvertisedDeviceIds[i].mDeviceId);
                break;
            case kTcatDeviceIdOui36:
                SerializeTcatAdvertisementTlv(aAdvertisementData, aLen, kTlvVendorOui36,
                                              mVendorInfo->mAdvertisedDeviceIds[i].mDeviceIdLen,
                                              mVendorInfo->mAdvertisedDeviceIds[i].mDeviceId);
                break;
            case kTcatDeviceIdDiscriminator:
                SerializeTcatAdvertisementTlv(aAdvertisementData, aLen, kTlvDeviceDiscriminator,
                                              mVendorInfo->mAdvertisedDeviceIds[i].mDeviceIdLen,
                                              mVendorInfo->mAdvertisedDeviceIds[i].mDeviceId);
                break;
            case kTcatDeviceIdIanaPen:
                SerializeTcatAdvertisementTlv(aAdvertisementData, aLen, kTlvVendorIanaPen,
                                              mVendorInfo->mAdvertisedDeviceIds[i].mDeviceIdLen,
                                              mVendorInfo->mAdvertisedDeviceIds[i].mDeviceId);
                break;
            default:
                break;
            }
        }
    }

    otPlatBleGetLinkCapabilities(&GetInstance(), &caps);

    if (caps.mGattNotifications || caps.mL2CapDirect)
    {
        SerializeTcatAdvertisementTlv(aAdvertisementData, aLen, kTlvBleLinkCapabilities, kTlvBleLinkCapabilitiesLength,
                                      reinterpret_cast<uint8_t *>(&caps));
    }

    tas.mRsv               = 0;
    tas.mMultiRadioSupport = otPlatBleSupportsMultiRadio(&GetInstance());
    tas.mStoresActiveOperationalDataset =
        Get<ActiveDatasetManager>().IsPartiallyComplete() || Get<ActiveDatasetManager>().IsCommissioned();
    tas.mIsCommissioned      = Get<ActiveDatasetManager>().IsCommissioned();
    tas.mThreadNetworkActive = Get<Mle::Mle>().IsAttached();
    tas.mDeviceType          = Get<Mle::Mle>().GetDeviceMode().IsFullThreadDevice();
    tas.mRxOnWhenIdle        = Get<Mle::Mle>().GetDeviceMode().IsRxOnWhenIdle();

#if OPENTHREAD_FTD && (OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE || OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE || \
                       OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE)
    tas.mIsBorderRouter = true;
#else
    tas.mIsBorderRouter = false;
#endif

    SerializeTcatAdvertisementTlv(aAdvertisementData, aLen, kTlvDeviceTypeAndStatus, kTlvDeviceTypeAndStatusLength,
                                  reinterpret_cast<uint8_t *>(&tas));
    OT_ASSERT(aLen <= OT_TCAT_ADVERTISEMENT_MAX_LEN);

exit:
    return error;
}

} // namespace MeshCoP
} // namespace ot

#endif // OPENTHREAD_CONFIG_BLE_TCAT_ENABLE
