/*
 *  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 CLI for Border Router.
 */

#include "cli_br.hpp"

#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE

#include <string.h>

#include "cli/cli.hpp"

namespace ot {
namespace Cli {

/**
 * @cli br init
 * @code
 * br init 2 1
 * Done
 * @endcode
 * @cparam br init @ca{infrastructure-network-index} @ca{is-running}
 * @par
 * Initializes the Border Routing Manager.
 * @sa otBorderRoutingInit
 */
template <> otError Br::Process<Cmd("init")>(Arg aArgs[])
{
    otError  error = OT_ERROR_NONE;
    uint32_t ifIndex;
    bool     isRunning;

    SuccessOrExit(error = aArgs[0].ParseAsUint32(ifIndex));
    SuccessOrExit(error = aArgs[1].ParseAsBool(isRunning));
    VerifyOrExit(aArgs[2].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
    error = otBorderRoutingInit(GetInstancePtr(), ifIndex, isRunning);

exit:
    return error;
}

/**
 * @cli br infraif
 * @code
 * br infraif
 * if-index:2, is-running:yes
 * Done
 * @endcode
 * @par
 * Gets the interface index and running state of the configured infrastructure interface.
 */
template <> otError Br::Process<Cmd("infraif")>(Arg aArgs[])
{
    otError  error = OT_ERROR_NONE;
    uint32_t ifIndex;
    bool     isRunning;

    VerifyOrExit(aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
    SuccessOrExit(error = otBorderRoutingGetInfraIfInfo(GetInstancePtr(), &ifIndex, &isRunning));
    OutputLine("if-index:%lu, is-running:%s", ToUlong(ifIndex), ToYesNo(isRunning));

exit:
    return error;
}

/**
 * @cli br enable
 * @code
 * br enable
 * Done
 * @endcode
 * @par
 * Enables the Border Routing Manager.
 * @sa otBorderRoutingSetEnabled
 */
template <> otError Br::Process<Cmd("enable")>(Arg aArgs[])
{
    otError error = OT_ERROR_NONE;

    VerifyOrExit(aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
    error = otBorderRoutingSetEnabled(GetInstancePtr(), true);

exit:
    return error;
}

/**
 * @cli br disable
 * @code
 * br disable
 * Done
 * @endcode
 * @par
 * Disables the Border Routing Manager.
 * @sa otBorderRoutingSetEnabled
 */
template <> otError Br::Process<Cmd("disable")>(Arg aArgs[])
{
    otError error = OT_ERROR_NONE;

    VerifyOrExit(aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
    error = otBorderRoutingSetEnabled(GetInstancePtr(), false);

exit:
    return error;
}

/**
 * @cli br state
 * @code
 * br state
 * running
 * @endcode
 * @par api_copy
 * #otBorderRoutingGetState
 */
template <> otError Br::Process<Cmd("state")>(Arg aArgs[])
{
    otError error = OT_ERROR_NONE;

    VerifyOrExit(aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
    OutputLine("%s", BorderRoutingStateToString(otBorderRoutingGetState(GetInstancePtr())));

exit:
    return error;
}

#if OPENTHREAD_CONFIG_BORDER_ROUTING_MULTI_AIL_DETECTION_ENABLE

template <> otError Br::Process<Cmd("multiail")>(Arg aArgs[])
{
    otError error = OT_ERROR_NONE;

    /**
     * @cli br multiail
     * @code
     * br multiail
     * not detected
     * @endcode
     * @par api_copy
     * #otBorderRoutingIsMultiAilDetected
     */
    if (aArgs[0].IsEmpty())
    {
        OutputLine("%sdetected", otBorderRoutingIsMultiAilDetected(GetInstancePtr()) ? "" : "not ");
    }
    /**
     * @cli br multiail callback
     * @code
     * br multiail callback enable
     * Done
     * @endcode
     * @cparam br multiail callback @ca{enable|disable}
     * @par api_copy
     * #otBorderRoutingSetMultiAilCallback
     */
    else if (aArgs[0] == "callback")
    {
        bool                            enable;
        otBorderRoutingMultiAilCallback callback = nullptr;

        SuccessOrExit(error = ParseEnableOrDisable(aArgs[1], enable));

        if (enable)
        {
            callback = &HandleMultiAilDetected;
        }

        otBorderRoutingSetMultiAilCallback(GetInstancePtr(), callback, this);
    }
    /**
     * @cli br multiail state
     * @code
     * br multiail state
     * Enabled: yes
     * Running: yes
     * Detected: no
     * Done
     * @endcode
     * @par
     * Outputs full state of multi-AIL detector:
     * - Whether the detector is enabled.
     * - Whether the detector is running (when it is enabled and the infra-if interface is also active).
     * - Whether multi-AIL was detected.
     */
    else if (aArgs[0] == "state")
    {
        OutputLine("Enabled: %s", ToYesNo(otBorderRoutingIsMultiAilDetectionEnabled(GetInstancePtr())));
        OutputLine("Running: %s", ToYesNo(otBorderRoutingIsMultiAilDetectionRunning(GetInstancePtr())));
        OutputLine("Detected: %s", ToYesNo(otBorderRoutingIsMultiAilDetected(GetInstancePtr())));
    }
    /**
     * @cli br multiail (enable, disable)
     * @code
     * br multiail enable
     * Done
     * @endcode
     * @cparam br multiail @ca{enable|disable}
     * @par api_copy
     * #otBorderRoutingSetMultiAilDetectionEnabled
     */
    else if (ProcessEnableDisable(aArgs, otBorderRoutingSetMultiAilDetectionEnabled) == OT_ERROR_NONE)
    {
    }
    else
    {
        error = OT_ERROR_INVALID_ARGS;
    }

exit:
    return error;
}

void Br::HandleMultiAilDetected(bool aDetected, void *aContext)
{
    static_cast<Br *>(aContext)->HandleMultiAilDetected(aDetected);
}

void Br::HandleMultiAilDetected(bool aDetected)
{
    OutputLine("BR multi AIL callback: %s", aDetected ? "detected" : "cleared");
}

#endif // OPENTHREAD_CONFIG_BORDER_ROUTING_MULTI_AIL_DETECTION_ENABLE

otError Br::ParsePrefixTypeArgs(Arg aArgs[], PrefixType &aFlags)
{
    otError error = OT_ERROR_NONE;

    aFlags = 0;

    if (aArgs[0].IsEmpty())
    {
        aFlags = kPrefixTypeFavored | kPrefixTypeLocal;
        ExitNow();
    }

    if (aArgs[0] == "local")
    {
        aFlags = kPrefixTypeLocal;
    }
    else if (aArgs[0] == "favored")
    {
        aFlags = kPrefixTypeFavored;
    }
    else
    {
        ExitNow(error = OT_ERROR_INVALID_ARGS);
    }

    VerifyOrExit(aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);

exit:
    return error;
}

template <> otError Br::Process<Cmd("omrconfig")>(Arg aArgs[])
{
    otError                  error = OT_ERROR_NONE;
    otIp6Prefix              customPrefix;
    otRoutePreference        preference;
    otBorderRoutingOmrConfig omrConfig;

    /**
     * @cli br omrconfig
     * @code
     * br omrconfig
     * auto
     * Done
     * @endcode
     * @code
     * br omrconfig
     * custom (fd00:0:0:0::/64, prf:med)
     * Done
     * @endcode
     * @par
     * Outputs current OMR prefix configuration mode.
     * @sa otBorderRoutingGetOmrConfig
     */
    if (aArgs[0].IsEmpty())
    {
        omrConfig = otBorderRoutingGetOmrConfig(GetInstancePtr(), &customPrefix, &preference);

        switch (omrConfig)
        {
        case OT_BORDER_ROUTING_OMR_CONFIG_AUTO:
            OutputLine("auto");
            break;
        case OT_BORDER_ROUTING_OMR_CONFIG_CUSTOM:
            OutputFormat("custom (");
            OutputIp6Prefix(customPrefix);
            OutputLine(", prf:%s)", PreferenceToString(preference));
            break;
        case OT_BORDER_ROUTING_OMR_CONFIG_DISABLED:
            OutputLine("disabled");
            break;
        }
    }
    else
    {
        ClearAllBytes(customPrefix);
        preference = OT_ROUTE_PREFERENCE_MED;

        /**
         * @cli br omrconfig auto
         * @code
         * br omrconfig auto
         * Done
         * @endcode
         * @par
         * Sets OMR prefix configuration mode to `auto` In this mode, the Border Routing Manager automatically
         * selects and manages the OMR prefix.
         */
        if (aArgs[0] == "auto")
        {
            omrConfig = OT_BORDER_ROUTING_OMR_CONFIG_AUTO;
            VerifyOrExit(aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
        }
        /**
         * @cli br omrconfig custom
         * @code
         * br omrconfig custom fd00::/64 med
         * Done
         * @endcode
         * @cparam br omrconfig custom @ca{prefix} [@ca{high}|@ca{med}|@ca{low}]
         * @par
         * Sets OMR prefix configuration mode to `custom`. In this mode, a custom OMR prefix and its associated
         * preference are used.
         */
        else if (aArgs[0] == "custom")
        {
            omrConfig = OT_BORDER_ROUTING_OMR_CONFIG_CUSTOM;

            SuccessOrExit(error = aArgs[1].ParseAsIp6Prefix(customPrefix));
            SuccessOrExit(error = Interpreter::ParsePreference(aArgs[2], preference));
            VerifyOrExit(aArgs[3].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
        }
        /**
         * @cli br omrconfig disable
         * @code
         * br omrconfig disable
         * Done
         * @endcode
         * @cparam br omrconfig disable
         * @par
         * Sets OMR prefix configuration mode to `disable` which prevents the Border Routing Manager from adding any
         * local or DHCPv6 PD OMR prefixes to the Network Data.
         */
        else if (aArgs[0] == "disable")
        {
            omrConfig = OT_BORDER_ROUTING_OMR_CONFIG_DISABLED;
        }
        else
        {
            ExitNow(error = OT_ERROR_INVALID_ARGS);
        }

        error = otBorderRoutingSetOmrConfig(GetInstancePtr(), omrConfig, &customPrefix, preference);
    }

exit:
    return error;
}

/**
 * @cli br omrprefix
 * @code
 * br omrprefix
 * Local: fdfc:1ff5:1512:5622::/64
 * Favored: fdfc:1ff5:1512:5622::/64 prf:low
 * Done
 * @endcode
 * @par
 * Outputs both local and favored OMR prefix.
 * @sa otBorderRoutingGetOmrPrefix
 * @sa otBorderRoutingGetFavoredOmrPrefix
 */
template <> otError Br::Process<Cmd("omrprefix")>(Arg aArgs[])
{
    otError    error = OT_ERROR_NONE;
    PrefixType outputPrefixTypes;

    SuccessOrExit(error = ParsePrefixTypeArgs(aArgs, outputPrefixTypes));

    /**
     * @cli br omrprefix local
     * @code
     * br omrprefix local
     * fdfc:1ff5:1512:5622::/64
     * Done
     * @endcode
     * @par api_copy
     * #otBorderRoutingGetOmrPrefix
     */
    if (outputPrefixTypes & kPrefixTypeLocal)
    {
        otIp6Prefix local;

        SuccessOrExit(error = otBorderRoutingGetOmrPrefix(GetInstancePtr(), &local));

        OutputFormat("%s", outputPrefixTypes == kPrefixTypeLocal ? "" : "Local: ");
        OutputIp6PrefixLine(local);
    }

    /**
     * @cli br omrprefix favored
     * @code
     * br omrprefix favored
     * fdfc:1ff5:1512:5622::/64 prf:low
     * Done
     * @endcode
     * @par api_copy
     * #otBorderRoutingGetFavoredOmrPrefix
     */
    if (outputPrefixTypes & kPrefixTypeFavored)
    {
        otIp6Prefix       favored;
        otRoutePreference preference;

        SuccessOrExit(error = otBorderRoutingGetFavoredOmrPrefix(GetInstancePtr(), &favored, &preference));

        OutputFormat("%s", outputPrefixTypes == kPrefixTypeFavored ? "" : "Favored: ");
        OutputIp6Prefix(favored);
        OutputLine(" prf:%s", PreferenceToString(preference));
    }

exit:
    return error;
}

/**
 * @cli br onlinkprefix
 * @code
 * br onlinkprefix
 * Local: fd41:2650:a6f5:0::/64
 * Favored: 2600::0:1234:da12::/64
 * Done
 * @endcode
 * @par
 * Outputs both local and favored on-link prefixes.
 * @sa otBorderRoutingGetOnLinkPrefix
 * @sa otBorderRoutingGetFavoredOnLinkPrefix
 */
template <> otError Br::Process<Cmd("onlinkprefix")>(Arg aArgs[])
{
    otError    error = OT_ERROR_NONE;
    PrefixType outputPrefixTypes;

#if OPENTHREAD_CONFIG_BORDER_ROUTING_TESTING_API_ENABLE
    if (aArgs[0] == "test")
    {
        otIp6Prefix prefix;

        SuccessOrExit(error = aArgs[1].ParseAsIp6Prefix(prefix));
        otBorderRoutingSetOnLinkPrefix(GetInstancePtr(), &prefix);
        ExitNow();
    }
#endif

    error = ParsePrefixTypeArgs(aArgs, outputPrefixTypes);

    SuccessOrExit(error);

    /**
     * @cli br onlinkprefix local
     * @code
     * br onlinkprefix local
     * fd41:2650:a6f5:0::/64
     * Done
     * @endcode
     * @par api_copy
     * #otBorderRoutingGetOnLinkPrefix
     */
    if (outputPrefixTypes & kPrefixTypeLocal)
    {
        otIp6Prefix local;

        SuccessOrExit(error = otBorderRoutingGetOnLinkPrefix(GetInstancePtr(), &local));

        OutputFormat("%s", outputPrefixTypes == kPrefixTypeLocal ? "" : "Local: ");
        OutputIp6PrefixLine(local);
    }

    /**
     * @cli br onlinkprefix favored
     * @code
     * br onlinkprefix favored
     * 2600::0:1234:da12::/64
     * Done
     * @endcode
     * @par api_copy
     * #otBorderRoutingGetFavoredOnLinkPrefix
     */
    if (outputPrefixTypes & kPrefixTypeFavored)
    {
        otIp6Prefix favored;

        SuccessOrExit(error = otBorderRoutingGetFavoredOnLinkPrefix(GetInstancePtr(), &favored));

        OutputFormat("%s", outputPrefixTypes == kPrefixTypeFavored ? "" : "Favored: ");
        OutputIp6PrefixLine(favored);
    }

exit:
    return error;
}

#if OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE

/**
 * @cli br nat64prefix
 * @code
 * br nat64prefix
 * Local: fd14:1078:b3d5:b0b0:0:0::/96
 * Favored: fd14:1078:b3d5:b0b0:0:0::/96 prf:low
 * Done
 * @endcode
 * @par
 * Outputs both local and favored NAT64 prefixes.
 * @sa otBorderRoutingGetNat64Prefix
 * @sa otBorderRoutingGetFavoredNat64Prefix
 */
template <> otError Br::Process<Cmd("nat64prefix")>(Arg aArgs[])
{
    otError    error = OT_ERROR_NONE;
    PrefixType outputPrefixTypes;

    SuccessOrExit(error = ParsePrefixTypeArgs(aArgs, outputPrefixTypes));

    /**
     * @cli br nat64prefix local
     * @code
     * br nat64prefix local
     * fd14:1078:b3d5:b0b0:0:0::/96
     * Done
     * @endcode
     * @par api_copy
     * #otBorderRoutingGetNat64Prefix
     */
    if (outputPrefixTypes & kPrefixTypeLocal)
    {
        otIp6Prefix local;

        SuccessOrExit(error = otBorderRoutingGetNat64Prefix(GetInstancePtr(), &local));

        OutputFormat("%s", outputPrefixTypes == kPrefixTypeLocal ? "" : "Local: ");
        OutputIp6PrefixLine(local);
    }

    /**
     * @cli br nat64prefix favored
     * @code
     * br nat64prefix favored
     * fd14:1078:b3d5:b0b0:0:0::/96 prf:low
     * Done
     * @endcode
     * @par api_copy
     * #otBorderRoutingGetFavoredNat64Prefix
     */
    if (outputPrefixTypes & kPrefixTypeFavored)
    {
        otIp6Prefix       favored;
        otRoutePreference preference;

        SuccessOrExit(error = otBorderRoutingGetFavoredNat64Prefix(GetInstancePtr(), &favored, &preference));

        OutputFormat("%s", outputPrefixTypes == kPrefixTypeFavored ? "" : "Favored: ");
        OutputIp6Prefix(favored);
        OutputLine(" prf:%s", PreferenceToString(preference));
    }

exit:
    return error;
}

/**
 * @cli br nat64prefixtable
 * @code
 * br nat64prefixtable
 * prefix:fd00:1234:5678:0:0:0::/96, ms-since-rx:29526, lifetime:1800, router:fe80:0:0:0:0:0:0:1 (M:0 O:0 S:1)
 * prefix:fd11:2233:4455:0:0:0::/96, ms-since-rx:29527, lifetime:1800, router:fe80:0:0:0:0:0:0:1 (M:0 O:0 S:1)
 * Done
 * @endcode
 * @par
 * Get the RA-discovered NAT64 prefixes by Border Routing Manager on the infrastructure link.
 * Info per prefix entry:
 * - The prefix
 * - Milliseconds since last received Router Advertisement containing this prefix
 * - Prefix lifetime in seconds
 * - The router IPv6 address which advertises this prefix
 * - Flags in received Router Advertisement header:
 *   - M: Managed Address Config flag
 *   - O: Other Config flag
 *   - S: SNAC Router flag
 * @sa otBorderRoutingGetNextNat64PrefixEntry
 */
template <> otError Br::Process<Cmd("nat64prefixtable")>(Arg aArgs[])
{
    otError                            error = OT_ERROR_NONE;
    otBorderRoutingPrefixTableIterator iterator;
    otBorderRoutingNat64PrefixEntry    entry;

    VerifyOrExit(aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);

    otBorderRoutingPrefixTableInitIterator(GetInstancePtr(), &iterator);

    while (otBorderRoutingGetNextNat64PrefixEntry(GetInstancePtr(), &iterator, &entry) == OT_ERROR_NONE)
    {
        char string[OT_IP6_PREFIX_STRING_SIZE];

        otIp6PrefixToString(&entry.mPrefix, string, sizeof(string));
        OutputFormat("prefix:%s, ms-since-rx:%lu, lifetime:%lu, router:", string, ToUlong(entry.mMsecSinceLastUpdate),
                     ToUlong(entry.mLifetime));
        OutputRouterInfo(entry.mRouter, kShortVersion);
    }

exit:
    return error;
}
#endif // OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE

#if OPENTHREAD_CONFIG_BORDER_ROUTING_TRACK_PEER_BR_INFO_ENABLE

template <> otError Br::Process<Cmd("peers")>(Arg aArgs[])
{
    otError error = OT_ERROR_NONE;

    /**
     * @cli br peers
     * @code
     * br peers
     * rloc16:0x5c00 age:00:00:49
     * rloc16:0xf800 age:00:01:51
     * Done
     * @endcode
     * @par
     * Get the list of peer BRs found in Network Data entries.
     * `OPENTHREAD_CONFIG_BORDER_ROUTING_TRACK_PEER_BR_INFO_ENABLE` is required.
     * Peer BRs are other devices within the Thread mesh that provide external IP connectivity. A device is considered
     * to provide external IP connectivity if at least one of the following conditions is met regarding its Network
     * Data entries:
     * - It has added at least one external route entry.
     * - It has added at least one prefix entry with both the default-route and on-mesh flags set.
     * - It has added at least one domain prefix (with both the domain and on-mesh flags set).
     * The list of peer BRs specifically excludes the current device, even if its is itself acting as a BR.
     * Info per BR entry:
     * - RLOC16 of the BR
     * - Age as the duration interval since this BR appeared in Network Data. It is formatted as `{hh}:{mm}:{ss}` for
     *   hours, minutes, seconds, if the duration is less than 24 hours. If the duration is 24 hours or more, the
     *   format is `{dd}d.{hh}:{mm}:{ss}` for days, hours, minutes, seconds.
     * @sa otBorderRoutingGetNextPrefixTableEntry
     */
    if (aArgs[0].IsEmpty())
    {
        otBorderRoutingPrefixTableIterator   iterator;
        otBorderRoutingPeerBorderRouterEntry peerBrEntry;
        char                                 ageString[OT_DURATION_STRING_SIZE];

        otBorderRoutingPrefixTableInitIterator(GetInstancePtr(), &iterator);

        while (otBorderRoutingGetNextPeerBrEntry(GetInstancePtr(), &iterator, &peerBrEntry) == OT_ERROR_NONE)
        {
            otConvertDurationInSecondsToString(peerBrEntry.mAge, ageString, sizeof(ageString));
            OutputLine("rloc16:0x%04x age:%s", peerBrEntry.mRloc16, ageString);
        }
    }
    /**
     * @cli br peers count
     * @code
     * br peers count
     * 2 min-age:00:00:47
     * Done
     * @endcode
     * @par api_copy
     * #otBorderRoutingCountPeerBrs
     */
    else if (aArgs[0] == "count")
    {
        uint32_t minAge;
        uint16_t count;
        char     ageString[OT_DURATION_STRING_SIZE];

        VerifyOrExit(aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);

        count = otBorderRoutingCountPeerBrs(GetInstancePtr(), &minAge);
        otConvertDurationInSecondsToString(minAge, ageString, sizeof(ageString));
        OutputLine("%u min-age:%s", count, ageString);
    }
    else
    {
        error = OT_ERROR_INVALID_ARGS;
    }

exit:
    return error;
}

#endif // OPENTHREAD_CONFIG_BORDER_ROUTING_TRACK_PEER_BR_INFO_ENABLE

/**
 * @cli br prefixtable
 * @code
 * br prefixtable
 * prefix:fd00:1234:5678:0::/64, on-link:no, ms-since-rx:29526, lifetime:1800, route-prf:med,
 * router:ff02:0:0:0:0:0:0:1 (M:0 O:0 S:1)
 * prefix:1200:abba:baba:0::/64, on-link:yes, ms-since-rx:29527, lifetime:1800, preferred:1800,
 * router:ff02:0:0:0:0:0:0:1 (M:0 O:0 S:1)
 * Done
 * @endcode
 * @par
 * Get the discovered prefixes by Border Routing Manager on the infrastructure link.
 * Info per prefix entry:
 * - The prefix
 * - Whether the prefix is on-link or route
 * - Milliseconds since last received Router Advertisement containing this prefix
 * - Prefix lifetime in seconds
 * - Preferred lifetime in seconds only if prefix is on-link
 * - Route preference (low, med, high) only if prefix is route (not on-link)
 * - The router IPv6 address which advertises this prefix
 * - Flags in received Router Advertisement header:
 *   - M: Managed Address Config flag
 *   - O: Other Config flag
 *   - S: SNAC Router flag
 * @sa otBorderRoutingGetNextPrefixTableEntry
 */
template <> otError Br::Process<Cmd("prefixtable")>(Arg aArgs[])
{
    otError                            error = OT_ERROR_NONE;
    otBorderRoutingPrefixTableIterator iterator;
    otBorderRoutingPrefixTableEntry    entry;

    VerifyOrExit(aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);

    otBorderRoutingPrefixTableInitIterator(GetInstancePtr(), &iterator);

    while (otBorderRoutingGetNextPrefixTableEntry(GetInstancePtr(), &iterator, &entry) == OT_ERROR_NONE)
    {
        char string[OT_IP6_PREFIX_STRING_SIZE];

        otIp6PrefixToString(&entry.mPrefix, string, sizeof(string));
        OutputFormat("prefix:%s, on-link:%s, ms-since-rx:%lu, lifetime:%lu, ", string, ToYesNo(entry.mIsOnLink),
                     ToUlong(entry.mMsecSinceLastUpdate), ToUlong(entry.mValidLifetime));

        if (entry.mIsOnLink)
        {
            OutputFormat("preferred:%lu, ", ToUlong(entry.mPreferredLifetime));
        }
        else
        {
            OutputFormat("route-prf:%s, ", PreferenceToString(entry.mRoutePreference));
        }

        OutputFormat("router:");
        OutputRouterInfo(entry.mRouter, kShortVersion);
    }

exit:
    return error;
}

/**
 * @cli br rdnsstable
 * @code
 * br rdnsstable
 * fd00:1234:5678::1, lifetime:500, ms-since-rx:29526, router:ff02:0:0:0:0:0:0:1 (M:0 O:0 S:1)
 * fd00:aaaa::2, lifetime:500, ms-since-rx:107, router:ff02:0:0:0:0:0:0:1 (M:0 O:0 S:1)
 * Done
 * @endcode
 * @par
 * Get the discovered Recursive DNS Server (RDNSS) address table by Border Routing Manager on the infrastructure link.
 * Info per entry:
 * - IPv6 address
 * - Lifetime in seconds
 * - Milliseconds since last received Router Advertisement containing this address
 * - The router IPv6 address which advertised this prefix
 * - Flags in received Router Advertisement header:
 *   - M: Managed Address Config flag
 *   - O: Other Config flag
 *   - S: SNAC Router flag
 * @sa otBorderRoutingGetNextRdnssAddrEntry
 */
template <> otError Br::Process<Cmd("rdnsstable")>(Arg aArgs[])
{
    otError                            error = OT_ERROR_NONE;
    otBorderRoutingPrefixTableIterator iterator;
    otBorderRoutingRdnssAddrEntry      entry;

    VerifyOrExit(aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);

    otBorderRoutingPrefixTableInitIterator(GetInstancePtr(), &iterator);

    while (otBorderRoutingGetNextRdnssAddrEntry(GetInstancePtr(), &iterator, &entry) == OT_ERROR_NONE)
    {
        char string[OT_IP6_ADDRESS_STRING_SIZE];

        otIp6AddressToString(&entry.mAddress, string, sizeof(string));
        OutputFormat("%s, lifetime:%lu, ms-since-rx:%lu, router:", string, ToUlong(entry.mLifetime),
                     ToUlong(entry.mMsecSinceLastUpdate));
        OutputRouterInfo(entry.mRouter, kShortVersion);
    }

exit:
    return error;
}

/**
 * @cli br ifaddrs
 * @code
 * br ifaddrs
 * fe80::896:228b:4ae0:8609, sec-since-use:15
 * Done
 * @endcode
 * @par
 * Get the infrastructure interface addresses. These are addresses used by the BR itself, for example, when sending
 * Router Advertisements.
 * Info per entry:
 * - IPv6 address
 * - Seconds since the last RA was sent from this BR using this address.
 * @sa otBorderRoutingGetNextIfAddrEntry
 */
template <> otError Br::Process<Cmd("ifaddrs")>(Arg aArgs[])
{
    otError                            error = OT_ERROR_NONE;
    otBorderRoutingPrefixTableIterator iterator;
    otBorderRoutingIfAddrEntry         entry;

    VerifyOrExit(aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);

    otBorderRoutingPrefixTableInitIterator(GetInstancePtr(), &iterator);

    while (otBorderRoutingGetNextIfAddrEntry(GetInstancePtr(), &iterator, &entry) == OT_ERROR_NONE)
    {
        char string[OT_IP6_ADDRESS_STRING_SIZE];

        otIp6AddressToString(&entry.mAddress, string, sizeof(string));
        OutputLine("%s, sec-since-use:%lu", string, ToUlong(entry.mSecSinceLastUse));
    }

exit:
    return error;
}

#if OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_ENABLE

const char *Br::Dhcp6PdStateToString(otBorderRoutingDhcp6PdState aState)
{
    static const char *const kDhcpv6PdStateStrings[] = {
        "disabled", // (0) OT_BORDER_ROUTING_DHCP6_PD_STATE_DISABLED
        "stopped",  // (1) OT_BORDER_ROUTING_DHCP6_PD_STATE_STOPPED
        "running",  // (2) OT_BORDER_ROUTING_DHCP6_PD_STATE_RUNNING
        "idle",     // (3) OT_BORDER_ROUTING_DHCP6_PD_STATE_IDLE
    };

    static_assert(0 == OT_BORDER_ROUTING_DHCP6_PD_STATE_DISABLED, "DHCP6_PD_STATE_DISABLED value is incorrect");
    static_assert(1 == OT_BORDER_ROUTING_DHCP6_PD_STATE_STOPPED, "DHCP6_PD_STATE_STOPPED value is incorrect");
    static_assert(2 == OT_BORDER_ROUTING_DHCP6_PD_STATE_RUNNING, "DHCP6_PD_STATE_RUNNING value is incorrect");
    static_assert(3 == OT_BORDER_ROUTING_DHCP6_PD_STATE_IDLE, "DHCP6_PD_STATE_IDLE value is incorrect");

    return Stringify(aState, kDhcpv6PdStateStrings);
}

template <> otError Br::Process<Cmd("pd")>(Arg aArgs[])
{
    otError error = OT_ERROR_NONE;

    /**
     * @cli br pd (enable,disable)
     * @code
     * br pd enable
     * Done
     * @endcode
     * @code
     * br pd disable
     * Done
     * @endcode
     * @cparam br pd @ca{enable|disable}
     * @par api_copy
     * #otBorderRoutingDhcp6PdSetEnabled
     */
    if (ProcessEnableDisable(aArgs, otBorderRoutingDhcp6PdSetEnabled) == OT_ERROR_NONE)
    {
    }
    /**
     * @cli br pd state
     * @code
     * br pd state
     * running
     * Done
     * @endcode
     * @par api_copy
     * #otBorderRoutingDhcp6PdGetState
     */
    else if (aArgs[0] == "state")
    {
        OutputLine("%s", Dhcp6PdStateToString(otBorderRoutingDhcp6PdGetState(GetInstancePtr())));
    }
    /**
     * @cli br pd omrprefix
     * @code
     * br pd omrprefix
     * 2001:db8:cafe:0:0/64 lifetime:1800 preferred:1800
     * Done
     * @endcode
     * @par api_copy
     * #otBorderRoutingGetPdOmrPrefix
     */
    else if (aArgs[0] == "omrprefix")
    {
        otBorderRoutingPrefixTableEntry entry;

        SuccessOrExit(error = otBorderRoutingGetPdOmrPrefix(GetInstancePtr(), &entry));

        OutputIp6Prefix(entry.mPrefix);
        OutputLine(" lifetime:%lu preferred:%lu", ToUlong(entry.mValidLifetime), ToUlong(entry.mPreferredLifetime));
    }
    else
    {
        ExitNow(error = OT_ERROR_INVALID_COMMAND);
    }

exit:
    return error;
}
#endif // OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_ENABLE

/**
 * @cli br routers
 * @code
 * br routers
 * ff02:0:0:0:0:0:0:1 (M:0 O:0 S:1) ms-since-rx:1505 reachable:yes age:00:18:13
 * Done
 * @endcode
 * @par
 * Get the list of discovered routers by Border Routing Manager on the infrastructure link.
 * Info per router:
 * - The router IPv6 address
 * - Flags in received Router Advertisement header:
 *   - M: Managed Address Config flag
 *   - O: Other Config flag
 *   - S: SNAC Router flag (indicates whether the router is a stub router)
 * - Milliseconds since last received message from this router
 * - Reachability flag: A router is marked as unreachable if it fails to respond to multiple Neighbor Solicitation
 *   probes.
 * - Age: Duration interval since this router was first discovered. It is formatted as `{hh}:{mm}:{ss}` for  hours,
 *   minutes, seconds, if the duration is less than 24 hours. If the duration is 24 hours or more, the format is
 *   `{dd}d.{hh}:{mm}:{ss}` for days, hours, minutes, seconds.
 * - `(this BR)` is appended when the router is the local device itself.
 * - `(peer BR)` is appended when the router is likely a peer BR connected to the same Thread mesh. This requires
 *   `OPENTHREAD_CONFIG_BORDER_ROUTING_TRACK_PEER_BR_INFO_ENABLE`.
 * @sa otBorderRoutingGetNextRouterEntry
 */
template <> otError Br::Process<Cmd("routers")>(Arg aArgs[])
{
    otError                            error = OT_ERROR_NONE;
    otBorderRoutingPrefixTableIterator iterator;
    otBorderRoutingRouterEntry         entry;

    VerifyOrExit(aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);

    otBorderRoutingPrefixTableInitIterator(GetInstancePtr(), &iterator);

    while (otBorderRoutingGetNextRouterEntry(GetInstancePtr(), &iterator, &entry) == OT_ERROR_NONE)
    {
        OutputRouterInfo(entry, kLongVersion);
    }

exit:
    return error;
}

void Br::OutputRouterInfo(const otBorderRoutingRouterEntry &aEntry, RouterOutputMode aMode)
{
    OutputIp6Address(aEntry.mAddress);
    OutputFormat(" (M:%u O:%u S:%u)", aEntry.mManagedAddressConfigFlag, aEntry.mOtherConfigFlag,
                 aEntry.mSnacRouterFlag);

    if (aMode == kLongVersion)
    {
        char ageString[OT_DURATION_STRING_SIZE];

        otConvertDurationInSecondsToString(aEntry.mAge, ageString, sizeof(ageString));

        OutputFormat(" ms-since-rx:%lu reachable:%s age:%s", ToUlong(aEntry.mMsecSinceLastUpdate),
                     ToYesNo(aEntry.mIsReachable), ageString);

        if (aEntry.mIsLocalDevice)
        {
            OutputFormat(" (this BR)");
        }

#if OPENTHREAD_CONFIG_BORDER_ROUTING_TRACK_PEER_BR_INFO_ENABLE
        if (aEntry.mIsPeerBr)
        {
            OutputFormat(" (peer BR)");
        }
#endif
    }

    OutputNewLine();
}

template <> otError Br::Process<Cmd("raoptions")>(Arg aArgs[])
{
    static constexpr uint16_t kMaxExtraOptions = 800;

    otError  error = OT_ERROR_NONE;
    uint8_t  options[kMaxExtraOptions];
    uint16_t length;

    /**
     * @cli br raoptions (set,clear)
     * @code
     * br raoptions 0400ff00020001
     * Done
     * @endcode
     * @code
     * br raoptions clear
     * Done
     * @endcode
     * @cparam br raoptions @ca{options|clear}
     * `br raoptions clear` passes a `nullptr` to #otBorderRoutingSetExtraRouterAdvertOptions.
     * Otherwise, you can pass the `options` byte as hex data.
     * @par api_copy
     * #otBorderRoutingSetExtraRouterAdvertOptions
     */
    if (aArgs[0] == "clear")
    {
        length = 0;
    }
    else
    {
        length = sizeof(options);
        SuccessOrExit(error = aArgs[0].ParseAsHexString(length, options));
    }

    error = otBorderRoutingSetExtraRouterAdvertOptions(GetInstancePtr(), length > 0 ? options : nullptr, length);

exit:
    return error;
}

template <> otError Br::Process<Cmd("rioprf")>(Arg aArgs[])
{
    otError error = OT_ERROR_NONE;

    /**
     * @cli br rioprf
     * @code
     * br rioprf
     * med
     * Done
     * @endcode
     * @par api_copy
     * #otBorderRoutingGetRouteInfoOptionPreference
     */
    if (aArgs[0].IsEmpty())
    {
        OutputLine("%s", PreferenceToString(otBorderRoutingGetRouteInfoOptionPreference(GetInstancePtr())));
    }
    /**
     * @cli br rioprf clear
     * @code
     * br rioprf clear
     * Done
     * @endcode
     * @par api_copy
     * #otBorderRoutingClearRouteInfoOptionPreference
     */
    else if (aArgs[0] == "clear")
    {
        otBorderRoutingClearRouteInfoOptionPreference(GetInstancePtr());
    }
    /**
     * @cli br rioprf (high,med,low)
     * @code
     * br rioprf low
     * Done
     * @endcode
     * @cparam br rioprf [@ca{high}|@ca{med}|@ca{low}]
     * @par api_copy
     * #otBorderRoutingSetRouteInfoOptionPreference
     */
    else
    {
        otRoutePreference preference;

        SuccessOrExit(error = Interpreter::ParsePreference(aArgs[0], preference));
        otBorderRoutingSetRouteInfoOptionPreference(GetInstancePtr(), preference);
    }

exit:
    return error;
}

template <> otError Br::Process<Cmd("routeprf")>(Arg aArgs[])
{
    otError error = OT_ERROR_NONE;

    /**
     * @cli br routeprf
     * @code
     * br routeprf
     * med
     * Done
     * @endcode
     * @par api_copy
     * #otBorderRoutingGetRoutePreference
     */
    if (aArgs[0].IsEmpty())
    {
        OutputLine("%s", PreferenceToString(otBorderRoutingGetRoutePreference(GetInstancePtr())));
    }
    /**
     * @cli br routeprf clear
     * @code
     * br routeprf clear
     * Done
     * @endcode
     * @par api_copy
     * #otBorderRoutingClearRoutePreference
     */
    else if (aArgs[0] == "clear")
    {
        otBorderRoutingClearRoutePreference(GetInstancePtr());
    }
    /**
     * @cli br routeprf (high,med,low)
     * @code
     * br routeprf low
     * Done
     * @endcode
     * @cparam br routeprf [@ca{high}|@ca{med}|@ca{low}]
     * @par api_copy
     * #otBorderRoutingSetRoutePreference
     */
    else
    {
        otRoutePreference preference;

        SuccessOrExit(error = Interpreter::ParsePreference(aArgs[0], preference));
        otBorderRoutingSetRoutePreference(GetInstancePtr(), preference);
    }

exit:
    return error;
}

#if OPENTHREAD_CONFIG_IP6_BR_COUNTERS_ENABLE

/**
 * @cli br counters
 * @code
 * br counters
 * Inbound Unicast: Packets 4 Bytes 320
 * Inbound Multicast: Packets 0 Bytes 0
 * Outbound Unicast: Packets 2 Bytes 160
 * Outbound Multicast: Packets 0 Bytes 0
 * RA Rx: 4
 * RA TxSuccess: 2
 * RA TxFailed: 0
 * RS Rx: 0
 * RS TxSuccess: 2
 * RS TxFailed: 0
 * Done
 * @endcode
 * @par api_copy
 * #otIp6GetBorderRoutingCounters
 */
template <> otError Br::Process<Cmd("counters")>(Arg aArgs[])
{
    otError error = OT_ERROR_NONE;

    VerifyOrExit(aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
    GetInterpreter().OutputBorderRouterCounters();

exit:
    return error;
}

#endif // OPENTHREAD_CONFIG_IP6_BR_COUNTERS_ENABLE

otError Br::Process(Arg aArgs[])
{
#define CmdEntry(aCommandString) {aCommandString, &Br::Process<Cmd(aCommandString)>}

    static constexpr Command kCommands[] = {
#if OPENTHREAD_CONFIG_IP6_BR_COUNTERS_ENABLE
        CmdEntry("counters"),
#endif
        CmdEntry("disable"),     CmdEntry("enable"),           CmdEntry("ifaddrs"),      CmdEntry("infraif"),
        CmdEntry("init"),
#if OPENTHREAD_CONFIG_BORDER_ROUTING_MULTI_AIL_DETECTION_ENABLE
        CmdEntry("multiail"),
#endif
#if OPENTHREAD_CONFIG_NAT64_BORDER_ROUTING_ENABLE
        CmdEntry("nat64prefix"), CmdEntry("nat64prefixtable"),
#endif
        CmdEntry("omrconfig"),   CmdEntry("omrprefix"),        CmdEntry("onlinkprefix"),
#if OPENTHREAD_CONFIG_BORDER_ROUTING_DHCP6_PD_ENABLE
        CmdEntry("pd"),
#endif
#if OPENTHREAD_CONFIG_BORDER_ROUTING_TRACK_PEER_BR_INFO_ENABLE
        CmdEntry("peers"),
#endif
        CmdEntry("prefixtable"), CmdEntry("raoptions"),        CmdEntry("rdnsstable"),   CmdEntry("rioprf"),
        CmdEntry("routeprf"),    CmdEntry("routers"),          CmdEntry("state"),
    };

#undef CmdEntry

    static_assert(BinarySearch::IsSorted(kCommands), "kCommands is not sorted");

    otError        error = OT_ERROR_INVALID_COMMAND;
    const Command *command;

    if (aArgs[0].IsEmpty() || (aArgs[0] == "help"))
    {
        OutputCommandTable(kCommands);
        ExitNow(error = aArgs[0].IsEmpty() ? error : OT_ERROR_NONE);
    }

    command = BinarySearch::Find(aArgs[0].GetCString(), kCommands);
    VerifyOrExit(command != nullptr);

    error = (this->*command->mHandler)(aArgs + 1);

exit:
    return error;
}

} // namespace Cli
} // namespace ot

#endif // OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
