/*
 * Copyright 2008-present MongoDB, Inc.
 *
 * 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 com.mongodb.internal.connection;

import com.mongodb.AuthenticationMechanism;
import com.mongodb.MongoException;
import com.mongodb.MongoSecurityException;
import com.mongodb.async.SingleResultCallback;
import com.mongodb.connection.ConnectionDescription;
import com.mongodb.connection.ServerVersion;
import org.bson.BsonArray;
import org.bson.BsonDocument;
import org.bson.BsonInt32;
import org.bson.BsonString;

import static com.mongodb.AuthenticationMechanism.SCRAM_SHA_1;
import static com.mongodb.AuthenticationMechanism.SCRAM_SHA_256;
import static com.mongodb.assertions.Assertions.isTrueArgument;
import static com.mongodb.internal.connection.CommandHelper.executeCommand;
import static com.mongodb.internal.connection.CommandHelper.executeCommandAsync;
import static com.mongodb.internal.operation.ServerVersionHelper.serverIsAtLeastVersionThreeDotZero;
import static com.mongodb.internal.operation.ServerVersionHelper.serverIsLessThanVersionFourDotZero;
import static java.lang.String.format;

class DefaultAuthenticator extends Authenticator {
    static final int USER_NOT_FOUND_CODE = 11;
    private static final ServerVersion FOUR_ZERO = new ServerVersion(4, 0);
    private static final ServerVersion THREE_ZERO = new ServerVersion(3, 0);
    private static final BsonString DEFAULT_MECHANISM_NAME = new BsonString(SCRAM_SHA_256.getMechanismName());

    DefaultAuthenticator(final MongoCredentialWithCache credential) {
        super(credential);
        isTrueArgument("unspecified authentication mechanism", credential.getAuthenticationMechanism() == null);
    }

    @Override
    void authenticate(final InternalConnection connection, final ConnectionDescription connectionDescription) {
        if (serverIsLessThanVersionFourDotZero(connectionDescription)) {
            getLegacyDefaultAuthenticator(connectionDescription)
                    .authenticate(connection, connectionDescription);
        } else {
            try {
                BsonDocument isMasterResult = executeCommand("admin", createIsMasterCommand(), connection);
                getAuthenticatorFromIsMasterResult(isMasterResult, connectionDescription)
                        .authenticate(connection, connectionDescription);
            } catch (Exception e) {
                throw wrapException(e);
            }
        }
    }

    @Override
    void authenticateAsync(final InternalConnection connection, final ConnectionDescription connectionDescription,
                           final SingleResultCallback<Void> callback) {
        if (serverIsLessThanVersionFourDotZero(connectionDescription)) {
            getLegacyDefaultAuthenticator(connectionDescription)
                    .authenticateAsync(connection, connectionDescription, callback);
        } else {
            executeCommandAsync("admin", createIsMasterCommand(), connection, new SingleResultCallback<BsonDocument>() {
                @Override
                public void onResult(final BsonDocument result, final Throwable t) {
                    if (t != null) {
                        callback.onResult(null, wrapException(t));
                    } else {
                        getAuthenticatorFromIsMasterResult(result, connectionDescription)
                                .authenticateAsync(connection, connectionDescription, callback);
                    }
                }
            });
        }
    }

    Authenticator getAuthenticatorFromIsMasterResult(final BsonDocument isMasterResult, final ConnectionDescription connectionDescription) {
        if (isMasterResult.containsKey("saslSupportedMechs")) {
            BsonArray saslSupportedMechs = isMasterResult.getArray("saslSupportedMechs");
            AuthenticationMechanism mechanism = saslSupportedMechs.contains(DEFAULT_MECHANISM_NAME) ? SCRAM_SHA_256 : SCRAM_SHA_1;
            return new ScramShaAuthenticator(getMongoCredentialWithCache().withMechanism(mechanism));
        } else {
            return getLegacyDefaultAuthenticator(connectionDescription);
        }
    }

    private Authenticator getLegacyDefaultAuthenticator(final ConnectionDescription connectionDescription) {
        if (serverIsAtLeastVersionThreeDotZero(connectionDescription)) {
            return new ScramShaAuthenticator(getMongoCredentialWithCache().withMechanism(SCRAM_SHA_1));
        } else {
            return new NativeAuthenticator(getMongoCredentialWithCache());
        }
    }

    private BsonDocument createIsMasterCommand() {
        BsonDocument isMasterCommandDocument = new BsonDocument("ismaster", new BsonInt32(1));
        isMasterCommandDocument.append("saslSupportedMechs",
                new BsonString(format("%s.%s", getMongoCredential().getSource(), getMongoCredential().getUserName())));
        return isMasterCommandDocument;
    }

    private MongoException wrapException(final Throwable t) {
        if (t instanceof MongoSecurityException) {
            return (MongoSecurityException) t;
        } else if (t instanceof MongoException && ((MongoException) t).getCode() == USER_NOT_FOUND_CODE) {
            return new MongoSecurityException(getMongoCredential(), format("Exception authenticating %s", getMongoCredential()), t);
        } else {
            return MongoException.fromThrowable(t);
        }
    }
}