/*
 * Copyright 2016 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.connection;

import com.mongodb.client.MongoDriverInformation;
import org.bson.BsonBinaryWriter;
import org.bson.BsonDocument;
import org.bson.BsonString;
import org.bson.codecs.BsonDocumentCodec;
import org.bson.codecs.EncoderContext;
import org.bson.io.BasicOutputBuffer;

import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URL;
import java.nio.charset.Charset;
import java.security.CodeSource;
import java.security.ProtectionDomain;
import java.util.List;
import java.util.jar.Attributes;
import java.util.jar.Manifest;

import static com.mongodb.assertions.Assertions.isTrueArgument;
import static java.lang.String.format;
import static java.lang.System.getProperty;

final class ClientMetadataHelper {
    public static final BsonDocument CLIENT_METADATA_DOCUMENT = new BsonDocument();

    private static final String SEPARATOR = "|";

    private static final String APPLICATION_FIELD = "application";
    private static final String APPLICATION_NAME_FIELD = "name";

    private static final String DRIVER_FIELD = "driver";
    private static final String DRIVER_NAME_FIELD = "name";
    private static final String DRIVER_VERSION_FIELD = "version";

    private static final String PLATFORM_FIELD = "platform";

    private static final String OS_FIELD = "os";
    private static final String OS_TYPE_FIELD = "type";
    private static final String OS_NAME_FIELD = "name";
    private static final String OS_ARCHITECTURE_FIELD = "architecture";
    private static final String OS_VERSION_FIELD = "version";

    private static final int MAXIMUM_CLIENT_METADATA_ENCODED_SIZE = 512;

    static {
        BsonDocument driverMetadataDocument = addDriverInformation(null, new BsonDocument());
        CLIENT_METADATA_DOCUMENT.append(DRIVER_FIELD, driverMetadataDocument.get(DRIVER_FIELD));

        try {
            String operatingSystemName = getProperty("os.name", "unknown");
            CLIENT_METADATA_DOCUMENT.append(OS_FIELD, new BsonDocument()
                                                              .append(OS_TYPE_FIELD,
                                                                      new BsonString(getOperatingSystemType(operatingSystemName)))
                                                              .append(OS_NAME_FIELD,
                                                                      new BsonString(operatingSystemName))
                                                              .append(OS_ARCHITECTURE_FIELD,
                                                                      new BsonString(getProperty("os.arch", "unknown")))
                                                              .append(OS_VERSION_FIELD,
                                                                      new BsonString(getProperty("os.version", "unknown"))))
                    .append(PLATFORM_FIELD, driverMetadataDocument.get(PLATFORM_FIELD, new BsonString("")));
        } catch (SecurityException e) {
            // do nothing
        }
    }

    private static String getOperatingSystemType(final String operatingSystemName) {
        if (nameMatches(operatingSystemName, "linux")) {
            return "Linux";
        } else if (nameMatches(operatingSystemName, "mac")) {
            return "Darwin";
        } else if (nameMatches(operatingSystemName, "windows")) {
            return  "Windows";
        } else if (nameMatches(operatingSystemName, "hp-ux", "aix", "irix", "solaris", "sunos")) {
            return "Unix";
        } else {
            return  "unknown";
        }
    }

    private static boolean nameMatches(final String name, final String... prefixes) {
        for (String prefix : prefixes) {
            if (name.toLowerCase().startsWith(prefix.toLowerCase())) {
                return true;
            }
        }
        return false;
    }

    private static String getDriverVersion() {
        String driverVersion = "unknown";
        String path = getCodeSourcePath();
        if (path != null) {
            try {
                URL jarUrl = path.endsWith(".jar") ? new URL("jar:file:" + path + "!/") : null;
                if (jarUrl != null) {
                    JarURLConnection jarURLConnection = (JarURLConnection) jarUrl.openConnection();
                    Manifest manifest = jarURLConnection.getManifest();
                    if (manifest != null) {
                        Attributes mainAttributes = manifest.getMainAttributes();
                        if (mainAttributes != null) {
                            String version = (String) mainAttributes.get(new Attributes.Name("Build-Version"));
                            if (version != null) {
                                driverVersion = version;
                            }
                        }
                    }
                }
            } catch (IOException e) {
                // do nothing
            }
        }
        return driverVersion;
    }

    private static String getCodeSourcePath() {
        String path = null;
        ProtectionDomain protectionDomain = InternalStreamConnectionInitializer.class.getProtectionDomain();
        if (protectionDomain != null) {
            CodeSource codeSource = protectionDomain.getCodeSource();
            if (codeSource != null) {
                URL location = codeSource.getLocation();
                if (location != null) {
                    path = location.getPath();
                }
            }
        }
        return path;
    }

    static BsonDocument createClientMetadataDocument(final String applicationName) {
        return createClientMetadataDocument(applicationName, null);
    }

    static BsonDocument createClientMetadataDocument(final String applicationName, final MongoDriverInformation mongoDriverInformation) {
        return createClientMetadataDocument(applicationName, mongoDriverInformation, CLIENT_METADATA_DOCUMENT);
    }

    static BsonDocument createClientMetadataDocument(final String applicationName, final MongoDriverInformation mongoDriverInformation,
                                                     final BsonDocument templateDocument) {
        if (applicationName != null) {
            isTrueArgument("applicationName UTF-8 encoding length <= 128",
                    applicationName.getBytes(Charset.forName("UTF-8")).length <= 128);
        }

        BsonDocument document = templateDocument.clone();
        if (applicationName != null) {
            document.append(APPLICATION_FIELD, new BsonDocument(APPLICATION_NAME_FIELD, new BsonString(applicationName)));
        }

        if (mongoDriverInformation != null) {
            addDriverInformation(mongoDriverInformation, document);
        }

        if (clientMetadataDocumentTooLarge(document)) {
            // first try: remove the three optional fields in the 'os' document, if it exists (may not if the security manager is configured
            // to disallow access to system properties)
            BsonDocument operatingSystemDocument = document.getDocument(OS_FIELD, null);
            if (operatingSystemDocument != null) {
                operatingSystemDocument.remove(OS_VERSION_FIELD);
                operatingSystemDocument.remove(OS_ARCHITECTURE_FIELD);
                operatingSystemDocument.remove(OS_NAME_FIELD);
            }
            if (operatingSystemDocument == null || clientMetadataDocumentTooLarge(document)) {
                // second try: remove the optional 'platform' field
                document.remove(PLATFORM_FIELD);
                if (clientMetadataDocumentTooLarge(document)) {
                    // Third try: Try the minimum required amount of data.
                    document = new BsonDocument(DRIVER_FIELD, templateDocument.getDocument(DRIVER_FIELD));
                    document.append(OS_FIELD, new BsonDocument(OS_TYPE_FIELD, new BsonString("unknown")));
                    if (clientMetadataDocumentTooLarge(document)) {
                        // Worst case scenario: give up and don't send any client metadata at all
                        document = null;
                    }
                }
            }
        }
        return document;
    }

    private static BsonDocument addDriverInformation(final MongoDriverInformation mongoDriverInformation, final BsonDocument document) {
        MongoDriverInformation driverInformation = getDriverInformation(mongoDriverInformation);
        BsonDocument driverMetadataDocument = new BsonDocument(DRIVER_NAME_FIELD, listToBsonString(driverInformation.getDriverNames()))
                .append(DRIVER_VERSION_FIELD, listToBsonString(driverInformation.getDriverVersions()));
        document.append(DRIVER_FIELD, driverMetadataDocument);
        document.append(PLATFORM_FIELD, listToBsonString(driverInformation.getDriverPlatforms()));
        return document;
    }

    static boolean clientMetadataDocumentTooLarge(final BsonDocument document) {
        BasicOutputBuffer buffer = new BasicOutputBuffer(MAXIMUM_CLIENT_METADATA_ENCODED_SIZE);
        new BsonDocumentCodec().encode(new BsonBinaryWriter(buffer), document, EncoderContext.builder().build());
        return buffer.getPosition() > MAXIMUM_CLIENT_METADATA_ENCODED_SIZE;
    }

    static MongoDriverInformation getDriverInformation(final MongoDriverInformation mongoDriverInformation) {
        MongoDriverInformation.Builder builder = mongoDriverInformation != null ? MongoDriverInformation.builder(mongoDriverInformation)
                : MongoDriverInformation.builder();
        return builder
                .driverName("mongo-java-driver")
                .driverVersion(getDriverVersion())
                .driverPlatform(format("Java/%s/%s", getProperty("java.vendor", "unknown-vendor"),
                        getProperty("java.runtime.version", "unknown-version")))
                .build();
    }

    static BsonString listToBsonString(final List<String> listOfStrings) {
        StringBuilder stringBuilder = new StringBuilder();
        int i = 0;
        for (String val : listOfStrings) {
            if (i > 0) {
                stringBuilder.append(SEPARATOR);
            }
            stringBuilder.append(val);
            i++;
        }
        return new BsonString(stringBuilder.toString());
    }

    private ClientMetadataHelper() {
    }
}
