// 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.cloudstack.userdata;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.cloud.domain.Domain;
import com.cloud.user.User;
import com.cloud.user.UserDataVO;
import com.cloud.user.dao.UserDataDao;
import com.cloud.utils.compression.CompressionUtil;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;

import com.cloud.exception.InvalidParameterValueException;
import com.cloud.utils.component.ManagerBase;
import com.cloud.utils.exception.CloudRuntimeException;

import javax.inject.Inject;

public class UserDataManagerImpl extends ManagerBase implements UserDataManager {
    @Inject
    UserDataDao userDataDao;

    private static final int MAX_USER_DATA_LENGTH_BYTES = 2048;
    private static final int MAX_HTTP_GET_LENGTH = 2 * MAX_USER_DATA_LENGTH_BYTES; // 4KB
    private static final int NUM_OF_2K_BLOCKS = 512;
    private static final int MAX_HTTP_POST_LENGTH = NUM_OF_2K_BLOCKS * MAX_USER_DATA_LENGTH_BYTES; // 1MB
    private List<UserDataProvider> userDataProviders;
    private static Map<String, UserDataProvider> userDataProvidersMap = new HashMap<>();

    public void setUserDataProviders(final List<UserDataProvider> userDataProviders) {
        this.userDataProviders = userDataProviders;
    }

    private void initializeUserdataProvidersMap() {
        if (userDataProviders != null) {
            for (final UserDataProvider provider : userDataProviders) {
                userDataProvidersMap.put(provider.getName().toLowerCase(), provider);
            }
        }
    }

    @Override
    public boolean start() {
        initializeUserdataProvidersMap();
        return true;
    }

    @Override
    public String getConfigComponentName() {
        return UserDataManagerImpl.class.getSimpleName();
    }

    @Override
    public ConfigKey<?>[] getConfigKeys() {
        return new ConfigKey[] {VM_USERDATA_MAX_LENGTH};
    }

    protected UserDataProvider getUserdataProvider(String name) {
        if (StringUtils.isEmpty(name)) {
            // Use cloud-init as the default userdata provider
            name = "cloud-init";
        }
        if (!userDataProvidersMap.containsKey(name)) {
            throw new CloudRuntimeException("Failed to find userdata provider by the name: " + name);
        }
        return userDataProvidersMap.get(name);
    }

    @Override
    public String concatenateUserData(String userdata1, String userdata2, String userdataProvider) {
        UserDataProvider provider = getUserdataProvider(userdataProvider);
        String appendUserData = provider.appendUserData(userdata1, userdata2);
        return Base64.encodeBase64String(appendUserData.getBytes());
    }

    @Override
    public String validateUserData(String userData, BaseCmd.HTTPMethod httpmethod) {
        logger.trace(String.format("Validating base64 encoded user data: [%s].", userData));
        if (StringUtils.isBlank(userData)) {
            logger.debug("Null/empty base64 encoded user data set");
            return null;
        }

        if (userData.contains("%")) {
            try {
                userData = URLDecoder.decode(userData, "UTF-8");
            } catch (UnsupportedEncodingException e) {
                throw new InvalidParameterValueException("Url decoding of user data failed.");
            }
        }

        if (!Base64.isBase64(userData)) {
            throw new InvalidParameterValueException("User data is not base64 encoded.");
        }

        byte[] decodedUserData = null;

        // If GET, use 4K. If POST, support up to 1M.
        if (httpmethod.equals(BaseCmd.HTTPMethod.GET)) {
            decodedUserData = validateAndDecodeByHTTPMethod(userData, MAX_HTTP_GET_LENGTH, BaseCmd.HTTPMethod.GET);
        } else if (httpmethod.equals(BaseCmd.HTTPMethod.POST)) {
            decodedUserData = validateAndDecodeByHTTPMethod(userData, MAX_HTTP_POST_LENGTH, BaseCmd.HTTPMethod.POST);
        }

        // Re-encode so that the '=' paddings are added if necessary since 'isBase64' does not require it, but python does on the VR.
        return Base64.encodeBase64String(decodedUserData);
    }

    @Override
    public String validateAndGetUserDataForSystemVM(String userDataUuid) throws IOException {
        if (StringUtils.isBlank(userDataUuid)) {
            return null;
        }
        UserDataVO userDataVo = userDataDao.findByUuid(userDataUuid);
        if (userDataVo == null) {
            return null;
        }
        if (userDataVo.getDomainId() == Domain.ROOT_DOMAIN && userDataVo.getAccountId() == User.UID_ADMIN) {
            // Decode base64 user data, compress it, then re-encode to reduce command line length
            String plainTextUserData = new String(java.util.Base64.getDecoder().decode(userDataVo.getUserData()));
            CompressionUtil compressionUtil = new CompressionUtil();
            byte[] compressedUserData = compressionUtil.compressString(plainTextUserData);
            return java.util.Base64.getEncoder().encodeToString(compressedUserData);
        }
        throw new CloudRuntimeException("User data can only be used by system VMs if it belongs to the ROOT domain and ADMIN account.");
    }

    private byte[] validateAndDecodeByHTTPMethod(String userData, int maxHTTPLength, BaseCmd.HTTPMethod httpMethod) {
        byte[] decodedUserData = Base64.decodeBase64(userData.getBytes());
        if (decodedUserData == null || decodedUserData.length < 1) {
            throw new InvalidParameterValueException("User data is too short.");
        }

        logger.trace(String.format("Decoded user data: [%s].", decodedUserData));
        int userDataLength = userData.length();
        int decodedUserDataLength = decodedUserData.length;
        logger.info(String.format("Configured base64 encoded user data size: %d bytes, actual user data size: %d bytes", userDataLength, decodedUserDataLength));

        if (userDataLength > maxHTTPLength) {
            logger.warn(String.format("Base64 encoded user data (size: %d bytes) too long for http %s request (accepted size: %d bytes)", userDataLength, httpMethod.toString(), maxHTTPLength));
            throw new InvalidParameterValueException(String.format("User data is too long for http %s request", httpMethod.toString()));
        }
        if (userDataLength > VM_USERDATA_MAX_LENGTH.value()) {
            logger.warn(String.format("Base64 encoded user data (size: %d bytes) has exceeded configurable max length of %d bytes", userDataLength, VM_USERDATA_MAX_LENGTH.value()));
            throw new InvalidParameterValueException("User data has exceeded configurable max length: " + VM_USERDATA_MAX_LENGTH.value());
        }

        return decodedUserData;
    }
}
