/*
 *  Copyright (c) 2018, 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 BorderAgent service.
 */

#include "border_agent.hpp"

#if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE

#include "common/num_utils.hpp"
#include "instance/instance.hpp"
#include "meshcop/border_agent_txt_data.hpp"

namespace ot {
namespace MeshCoP {
namespace BorderAgent {

RegisterLogModule("BorderAgent");

//----------------------------------------------------------------------------------------------------------------------
// `Manager`

#if OPENTHREAD_CONFIG_BORDER_AGENT_MESHCOP_SERVICE_ENABLE

const char Manager::kServiceType[]            = "_meshcop._udp";
const char Manager::kDefaultBaseServiceName[] = OPENTHREAD_CONFIG_BORDER_AGENT_MESHCOP_SERVICE_BASE_NAME;

#if OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE
const char        Manager::kAdmitterSubType[] = "_admitter";
const char *const Manager::kServiceSubTypes[] = {kAdmitterSubType};
#endif

#endif

Manager::Manager(Instance &aInstance)
    : InstanceLocator(aInstance)
    , mEnabled(true)
    , mIsRunning(false)
    , mSessionIndex(0)
    , mDtlsTransport(aInstance, kNoLinkSecurity)
    , mCommissionerSession(nullptr)
    , mCommissionerUdpReceiver(HandleUdpReceive, this)
#if OPENTHREAD_CONFIG_BORDER_AGENT_ID_ENABLE
    , mIdInitialized(false)
#endif
#if OPENTHREAD_CONFIG_BORDER_AGENT_MESHCOP_SERVICE_ENABLE
    , mServiceRenameIndex(0)
#endif
{
    mCommissionerAloc.InitAsThreadOriginMeshLocal();

    ClearAllBytes(mCounters);

#if OPENTHREAD_CONFIG_BORDER_AGENT_MESHCOP_SERVICE_ENABLE
    ClearAllBytes(mServiceName);

    static_assert(sizeof(kDefaultBaseServiceName) - 1 <= kBaseServiceNameMaxLen,
                  "OPENTHREAD_CONFIG_BORDER_AGENT_MESHCOP_SERVICE_BASE_NAME is too long");

    SuccessOrAssert(StringCopy(mBaseServiceName, kDefaultBaseServiceName));
#endif
}

#if OPENTHREAD_CONFIG_BORDER_AGENT_ID_ENABLE
void Manager::GetId(Id &aId)
{
    if (mIdInitialized)
    {
        aId = mId;
        ExitNow();
    }

    if (Get<Settings>().Read<Settings::BorderAgentId>(mId) != kErrorNone)
    {
        mId.GenerateRandom();
        Get<Settings>().Save<Settings::BorderAgentId>(mId);
    }

    mIdInitialized = true;
    aId            = mId;

exit:
    return;
}

void Manager::SetId(const Id &aId)
{
    if (mIdInitialized)
    {
        VerifyOrExit(aId != mId);
    }

    Get<Settings>().Save<Settings::BorderAgentId>(aId);
    mId            = aId;
    mIdInitialized = true;

    Get<TxtData>().Refresh();

exit:
    return;
}
#endif // OPENTHREAD_CONFIG_BORDER_AGENT_ID_ENABLE

void Manager::SetEnabled(bool aEnabled)
{
    VerifyOrExit(mEnabled != aEnabled);
    mEnabled = aEnabled;
    LogInfo("%sabling Border Agent", mEnabled ? "En" : "Dis");

    Get<TxtData>().Refresh();

    UpdateState();

#if OPENTHREAD_CONFIG_BORDER_AGENT_MESHCOP_SERVICE_ENABLE
    if (!mEnabled)
    {
        UnregisterService();
    }
#endif

exit:
    return;
}

void Manager::UpdateState(void)
{
    if (mEnabled && Get<Mle::Mle>().IsAttached())
    {
        Start();
    }
    else
    {
        Stop();
    }
}

void Manager::Start(void)
{
    Error error = kErrorNone;
    Pskc  pskc;

    VerifyOrExit(!mIsRunning);

    mDtlsTransport.SetAcceptCallback(Manager::HandleAcceptSession, this);
    mDtlsTransport.SetRemoveSessionCallback(Manager::HandleRemoveSession, this);

    SuccessOrExit(error = mDtlsTransport.Open(kUdpPort));

    Get<KeyManager>().GetPskc(pskc);
    SuccessOrExit(error = mDtlsTransport.SetPsk(pskc.m8, Pskc::kSize));
    pskc.Clear();

    mIsRunning = true;
    LogInfo("Border Agent start listening on port %u", GetUdpPort());

    Get<TxtData>().Refresh();

#if OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE
    Get<Admitter>().EvaluateOperation();
#endif

exit:
    if (!mIsRunning)
    {
        mDtlsTransport.Close();
    }

    LogWarnOnError(error, "start agent");
}

void Manager::Stop(void)
{
    VerifyOrExit(mIsRunning);

    if (mCommissionerSession != nullptr)
    {
        RevokeRoleIfActiveCommissioner(*mCommissionerSession);
    }

    mDtlsTransport.Close();
    mIsRunning = false;

    LogInfo("Border Agent stopped");

    Get<TxtData>().Refresh();

#if OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE
    Get<Admitter>().EvaluateOperation();
#endif

exit:
    return;
}

uint16_t Manager::GetUdpPort(void) const { return mDtlsTransport.GetUdpPort(); }

void Manager::HandleNotifierEvents(Events aEvents)
{
    if (aEvents.Contains(kEventThreadRoleChanged))
    {
        UpdateState();
    }

    VerifyOrExit(mEnabled);

    if (aEvents.ContainsAny(kEventPskcChanged))
    {
        Pskc pskc;

        VerifyOrExit(mIsRunning);

        Get<KeyManager>().GetPskc(pskc);

        // If there is secure session already established, it won't be impacted,
        // new pskc will be applied for next connection.
        SuccessOrExit(mDtlsTransport.SetPsk(pskc.m8, Pskc::kSize));
        pskc.Clear();
    }

exit:
    return;
}

SecureSession *Manager::HandleAcceptSession(void *aContext, const Ip6::MessageInfo &aMessageInfo)
{
    OT_UNUSED_VARIABLE(aMessageInfo);

    return static_cast<Manager *>(aContext)->HandleAcceptSession();
}

Manager::CoapDtlsSession *Manager::HandleAcceptSession(void)
{
    CoapDtlsSession *session = nullptr;

    if (mDtlsTransport.GetSessions().CountAllEntries() >= kMaxSessions)
    {
        LogWarn("Accept session failed: reached max concurrent secure sessions limit (%lu)", ToUlong(kMaxSessions));
        ExitNow();
    }

    session = CoapDtlsSession::Allocate(GetInstance(), mDtlsTransport);

exit:
    return session;
}

void Manager::HandleRemoveSession(void *aContext, SecureSession &aSession)
{
    static_cast<Manager *>(aContext)->HandleRemoveSession(aSession);
}

void Manager::HandleRemoveSession(SecureSession &aSession)
{
    CoapDtlsSession &coapSession = static_cast<CoapDtlsSession &>(aSession);

    LogInfo("Deleting session %u", coapSession.GetIndex());

    coapSession.Cleanup();
    coapSession.Free();
}

void Manager::HandleSessionConnected(CoapDtlsSession &aSession)
{
    OT_UNUSED_VARIABLE(aSession);

#if OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE
    if (Get<EphemeralKeyManager>().OwnsSession(aSession))
    {
        Get<EphemeralKeyManager>().HandleSessionConnected();
    }
    else
#endif
    {
        mCounters.mPskcSecureSessionSuccesses++;
    }
}

void Manager::HandleSessionDisconnected(CoapDtlsSession &aSession, CoapDtlsSession::ConnectEvent aEvent)
{
    RevokeRoleIfActiveCommissioner(aSession);

#if OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE
    if (Get<EphemeralKeyManager>().OwnsSession(aSession))
    {
        Get<EphemeralKeyManager>().HandleSessionDisconnected(aEvent);
    }
    else
#endif
    {
        if (aEvent == CoapDtlsSession::kDisconnectedError)
        {
            mCounters.mPskcSecureSessionFailures++;
        }
    }
}

void Manager::HandleCommissionerPetitionAccepted(CoapDtlsSession &aSession, uint16_t aSessionId)
{
    if (mCommissionerSession != nullptr)
    {
        RevokeRoleIfActiveCommissioner(*mCommissionerSession);
    }

    mCommissionerSession = &aSession;

    Get<Mle::Mle>().ComposeCommissionerAloc(aSessionId, mCommissionerAloc.GetAddress());
    Get<ThreadNetif>().AddUnicastAddress(mCommissionerAloc);

    IgnoreError(Get<Ip6::Udp>().AddReceiver(mCommissionerUdpReceiver));

    LogInfo("Session %u accepted as active commissioner - Id:0x%04x ALOC:%s", aSession.GetIndex(), aSessionId,
            mCommissionerAloc.GetAddress().ToString().AsCString());

#if OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE
    if (Get<EphemeralKeyManager>().OwnsSession(aSession))
    {
        Get<EphemeralKeyManager>().HandleCommissionerPetitionAccepted();
    }
    else
#endif
    {
        mCounters.mPskcCommissionerPetitions++;
    }
}

void Manager::RevokeRoleIfActiveCommissioner(CoapDtlsSession &aSession)
{
    VerifyOrExit(IsCommissionerSession(aSession));

    LogInfo("Revoked active commissioner role from session %u", aSession.GetIndex());

    IgnoreError(Get<Ip6::Udp>().RemoveReceiver(mCommissionerUdpReceiver));
    Get<ThreadNetif>().RemoveUnicastAddress(mCommissionerAloc);

    mCommissionerSession = nullptr;

exit:
    return;
}

bool Manager::HandleUdpReceive(void *aContext, const otMessage *aMessage, const otMessageInfo *aMessageInfo)
{
    return static_cast<Manager *>(aContext)->HandleUdpReceive(AsCoreType(aMessage), AsCoreType(aMessageInfo));
}

bool Manager::HandleUdpReceive(const Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
{
    bool didHandle = false;

    VerifyOrExit(mCommissionerSession != nullptr);
    VerifyOrExit(aMessageInfo.GetSockAddr() == mCommissionerAloc.GetAddress());

    mCommissionerSession->ForwardUdpProxyToCommissioner(aMessage, aMessageInfo);
    didHandle = true;

exit:
    return didHandle;
}

template <> void Manager::HandleTmf<kUriRelayRx>(Coap::Msg &aMsg)
{
    // This is from TMF agent.

    VerifyOrExit(mIsRunning);

    VerifyOrExit(aMsg.IsNonConfirmable());

    LogInfo("Received %s from %s", UriToString<kUriRelayRx>(), aMsg.mMessageInfo.GetPeerAddr().ToString().AsCString());

#if OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE
    Get<Admitter>().ForwardJoinerRelayToEnrollers(aMsg);
#endif

    VerifyOrExit(mCommissionerSession != nullptr);
    mCommissionerSession->ForwardUdpRelayToCommissioner(aMsg.mMessage);

exit:
    return;
}

#if OPENTHREAD_CONFIG_BORDER_AGENT_MESHCOP_SERVICE_ENABLE

Error Manager::SetServiceBaseName(const char *aBaseName)
{
    Error error = kErrorNone;

    VerifyOrExit(!StringMatch(mBaseServiceName, aBaseName));

    SuccessOrExit(error = StringCopy(mBaseServiceName, aBaseName));
    mServiceRenameIndex = 0;

    UnregisterService();
    ConstructServiceName();
    RegisterService();

exit:
    return error;
}

const char *Manager::GetServiceName(void)
{
    if (IsServiceNameEmpty())
    {
        ConstructServiceName();
    }

    return mServiceName;
}

void Manager::ConstructServiceName(void) { ConstructServiceName(mServiceRenameIndex, mServiceName); }

void Manager::ConstructServiceName(uint16_t aRenameIndex, Dns::Name::LabelBuffer &aNameBuffer)
{
    static constexpr uint8_t kExtAddrSuffixLength = 2;

    StringWriter           writer(aNameBuffer, sizeof(Dns::Name::LabelBuffer));
    const Mac::ExtAddress &extAddr = Get<Mac::Mac>().GetExtAddress();

    writer.Append("%.*s #", kBaseServiceNameMaxLen, mBaseServiceName);
    writer.AppendHexBytesUppercase(GetArrayEnd(extAddr.m8) - kExtAddrSuffixLength, kExtAddrSuffixLength);

    if (aRenameIndex != 0)
    {
        writer.Append(" (%u)", aRenameIndex % 1000);
    }
}

void Manager::RegisterService(void)
{
    Dnssd::Service service;
    Heap::Data     txtData;

    VerifyOrExit(IsEnabled());
    VerifyOrExit(Get<Dnssd>().IsReady());

    Get<TxtData>().PrepareWithVendorData(txtData);

    service.Clear();
    service.mServiceInstance = GetServiceName();
    service.mServiceType     = kServiceType;
    service.mPort            = IsRunning() ? GetUdpPort() : kDummyUdpPort;
    service.mTxtData         = txtData.GetBytes();
    service.mTxtDataLength   = txtData.GetLength();

#if OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE
    if (Get<Admitter>().IsPrimeAdmitter())
    {
        service.mSubTypeLabels       = kServiceSubTypes;
        service.mSubTypeLabelsLength = GetArrayLength(kServiceSubTypes);
    }
#endif

    LogInfo("Registering service %s %s", service.mServiceInstance, kServiceType);
    Get<Dnssd>().RegisterService(service, /* aRequestId */ 0, HandleRegisterDone);

exit:
    return;
}

void Manager::UnregisterService(void)
{
    Dnssd::Service service;

    VerifyOrExit(Get<Dnssd>().IsReady());
    VerifyOrExit(!IsServiceNameEmpty());

    service.Clear();
    service.mServiceInstance = GetServiceName();
    service.mServiceType     = kServiceType;

    LogInfo("Unregistering service %s %s", service.mServiceInstance, kServiceType);

    Get<Dnssd>().UnregisterService(service, /* aRequestId */ 0, /* aCallback */ nullptr);

exit:
    return;
}

void Manager::HandleRegisterDone(otInstance *aInstance, otPlatDnssdRequestId aRequestId, otError aError)
{
    OT_UNUSED_VARIABLE(aRequestId);
    AsCoreType(aInstance).Get<Manager>().HandleRegisterDone(aError);
}

void Manager::HandleRegisterDone(Error aError)
{
    VerifyOrExit(aError == kErrorDuplicated);

    LogInfoOnError(aError, "register service %s %s - retrying with a new name", GetServiceName(), kServiceType);

    UnregisterService();

    mServiceRenameIndex++;
    ConstructServiceName();

    RegisterService();

exit:
    return;
}

#endif // OPENTHREAD_CONFIG_BORDER_AGENT_MESHCOP_SERVICE_ENABLE

#if OPENTHREAD_CONFIG_BORDER_AGENT_COMMISSIONER_EVICTION_API_ENABLE

Error Manager::EvictActiveCommissioner(void)
{
    Error                   error = kErrorNone;
    uint16_t                sessionId;
    uint16_t                baRloc16;
    OwnedPtr<Coap::Message> message;

    SuccessOrExit(error = Get<NetworkData::Leader>().FindBorderAgentRloc(baRloc16));
    SuccessOrExit(error = Get<NetworkData::Leader>().FindCommissioningSessionId(sessionId));

    message.Reset(Get<Tmf::Agent>().AllocateAndInitPriorityConfirmablePostMessage(kUriLeaderKeepAlive));
    VerifyOrExit(message != nullptr, error = kErrorNoBufs);

    SuccessOrExit(error = Tlv::Append<StateTlv>(*message, StateTlv::kReject));
    SuccessOrExit(error = Tlv::Append<CommissionerSessionIdTlv>(*message, sessionId));

    SuccessOrExit(error = Get<Tmf::Agent>().SendMessageToLeaderAloc(*message));
    message.Release();

exit:
    return error;
}

#endif // OPENTHREAD_CONFIG_BORDER_AGENT_COMMISSIONER_EVICTION_API_ENABLE

//----------------------------------------------------------------------------------------------------------------------
// Manager::SessionIterator

void Manager::SessionIterator::Init(Instance &aInstance)
{
    SetSession(static_cast<CoapDtlsSession *>(aInstance.Get<Manager>().mDtlsTransport.GetSessions().GetHead()));
    SetInitTime(aInstance.Get<UptimeTracker>().GetUptime());
}

Error Manager::SessionIterator::GetNextSessionInfo(SessionInfo &aSessionInfo)
{
    Error            error   = kErrorNone;
    CoapDtlsSession *session = GetSession();

    VerifyOrExit(session != nullptr, error = kErrorNotFound);

    SetSession(static_cast<CoapDtlsSession *>(session->GetNext()));
    session->CopyInfoTo(aSessionInfo, GetInitTime());

exit:
    return error;
}

//----------------------------------------------------------------------------------------------------------------------
// `Manager::CoapDtlsSession

Manager::CoapDtlsSession::CoapDtlsSession(Instance &aInstance, Dtls::Transport &aDtlsTransport)
    : Coap::SecureSession(aInstance, aDtlsTransport)
    , mTimer(aInstance, HandleTimer, this)
    , mAllocationTime(aInstance.Get<UptimeTracker>().GetUptime())
    , mIndex(aInstance.Get<Manager>().GetNextSessionIndex())
{
    SetResourceHandler(&HandleResource);
    SetConnectCallback(&HandleConnected, this);

    LogInfo("Allocating session %u - starting handshake timer for %lu ms", mIndex, ToUlong(kHandshakeTimeout));
    mTimer.Start(kHandshakeTimeout);
}

Error Manager::CoapDtlsSession::SendMessage(OwnedPtr<Coap::Message> aMessage)
{
    Error error;

    // On success the ownership is transferred.
    SuccessOrExit(error = Coap::SecureSession::SendMessage(*aMessage));
    aMessage.Release();

exit:
    return error;
}

bool Manager::CoapDtlsSession::IsActiveCommissioner(void) const { return Get<Manager>().IsCommissionerSession(*this); }

void Manager::CoapDtlsSession::Cleanup(void)
{
    while (!mForwardContexts.IsEmpty())
    {
        ForwardContext *forwardContext = mForwardContexts.Pop();

        IgnoreError(Get<Tmf::Agent>().AbortTransaction(HandleLeaderResponseToFwdTmf, forwardContext));
    }

    mTimer.Stop();

    Get<Manager>().RevokeRoleIfActiveCommissioner(*this);

#if OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE
    ResignEnroller();
#endif

    Coap::SecureSession::Cleanup();
}

bool Manager::CoapDtlsSession::HandleResource(CoapBase &aCoapBase, const char *aUriPath, Coap::Msg &aMsg)
{
    return static_cast<CoapDtlsSession &>(aCoapBase).HandleResource(aUriPath, aMsg);
}

bool Manager::CoapDtlsSession::HandleResource(const char *aUriPath, Coap::Msg &aMsg)

{
    bool didHandle = true;
    Uri  uri       = UriFromPath(aUriPath);

    if ((uri != kUriUnknown) && !aMsg.IsPostRequest())
    {
        IgnoreError(SendAckResponse(aMsg, ot::Coap::kCodeMethodNotAllowed));
        ExitNow();
    }

    switch (uri)
    {
    case kUriCommissionerPetition:
        Log<kUriCommissionerPetition>(kReceive);
        IgnoreError(ForwardToLeader(aMsg, kUriLeaderPetition));
        break;
    case kUriCommissionerKeepAlive:
        HandleTmfCommissionerKeepAlive(aMsg);
        break;
    case kUriRelayTx:
        HandleTmfRelayTx(aMsg);
        break;
    case kUriCommissionerGet:
    case kUriActiveGet:
    case kUriPendingGet:
        HandleTmfDatasetGet(aMsg.mMessage, uri);
        break;
    case kUriProxyTx:
        HandleTmfProxyTx(aMsg);
        break;
#if OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE
    case kUriEnrollerRegister:
    case kUriEnrollerKeepAlive:
    case kUriEnrollerJoinerAccept:
    case kUriEnrollerJoinerRelease:
        HandleEnrollerTmf(uri, aMsg);
        break;
#endif

    default:
        didHandle = false;
        break;
    }

exit:
    return didHandle;
}

void Manager::CoapDtlsSession::HandleConnected(ConnectEvent aEvent, void *aContext)
{
    static_cast<CoapDtlsSession *>(aContext)->HandleConnected(aEvent);
}

void Manager::CoapDtlsSession::HandleConnected(ConnectEvent aEvent)
{
    if (aEvent == kConnected)
    {
        LogInfo("Session %u connected", mIndex);
        mTimer.Start(kKeepAliveTimeout);
        Get<Manager>().HandleSessionConnected(*this);
    }
    else
    {
        LogInfo("Session %u disconnected", mIndex);
#if OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE
        ResignEnroller();
#endif
        Get<Manager>().HandleSessionDisconnected(*this, aEvent);
    }
}

void Manager::CoapDtlsSession::HandleTmfCommissionerKeepAlive(Coap::Msg &aMsg)
{
    VerifyOrExit(IsActiveCommissioner());

    Log<kUriCommissionerKeepAlive>(kReceive);

    SuccessOrExit(ForwardToLeader(aMsg, kUriLeaderKeepAlive));
    mTimer.Start(kKeepAliveTimeout);
#if OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE && OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
    if (Get<EphemeralKeyManager>().OwnsSession(*this))
    {
        Get<HistoryTracker::Local>().RecordEpskcEvent(HistoryTracker::Local::kEpskcKeepAlive);
    }
#endif

exit:
    return;
}

Error Manager::CoapDtlsSession::ForwardToLeader(const Coap::Msg &aMsg, Uri aUri)
{
    Error                    error = kErrorNone;
    OwnedPtr<ForwardContext> forwardContext;
    OwnedPtr<Coap::Message>  message;
    OffsetRange              offsetRange;
    Coap::Token              token;

    switch (aUri)
    {
    case kUriLeaderPetition:
    case kUriLeaderKeepAlive:
        break;
    default:
        OT_ASSERT(false);
    }

    if (aMsg.IsConfirmable())
    {
        SuccessOrExit(error = SendEmptyMessage(Coap::kTypeAck, aMsg));
    }

    forwardContext.Reset(ForwardContext::Allocate(*this, aMsg.mMessage, aUri));
    VerifyOrExit(!forwardContext.IsNull(), error = kErrorNoBufs);

    message.Reset(Get<Tmf::Agent>().AllocateAndInitPriorityConfirmablePostMessage(aUri));
    VerifyOrExit(message != nullptr, error = kErrorNoBufs);

    offsetRange.InitFromMessageOffsetToEnd(aMsg.mMessage);
    SuccessOrExit(error = message->AppendBytesFromMessage(aMsg.mMessage, offsetRange));

    SuccessOrExit(error = Get<Tmf::Agent>().SendMessageToLeaderAloc(*message, HandleLeaderResponseToFwdTmf,
                                                                    forwardContext.Get()));
    message.Release();

    // Release the ownership of `forwardContext` since `SendMessage()`
    // will own it. We take back ownership when the callback
    // `HandleLeaderResponseToFwdTmf()` is invoked.

    mForwardContexts.Push(*forwardContext.Release());

    switch (aUri)
    {
    case kUriLeaderPetition:
        Log<kUriLeaderPetition>(kForward, " to leader");
        break;
    case kUriLeaderKeepAlive:
        Log<kUriLeaderKeepAlive>(kForward, " to leader");
        break;
    default:
        break;
    }

exit:
    LogWarnOnError(error, "forward to leader");

    if ((error != kErrorNone) && (aMsg.mMessage.ReadToken(token) == kErrorNone))
    {
        SendErrorMessage(error, token);
    }

    return error;
}

void Manager::CoapDtlsSession::HandleLeaderResponseToFwdTmf(void *aContext, Coap::Msg *aMsg, otError aResult)
{
    OwnedPtr<ForwardContext> forwardContext(static_cast<ForwardContext *>(aContext));

    forwardContext->mSession.HandleLeaderResponseToFwdTmf(*forwardContext.Get(), aMsg, aResult);
}

void Manager::CoapDtlsSession::HandleLeaderResponseToFwdTmf(const ForwardContext &aForwardContext,
                                                            const Coap::Msg      *aResponse,
                                                            Error                 aResult)
{
    OwnedPtr<Coap::Message> forwardMessage;
    Error                   error;

    IgnoreError(mForwardContexts.Remove(aForwardContext));

    switch (aForwardContext.mUri)
    {
    case kUriLeaderPetition:
        Log<kUriLeaderPetition>(kReceive, " response from leader");
        break;
    case kUriLeaderKeepAlive:
        Log<kUriLeaderKeepAlive>(kReceive, " response from leader");
        break;
    default:
        break;
    }

    SuccessOrExit(error = aResult);

    forwardMessage.Reset(NewNetPriorityMessage());
    VerifyOrExit(forwardMessage != nullptr, error = kErrorNoBufs);

    if (aResponse->GetCode() == Coap::kCodeChanged)
    {
        uint8_t state;

        SuccessOrExit(error = Tlv::Find<StateTlv>(aResponse->mMessage, state));

        switch (state)
        {
        case StateTlv::kAccept:
            if (aForwardContext.mUri == kUriLeaderPetition)
            {
                uint16_t sessionId;

                SuccessOrExit(error = Tlv::Find<CommissionerSessionIdTlv>(aResponse->mMessage, sessionId));
                Get<Manager>().HandleCommissionerPetitionAccepted(*this, sessionId);
            }

            break;

        case StateTlv::kReject:
            Get<Manager>().RevokeRoleIfActiveCommissioner(*this);
            break;

        default:
            break;
        }
    }

    SuccessOrExit(error =
                      forwardMessage->Init(Coap::kTypeNonConfirmable, static_cast<Coap::Code>(aResponse->GetCode())));
    SuccessOrExit(error = forwardMessage->WriteToken(aForwardContext.mToken));

    if (aResponse->mMessage.DetermineLengthAfterOffset() > 0)
    {
        SuccessOrExit(error = forwardMessage->AppendPayloadMarker());
    }

    SuccessOrExit(error = ForwardToCommissioner(forwardMessage.PassOwnership(), aResponse->mMessage));

exit:
    if (error != kErrorNone)
    {
#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_WARN)
        const char *uriString = "Unknown";

        switch (aForwardContext.mUri)
        {
        case kUriLeaderPetition:
            uriString = UriToString<kUriLeaderPetition>();
            break;
        case kUriLeaderKeepAlive:
            uriString = UriToString<kUriLeaderKeepAlive>();
            break;
        default:
            break;
        }

        LogWarn("Forwarded %s failed - session %u, error:%s", uriString, mIndex, ErrorToString(error));
#endif

        SendErrorMessage(error, aForwardContext.mToken);
    }
}

void Manager::CoapDtlsSession::ForwardUdpProxyToCommissioner(const Message          &aMessage,
                                                             const Ip6::MessageInfo &aMessageInfo)
{
    Error error;

    SuccessOrExit(error = ForwardUdpProxy(aMessage, aMessageInfo));
    Log<kUriProxyRx>(kForward);

exit:
    LogWarnOnError(error, "forward UDP proxy");
}

Error Manager::CoapDtlsSession::ForwardUdpProxy(const Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
{
    Error                     error = kErrorNone;
    OwnedPtr<Coap::Message>   message;
    UdpEncapsulationTlvHeader udpEncapHeader;
    OffsetRange               offsetRange;

    VerifyOrExit(aMessage.GetLength() > 0);

    message.Reset(AllocateAndInitPriorityNonConfirmablePostMessage(kUriProxyRx));
    VerifyOrExit(message != nullptr, error = kErrorNoBufs);

    offsetRange.InitFromMessageOffsetToEnd(aMessage);

    udpEncapHeader.SetSourcePort(aMessageInfo.GetPeerPort());
    udpEncapHeader.SetDestinationPort(aMessageInfo.GetSockPort());

    SuccessOrExit(error = Tlv::AppendTlvHeader(*message, Tlv::kUdpEncapsulation,
                                               sizeof(UdpEncapsulationTlvHeader) + offsetRange.GetLength()));
    SuccessOrExit(error = message->Append(udpEncapHeader));
    SuccessOrExit(error = message->AppendBytesFromMessage(aMessage, offsetRange));

    SuccessOrExit(error = Tlv::Append<Ip6AddressTlv>(*message, aMessageInfo.GetPeerAddr()));

    error = SendMessage(message.PassOwnership());

exit:
    return error;
}

void Manager::CoapDtlsSession::ForwardUdpRelayToCommissioner(const Message &aMessage)
{
    Error error;

    SuccessOrExit(error = ForwardUdpRelay(aMessage));
    Log<kUriRelayRx>(kForward);

exit:
    LogWarnOnError(error, "forward UDP relay");
}

Error Manager::CoapDtlsSession::ForwardUdpRelay(const Message &aMessage)
{
    OwnedPtr<Coap::Message> forwardMessage;
    Error                   error = kErrorNone;

    forwardMessage.Reset(AllocateAndInitPriorityNonConfirmablePostMessage(kUriRelayRx));
    VerifyOrExit(forwardMessage != nullptr, error = kErrorNoBufs);

    error = ForwardToCommissioner(forwardMessage.PassOwnership(), aMessage);

exit:
    return error;
}

Error Manager::CoapDtlsSession::ForwardToCommissioner(OwnedPtr<Coap::Message> aForwardMessage, const Message &aMessage)
{
    Error       error = kErrorNone;
    OffsetRange offsetRange;

    offsetRange.InitFromMessageOffsetToEnd(aMessage);
    SuccessOrExit(error = aForwardMessage->AppendBytesFromMessage(aMessage, offsetRange));

    error = SendMessage(aForwardMessage.PassOwnership());

exit:
    return error;
}

void Manager::CoapDtlsSession::SendErrorMessage(Error aError, const Coap::Token &aToken)
{
    Error                   error = kErrorNone;
    OwnedPtr<Coap::Message> message;
    Coap::Message::Code     code;

    message.Reset(NewNetPriorityMessage());
    VerifyOrExit(message != nullptr, error = kErrorNoBufs);

    code = (aError == kErrorParse) ? Coap::kCodeBadRequest : Coap::kCodeInternalError;

    SuccessOrExit(error = message->Init(Coap::kTypeNonConfirmable, code));
    SuccessOrExit(error = message->WriteToken(aToken));

    SuccessOrExit(error = SendMessage(message.PassOwnership()));

exit:
    LogWarnOnError(error, "send error CoAP message");
}

void Manager::CoapDtlsSession::HandleTmfProxyTx(Coap::Msg &aMsg)
{
    Error                     error = kErrorNone;
    OwnedPtr<Message>         message;
    Ip6::MessageInfo          messageInfo;
    OffsetRange               offsetRange;
    UdpEncapsulationTlvHeader udpEncapHeader;

    Log<kUriProxyTx>(kReceive);

#if OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE
    if (IsEnroller())
    {
        VerifyOrExit(Get<Admitter>().IsActiveCommissioner(), error = kErrorInvalidState);
        messageInfo.SetSockAddr(Get<Admitter>().mCommissionerPetitioner.GetAloc());
    }
    else
#endif
    {
        VerifyOrExit(IsActiveCommissioner(), error = kErrorInvalidState);
        messageInfo.SetSockAddr(Get<Manager>().GetCommissionerAloc());
    }

    SuccessOrExit(error = Tlv::FindTlvValueOffsetRange(aMsg.mMessage, Tlv::kUdpEncapsulation, offsetRange));

    SuccessOrExit(error = aMsg.mMessage.ReadAndAdvance(offsetRange, udpEncapHeader));

    VerifyOrExit(udpEncapHeader.GetSourcePort() > 0 && udpEncapHeader.GetDestinationPort() > 0, error = kErrorDrop);

    message.Reset(Get<Ip6::Udp>().NewMessage());
    VerifyOrExit(message != nullptr, error = kErrorNoBufs);

    SuccessOrExit(error = message->AppendBytesFromMessage(aMsg.mMessage, offsetRange));

    messageInfo.SetSockPort(udpEncapHeader.GetSourcePort());
    messageInfo.SetPeerPort(udpEncapHeader.GetDestinationPort());

    SuccessOrExit(error = Tlv::Find<Ip6AddressTlv>(aMsg.mMessage, messageInfo.GetPeerAddr()));

    // On success the message ownership is transferred.
    SuccessOrExit(error = Get<Ip6::Udp>().SendDatagram(*message, messageInfo));
    message.Release();

    LogInfo("Sent proxy UDP to %s", messageInfo.GetPeerAddr().ToString().AsCString());

exit:
    LogWarnOnError(error, "send proxy stream");
}

void Manager::CoapDtlsSession::HandleTmfRelayTx(Coap::Msg &aMsg)
{
    Error                   error = kErrorNone;
    uint16_t                joinerRouterRloc;
    OwnedPtr<Coap::Message> message;
    OffsetRange             offsetRange;

    VerifyOrExit(aMsg.IsNonConfirmable());

#if OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE
    if (IsEnroller())
    {
        VerifyOrExit(Get<Admitter>().IsActiveCommissioner(), error = kErrorInvalidState);
    }
    else
#endif
    {
        VerifyOrExit(IsActiveCommissioner(), error = kErrorInvalidState);
    }

    Log<kUriRelayTx>(kReceive);

    SuccessOrExit(error = Tlv::Find<JoinerRouterLocatorTlv>(aMsg.mMessage, joinerRouterRloc));

    message.Reset(Get<Tmf::Agent>().AllocateAndInitPriorityNonConfirmablePostMessage(kUriRelayTx));
    VerifyOrExit(message != nullptr, error = kErrorNoBufs);

    offsetRange.InitFromMessageOffsetToEnd(aMsg.mMessage);
    SuccessOrExit(error = message->AppendBytesFromMessage(aMsg.mMessage, offsetRange));

    SuccessOrExit(error = Get<Tmf::Agent>().SendMessageToRloc(*message, joinerRouterRloc));
    message.Release();

    LogInfo("Forward %s to joiner router 0x%04x", UriToString<kUriRelayTx>(), joinerRouterRloc);

exit:
    LogWarnOnError(error, "forward to joiner router");
}

void Manager::CoapDtlsSession::HandleTmfDatasetGet(Coap::Message &aMessage, Uri aUri)
{
    Error                   error = kErrorNone;
    OwnedPtr<Coap::Message> response;

    // When processing `MGMT_GET` request directly on Border Agent,
    // the Security Policy flags (O-bit) should be ignored to allow
    // the commissioner candidate to get the full Operational Dataset.

    switch (aUri)
    {
    case kUriActiveGet:
        Log<kUriActiveGet>(kReceive);
        response.Reset(
            Get<ActiveDatasetManager>().ProcessGetRequest(aMessage, DatasetManager::kIgnoreSecurityPolicyFlags));
        Get<Manager>().mCounters.mMgmtActiveGets++;
#if OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE && OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
        if (Get<EphemeralKeyManager>().OwnsSession(*this))
        {
            Get<HistoryTracker::Local>().RecordEpskcEvent(HistoryTracker::Local::kEpskcRetrievedActiveDataset);
        }
#endif
        break;

    case kUriPendingGet:
        Log<kUriPendingGet>(kReceive);
        response.Reset(
            Get<PendingDatasetManager>().ProcessGetRequest(aMessage, DatasetManager::kIgnoreSecurityPolicyFlags));
        Get<Manager>().mCounters.mMgmtPendingGets++;
#if OPENTHREAD_CONFIG_BORDER_AGENT_EPHEMERAL_KEY_ENABLE && OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
        if (Get<EphemeralKeyManager>().OwnsSession(*this))
        {
            Get<HistoryTracker::Local>().RecordEpskcEvent(HistoryTracker::Local::kEpskcRetrievedPendingDataset);
        }
#endif
        break;

    case kUriCommissionerGet:
        Log<kUriCommissionerGet>(kReceive);
        response.Reset(Get<NetworkData::Leader>().ProcessCommissionerGetRequest(aMessage));
        break;

    default:
        break;
    }

    VerifyOrExit(response != nullptr, error = kErrorParse);

    SuccessOrExit(error = SendMessage(response.PassOwnership()));

    switch (aUri)
    {
    case kUriActiveGet:
        Log<kUriActiveGet>(kSend, " response");
        break;
    case kUriPendingGet:
        Log<kUriPendingGet>(kSend, " response");
        break;
    case kUriCommissionerGet:
        Log<kUriCommissionerGet>(kSend, " response");
        break;
    default:
        break;
    }

exit:
    LogWarnOnError(error, "send Active/Pending/CommissionerGet response");
}

void Manager::CoapDtlsSession::HandleTimer(Timer &aTimer)
{
    static_cast<CoapDtlsSession *>(static_cast<TimerMilliContext &>(aTimer).GetContext())->HandleTimer();
}

void Manager::CoapDtlsSession::HandleTimer(void)
{
    if (IsConnected())
    {
#if OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE
        ResignEnroller();
#endif
        LogInfo("Session %u timed out - disconnecting", mIndex);
    }
    else
    {
        LogInfo("Session %u handshake timeout - disconnecting", mIndex);
    }

    DisconnectTimeout();
}

void Manager::CoapDtlsSession::CopyInfoTo(SessionInfo &aInfo, UptimeMsec aUptimeNow) const
{
    aInfo.mPeerSockAddr.mAddress = GetMessageInfo().GetPeerAddr();
    aInfo.mPeerSockAddr.mPort    = GetMessageInfo().GetPeerPort();
    aInfo.mIsConnected           = IsConnected();
    aInfo.mIsCommissioner        = IsActiveCommissioner();
    aInfo.mLifetime              = aUptimeNow - GetAllocationTime();
}

#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)

void Manager::CoapDtlsSession::LogUri(Action aAction, const char *aUriString, const char *aTxt)
{
#define ActionMapList(_)   \
    _(kReceive, "Receive") \
    _(kSend, "Send")       \
    _(kForward, "Forward")

    DefineEnumStringArray(ActionMapList);

    LogInfo("%s %s%s - session %u", kStrings[aAction], aUriString, aTxt, mIndex);
}

#endif

//----------------------------------------------------------------------------------------------------------------------
// `Manager::CoapDtlsSession::ForwardContext`

Manager::CoapDtlsSession::ForwardContext::ForwardContext(CoapDtlsSession     &aSession,
                                                         const Coap::Message &aMessage,
                                                         Uri                  aUri)
    : mSession(aSession)
    , mUri(aUri)
{
    if (aMessage.ReadToken(mToken) != kErrorNone)
    {
        mToken.Clear();
    }
}

} // namespace BorderAgent
} // namespace MeshCoP
} // namespace ot

#endif // OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE
