// 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.cloudian.client;

import java.io.IOException;
import java.net.SocketTimeoutException;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import javax.net.ssl.SSLContext;
import javax.net.ssl.X509TrustManager;

import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.utils.security.SSLUtils;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.AuthCache;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.BasicAuthCache;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.log4j.Logger;

import com.cloud.utils.nio.TrustAllManager;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Strings;

public class CloudianClient {
    private static final Logger LOG = Logger.getLogger(CloudianClient.class);

    private final HttpClient httpClient;
    private final HttpClientContext httpContext;
    private final String adminApiUrl;

    public CloudianClient(final String host, final Integer port, final String scheme, final String username, final String password, final boolean validateSSlCertificate, final int timeout) throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException {
        final CredentialsProvider provider = new BasicCredentialsProvider();
        provider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(username, password));
        final HttpHost adminHost = new HttpHost(host, port, scheme);
        final AuthCache authCache = new BasicAuthCache();
        authCache.put(adminHost, new BasicScheme());

        this.adminApiUrl = adminHost.toURI();
        this.httpContext = HttpClientContext.create();
        this.httpContext.setCredentialsProvider(provider);
        this.httpContext.setAuthCache(authCache);

        final RequestConfig config = RequestConfig.custom()
                .setConnectTimeout(timeout * 1000)
                .setConnectionRequestTimeout(timeout * 1000)
                .setSocketTimeout(timeout * 1000)
                .build();

        if (!validateSSlCertificate) {
            final SSLContext sslcontext = SSLUtils.getSSLContext();
            sslcontext.init(null, new X509TrustManager[]{new TrustAllManager()}, new SecureRandom());
            final SSLConnectionSocketFactory factory = new SSLConnectionSocketFactory(sslcontext, NoopHostnameVerifier.INSTANCE);
            this.httpClient = HttpClientBuilder.create()
                    .setDefaultCredentialsProvider(provider)
                    .setDefaultRequestConfig(config)
                    .setSSLSocketFactory(factory)
                    .build();
        } else {
            this.httpClient = HttpClientBuilder.create()
                    .setDefaultCredentialsProvider(provider)
                    .setDefaultRequestConfig(config)
                    .build();
        }
    }

    private void checkAuthFailure(final HttpResponse response) {
        if (response != null && response.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED) {
            final Credentials credentials = httpContext.getCredentialsProvider().getCredentials(AuthScope.ANY);
            LOG.error("Cloudian admin API authentication failed, please check Cloudian configuration. Admin auth principal=" + credentials.getUserPrincipal() + ", password=" + credentials.getPassword() + ", API url=" + adminApiUrl);
            throw new ServerApiException(ApiErrorCode.UNAUTHORIZED, "Cloudian backend API call unauthorized, please ask your administrator to fix integration issues.");
        }
    }

    private void checkResponseOK(final HttpResponse response) {
        if (response.getStatusLine().getStatusCode() == HttpStatus.SC_NO_CONTENT) {
            LOG.debug("Requested Cloudian resource does not exist");
            return;
        }
        if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK && response.getStatusLine().getStatusCode() != HttpStatus.SC_NO_CONTENT) {
            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to find the requested resource and get valid response from Cloudian backend API call, please ask your administrator to diagnose and fix issues.");
        }
    }

    private boolean checkEmptyResponse(final HttpResponse response) throws IOException {
        return response != null && (response.getStatusLine().getStatusCode() == HttpStatus.SC_NO_CONTENT ||
                response.getEntity() == null ||
                response.getEntity().getContent() == null);
    }

    private void checkResponseTimeOut(final Exception e) {
        if (e instanceof ConnectTimeoutException || e instanceof SocketTimeoutException) {
            throw new ServerApiException(ApiErrorCode.RESOURCE_UNAVAILABLE_ERROR, "Operation timed out, please try again.");
        }
    }

    private HttpResponse delete(final String path) throws IOException {
        final HttpResponse response = httpClient.execute(new HttpDelete(adminApiUrl + path), httpContext);
        checkAuthFailure(response);
        return response;
    }

    private HttpResponse get(final String path) throws IOException {
        final HttpResponse response = httpClient.execute(new HttpGet(adminApiUrl + path), httpContext);
        checkAuthFailure(response);
        return response;
    }

    private HttpResponse post(final String path, final Object item) throws IOException {
        final ObjectMapper mapper = new ObjectMapper();
        final String json = mapper.writeValueAsString(item);
        final StringEntity entity = new StringEntity(json);
        final HttpPost request = new HttpPost(adminApiUrl + path);
        request.setHeader("Content-type", "application/json");
        request.setEntity(entity);
        final HttpResponse response = httpClient.execute(request, httpContext);
        checkAuthFailure(response);
        return response;
    }

    private HttpResponse put(final String path, final Object item) throws IOException {
        final ObjectMapper mapper = new ObjectMapper();
        final String json = mapper.writeValueAsString(item);
        final StringEntity entity = new StringEntity(json);
        final HttpPut request = new HttpPut(adminApiUrl + path);
        request.setHeader("Content-type", "application/json");
        request.setEntity(entity);
        final HttpResponse response = httpClient.execute(request, httpContext);
        checkAuthFailure(response);
        return response;
    }

    ////////////////////////////////////////////////////////
    //////////////// Public APIs: User /////////////////////
    ////////////////////////////////////////////////////////

    public boolean addUser(final CloudianUser user) {
        if (user == null) {
            return false;
        }
        LOG.debug("Adding Cloudian user: " + user);
        try {
            final HttpResponse response = put("/user", user);
            return response.getStatusLine().getStatusCode() == HttpStatus.SC_OK;
        } catch (final IOException e) {
            LOG.error("Failed to add Cloudian user due to:", e);
            checkResponseTimeOut(e);
        }
        return false;
    }

    public CloudianUser listUser(final String userId, final String groupId) {
        if (Strings.isNullOrEmpty(userId) || Strings.isNullOrEmpty(groupId)) {
            return null;
        }
        LOG.debug("Trying to find Cloudian user with id=" + userId + " and group id=" + groupId);
        try {
            final HttpResponse response = get(String.format("/user?userId=%s&groupId=%s", userId, groupId));
            checkResponseOK(response);
            if (checkEmptyResponse(response)) {
                return null;
            }
            final ObjectMapper mapper = new ObjectMapper();
            return mapper.readValue(response.getEntity().getContent(), CloudianUser.class);
        } catch (final IOException e) {
            LOG.error("Failed to list Cloudian user due to:", e);
            checkResponseTimeOut(e);
        }
        return null;
    }

    public List<CloudianUser> listUsers(final String groupId) {
        if (Strings.isNullOrEmpty(groupId)) {
            return new ArrayList<>();
        }
        LOG.debug("Trying to list Cloudian users in group id=" + groupId);
        try {
            final HttpResponse response = get(String.format("/user/list?groupId=%s&userType=all&userStatus=active", groupId));
            checkResponseOK(response);
            if (checkEmptyResponse(response)) {
                return new ArrayList<>();
            }
            final ObjectMapper mapper = new ObjectMapper();
            return Arrays.asList(mapper.readValue(response.getEntity().getContent(), CloudianUser[].class));
        } catch (final IOException e) {
            LOG.error("Failed to list Cloudian users due to:", e);
            checkResponseTimeOut(e);
        }
        return new ArrayList<>();
    }

    public boolean updateUser(final CloudianUser user) {
        if (user == null) {
            return false;
        }
        LOG.debug("Updating Cloudian user: " + user);
        try {
            final HttpResponse response = post("/user", user);
            return response.getStatusLine().getStatusCode() == HttpStatus.SC_OK;
        } catch (final IOException e) {
            LOG.error("Failed to update Cloudian user due to:", e);
            checkResponseTimeOut(e);
        }
        return false;
    }

    public boolean removeUser(final String userId, final String groupId) {
        if (Strings.isNullOrEmpty(userId) || Strings.isNullOrEmpty(groupId)) {
            return false;
        }
        LOG.debug("Removing Cloudian user with user id=" + userId + " in group id=" + groupId);
        try {
            final HttpResponse response = delete(String.format("/user?userId=%s&groupId=%s", userId, groupId));
            return response.getStatusLine().getStatusCode() == HttpStatus.SC_OK;
        } catch (final IOException e) {
            LOG.error("Failed to remove Cloudian user due to:", e);
            checkResponseTimeOut(e);
        }
        return false;
    }

    /////////////////////////////////////////////////////////
    //////////////// Public APIs: Group /////////////////////
    /////////////////////////////////////////////////////////

    public boolean addGroup(final CloudianGroup group) {
        if (group == null) {
            return false;
        }
        LOG.debug("Adding Cloudian group: " + group);
        try {
            final HttpResponse response = put("/group", group);
            return response.getStatusLine().getStatusCode() == HttpStatus.SC_OK;
        } catch (final IOException e) {
            LOG.error("Failed to add Cloudian group due to:", e);
            checkResponseTimeOut(e);
        }
        return false;
    }

    public CloudianGroup listGroup(final String groupId) {
        if (Strings.isNullOrEmpty(groupId)) {
            return null;
        }
        LOG.debug("Trying to find Cloudian group with id=" + groupId);
        try {
            final HttpResponse response = get(String.format("/group?groupId=%s", groupId));
            checkResponseOK(response);
            if (checkEmptyResponse(response)) {
                return null;
            }
            final ObjectMapper mapper = new ObjectMapper();
            return mapper.readValue(response.getEntity().getContent(), CloudianGroup.class);
        } catch (final IOException e) {
            LOG.error("Failed to list Cloudian group due to:", e);
            checkResponseTimeOut(e);
        }
        return null;
    }

    public List<CloudianGroup> listGroups() {
        LOG.debug("Trying to list Cloudian groups");
        try {
            final HttpResponse response = get("/group/list");
            checkResponseOK(response);
            if (checkEmptyResponse(response)) {
                return new ArrayList<>();
            }
            final ObjectMapper mapper = new ObjectMapper();
            return Arrays.asList(mapper.readValue(response.getEntity().getContent(), CloudianGroup[].class));
        } catch (final IOException e) {
            LOG.error("Failed to list Cloudian groups due to:", e);
            checkResponseTimeOut(e);
        }
        return new ArrayList<>();
    }

    public boolean updateGroup(final CloudianGroup group) {
        if (group == null) {
            return false;
        }
        LOG.debug("Updating Cloudian group: " + group);
        try {
            final HttpResponse response = post("/group", group);
            return response.getStatusLine().getStatusCode() == HttpStatus.SC_OK;
        } catch (final IOException e) {
            LOG.error("Failed to remove group due to:", e);
            checkResponseTimeOut(e);
        }
        return false;
    }

    public boolean removeGroup(final String groupId) {
        if (Strings.isNullOrEmpty(groupId)) {
            return false;
        }
        LOG.debug("Removing Cloudian group id=" + groupId);
        try {
            final HttpResponse response = delete(String.format("/group?groupId=%s", groupId));
            return response.getStatusLine().getStatusCode() == HttpStatus.SC_OK;
        } catch (final IOException e) {
            LOG.error("Failed to remove group due to:", e);
            checkResponseTimeOut(e);
        }
        return false;
    }
}
