// 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.hypervisor.xenserver.resource;

import com.cloud.utils.NumbersUtil;
import com.cloud.utils.PropertiesUtil;
import com.cloud.utils.exception.CloudRuntimeException;
import com.xensource.xenapi.APIVersion;
import com.xensource.xenapi.Connection;
import com.xensource.xenapi.ConnectionNew;
import com.xensource.xenapi.Host;
import com.xensource.xenapi.Pool;
import com.xensource.xenapi.Session;
import com.xensource.xenapi.Types;
import com.xensource.xenapi.Types.BadServerResponse;
import com.xensource.xenapi.Types.XenAPIException;
import org.apache.cloudstack.utils.security.SSLUtils;
import org.apache.cloudstack.utils.security.SecureSSLSocketFactory;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import org.apache.xmlrpc.XmlRpcException;
import org.apache.xmlrpc.client.XmlRpcClientException;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSession;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Queue;

public class XenServerConnectionPool {
    protected static Logger LOGGER = LogManager.getLogger(XenServerConnectionPool.class);
    protected HashMap<String /* poolUuid */, XenServerConnection> _conns = new HashMap<String, XenServerConnection>();
    protected int _retries;
    protected int _interval;
    protected int _connWait = 5;
    protected static long s_sleepOnError = 10 * 1000; // in ms
    static {
        File file = PropertiesUtil.findConfigFile("environment.properties");
        if (file == null) {
            LOGGER.debug("Unable to find environment.properties");
        } else {
            try {
                final Properties props = PropertiesUtil.loadFromFile(file);
                String search = props.getProperty("sleep.interval.on.error");
                if (search != null) {
                    s_sleepOnError = NumbersUtil.parseInterval(search, 10) * 1000;
                }
                LOGGER.info("XenServer Connection Pool Configs: sleep.interval.on.error=" + s_sleepOnError);
            } catch (FileNotFoundException e) {
                LOGGER.debug("File is not found", e);
            } catch (IOException e) {
                LOGGER.debug("IO Exception while reading file", e);
            }
        }
        try {
            javax.net.ssl.TrustManager[] trustAllCerts = new javax.net.ssl.TrustManager[1];
            javax.net.ssl.TrustManager tm = new TrustAllManager();
            trustAllCerts[0] = tm;
            javax.net.ssl.SSLContext sc = SSLUtils.getSSLContext();
            sc.init(null, trustAllCerts, null);
            javax.net.ssl.HttpsURLConnection.setDefaultSSLSocketFactory(new SecureSSLSocketFactory(sc));
            HostnameVerifier hv = new HostnameVerifier() {
                @Override
                public boolean verify(String hostName, SSLSession session) {
                    return true;
                }
            };
            HttpsURLConnection.setDefaultHostnameVerifier(hv);
        } catch (NoSuchAlgorithmException e) {
            //ignore this
        } catch (KeyManagementException e) {
            LOGGER.debug("Init SSLContext failed ", e);
        }
    }

    protected XenServerConnectionPool() {
        _retries = 1;
        _interval = 3;
    }

    private void addConnect(String poolUuid, XenServerConnection conn) {
        if (poolUuid == null)
            return;
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Add master connection through " + conn.getIp() + " for pool(" + conn.getPoolUuid() + ")");
        }
        synchronized (_conns) {
            _conns.put(poolUuid, conn);
        }
    }

    private XenServerConnection getConnect(String poolUuid) {
        if (poolUuid == null)
            return null;
        synchronized (_conns) {
            return _conns.get(poolUuid);
        }
    }

    private void removeConnect(String poolUuid) {
        if (poolUuid == null) {
            return;
        }
        XenServerConnection conn = null;
        synchronized (_conns) {
            conn = _conns.remove(poolUuid);
        }
        if (conn != null) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Remove master connection through " + conn.getIp() + " for pool(" + conn.getPoolUuid() + ")");
            }

        }
    }

    static void forceSleep(long sec) {
        long firetime = System.currentTimeMillis() + (sec * 1000);
        long msec = sec * 1000;
        while (true) {
            if (msec < 100)
                break;
            try {
                Thread.sleep(msec);
                return;
            } catch (InterruptedException e) {
                msec = firetime - System.currentTimeMillis();
            }
        }
    }

    public Connection getConnect(String ip, String username, Queue<String> password) {
        Connection conn = new ConnectionNew(getURL(ip), 10, _connWait);
        try {
            loginWithPassword(conn, username, password, APIVersion.latest().toString());
        }  catch (Types.HostIsSlave e) {
            String maddress = e.masterIPAddress;
            conn = new ConnectionNew(getURL(maddress), 10, _connWait);
            try {
                loginWithPassword(conn, username, password, APIVersion.latest().toString());
            }  catch (Exception e1) {
                String msg = "Unable to create master connection to host(" + maddress +") , due to " + e1.toString();
                LOGGER.debug(msg);
                throw new CloudRuntimeException(msg, e1);
            }
        } catch (Exception e) {
            String msg = "Unable to create master connection to host(" + ip +") , due to " + e.toString();
            LOGGER.debug(msg);
            throw new CloudRuntimeException(msg, e);
        }
        return conn;
    }

    public URL getURL(String ip) {
        try {
            return new URL("https://" + ip);
        } catch (Exception e) {
            String msg = "Unable to convert IP " + ip + " to URL due to " + e.toString();
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug(msg);
            }
            throw new CloudRuntimeException(msg, e);
        }
    }

    public Connection connect(String hostUuid, String poolUuid, String ipAddress,
            String username, Queue<String> password, int wait) {
        XenServerConnection mConn = null;
        if (hostUuid == null || poolUuid == null || ipAddress == null || username == null || password == null) {
            String msg = "Connect some parameter are null hostUuid:" + hostUuid + " ,poolUuid:" + poolUuid
                    + " ,ipAddress:" + ipAddress;
            LOGGER.debug(msg);
            throw new CloudRuntimeException(msg);
        }
        synchronized (poolUuid.intern()) {
            mConn = getConnect(poolUuid);
            if (mConn != null){
                try{
                    Host host = Host.getByUuid(mConn, hostUuid);
                    if (!host.getEnabled(mConn)) {
                        String msg = "Cannot connect this host " + ipAddress + " due to the host is not enabled";
                        LOGGER.debug(msg);
                        if (mConn.getIp().equalsIgnoreCase(ipAddress)) {
                            removeConnect(poolUuid);
                            mConn = null;
                        }
                        throw new CloudRuntimeException(msg);
                    }
                    return mConn;
                } catch (CloudRuntimeException e) {
                        throw e;
                } catch (Exception e) {
                    if (LOGGER.isDebugEnabled()) {
                        String ip = mConn != null ? mConn.getIp() : null;
                        LOGGER.debug("connect through IP(" + ip + ") for pool(" + poolUuid + ") is broken due to " + e.toString());
                    }
                    removeConnect(poolUuid);
                    mConn = null;
                }
            }

            if ( mConn == null ) {
                try {
                    Connection conn = new ConnectionNew(getURL(ipAddress), 5, _connWait);
                    Session sess = loginWithPassword(conn, username, password, APIVersion.latest().toString());
                    Host host = sess.getThisHost(conn);
                    Boolean hostenabled = host.getEnabled(conn);
                    if( sess != null ){
                        try{
                            Session.logout(conn);
                        } catch (Exception e) {
                            LOGGER.debug("Caught exception during logout", e);
                        }
                    }
                    if (!hostenabled) {
                        String msg = "Unable to create master connection, due to master Host " + ipAddress + " is not enabled";
                        LOGGER.debug(msg);
                        throw new CloudRuntimeException(msg);
                    }
                    mConn = new XenServerConnection(getURL(ipAddress), ipAddress, username, password, _retries, _interval, wait, _connWait);
                    loginWithPassword(mConn, username, password, APIVersion.latest().toString());
                }  catch (Types.HostIsSlave e) {
                    String maddress = e.masterIPAddress;
                    mConn = new XenServerConnection(getURL(maddress), maddress, username, password, _retries, _interval, wait, _connWait);
                    try {
                        Session session = loginWithPassword(mConn, username, password, APIVersion.latest().toString());
                        Host host = session.getThisHost(mConn);
                        if (!host.getEnabled(mConn)) {
                            String msg = "Unable to create master connection, due to master Host " + maddress + " is not enabled";
                            LOGGER.debug(msg);
                            throw new CloudRuntimeException(msg);
                        }
                    }  catch (Exception e1) {
                        String msg = "Unable to create master connection to host(" + maddress +") , due to " + e1.toString();
                        LOGGER.debug(msg);
                        throw new CloudRuntimeException(msg, e1);

                    }
                } catch (CloudRuntimeException e) {
                        throw e;
                } catch (Exception e) {
                    String msg = "Unable to create master connection to host(" + ipAddress +") , due to " + e.toString();
                    LOGGER.debug(msg);
                    throw new CloudRuntimeException(msg, e);
                }
                addConnect(poolUuid, mConn);
            }
        }
        return mConn;
    }



    protected Session slaveLocalLoginWithPassword(Connection conn, String username, Queue<String> password) throws BadServerResponse, XenAPIException, XmlRpcException {
        Session s = null;
        boolean logged_in = false;
        Exception ex = null;
        while (!logged_in) {
            try {
                s = Session.slaveLocalLoginWithPassword(conn, username, password.peek());
                logged_in = true;
            } catch (BadServerResponse e) {
                logged_in = false;
                ex = e;
            } catch (XenAPIException e) {
                logged_in = false;
                ex = e;
            } catch (XmlRpcException e) {
                logged_in = false;
                ex = e;
            }
            if (logged_in && conn != null) {
                break;
            } else {
                if (password.size() > 1) {
                    password.remove();
                    continue;
                } else {
                    // the last password did not work leave it and flag error
                    if (ex instanceof BadServerResponse) {
                        throw (BadServerResponse)ex;
                    } else if (ex instanceof XmlRpcException) {
                        throw (XmlRpcException)ex;
                    } else if (ex instanceof Types.SessionAuthenticationFailed) {
                        throw (Types.SessionAuthenticationFailed)ex;
                    } else if (ex instanceof XenAPIException) {
                        throw (XenAPIException)ex;
                    }
                    break;
                }
            }
        }
        return s;
    }

    protected Session loginWithPassword(Connection conn, String username, Queue<String> password, String version) throws BadServerResponse, XenAPIException,
        XmlRpcException {
        Session s = null;
        boolean logged_in = false;
        Exception ex = null;
        while (!logged_in) {
            try {
                s = Session.loginWithPassword(conn, username, password.peek(), APIVersion.latest().toString());
                logged_in = true;
            } catch (BadServerResponse e) {
                logged_in = false;
                ex = e;
            } catch (XenAPIException e) {
                logged_in = false;
                ex = e;
            } catch (XmlRpcException e) {
                logged_in = false;
                ex = e;
            }

            if (logged_in && conn != null) {
                break;
            } else {
                if (password.size() > 1) {
                    password.remove();
                    continue;
                } else {
                    // the last password did not work leave it and flag error
                    if (ex instanceof BadServerResponse) {
                        throw (BadServerResponse)ex;
                    } else if (ex instanceof XmlRpcException) {
                        throw (XmlRpcException)ex;
                    } else if (ex instanceof Types.SessionAuthenticationFailed) {
                        throw (Types.SessionAuthenticationFailed)ex;
                    } else if (ex instanceof XenAPIException) {
                        throw (XenAPIException)ex;
                    }
                }
            }
        }
        return s;
    }

    protected void join(Connection conn, String masterIp, String username, Queue<String> password) throws BadServerResponse, XenAPIException, XmlRpcException,
        Types.JoiningHostCannotContainSharedSrs {

        boolean logged_in = false;
        Exception ex = null;
        while (!logged_in) {
            try {
                Pool.join(conn, masterIp, username, password.peek());
                logged_in = true;
            } catch (BadServerResponse e) {
                logged_in = false;
                ex = e;
            } catch (XenAPIException e) {
                logged_in = false;
                ex = e;
            } catch (XmlRpcException e) {
                logged_in = false;
                ex = e;
            }
            if (logged_in && conn != null) {
                break;
            } else {
                if (password.size() > 1) {
                    password.remove();
                    continue;
                } else {
                    // the last password did not work leave it and flag error
                    if (ex instanceof BadServerResponse) {
                        throw (BadServerResponse)ex;
                    } else if (ex instanceof XmlRpcException) {
                        throw (XmlRpcException)ex;
                    } else if (ex instanceof Types.SessionAuthenticationFailed) {
                        throw (Types.SessionAuthenticationFailed)ex;
                    } else if (ex instanceof XenAPIException) {
                        throw (XenAPIException)ex;
                    }
                    break;
                }
            }
        }
    }

    static public Pool.Record getPoolRecord(Connection conn) throws XmlRpcException, XenAPIException {
        Map<Pool, Pool.Record> pools = Pool.getAllRecords(conn);
        assert pools.size() == 1 : "Pool size is not one....hmmm....wth? " + pools.size();

        return pools.values().iterator().next();
    }

    private static final XenServerConnectionPool s_instance = new XenServerConnectionPool();

    public static XenServerConnectionPool getInstance() {
        return s_instance;
    }

    public class XenServerConnection extends ConnectionNew {
        long _interval;
        int _retries;
        String _ip;
        String _username;
        Queue<String> _password;
        String _poolUuid;

        public XenServerConnection(URL url, String ip, String username, Queue<String> password, int retries, int interval, int wait, int connwait) {
            super(url, wait, connwait);
            _ip = ip;
            _retries = retries;
            _username = username;
            _password = password;
            _interval = (long)interval * 1000;

        }

        public String getPoolUuid() {
            return _poolUuid;
        }

        public String getUsername() {
            return _username;
        }

        public Queue<String> getPassword() {
            return _password;
        }

        public String getIp() {
            return _ip;
        }

        @Override
        protected Map dispatch(String methodcall, Object[] methodparams)  throws XmlRpcException, XenAPIException {
            if (methodcall.equals("session.local_logout")
                    || methodcall.equals("session.slave_local_login_with_password")
                    || methodcall.equals("session.logout")
                    || methodcall.equals("session.login_with_password")) {
                return super.dispatch(methodcall, methodparams);
            }

            try {
                return super.dispatch(methodcall, methodparams);
            } catch (Types.SessionInvalid e) {
                LOGGER.debug("Session is invalid for method: " + methodcall + " due to " + e.toString());
                removeConnect(_poolUuid);
                throw e;
            } catch (XmlRpcClientException e) {
                LOGGER.debug("XmlRpcClientException for method: " + methodcall + " due to " + e.toString());
                removeConnect(_poolUuid);
                throw e;
            } catch (XmlRpcException e) {
                LOGGER.debug("XmlRpcException for method: " + methodcall + " due to " + e.toString());
                removeConnect(_poolUuid);
                throw e;
            } catch (Types.HostIsSlave e) {
                 LOGGER.debug("HostIsSlave Exception for method: " + methodcall + " due to " + e.toString());
                 removeConnect(_poolUuid);
                 throw e;
            }
        }
    }

    public static class TrustAllManager implements javax.net.ssl.TrustManager, javax.net.ssl.X509TrustManager {
        @Override
        public java.security.cert.X509Certificate[] getAcceptedIssuers() {
            return null;
        }

        public boolean isServerTrusted(java.security.cert.X509Certificate[] certs) {
            return true;
        }

        public boolean isClientTrusted(java.security.cert.X509Certificate[] certs) {
            return true;
        }

        @Override
        public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) throws java.security.cert.CertificateException {
            return;
        }

        @Override
        public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) throws java.security.cert.CertificateException {
            return;
        }
    }

}
