//
// 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 com.cloud.network.bigswitch;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Type;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.HttpMethodBase;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.cookie.CookiePolicy;
import org.apache.commons.httpclient.methods.DeleteMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.PutMethod;
import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.apache.commons.httpclient.protocol.Protocol;
import org.apache.commons.httpclient.protocol.ProtocolSocketFactory;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

public class BigSwitchBcfApi {
    protected Logger logger = LogManager.getLogger(getClass());
    private final static String S_PROTOCOL = "https";
    private final static String S_NS_BASE_URL = "/networkService/v1.1";
    private final static String CONTENT_TYPE = "content-type";
    private final static String ACCEPT = "Accept";
    private final static String CONTENT_JSON = "application/json";
    private final static String HTTP_HEADER_INSTANCE_ID = "Instance-ID";
    private final static String CLOUDSTACK_INSTANCE_ID = "cloudstack";
    private final static String HASH_MATCH = "X-BSN-BVS-HASH-MATCH";
    private final static MultiThreadedHttpConnectionManager S_HTTP_CLIENT_MANAGER = new MultiThreadedHttpConnectionManager();

    private String host;
    private String username;
    private String password;
    private String hash;
    private String zoneId;
    private Boolean nat;

    private boolean isPrimary;

    private int _port = 8000;

    private HttpClient _client;
    private Gson gson = new Gson();

    public final static String HASH_CONFLICT = "HASH_CONFLICT";
    public final static String HASH_IGNORE = "HASH_IGNORE";

    /* This factory method is protected so we can extend this
     * in the unittests.
     */
    protected HttpClient createHttpClient() {
        return new HttpClient(S_HTTP_CLIENT_MANAGER);
    }

    protected HttpMethod createMethod(final String type, final String uri, final int port) throws BigSwitchBcfApiException {
        String url;
        try {
            url = new URL(S_PROTOCOL, host, port, uri).toString();
        } catch (MalformedURLException e) {
            logger.error("Unable to build Big Switch API URL", e);
            throw new BigSwitchBcfApiException("Unable to build Big Switch API URL", e);
        }

        if ("post".equalsIgnoreCase(type)) {
            return new PostMethod(url);
        } else if ("get".equalsIgnoreCase(type)) {
            return new GetMethod(url);
        } else if ("delete".equalsIgnoreCase(type)) {
            return new DeleteMethod(url);
        } else if ("put".equalsIgnoreCase(type)) {
            return new PutMethod(url);
        } else {
            throw new BigSwitchBcfApiException("Requesting unknown method type");
        }
    }

    public BigSwitchBcfApi() {
        _client = createHttpClient();
        _client.getParams().setCookiePolicy(CookiePolicy.BROWSER_COMPATIBILITY);

        try {
            // Cast to ProtocolSocketFactory to avoid the deprecated constructor with the SecureProtocolSocketFactory parameter
            Protocol.registerProtocol("https", new Protocol("https", (ProtocolSocketFactory) new TrustingProtocolSocketFactory(), _port));
        } catch (IOException e) {
            logger.warn("Failed to register the TrustingProtocolSocketFactory, falling back to default SSLSocketFactory", e);
        }
    }

    /**
     * Setter used by UI to set BSN controller address
     * @param address
     */
    public void setControllerAddress(final String address) {
        this.host = address;
    }

    /**
     * Setter used by UI to set BSN controller user name
     * @param username
     */
    public void setControllerUsername(final String username) {
        this.username = username;
    }

    /**
     * Setter used by UI to set BSN controller password
     * @param password
     */
    public void setControllerPassword(final String password) {
        this.password = password;
    }

    /**
     * Setter used by UI to set BSN controller NAT mode
     * @param nat
     */
    public void setControllerNat(final Boolean nat) {
        this.nat = nat;
    }

    public boolean isNatEnabled() {
        return this.nat;
    }

    public void setZoneId(final String zoneId) {
        this.zoneId = zoneId;
    }

    public String createNetwork(final NetworkData network) throws BigSwitchBcfApiException {
        String uri = S_NS_BASE_URL + "/tenants/" + network.getNetwork().getTenantId() + "/networks";
        return executeCreateObject(network, uri, Collections.<String, String> emptyMap());
    }

    public String deleteNetwork(final String tenantId, final String networkId) throws BigSwitchBcfApiException {
        String uri = S_NS_BASE_URL + "/tenants/" + tenantId + "/networks/" + networkId;
        return executeDeleteObject(uri);
    }

    public String createAttachment(final String tenantId, final String networkId,
            final AttachmentData attachment) throws BigSwitchBcfApiException {
        String uri = S_NS_BASE_URL + "/tenants/" + tenantId + "/networks/" + networkId + "/ports/" + attachment.getAttachment().getId() + "/attachment";
        return executeCreateObject(attachment, uri, Collections.<String, String> emptyMap());
    }

    public String modifyAttachment(final String tenantId, final String networkId,
            final AttachmentData attachment) throws BigSwitchBcfApiException {
        String uri = S_NS_BASE_URL + "/tenants/" + tenantId + "/networks/" + networkId + "/ports/" + attachment.getAttachment().getId() + "/attachment";
        return executeUpdateObject(attachment, uri, Collections.<String, String> emptyMap());
    }

    public String deleteAttachment(final String tenantId, final String networkId,
            final String attachmentId) throws BigSwitchBcfApiException {
        String uri = S_NS_BASE_URL + "/tenants/" + tenantId + "/networks/" + networkId + "/ports/" + attachmentId + "/attachment";
        return executeDeleteObject(uri);
    }

    public String createRouter(final String tenantId, final RouterData router) throws BigSwitchBcfApiException {
        String uri = S_NS_BASE_URL + "/tenants/" + tenantId + "/routers";
        return executeCreateObject(router, uri, Collections.<String, String> emptyMap());
    }

    public String modifyRouter(final String tenantId, final RouterData router) throws BigSwitchBcfApiException,
    IllegalArgumentException{
        String uri = S_NS_BASE_URL + "/tenants/" + tenantId + "/routers";
        return executeCreateObject(router, uri, Collections.<String, String> emptyMap());
    }

    public String createRouterInterface(final String tenantId, final String routerId,
            final RouterInterfaceData routerInterface) throws BigSwitchBcfApiException {
        String uri = S_NS_BASE_URL + "/tenants/" + tenantId + "/routers/" + routerId + "/interfaces";
        return executeCreateObject(routerInterface, uri, Collections.<String, String> emptyMap());
    }

    public String createFloatingIp(final String tenantId, final FloatingIpData fip) throws BigSwitchBcfApiException {
        String uri = S_NS_BASE_URL + "/tenants/" + tenantId + "/floatingips";
        return executeCreateObject(fip, uri, Collections.<String, String> emptyMap());
    }

    public String deleteFloatingIp(final String tenantId, final String fipId) throws BigSwitchBcfApiException {
        String uri = S_NS_BASE_URL + "/tenants/" + tenantId + "/floatingips/" + fipId;
        return executeDeleteObject(uri);
    }

    public ControlClusterStatus getControlClusterStatus() throws BigSwitchBcfApiException {
        String uri = S_NS_BASE_URL + "/health";
        ControlClusterStatus ccs = executeRetrieveObject(new TypeToken<ControlClusterStatus>() {
        }.getType(), uri, null);
        ccs.setStatus(true);
        return ccs;
    }

    public Capabilities getCapabilities() throws BigSwitchBcfApiException {
        String uri = S_NS_BASE_URL + "/capabilities";
        List<String> capslist = executeRetrieveObject(new TypeToken<List<String>>() {
        }.getType(), uri, null);
        Capabilities caps = new Capabilities();
        caps.setCapabilities(capslist);
        return caps;
    }

    public String syncTopology(final TopologyData topo) throws BigSwitchBcfApiException {
        String uri = S_NS_BASE_URL + "/topology";
        return executeCreateObject(topo, uri, Collections.<String, String> emptyMap());
    }

    public ControllerData getControllerData() {
        return new ControllerData(host, isPrimary);
    }

    private void checkInvariants() throws BigSwitchBcfApiException{
        if (host == null || host.isEmpty()) {
            throw new BigSwitchBcfApiException("Hostname is null or empty");
        }
        if (username == null || username.isEmpty()){
            throw new BigSwitchBcfApiException("Username is null or empty");
        }
        if (password == null || password.isEmpty()){
            throw new BigSwitchBcfApiException("Password is null or empty");
        }
    }

    private String checkResponse(final HttpMethodBase m, final String errorMessageBase) throws BigSwitchBcfApiException,
    IllegalArgumentException{
        String customErrorMsg = null;
        if (m.getStatusCode() == HttpStatus.SC_OK) {
            String hash = "";
            if (m.getResponseHeader(HASH_MATCH) != null) {
                hash = m.getResponseHeader(HASH_MATCH).getValue();
                set_hash(hash);
            }
            return hash;
        }
        if (m.getStatusCode() == HttpStatus.SC_CONFLICT) {
            if(m instanceof GetMethod) {
                return HASH_CONFLICT;
            }
            throw new BigSwitchBcfApiException("BCF topology sync required", true);
        }
        if (m.getStatusCode() == HttpStatus.SC_SEE_OTHER) {
            isPrimary = false;
            set_hash(HASH_IGNORE);
            return HASH_IGNORE;
        }
        if (m.getStatusCode() == HttpStatus.SC_NOT_FOUND) {
            if (m instanceof DeleteMethod){
                return "";
            }
        }
        if (m.getStatusCode() == HttpStatus.SC_BAD_REQUEST) {
            customErrorMsg = " Invalid data in BCF request";
            throw new IllegalArgumentException(customErrorMsg);
        }
        String errorMessage = responseToErrorMessage(m);
        m.releaseConnection();
        logger.error(errorMessageBase + errorMessage);
        throw new BigSwitchBcfApiException(errorMessageBase + errorMessage + customErrorMsg);
    }

    private void setHttpHeader(final HttpMethodBase m) {
        m.setRequestHeader(CONTENT_TYPE, CONTENT_JSON);
        m.setRequestHeader(ACCEPT, CONTENT_JSON);
        m.setRequestHeader(HTTP_HEADER_INSTANCE_ID, CLOUDSTACK_INSTANCE_ID + "-" + zoneId);
        if (StringUtils.isNotEmpty(hash)) {
            m.setRequestHeader(HASH_MATCH, hash);
        }

        String authString = username + ":" + password;
        String encodedAuthString = "Basic " + Base64.encodeBase64String(authString.getBytes(Charset.forName("UTF-8")));
        m.setRequestHeader("Authorization", encodedAuthString);
    }

    protected <T> String executeUpdateObject(final T newObject, final String uri,
            final Map<String, String> parameters) throws BigSwitchBcfApiException,
    IllegalArgumentException{
        checkInvariants();

        PutMethod pm = (PutMethod)createMethod("put", uri, _port);

        setHttpHeader(pm);

        try {
            pm.setRequestEntity(new StringRequestEntity(gson.toJson(newObject), CONTENT_JSON, null));
        } catch (UnsupportedEncodingException e) {
            throw new BigSwitchBcfApiException("Failed to encode json request body", e);
        }

        executeMethod(pm);

        String hash = checkResponse(pm, "BigSwitch HTTP update failed: ");

        pm.releaseConnection();

        return hash;
    }

    protected <T> String executeCreateObject(final T newObject, final String uri,
            final Map<String, String> parameters) throws BigSwitchBcfApiException {
        checkInvariants();

        PostMethod pm = (PostMethod)createMethod("post", uri, _port);

        setHttpHeader(pm);

        try {
            pm.setRequestEntity(new StringRequestEntity(gson.toJson(newObject), CONTENT_JSON, null));
        } catch (UnsupportedEncodingException e) {
            throw new BigSwitchBcfApiException("Failed to encode json request body", e);
        }

        executeMethod(pm);

        String hash = checkResponse(pm, "BigSwitch HTTP create failed: ");

        pm.releaseConnection();

        return hash;
    }

    protected String executeDeleteObject(final String uri) throws BigSwitchBcfApiException {
        checkInvariants();

        DeleteMethod dm = (DeleteMethod)createMethod("delete", uri, _port);

        setHttpHeader(dm);

        executeMethod(dm);

        String hash = checkResponse(dm, "BigSwitch HTTP delete failed: ");

        dm.releaseConnection();

        return hash;
    }

    @SuppressWarnings("unchecked")
    protected <T> T executeRetrieveObject(final Type returnObjectType,
            final String uri, final Map<String, String> parameters) throws BigSwitchBcfApiException {
        checkInvariants();

        GetMethod gm = (GetMethod)createMethod("get", uri, _port);

        setHttpHeader(gm);

        if (parameters != null && !parameters.isEmpty()) {
            List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(parameters.size());
            for (Entry<String, String> e : parameters.entrySet()) {
                nameValuePairs.add(new NameValuePair(e.getKey(), e.getValue()));
            }
            gm.setQueryString(nameValuePairs.toArray(new NameValuePair[0]));
        }

        executeMethod(gm);

        String hash = checkResponse(gm, "BigSwitch HTTP get failed: ");

        T returnValue;
        try {
            // CAUTIOUS: Safety margin of 2048 characters - extend if needed.
            returnValue = (T)gson.fromJson(gm.getResponseBodyAsString(2048), returnObjectType);
        } catch (IOException e) {
            logger.error("IOException while retrieving response body", e);
            throw new BigSwitchBcfApiException(e);
        } finally {
            gm.releaseConnection();
        }
        if(returnValue instanceof ControlClusterStatus) {
            if(HASH_CONFLICT.equals(hash)) {
                isPrimary = true;
                ((ControlClusterStatus) returnValue).setTopologySyncRequested(true);
            } else if (!HASH_IGNORE.equals(hash) && !isPrimary) {
                isPrimary = true;
                ((ControlClusterStatus) returnValue).setTopologySyncRequested(true);
            }
        }
        return returnValue;
    }

    protected void executeMethod(final HttpMethodBase method) throws BigSwitchBcfApiException {
        try {
            _client.executeMethod(method);
            if (method.getStatusCode() == HttpStatus.SC_UNAUTHORIZED) {
                method.releaseConnection();
            }
        } catch (HttpException e) {
            logger.error("HttpException caught while trying to connect to the BigSwitch Controller", e);
            method.releaseConnection();
            throw new BigSwitchBcfApiException("API call to BigSwitch Controller Failed", e);
        } catch (IOException e) {
            logger.error("IOException caught while trying to connect to the BigSwitch Controller", e);
            method.releaseConnection();
            throw new BigSwitchBcfApiException("API call to BigSwitch Controller Failed", e);
        }
    }

    private String responseToErrorMessage(final HttpMethodBase method) {
        assert method.isRequestSent() : "no use getting an error message unless the request is sent";

        if ("text/html".equals(method.getResponseHeader(CONTENT_TYPE).getValue())) {
            // The error message is the response content
            // Safety margin of 2048 characters, anything longer is probably useless
            // and will clutter the logs
            try {
                return method.getResponseBodyAsString(2048);
            } catch (IOException e) {
                logger.debug("Error while loading response body", e);
            }
        }

        // The default
        return method.getStatusText();
    }

    public static String getCloudstackInstanceId() {
        return CLOUDSTACK_INSTANCE_ID;
    }

    public String get_hash() {
        return hash;
    }

    public void set_hash(final String hash) {
        this.hash = hash;
    }

}
