/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.cassandra.net;

import java.util.function.Function;

import com.google.common.base.Preconditions;

import org.apache.cassandra.auth.IInternodeAuthenticator;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.config.EncryptionOptions.ServerEncryptionOptions;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.utils.FBUtilities;

import static java.lang.String.format;
import static org.apache.cassandra.net.MessagingService.accept_messaging;
import static org.apache.cassandra.net.MessagingService.accept_streaming;
import static org.apache.cassandra.net.MessagingService.instance;

public class InboundConnectionSettings
{
    public final IInternodeAuthenticator authenticator;
    public final InetAddressAndPort bindAddress;
    public final ServerEncryptionOptions encryption;
    public final Integer socketReceiveBufferSizeInBytes;
    public final Integer applicationReceiveQueueCapacityInBytes;
    public final AcceptVersions acceptMessaging;
    public final AcceptVersions acceptStreaming;
    public final SocketFactory socketFactory;
    public final Function<InetAddressAndPort, InboundMessageHandlers> handlers;

    private InboundConnectionSettings(IInternodeAuthenticator authenticator,
                                      InetAddressAndPort bindAddress,
                                      ServerEncryptionOptions encryption,
                                      Integer socketReceiveBufferSizeInBytes,
                                      Integer applicationReceiveQueueCapacityInBytes,
                                      AcceptVersions acceptMessaging,
                                      AcceptVersions acceptStreaming,
                                      SocketFactory socketFactory,
                                      Function<InetAddressAndPort, InboundMessageHandlers> handlers)
    {
        this.authenticator = authenticator;
        this.bindAddress = bindAddress;
        this.encryption = encryption;
        this.socketReceiveBufferSizeInBytes = socketReceiveBufferSizeInBytes;
        this.applicationReceiveQueueCapacityInBytes = applicationReceiveQueueCapacityInBytes;
        this.acceptMessaging = acceptMessaging;
        this.acceptStreaming = acceptStreaming;
        this.socketFactory = socketFactory;
        this.handlers = handlers;
    }

    public InboundConnectionSettings()
    {
        this(null, null, null, null, null, null, null, null, null);
    }

    public String toString()
    {
        return format("address: (%s), nic: %s, encryption: %s",
                      bindAddress, FBUtilities.getNetworkInterface(bindAddress.getAddress()), SocketFactory.encryptionOptionsSummary(encryption));
    }

    public InboundConnectionSettings withAuthenticator(IInternodeAuthenticator authenticator)
    {
        return new InboundConnectionSettings(authenticator, bindAddress, encryption,
                                             socketReceiveBufferSizeInBytes, applicationReceiveQueueCapacityInBytes,
                                             acceptMessaging, acceptStreaming, socketFactory, handlers);
    }

    @SuppressWarnings("unused")
    public InboundConnectionSettings withBindAddress(InetAddressAndPort bindAddress)
    {
        return new InboundConnectionSettings(authenticator, bindAddress, encryption,
                                             socketReceiveBufferSizeInBytes, applicationReceiveQueueCapacityInBytes,
                                             acceptMessaging, acceptStreaming, socketFactory, handlers);
    }

    public InboundConnectionSettings withEncryption(ServerEncryptionOptions encryption)
    {
        return new InboundConnectionSettings(authenticator, bindAddress, encryption,
                                             socketReceiveBufferSizeInBytes, applicationReceiveQueueCapacityInBytes,
                                             acceptMessaging, acceptStreaming, socketFactory, handlers);
    }

    public InboundConnectionSettings withSocketReceiveBufferSizeInBytes(int socketReceiveBufferSizeInBytes)
    {
        return new InboundConnectionSettings(authenticator, bindAddress, encryption,
                                             socketReceiveBufferSizeInBytes, applicationReceiveQueueCapacityInBytes,
                                             acceptMessaging, acceptStreaming, socketFactory, handlers);
    }

    @SuppressWarnings("unused")
    public InboundConnectionSettings withApplicationReceiveQueueCapacityInBytes(int applicationReceiveQueueCapacityInBytes)
    {
        return new InboundConnectionSettings(authenticator, bindAddress, encryption,
                                             socketReceiveBufferSizeInBytes, applicationReceiveQueueCapacityInBytes,
                                             acceptMessaging, acceptStreaming, socketFactory, handlers);
    }

    public InboundConnectionSettings withAcceptMessaging(AcceptVersions acceptMessaging)
    {
        return new InboundConnectionSettings(authenticator, bindAddress, encryption,
                                             socketReceiveBufferSizeInBytes, applicationReceiveQueueCapacityInBytes,
                                             acceptMessaging, acceptStreaming, socketFactory, handlers);
    }

    public InboundConnectionSettings withAcceptStreaming(AcceptVersions acceptMessaging)
    {
        return new InboundConnectionSettings(authenticator, bindAddress, encryption,
                                             socketReceiveBufferSizeInBytes, applicationReceiveQueueCapacityInBytes,
                                             acceptMessaging, acceptStreaming, socketFactory, handlers);
    }

    public InboundConnectionSettings withSocketFactory(SocketFactory socketFactory)
    {
        return new InboundConnectionSettings(authenticator, bindAddress, encryption,
                                             socketReceiveBufferSizeInBytes, applicationReceiveQueueCapacityInBytes,
                                             acceptMessaging, acceptStreaming, socketFactory, handlers);
    }

    public InboundConnectionSettings withHandlers(Function<InetAddressAndPort, InboundMessageHandlers> handlers)
    {
        return new InboundConnectionSettings(authenticator, bindAddress, encryption,
                                             socketReceiveBufferSizeInBytes, applicationReceiveQueueCapacityInBytes,
                                             acceptMessaging, acceptStreaming, socketFactory, handlers);
    }

    public InboundConnectionSettings withLegacySslStoragePortDefaults()
    {
        ServerEncryptionOptions encryption = this.encryption;
        if (encryption == null)
            encryption = DatabaseDescriptor.getInternodeMessagingEncyptionOptions();
        encryption = new ServerEncryptionOptions.Builder(encryption).withInternodeEncryption(ServerEncryptionOptions.InternodeEncryption.all).withOptional(false).build();

        return this.withBindAddress(bindAddress.withPort(DatabaseDescriptor.getSSLStoragePort()))
                   .withEncryption(encryption)
                   .withDefaults();
    }

    // note that connectTo is updated even if specified, in the case of pre40 messaging and using encryption (to update port)
    public InboundConnectionSettings withDefaults()
    {
        // this is for the socket that can be plain, only ssl, or optional plain/ssl
        if (bindAddress.getPort() != DatabaseDescriptor.getStoragePort() && bindAddress.getPort() != DatabaseDescriptor.getSSLStoragePort())
            throw new ConfigurationException(format("Local endpoint port %d doesn't match YAML configured port %d or legacy SSL port %d",
                                                    bindAddress.getPort(), DatabaseDescriptor.getStoragePort(), DatabaseDescriptor.getSSLStoragePort()));

        IInternodeAuthenticator authenticator = this.authenticator;
        ServerEncryptionOptions encryption = this.encryption;
        Integer socketReceiveBufferSizeInBytes = this.socketReceiveBufferSizeInBytes;
        Integer applicationReceiveQueueCapacityInBytes = this.applicationReceiveQueueCapacityInBytes;
        AcceptVersions acceptMessaging = this.acceptMessaging;
        AcceptVersions acceptStreaming = this.acceptStreaming;
        SocketFactory socketFactory = this.socketFactory;
        Function<InetAddressAndPort, InboundMessageHandlers> handlersFactory = this.handlers;

        if (authenticator == null)
            authenticator = DatabaseDescriptor.getInternodeAuthenticator();

        if (encryption == null)
            encryption = DatabaseDescriptor.getInternodeMessagingEncyptionOptions();

        if (socketReceiveBufferSizeInBytes == null)
            socketReceiveBufferSizeInBytes = DatabaseDescriptor.getInternodeSocketReceiveBufferSizeInBytes();

        if (applicationReceiveQueueCapacityInBytes == null)
            applicationReceiveQueueCapacityInBytes = DatabaseDescriptor.getInternodeApplicationReceiveQueueCapacityInBytes();

        if (acceptMessaging == null)
            acceptMessaging = accept_messaging;

        if (acceptStreaming == null)
            acceptStreaming = accept_streaming;

        if (socketFactory == null)
            socketFactory = instance().socketFactory;

        if (handlersFactory == null)
            handlersFactory = instance()::getInbound;

        Preconditions.checkArgument(socketReceiveBufferSizeInBytes == 0 || socketReceiveBufferSizeInBytes >= 1 << 10, "illegal socket send buffer size: " + socketReceiveBufferSizeInBytes);
        Preconditions.checkArgument(applicationReceiveQueueCapacityInBytes >= 1 << 10, "illegal application receive queue capacity: " + applicationReceiveQueueCapacityInBytes);

        return new InboundConnectionSettings(authenticator, bindAddress, encryption, socketReceiveBufferSizeInBytes, applicationReceiveQueueCapacityInBytes, acceptMessaging, acceptStreaming, socketFactory, handlersFactory);
    }
}
