/*
 *  Copyright (c) 2020, 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 includes definitions for Thread Link Metrics.
 */

#include "link_metrics.hpp"

#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE || OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE

#include "common/code_utils.hpp"
#include "common/encoding.hpp"
#include "common/instance.hpp"
#include "common/locator_getters.hpp"
#include "common/log.hpp"
#include "mac/mac.hpp"
#include "thread/link_metrics_tlvs.hpp"
#include "thread/neighbor_table.hpp"

namespace ot {
namespace LinkMetrics {

RegisterLogModule("LinkMetrics");

using ot::Encoding::BigEndian::HostSwap32;

void SeriesInfo::Init(uint8_t aSeriesId, const SeriesFlags &aSeriesFlags, const Metrics &aMetrics)
{
    mSeriesId    = aSeriesId;
    mSeriesFlags = aSeriesFlags;
    mMetrics     = aMetrics;
    mRssAverager.Clear();
    mLqiAverager.Clear();
    mPduCount = 0;
}

void SeriesInfo::AggregateLinkMetrics(uint8_t aFrameType, uint8_t aLqi, int8_t aRss)
{
    if (IsFrameTypeMatch(aFrameType))
    {
        mPduCount++;
        mLqiAverager.Add(aLqi);
        IgnoreError(mRssAverager.Add(aRss));
    }
}

bool SeriesInfo::IsFrameTypeMatch(uint8_t aFrameType) const
{
    bool match = false;

    switch (aFrameType)
    {
    case kSeriesTypeLinkProbe:
        VerifyOrExit(!mSeriesFlags.IsMacDataFlagSet()); // Ignore this when Mac Data is accounted
        match = mSeriesFlags.IsLinkProbeFlagSet();
        break;
    case Mac::Frame::kFcfFrameData:
        match = mSeriesFlags.IsMacDataFlagSet();
        break;
    case Mac::Frame::kFcfFrameMacCmd:
        match = mSeriesFlags.IsMacDataRequestFlagSet();
        break;
    case Mac::Frame::kFcfFrameAck:
        match = mSeriesFlags.IsMacAckFlagSet();
        break;
    default:
        break;
    }

exit:
    return match;
}

LinkMetrics::LinkMetrics(Instance &aInstance)
    : InstanceLocator(aInstance)
    , mReportCallback(nullptr)
    , mReportCallbackContext(nullptr)
    , mMgmtResponseCallback(nullptr)
    , mMgmtResponseCallbackContext(nullptr)
    , mEnhAckProbingIeReportCallback(nullptr)
    , mEnhAckProbingIeReportCallbackContext(nullptr)
{
}

Error LinkMetrics::Query(const Ip6::Address &aDestination, uint8_t aSeriesId, const Metrics *aMetrics)
{
    Error       error;
    TypeIdFlags typeIdFlags[kMaxTypeIdFlags];
    uint8_t     typeIdFlagsCount = 0;
    Neighbor *  neighbor         = GetNeighborFromLinkLocalAddr(aDestination);

    VerifyOrExit(neighbor != nullptr, error = kErrorUnknownNeighbor);
    VerifyOrExit(neighbor->IsThreadVersion1p2OrHigher(), error = kErrorNotCapable);

    if (aMetrics != nullptr)
    {
        typeIdFlagsCount = TypeIdFlagsFromMetrics(typeIdFlags, *aMetrics);
    }

    if (aSeriesId != 0)
    {
        VerifyOrExit(typeIdFlagsCount == 0, error = kErrorInvalidArgs);
    }

    error = SendLinkMetricsQuery(aDestination, aSeriesId, typeIdFlags, typeIdFlagsCount);

exit:
    return error;
}

#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
Error LinkMetrics::SendMgmtRequestForwardTrackingSeries(const Ip6::Address &     aDestination,
                                                        uint8_t                  aSeriesId,
                                                        const SeriesFlags::Info &aSeriesFlags,
                                                        const Metrics *          aMetrics)
{
    Error        error = kErrorNone;
    uint8_t      subTlvs[sizeof(Tlv) + sizeof(uint8_t) * 2 + sizeof(TypeIdFlags) * kMaxTypeIdFlags];
    Tlv *        fwdProbingSubTlv  = reinterpret_cast<Tlv *>(subTlvs);
    SeriesFlags *seriesFlags       = reinterpret_cast<SeriesFlags *>(subTlvs + sizeof(Tlv) + sizeof(aSeriesId));
    uint8_t      typeIdFlagsOffset = sizeof(Tlv) + sizeof(uint8_t) * 2;
    uint8_t      typeIdFlagsCount  = 0;
    Neighbor *   neighbor          = GetNeighborFromLinkLocalAddr(aDestination);

    VerifyOrExit(neighbor != nullptr, error = kErrorUnknownNeighbor);
    VerifyOrExit(neighbor->IsThreadVersion1p2OrHigher(), error = kErrorNotCapable);

    // Directly transform `aMetrics` into TypeIdFlags and put them into `subTlvs`
    if (aMetrics != nullptr)
    {
        typeIdFlagsCount =
            TypeIdFlagsFromMetrics(reinterpret_cast<TypeIdFlags *>(subTlvs + typeIdFlagsOffset), *aMetrics);
    }

    VerifyOrExit(aSeriesId > kQueryIdSingleProbe, error = kErrorInvalidArgs);

    fwdProbingSubTlv->SetType(SubTlv::kFwdProbingReg);

    // SeriesId + SeriesFlags + typeIdFlagsCount * TypeIdFlags
    fwdProbingSubTlv->SetLength(sizeof(uint8_t) * 2 + sizeof(TypeIdFlags) * typeIdFlagsCount);

    memcpy(subTlvs + sizeof(Tlv), &aSeriesId, sizeof(aSeriesId));

    seriesFlags->SetFrom(aSeriesFlags);

    error = Get<Mle::MleRouter>().SendLinkMetricsManagementRequest(aDestination, subTlvs, fwdProbingSubTlv->GetSize());

exit:
    LogDebg("SendMgmtRequestForwardTrackingSeries, error:%s, Series ID:%u", ErrorToString(error), aSeriesId);
    return error;
}

Error LinkMetrics::SendMgmtRequestEnhAckProbing(const Ip6::Address &aDestination,
                                                const EnhAckFlags   aEnhAckFlags,
                                                const Metrics *     aMetrics)
{
    Error              error = kErrorNone;
    EnhAckConfigSubTlv enhAckConfigSubTlv;
    Mac::Address       macAddress;
    Neighbor *         neighbor = GetNeighborFromLinkLocalAddr(aDestination);

    VerifyOrExit(neighbor != nullptr, error = kErrorUnknownNeighbor);
    VerifyOrExit(neighbor->IsThreadVersion1p2OrHigher(), error = kErrorNotCapable);

    if (aEnhAckFlags == kEnhAckClear)
    {
        VerifyOrExit(aMetrics == nullptr, error = kErrorInvalidArgs);
    }

    enhAckConfigSubTlv.SetEnhAckFlags(aEnhAckFlags);

    if (aMetrics != nullptr)
    {
        enhAckConfigSubTlv.SetTypeIdFlags(*aMetrics);
    }

    error = Get<Mle::MleRouter>().SendLinkMetricsManagementRequest(
        aDestination, reinterpret_cast<const uint8_t *>(&enhAckConfigSubTlv), enhAckConfigSubTlv.GetSize());

    if (aMetrics != nullptr)
    {
        neighbor->SetEnhAckProbingMetrics(*aMetrics);
    }
    else
    {
        Metrics metrics;

        metrics.Clear();
        neighbor->SetEnhAckProbingMetrics(metrics);
    }

exit:
    return error;
}

Error LinkMetrics::SendLinkProbe(const Ip6::Address &aDestination, uint8_t aSeriesId, uint8_t aLength)
{
    Error     error = kErrorNone;
    uint8_t   buf[kLinkProbeMaxLen];
    Neighbor *neighbor = GetNeighborFromLinkLocalAddr(aDestination);

    VerifyOrExit(neighbor != nullptr, error = kErrorUnknownNeighbor);
    VerifyOrExit(neighbor->IsThreadVersion1p2OrHigher(), error = kErrorNotCapable);

    VerifyOrExit(aLength <= LinkMetrics::kLinkProbeMaxLen && aSeriesId != kQueryIdSingleProbe &&
                     aSeriesId != kSeriesIdAllSeries,
                 error = kErrorInvalidArgs);

    error = Get<Mle::MleRouter>().SendLinkProbe(aDestination, aSeriesId, buf, aLength);
exit:
    LogDebg("SendLinkProbe, error:%s, Series ID:%u", ErrorToString(error), aSeriesId);
    return error;
}
#endif // OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE

#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
Error LinkMetrics::AppendReport(Message &aMessage, const Message &aRequestMessage, Neighbor &aNeighbor)
{
    Error         error = kErrorNone;
    Tlv           tlv;
    uint8_t       queryId;
    bool          hasQueryId  = false;
    uint8_t       length      = 0;
    uint16_t      startOffset = aMessage.GetLength();
    uint16_t      offset;
    uint16_t      endOffset;
    MetricsValues values;

    values.Clear();

    SuccessOrExit(error = Tlv::FindTlvValueOffset(aRequestMessage, Mle::Tlv::Type::kLinkMetricsQuery, offset,
                                                  endOffset)); // `endOffset` is used to store tlv length here

    endOffset = offset + endOffset;

    while (offset < endOffset)
    {
        SuccessOrExit(error = aRequestMessage.Read(offset, tlv));

        switch (tlv.GetType())
        {
        case SubTlv::kQueryId:
            SuccessOrExit(error = Tlv::Read<QueryIdSubTlv>(aRequestMessage, offset, queryId));
            hasQueryId = true;
            break;

        case SubTlv::kQueryOptions:
            SuccessOrExit(error = ReadTypeIdFlagsFromMessage(aRequestMessage, offset + sizeof(tlv),
                                                             static_cast<uint16_t>(offset + tlv.GetSize()),
                                                             values.GetMetrics()));
            break;

        default:
            break;
        }

        offset += tlv.GetSize();
    }

    VerifyOrExit(hasQueryId, error = kErrorParse);

    // Link Metrics Report TLV
    tlv.SetType(Mle::Tlv::kLinkMetricsReport);
    SuccessOrExit(error = aMessage.Append(tlv));

    if (queryId == kQueryIdSingleProbe)
    {
        values.mPduCountValue = HostSwap32(aRequestMessage.GetPsduCount());
        values.mLqiValue      = aRequestMessage.GetAverageLqi();
        // Linearly scale Link Margin from [0, 130] to [0, 255]
        values.mLinkMarginValue =
            LinkQualityInfo::ConvertRssToLinkMargin(Get<Mac::Mac>().GetNoiseFloor(), aRequestMessage.GetAverageRss()) *
            255 / 130;
        // Linearly scale rss from [-130, 0] to [0, 255]
        values.mRssiValue = (aRequestMessage.GetAverageRss() + 130) * 255 / 130;

        SuccessOrExit(error = AppendReportSubTlvToMessage(aMessage, length, values));
    }
    else
    {
        SeriesInfo *seriesInfo = aNeighbor.GetForwardTrackingSeriesInfo(queryId);

        if (seriesInfo == nullptr)
        {
            SuccessOrExit(error = AppendStatusSubTlvToMessage(aMessage, length, kStatusSeriesIdNotRecognized));
        }
        else if (seriesInfo->GetPduCount() == 0)
        {
            SuccessOrExit(error = AppendStatusSubTlvToMessage(aMessage, length, kStatusNoMatchingFramesReceived));
        }
        else
        {
            values.SetMetrics(seriesInfo->GetLinkMetrics());
            values.mPduCountValue = HostSwap32(seriesInfo->GetPduCount());
            values.mLqiValue      = seriesInfo->GetAverageLqi();
            // Linearly scale Link Margin from [0, 130] to [0, 255]
            values.mLinkMarginValue =
                LinkQualityInfo::ConvertRssToLinkMargin(Get<Mac::Mac>().GetNoiseFloor(), seriesInfo->GetAverageRss()) *
                255 / 130;
            // Linearly scale RSSI from [-130, 0] to [0, 255]
            values.mRssiValue = (seriesInfo->GetAverageRss() + 130) * 255 / 130;
            SuccessOrExit(error = AppendReportSubTlvToMessage(aMessage, length, values));
        }
    }

    tlv.SetLength(length);
    aMessage.Write(startOffset, tlv);

exit:
    LogDebg("AppendReport, error:%s", ErrorToString(error));
    return error;
}

Error LinkMetrics::HandleManagementRequest(const Message &aMessage, Neighbor &aNeighbor, Status &aStatus)
{
    Error       error = kErrorNone;
    Tlv         tlv;
    uint8_t     seriesId;
    SeriesFlags seriesFlags;
    EnhAckFlags enhAckFlags;
    Metrics     metrics;
    bool        hasForwardProbingRegistrationTlv = false;
    bool        hasEnhAckProbingTlv              = false;
    uint16_t    offset;
    uint16_t    length;
    uint16_t    index = 0;

    SuccessOrExit(error = Tlv::FindTlvValueOffset(aMessage, Mle::Tlv::Type::kLinkMetricsManagement, offset, length));

    while (index < length)
    {
        uint16_t pos = offset + index;

        SuccessOrExit(aMessage.Read(pos, tlv));

        pos += sizeof(tlv);

        switch (tlv.GetType())
        {
        case SubTlv::kFwdProbingReg:
            VerifyOrExit(!hasForwardProbingRegistrationTlv && !hasEnhAckProbingTlv, error = kErrorParse);
            VerifyOrExit(tlv.GetLength() >= sizeof(seriesId) + sizeof(seriesFlags), error = kErrorParse);
            SuccessOrExit(aMessage.Read(pos, seriesId));
            pos += sizeof(seriesId);
            SuccessOrExit(aMessage.Read(pos, seriesFlags));
            pos += sizeof(seriesFlags);
            SuccessOrExit(error = ReadTypeIdFlagsFromMessage(
                              aMessage, pos, static_cast<uint16_t>(offset + index + tlv.GetSize()), metrics));
            hasForwardProbingRegistrationTlv = true;
            break;

        case SubTlv::kEnhAckConfig:
            VerifyOrExit(!hasForwardProbingRegistrationTlv && !hasEnhAckProbingTlv, error = kErrorParse);
            VerifyOrExit(tlv.GetLength() >= sizeof(EnhAckFlags), error = kErrorParse);
            SuccessOrExit(aMessage.Read(pos, enhAckFlags));
            pos += sizeof(enhAckFlags);
            SuccessOrExit(error = ReadTypeIdFlagsFromMessage(
                              aMessage, pos, static_cast<uint16_t>(offset + index + tlv.GetSize()), metrics));
            hasEnhAckProbingTlv = true;
            break;

        default:
            break;
        }

        index += tlv.GetSize();
    }

    if (hasForwardProbingRegistrationTlv)
    {
        aStatus = ConfigureForwardTrackingSeries(seriesId, seriesFlags, metrics, aNeighbor);
    }
    else if (hasEnhAckProbingTlv)
    {
        aStatus = ConfigureEnhAckProbing(enhAckFlags, metrics, aNeighbor);
    }

exit:
    return error;
}
#endif // OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE

Error LinkMetrics::HandleManagementResponse(const Message &aMessage, const Ip6::Address &aAddress)
{
    Error    error = kErrorNone;
    Tlv      tlv;
    uint16_t offset;
    uint16_t length;
    uint16_t index = 0;
    Status   status;
    bool     hasStatus = false;

    VerifyOrExit(mMgmtResponseCallback != nullptr);

    SuccessOrExit(error = Tlv::FindTlvValueOffset(aMessage, Mle::Tlv::Type::kLinkMetricsManagement, offset, length));

    while (index < length)
    {
        SuccessOrExit(aMessage.Read(offset + index, tlv));

        switch (tlv.GetType())
        {
        case SubTlv::kStatus:
            VerifyOrExit(!hasStatus, error = kErrorParse);
            VerifyOrExit(tlv.GetLength() == sizeof(status), error = kErrorParse);
            SuccessOrExit(aMessage.Read(offset + index + sizeof(tlv), status));
            hasStatus = true;
            break;

        default:
            break;
        }

        index += tlv.GetSize();
    }

    VerifyOrExit(hasStatus, error = kErrorParse);

    mMgmtResponseCallback(&aAddress, status, mMgmtResponseCallbackContext);

exit:
    return error;
}

void LinkMetrics::HandleReport(const Message &     aMessage,
                               uint16_t            aOffset,
                               uint16_t            aLength,
                               const Ip6::Address &aAddress)
{
    Error         error = kErrorNone;
    MetricsValues values;
    uint8_t       rawValue;
    uint16_t      pos    = aOffset;
    uint16_t      endPos = aOffset + aLength;
    Tlv           tlv;
    TypeIdFlags   typeIdFlags;
    bool          hasStatus = false;
    bool          hasReport = false;
    Status        status;

    OT_UNUSED_VARIABLE(error);

    VerifyOrExit(mReportCallback != nullptr);

    values.Clear();

    while (pos < endPos)
    {
        SuccessOrExit(aMessage.Read(pos, tlv));
        VerifyOrExit(tlv.GetType() == SubTlv::kReport);
        pos += sizeof(Tlv);

        VerifyOrExit(pos + tlv.GetLength() <= endPos, error = kErrorParse);

        switch (tlv.GetType())
        {
        case SubTlv::kStatus:
            // There should be either: one Status TLV or some Report-Sub TLVs
            VerifyOrExit(!hasStatus && !hasReport, error = kErrorDrop);
            VerifyOrExit(tlv.GetLength() == sizeof(status), error = kErrorParse);
            SuccessOrExit(aMessage.Read(pos, status));
            hasStatus = true;
            pos += sizeof(status);
            break;

        case SubTlv::kReport:
            // There shouldn't be any Report-Sub TLV when there's a Status TLV
            VerifyOrExit(!hasStatus, error = kErrorDrop);
            VerifyOrExit(tlv.GetLength() > sizeof(typeIdFlags), error = kErrorParse);
            SuccessOrExit(aMessage.Read(pos, typeIdFlags));

            if (typeIdFlags.IsExtendedFlagSet())
            {
                pos += tlv.GetLength(); // Skip the whole sub-TLV if `E` flag is set
                continue;
            }

            hasReport = true;
            pos += sizeof(TypeIdFlags);

            switch (typeIdFlags.GetRawValue())
            {
            case TypeIdFlags::kPdu:
                values.GetMetrics().mPduCount = true;
                SuccessOrExit(aMessage.Read(pos, values.mPduCountValue));
                values.mPduCountValue = HostSwap32(values.mPduCountValue);
                pos += sizeof(uint32_t);
                LogDebg(" - PDU Counter: %d (Count/Summation)", values.mPduCountValue);
                break;

            case TypeIdFlags::kLqi:
                values.GetMetrics().mLqi = true;
                SuccessOrExit(aMessage.Read(pos, values.mLqiValue));
                pos += sizeof(uint8_t);
                LogDebg(" - LQI: %d (Exponential Moving Average)", values.mLqiValue);
                break;

            case TypeIdFlags::kLinkMargin:
                values.GetMetrics().mLinkMargin = true;
                SuccessOrExit(aMessage.Read(pos, rawValue));
                // Reverse operation for linear scale, map from [0, 255] to [0, 130]
                values.mLinkMarginValue = rawValue * 130 / 255;
                pos += sizeof(uint8_t);
                LogDebg(" - Margin: %d (dB) (Exponential Moving Average)", values.mLinkMarginValue);
                break;

            case TypeIdFlags::kRssi:
                values.GetMetrics().mRssi = true;
                SuccessOrExit(aMessage.Read(pos, rawValue));
                // Reverse operation for linear scale, map from [0, 255] to [-130, 0]
                values.mRssiValue = rawValue * 130 / 255 - 130;
                pos += sizeof(uint8_t);
                LogDebg(" - RSSI: %d (dBm) (Exponential Moving Average)", values.mRssiValue);
                break;

            default:
                break;
            }
            break;
        }
    }

    if (hasStatus)
    {
        mReportCallback(&aAddress, nullptr, status, mReportCallbackContext);
    }
    else if (hasReport)
    {
        mReportCallback(&aAddress, &values, OT_LINK_METRICS_STATUS_SUCCESS, mReportCallbackContext);
    }

exit:
    LogDebg("HandleReport, error:%s", ErrorToString(error));
    return;
}

Error LinkMetrics::HandleLinkProbe(const Message &aMessage, uint8_t &aSeriesId)
{
    Error    error = kErrorNone;
    uint16_t offset;
    uint16_t length;

    SuccessOrExit(error = Tlv::FindTlvValueOffset(aMessage, Mle::Tlv::Type::kLinkProbe, offset, length));
    VerifyOrExit(length >= sizeof(aSeriesId), error = kErrorParse);
    error = aMessage.Read(offset, aSeriesId);

exit:
    return error;
}

void LinkMetrics::SetReportCallback(ReportCallback aCallback, void *aContext)
{
    mReportCallback        = aCallback;
    mReportCallbackContext = aContext;
}

void LinkMetrics::SetMgmtResponseCallback(MgmtResponseCallback aCallback, void *aContext)
{
    mMgmtResponseCallback        = aCallback;
    mMgmtResponseCallbackContext = aContext;
}

void LinkMetrics::SetEnhAckProbingCallback(EnhAckProbingIeReportCallback aCallback, void *aContext)
{
    mEnhAckProbingIeReportCallback        = aCallback;
    mEnhAckProbingIeReportCallbackContext = aContext;
}

void LinkMetrics::ProcessEnhAckIeData(const uint8_t *aData, uint8_t aLength, const Neighbor &aNeighbor)
{
    MetricsValues values;
    uint8_t       idx = 0;

    VerifyOrExit(mEnhAckProbingIeReportCallback != nullptr);

    values.SetMetrics(aNeighbor.GetEnhAckProbingMetrics());

    if (values.GetMetrics().mLqi && idx < aLength)
    {
        values.mLqiValue = aData[idx++];
    }
    if (values.GetMetrics().mLinkMargin && idx < aLength)
    {
        // Reverse operation for linear scale, map from [0, 255] to [0, 130]
        values.mLinkMarginValue = aData[idx++] * 130 / 255;
    }
    if (values.GetMetrics().mRssi && idx < aLength)
    {
        // Reverse operation for linear scale, map from [0, 255] to [-130, 0]
        values.mRssiValue = aData[idx++] * 130 / 255 - 130;
    }

    mEnhAckProbingIeReportCallback(aNeighbor.GetRloc16(), &aNeighbor.GetExtAddress(), &values,
                                   mEnhAckProbingIeReportCallbackContext);

exit:
    return;
}

Error LinkMetrics::SendLinkMetricsQuery(const Ip6::Address &aDestination,
                                        uint8_t             aSeriesId,
                                        const TypeIdFlags * aTypeIdFlags,
                                        uint8_t             aTypeIdFlagsCount)
{
    // LinkMetricsQuery Tlv + LinkMetricsQueryId sub-TLV (value-length: 1 byte) +
    // LinkMetricsQueryOptions sub-TLV (value-length: `kMaxTypeIdFlags` bytes)
    constexpr uint16_t kBufferSize = sizeof(Tlv) * 3 + sizeof(uint8_t) + sizeof(TypeIdFlags) * kMaxTypeIdFlags;

    Error                error = kErrorNone;
    QueryOptionsSubTlv   queryOptionsTlv;
    uint8_t              length = 0;
    static const uint8_t tlvs[] = {Mle::Tlv::kLinkMetricsReport};
    uint8_t              buf[kBufferSize];
    Tlv *                tlv = reinterpret_cast<Tlv *>(buf);
    Tlv                  subTlv;

    // Link Metrics Query TLV
    tlv->SetType(Mle::Tlv::kLinkMetricsQuery);
    length += sizeof(Tlv);

    // Link Metrics Query ID sub-TLV
    subTlv.SetType(SubTlv::kQueryId);
    subTlv.SetLength(sizeof(uint8_t));
    memcpy(buf + length, &subTlv, sizeof(subTlv));
    length += sizeof(subTlv);
    memcpy(buf + length, &aSeriesId, sizeof(aSeriesId));
    length += sizeof(aSeriesId);

    // Link Metrics Query Options sub-TLV
    if (aTypeIdFlagsCount > 0)
    {
        queryOptionsTlv.Init();
        queryOptionsTlv.SetLength(aTypeIdFlagsCount * sizeof(TypeIdFlags));

        memcpy(buf + length, &queryOptionsTlv, sizeof(queryOptionsTlv));
        length += sizeof(queryOptionsTlv);
        memcpy(buf + length, aTypeIdFlags, queryOptionsTlv.GetLength());
        length += queryOptionsTlv.GetLength();
    }

    // Set Length for Link Metrics Report TLV
    tlv->SetLength(length - sizeof(Tlv));

    SuccessOrExit(error = Get<Mle::MleRouter>().SendDataRequest(aDestination, tlvs, sizeof(tlvs), 0, buf, length));

exit:
    return error;
}

Status LinkMetrics::ConfigureForwardTrackingSeries(uint8_t            aSeriesId,
                                                   const SeriesFlags &aSeriesFlags,
                                                   const Metrics &    aMetrics,
                                                   Neighbor &         aNeighbor)
{
    Status status = kStatusSuccess;

    VerifyOrExit(0 < aSeriesId, status = kStatusOtherError);
    if (aSeriesFlags.GetRawValue() == 0) // Remove the series
    {
        if (aSeriesId == kSeriesIdAllSeries) // Remove all
        {
            aNeighbor.RemoveAllForwardTrackingSeriesInfo();
        }
        else
        {
            SeriesInfo *seriesInfo = aNeighbor.RemoveForwardTrackingSeriesInfo(aSeriesId);
            VerifyOrExit(seriesInfo != nullptr, status = kStatusSeriesIdNotRecognized);
            mSeriesInfoPool.Free(*seriesInfo);
        }
    }
    else // Add a new series
    {
        SeriesInfo *seriesInfo = aNeighbor.GetForwardTrackingSeriesInfo(aSeriesId);
        VerifyOrExit(seriesInfo == nullptr, status = kStatusSeriesIdAlreadyRegistered);
        seriesInfo = mSeriesInfoPool.Allocate();
        VerifyOrExit(seriesInfo != nullptr, status = kStatusCannotSupportNewSeries);

        seriesInfo->Init(aSeriesId, aSeriesFlags, aMetrics);

        aNeighbor.AddForwardTrackingSeriesInfo(*seriesInfo);
    }

exit:
    return status;
}

#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
Status LinkMetrics::ConfigureEnhAckProbing(EnhAckFlags aEnhAckFlags, const Metrics &aMetrics, Neighbor &aNeighbor)
{
    Status status = kStatusSuccess;
    Error  error  = kErrorNone;

    VerifyOrExit(!aMetrics.mReserved, status = kStatusOtherError);

    if (aEnhAckFlags == kEnhAckRegister)
    {
        VerifyOrExit(!aMetrics.mPduCount, status = kStatusOtherError);
        VerifyOrExit(aMetrics.mLqi || aMetrics.mLinkMargin || aMetrics.mRssi, status = kStatusOtherError);
        VerifyOrExit(!(aMetrics.mLqi && aMetrics.mLinkMargin && aMetrics.mRssi), status = kStatusOtherError);

        error = Get<Radio>().ConfigureEnhAckProbing(aMetrics, aNeighbor.GetRloc16(), aNeighbor.GetExtAddress());
    }
    else if (aEnhAckFlags == kEnhAckClear)
    {
        VerifyOrExit(!aMetrics.mLqi && !aMetrics.mLinkMargin && !aMetrics.mRssi, status = kStatusOtherError);
        error = Get<Radio>().ConfigureEnhAckProbing(aMetrics, aNeighbor.GetRloc16(), aNeighbor.GetExtAddress());
    }
    else
    {
        status = kStatusOtherError;
    }

    VerifyOrExit(error == kErrorNone, status = kStatusOtherError);

exit:
    return status;
}
#endif // OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE

Neighbor *LinkMetrics::GetNeighborFromLinkLocalAddr(const Ip6::Address &aDestination)
{
    Neighbor *   neighbor = nullptr;
    Mac::Address macAddress;

    VerifyOrExit(aDestination.IsLinkLocal());
    aDestination.GetIid().ConvertToMacAddress(macAddress);
    neighbor = Get<NeighborTable>().FindNeighbor(macAddress);

exit:
    return neighbor;
}

Error LinkMetrics::ReadTypeIdFlagsFromMessage(const Message &aMessage,
                                              uint8_t        aStartPos,
                                              uint8_t        aEndPos,
                                              Metrics &      aMetrics)
{
    Error error = kErrorNone;

    memset(&aMetrics, 0, sizeof(aMetrics));

    for (uint16_t pos = aStartPos; pos < aEndPos; pos += sizeof(TypeIdFlags))
    {
        TypeIdFlags typeIdFlags;

        SuccessOrExit(aMessage.Read(pos, typeIdFlags));

        switch (typeIdFlags.GetRawValue())
        {
        case TypeIdFlags::kPdu:
            VerifyOrExit(!aMetrics.mPduCount, error = kErrorParse);
            aMetrics.mPduCount = true;
            break;

        case TypeIdFlags::kLqi:
            VerifyOrExit(!aMetrics.mLqi, error = kErrorParse);
            aMetrics.mLqi = true;
            break;

        case TypeIdFlags::kLinkMargin:
            VerifyOrExit(!aMetrics.mLinkMargin, error = kErrorParse);
            aMetrics.mLinkMargin = true;
            break;

        case TypeIdFlags::kRssi:
            VerifyOrExit(!aMetrics.mRssi, error = kErrorParse);
            aMetrics.mRssi = true;
            break;

        default:
            if (typeIdFlags.IsExtendedFlagSet())
            {
                pos += sizeof(uint8_t); // Skip the additional second flags byte.
            }
            else
            {
                aMetrics.mReserved = true;
            }
            break;
        }
    }

exit:
    return error;
}

Error LinkMetrics::AppendReportSubTlvToMessage(Message &aMessage, uint8_t &aLength, const MetricsValues &aValues)
{
    Error        error = kErrorNone;
    ReportSubTlv metric;

    aLength = 0;

    // Link Metrics Report sub-TLVs
    if (aValues.mMetrics.mPduCount)
    {
        metric.Init();
        metric.SetMetricsTypeId(TypeIdFlags(TypeIdFlags::kPdu));
        metric.SetMetricsValue32(aValues.mPduCountValue);
        SuccessOrExit(error = aMessage.AppendBytes(&metric, metric.GetSize()));
        aLength += metric.GetSize();
    }

    if (aValues.mMetrics.mLqi)
    {
        metric.Init();
        metric.SetMetricsTypeId(TypeIdFlags(TypeIdFlags::kLqi));
        metric.SetMetricsValue8(aValues.mLqiValue);
        SuccessOrExit(error = aMessage.AppendBytes(&metric, metric.GetSize()));
        aLength += metric.GetSize();
    }

    if (aValues.mMetrics.mLinkMargin)
    {
        metric.Init();
        metric.SetMetricsTypeId(TypeIdFlags(TypeIdFlags::kLinkMargin));
        metric.SetMetricsValue8(aValues.mLinkMarginValue);
        SuccessOrExit(error = aMessage.AppendBytes(&metric, metric.GetSize()));
        aLength += metric.GetSize();
    }

    if (aValues.mMetrics.mRssi)
    {
        metric.Init();
        metric.SetMetricsTypeId(TypeIdFlags(TypeIdFlags::kRssi));
        metric.SetMetricsValue8(aValues.mRssiValue);
        SuccessOrExit(error = aMessage.AppendBytes(&metric, metric.GetSize()));
        aLength += metric.GetSize();
    }

exit:
    return error;
}

Error LinkMetrics::AppendStatusSubTlvToMessage(Message &aMessage, uint8_t &aLength, Status aStatus)
{
    Error error = kErrorNone;
    Tlv   statusTlv;

    statusTlv.SetType(SubTlv::kStatus);
    statusTlv.SetLength(sizeof(uint8_t));
    SuccessOrExit(error = aMessage.AppendBytes(&statusTlv, sizeof(statusTlv)));
    SuccessOrExit(error = aMessage.AppendBytes(&aStatus, sizeof(aStatus)));
    aLength += statusTlv.GetSize();

exit:
    return error;
}

} // namespace LinkMetrics
} // namespace ot

#endif // OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE || OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
