/*
 * Copyright 2017-present Open Networking Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.onosproject.routing.bgp;

import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.onlab.packet.Ip4Address;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A class for handling BGP OPEN messages.
 */
final class BgpOpen {
    private static final Logger log = LoggerFactory.getLogger(BgpOpen.class);

    /**
     * Default constructor.
     * <p>
     * The constructor is private to prevent creating an instance of
     * this utility class.
     */
    private BgpOpen() {
    }

    /**
     * Processes BGP OPEN message.
     *
     * @param bgpSession the BGP Session to use
     * @param ctx the Channel Handler Context
     * @param message the message to process
     */
    static void processBgpOpen(BgpSession bgpSession,
                               ChannelHandlerContext ctx,
                               ChannelBuffer message) {
        int minLength =
            BgpConstants.BGP_OPEN_MIN_LENGTH - BgpConstants.BGP_HEADER_LENGTH;
        if (message.readableBytes() < minLength) {
            log.debug("BGP RX OPEN Error from {}: " +
                      "Message length {} too short. Must be at least {}",
                      bgpSession.remoteInfo().address(),
                      message.readableBytes(), minLength);
            //
            // ERROR: Bad Message Length
            //
            // Send NOTIFICATION and close the connection
            ChannelBuffer txMessage =
                BgpNotification.prepareBgpNotificationBadMessageLength(
                        message.readableBytes() + BgpConstants.BGP_HEADER_LENGTH);
            ctx.getChannel().write(txMessage);
            bgpSession.closeSession(ctx);
            return;
        }

        //
        // Parse the OPEN message
        //
        // Remote BGP version
        int remoteBgpVersion = message.readUnsignedByte();
        if (remoteBgpVersion != BgpConstants.BGP_VERSION) {
            log.debug("BGP RX OPEN Error from {}: " +
                      "Unsupported BGP version {}. Should be {}",
                      bgpSession.remoteInfo().address(), remoteBgpVersion,
                      BgpConstants.BGP_VERSION);
            //
            // ERROR: Unsupported Version Number
            //
            // Send NOTIFICATION and close the connection
            int errorCode = BgpConstants.Notifications.OpenMessageError.ERROR_CODE;
            int errorSubcode = BgpConstants.Notifications.OpenMessageError.UNSUPPORTED_VERSION_NUMBER;
            ChannelBuffer data = ChannelBuffers.buffer(2);
            data.writeShort(BgpConstants.BGP_VERSION);
            ChannelBuffer txMessage =
                BgpNotification.prepareBgpNotification(errorCode, errorSubcode,
                                                       data);
            ctx.getChannel().write(txMessage);
            bgpSession.closeSession(ctx);
            return;
        }
        bgpSession.remoteInfo().setBgpVersion(remoteBgpVersion);

        // Remote AS number
        long remoteAs = message.readUnsignedShort();
        bgpSession.remoteInfo().setAsNumber(remoteAs);
        //
        // NOTE: Currently, the local AS number is always set to the remote AS.
        // This is done, because the peer setup is always iBGP.
        // In the future, the local AS number should be configured as part
        // of an explicit BGP peering configuration.
        //
        bgpSession.localInfo().setAsNumber(remoteAs);

        // Remote Hold Time
        long remoteHoldtime = message.readUnsignedShort();
        if ((remoteHoldtime != 0) &&
            (remoteHoldtime < BgpConstants.BGP_KEEPALIVE_MIN_HOLDTIME)) {
            log.debug("BGP RX OPEN Error from {}: " +
                      "Unacceptable Hold Time field {}. " +
                      "Should be 0 or at least {}",
                      bgpSession.remoteInfo().address(), remoteHoldtime,
                      BgpConstants.BGP_KEEPALIVE_MIN_HOLDTIME);
            //
            // ERROR: Unacceptable Hold Time
            //
            // Send NOTIFICATION and close the connection
            int errorCode = BgpConstants.Notifications.OpenMessageError.ERROR_CODE;
            int errorSubcode = BgpConstants.Notifications.OpenMessageError.UNACCEPTABLE_HOLD_TIME;
            ChannelBuffer txMessage =
                BgpNotification.prepareBgpNotification(errorCode, errorSubcode,
                                                       null);
            ctx.getChannel().write(txMessage);
            bgpSession.closeSession(ctx);
            return;
        }
        bgpSession.remoteInfo().setHoldtime(remoteHoldtime);
        //
        // NOTE: Currently. the local BGP Holdtime is always set to the remote
        // BGP holdtime.
        // In the future, the local BGP Holdtime should be configured as part
        // of an explicit BGP peering configuration.
        //
        bgpSession.localInfo().setHoldtime(remoteHoldtime);

        // Remote BGP Identifier
        Ip4Address remoteBgpId =
            Ip4Address.valueOf((int) message.readUnsignedInt());
        bgpSession.remoteInfo().setBgpId(remoteBgpId);

        // Parse the Optional Parameters
        try {
            parseOptionalParameters(bgpSession, ctx, message);
        } catch (BgpMessage.BgpParseException e) {
            // ERROR: Error parsing optional parameters
            log.debug("BGP RX OPEN Error from {}: " +
                      "Exception parsing Optional Parameters: {}",
                      bgpSession.remoteInfo().address(), e);
            //
            // ERROR: Invalid Optional Parameters: Unspecific
            //
            // Send NOTIFICATION and close the connection
            int errorCode = BgpConstants.Notifications.OpenMessageError.ERROR_CODE;
            int errorSubcode = BgpConstants.Notifications.ERROR_SUBCODE_UNSPECIFIC;
            ChannelBuffer txMessage =
                BgpNotification.prepareBgpNotification(errorCode, errorSubcode,
                                                       null);
            ctx.getChannel().write(txMessage);
            bgpSession.closeSession(ctx);
            return;
        }

        //
        // NOTE: Prepare the BGP OPEN message before the original local AS
        // is overwritten by the 4-octet AS number
        //
        ChannelBuffer txOpenMessage = prepareBgpOpen(bgpSession.localInfo());

        //
        // Use the 4-octet AS number in lieu of the "My AS" field
        // See RFC 6793, Section 4.1, second paragraph.
        //
        if (bgpSession.remoteInfo().as4OctetCapability()) {
            long as4Number = bgpSession.remoteInfo().as4Number();
            bgpSession.remoteInfo().setAsNumber(as4Number);
            bgpSession.localInfo().setAsNumber(as4Number);
        }

        //
        // Verify that the AS number is same for all other BGP Sessions
        // NOTE: This check applies only for our use-case where all BGP
        // sessions are iBGP.
        //
        for (BgpSession bs : bgpSession.getBgpSessionManager().getBgpSessions()) {
            if ((bs.remoteInfo().asNumber() != 0) &&
                (bgpSession.remoteInfo().asNumber() !=
                 bs.remoteInfo().asNumber())) {
                log.debug("BGP RX OPEN Error from {}: Bad Peer AS {}. " +
                          "Expected {}",
                          bgpSession.remoteInfo().address(),
                          bgpSession.remoteInfo().asNumber(),
                          bs.remoteInfo().asNumber());
                //
                // ERROR: Bad Peer AS
                //
                // Send NOTIFICATION and close the connection
                int errorCode = BgpConstants.Notifications.OpenMessageError.ERROR_CODE;
                int errorSubcode = BgpConstants.Notifications.OpenMessageError.BAD_PEER_AS;
                ChannelBuffer txMessage =
                    BgpNotification.prepareBgpNotification(errorCode,
                                                           errorSubcode, null);
                ctx.getChannel().write(txMessage);
                bgpSession.closeSession(ctx);
                return;
            }
        }

        log.debug("BGP RX OPEN message from {}: " +
                  "BGPv{} AS {} BGP-ID {} Holdtime {}",
                  bgpSession.remoteInfo().address(),
                  bgpSession.remoteInfo().bgpVersion(),
                  bgpSession.remoteInfo().asNumber(),
                  bgpSession.remoteInfo().bgpId(),
                  bgpSession.remoteInfo().holdtime());

        // Send my OPEN followed by KEEPALIVE
        ctx.getChannel().write(txOpenMessage);
        //
        ChannelBuffer txMessage = BgpKeepalive.prepareBgpKeepalive();
        ctx.getChannel().write(txMessage);

        // Start the KEEPALIVE timer
        bgpSession.restartKeepaliveTimer(ctx);

        // Start the Session Timeout timer
        bgpSession.restartSessionTimeoutTimer(ctx);
    }

    /**
     * Prepares BGP OPEN message.
     *
     * @param localInfo the BGP Session local information to use
     * @return the message to transmit (BGP header included)
     */
    static ChannelBuffer prepareBgpOpen(BgpSessionInfo localInfo) {
        ChannelBuffer message =
            ChannelBuffers.buffer(BgpConstants.BGP_MESSAGE_MAX_LENGTH);

        //
        // Prepare the OPEN message payload
        //
        message.writeByte(localInfo.bgpVersion());
        message.writeShort((int) localInfo.asNumber());
        message.writeShort((int) localInfo.holdtime());
        message.writeInt(localInfo.bgpId().toInt());

        // Prepare the optional BGP Capabilities
        ChannelBuffer capabilitiesMessage =
            prepareBgpOpenCapabilities(localInfo);
        message.writeByte(capabilitiesMessage.readableBytes());
        message.writeBytes(capabilitiesMessage);

        return BgpMessage.prepareBgpMessage(BgpConstants.BGP_TYPE_OPEN,
                                            message);
    }

    /**
     * Parses BGP OPEN Optional Parameters.
     *
     * @param bgpSession the BGP Session to use
     * @param ctx the Channel Handler Context
     * @param message the message to process
     * @throws BgpMessage.BgpParseException
     */
    private static void parseOptionalParameters(BgpSession bgpSession,
                                                ChannelHandlerContext ctx,
                                                ChannelBuffer message)
        throws BgpMessage.BgpParseException {

        //
        // Get and verify the Optional Parameters Length
        //
        int optParamLength = message.readUnsignedByte();
        if (optParamLength > message.readableBytes()) {
            // ERROR: Invalid Optional Parameter Length
            String errorMsg = "Invalid Optional Parameter Length field " +
                optParamLength + ". Remaining Optional Parameters " +
                message.readableBytes();
            throw new BgpMessage.BgpParseException(errorMsg);
        }
        if (optParamLength == 0) {
            return;                     // No Optional Parameters
        }

        //
        // Parse the Optional Parameters
        //
        int optParamEnd = message.readerIndex() + optParamLength;
        while (message.readerIndex() < optParamEnd) {
            int paramType = message.readUnsignedByte();
            if (message.readerIndex() >= optParamEnd) {
                // ERROR: Malformed Optional Parameters
                String errorMsg = "Malformed Optional Parameters";
                throw new BgpMessage.BgpParseException(errorMsg);
            }
            int paramLen = message.readUnsignedByte();
            if (message.readerIndex() + paramLen > optParamEnd) {
                // ERROR: Malformed Optional Parameters
                String errorMsg = "Malformed Optional Parameters";
                throw new BgpMessage.BgpParseException(errorMsg);
            }

            //
            // Extract the Optional Parameter Value based on the Parameter Type
            //
            switch (paramType) {
            case BgpConstants.Open.Capabilities.TYPE:
                // Optional Parameter Type: Capabilities
                if (paramLen < BgpConstants.Open.Capabilities.MIN_LENGTH) {
                    // ERROR: Malformed Param Type
                    String errorMsg = "Malformed Capabilities Optional "
                            + "Parameter Type " + paramType;
                    throw new BgpMessage.BgpParseException(errorMsg);
                }
                int paramEnd = message.readerIndex() + paramLen;
                // Parse Capabilities
                while (message.readerIndex() < paramEnd) {
                    if (paramEnd - message.readerIndex() <
                            BgpConstants.Open.Capabilities.MIN_LENGTH) {
                        String errorMsg = "Malformed Capabilities";
                        throw new BgpMessage.BgpParseException(errorMsg);
                    }
                    int capabCode = message.readUnsignedByte();
                    int capabLen = message.readUnsignedByte();
                    if (message.readerIndex() + capabLen > paramEnd) {
                        // ERROR: Malformed Capability
                        String errorMsg = "Malformed Capability instance with "
                                + "code " + capabCode;
                        throw new BgpMessage.BgpParseException(errorMsg);
                    }

                    switch (capabCode) {
                    case BgpConstants.Open.Capabilities.MultiprotocolExtensions.CODE:
                        // Multiprotocol Extensions Capabilities (RFC 4760)
                        if (capabLen != BgpConstants.Open.Capabilities.MultiprotocolExtensions.LENGTH) {
                            // ERROR: Multiprotocol Extension Length Error
                            String errorMsg = "Multiprotocol Extension Length Error";
                            throw new BgpMessage.BgpParseException(errorMsg);
                        }
                        // Decode the AFI (2 octets) and SAFI (1 octet)
                        int afi = message.readUnsignedShort();
                        int reserved = message.readUnsignedByte();
                        int safi = message.readUnsignedByte();
                        log.debug("BGP RX OPEN Capability: AFI = {} SAFI = {}",
                                  afi, safi);
                        //
                        // Setup the AFI/SAFI in the BgpSession
                        //
                        // NOTE: For now we just copy the remote AFI/SAFI setting
                        // to the local configuration.
                        //
                        if (afi == BgpConstants.Open.Capabilities.MultiprotocolExtensions.AFI_IPV4 &&
                            safi == BgpConstants.Open.Capabilities.MultiprotocolExtensions.SAFI_UNICAST) {
                            bgpSession.remoteInfo().setIpv4Unicast();
                            bgpSession.localInfo().setIpv4Unicast();
                        } else if (afi == BgpConstants.Open.Capabilities.MultiprotocolExtensions.AFI_IPV4 &&
                                   safi == BgpConstants.Open.Capabilities.MultiprotocolExtensions.SAFI_MULTICAST) {
                            bgpSession.remoteInfo().setIpv4Multicast();
                            bgpSession.localInfo().setIpv4Multicast();
                        } else if (afi == BgpConstants.Open.Capabilities.MultiprotocolExtensions.AFI_IPV6 &&
                                   safi == BgpConstants.Open.Capabilities.MultiprotocolExtensions.SAFI_UNICAST) {
                            bgpSession.remoteInfo().setIpv6Unicast();
                            bgpSession.localInfo().setIpv6Unicast();
                        } else if (afi == BgpConstants.Open.Capabilities.MultiprotocolExtensions.AFI_IPV6 &&
                                   safi == BgpConstants.Open.Capabilities.MultiprotocolExtensions.SAFI_MULTICAST) {
                            bgpSession.remoteInfo().setIpv6Multicast();
                            bgpSession.localInfo().setIpv6Multicast();
                        } else {
                            log.debug("BGP RX OPEN Capability: Unknown AFI = {} SAFI = {}",
                                      afi, safi);
                        }
                        break;

                    case BgpConstants.Open.Capabilities.As4Octet.CODE:
                        // Support for 4-octet AS Number Capabilities (RFC 6793)
                        if (capabLen != BgpConstants.Open.Capabilities.As4Octet.LENGTH) {
                            // ERROR: 4-octet AS Number Capability Length Error
                            String errorMsg = "4-octet AS Number Capability Length Error";
                            throw new BgpMessage.BgpParseException(errorMsg);
                        }
                        long as4Number = message.readUnsignedInt();

                        bgpSession.remoteInfo().setAs4OctetCapability();
                        bgpSession.remoteInfo().setAs4Number(as4Number);

                        //
                        // Copy remote 4-octet AS Number Capabilities and AS
                        // Number. This is a temporary setting until local AS
                        // number configuration is supported.
                        //
                        bgpSession.localInfo().setAs4OctetCapability();
                        bgpSession.localInfo().setAs4Number(as4Number);
                        log.debug("BGP RX OPEN Capability: AS4 Number = {}",
                                  as4Number);
                        break;

                    default:
                        // Unknown Capability: ignore it
                        log.debug("BGP RX OPEN Capability Code = {} Length = {}",
                                  capabCode, capabLen);
                        message.readBytes(capabLen);
                        break;
                    }


                }
                break;
            default:
                // Unknown Parameter Type: ignore it
                log.debug("BGP RX OPEN Parameter Type = {} Length = {}",
                          paramType, paramLen);
                message.readBytes(paramLen);
                break;
            }
        }
    }

    /**
     * Prepares the Capabilities for the BGP OPEN message.
     *
     * @param localInfo the BGP Session local information to use
     * @return the buffer with the BGP Capabilities to transmit
     */
    private static ChannelBuffer prepareBgpOpenCapabilities(
                                        BgpSessionInfo localInfo) {
        ChannelBuffer message =
            ChannelBuffers.buffer(BgpConstants.BGP_MESSAGE_MAX_LENGTH);

        //
        // Write the Multiprotocol Extensions Capabilities
        //

        // IPv4 unicast
        if (localInfo.ipv4Unicast()) {
            message.writeByte(BgpConstants.Open.Capabilities.TYPE);               // Param type
            message.writeByte(BgpConstants.Open.Capabilities.MIN_LENGTH +
                              BgpConstants.Open.Capabilities.MultiprotocolExtensions.LENGTH);  // Param len
            message.writeByte(
                    BgpConstants.Open.Capabilities.MultiprotocolExtensions.CODE);    // Capab. code
            message.writeByte(
                    BgpConstants.Open.Capabilities.MultiprotocolExtensions.LENGTH);  // Capab. len
            message.writeShort(
                    BgpConstants.Open.Capabilities.MultiprotocolExtensions.AFI_IPV4);
            message.writeByte(0);               // Reserved field
            message.writeByte(
                    BgpConstants.Open.Capabilities.MultiprotocolExtensions.SAFI_UNICAST);
        }
        // IPv4 multicast
        if (localInfo.ipv4Multicast()) {
            message.writeByte(BgpConstants.Open.Capabilities.TYPE);               // Param type
            message.writeByte(BgpConstants.Open.Capabilities.MIN_LENGTH +
                              BgpConstants.Open.Capabilities.MultiprotocolExtensions.LENGTH);  // Param len
            message.writeByte(
                    BgpConstants.Open.Capabilities.MultiprotocolExtensions.CODE);    // Capab. code
            message.writeByte(
                    BgpConstants.Open.Capabilities.MultiprotocolExtensions.LENGTH);  // Capab. len
            message.writeShort(
                    BgpConstants.Open.Capabilities.MultiprotocolExtensions.AFI_IPV4);
            message.writeByte(0);               // Reserved field
            message.writeByte(
                    BgpConstants.Open.Capabilities.MultiprotocolExtensions.SAFI_MULTICAST);
        }
        // IPv6 unicast
        if (localInfo.ipv6Unicast()) {
            message.writeByte(BgpConstants.Open.Capabilities.TYPE);               // Param type
            message.writeByte(BgpConstants.Open.Capabilities.MIN_LENGTH +
                              BgpConstants.Open.Capabilities.MultiprotocolExtensions.LENGTH);  // Param len
            message.writeByte(
                    BgpConstants.Open.Capabilities.MultiprotocolExtensions.CODE);    // Capab. code
            message.writeByte(
                    BgpConstants.Open.Capabilities.MultiprotocolExtensions.LENGTH);  // Capab. len
            message.writeShort(
                    BgpConstants.Open.Capabilities.MultiprotocolExtensions.AFI_IPV6);
            message.writeByte(0);               // Reserved field
            message.writeByte(
                    BgpConstants.Open.Capabilities.MultiprotocolExtensions.SAFI_UNICAST);
        }
        // IPv6 multicast
        if (localInfo.ipv6Multicast()) {
            message.writeByte(BgpConstants.Open.Capabilities.TYPE);               // Param type
            message.writeByte(BgpConstants.Open.Capabilities.MIN_LENGTH +
                              BgpConstants.Open.Capabilities.MultiprotocolExtensions.LENGTH);  // Param len
            message.writeByte(
                    BgpConstants.Open.Capabilities.MultiprotocolExtensions.CODE);    // Capab. code
            message.writeByte(
                    BgpConstants.Open.Capabilities.MultiprotocolExtensions.LENGTH);  // Capab. len
            message.writeShort(
                    BgpConstants.Open.Capabilities.MultiprotocolExtensions.AFI_IPV6);
            message.writeByte(0);               // Reserved field
            message.writeByte(
                    BgpConstants.Open.Capabilities.MultiprotocolExtensions.SAFI_MULTICAST);
        }

        // 4 octet AS path capability
        if (localInfo.as4OctetCapability()) {
            message.writeByte(BgpConstants.Open.Capabilities.TYPE);               // Param type
            message.writeByte(BgpConstants.Open.Capabilities.MIN_LENGTH +
                              BgpConstants.Open.Capabilities.As4Octet.LENGTH);                 // Param len
            message.writeByte(BgpConstants.Open.Capabilities.As4Octet.CODE);                   // Capab. code
            message.writeByte(BgpConstants.Open.Capabilities.As4Octet.LENGTH);                 // Capab. len
            message.writeInt((int) localInfo.as4Number());
        }
        return message;
    }
}
