package org.holer.server.util;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;

import java.io.File;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.holer.common.constant.HolerConst;
import org.holer.common.model.HolerMsg;
import org.holer.common.util.HolerUtil;
import org.holer.server.constant.HolerType;
import org.holer.server.constant.ServerConst;
import org.holer.server.container.ServerContainer;
import org.holer.server.db.DBClientService;
import org.holer.server.db.DBPortService;
import org.holer.server.model.HolerChannel;
import org.holer.server.model.HolerClient;
import org.holer.server.model.HolerPort;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ServerMgr {
    private static final Logger log = LoggerFactory.getLogger(ServerMgr.class);

    private static final Map<Integer, Channel> bindChannels = new ConcurrentHashMap();
    private static final Map<String, HolerChannel> holerChannels = new ConcurrentHashMap();
    private static final Map<String, HolerClient> trialClients = new ConcurrentHashMap();
    private static final AtomicLong extraClientId = new AtomicLong(0L);


    private static Integer webServerPort;

    private static String serverDomain;

    private static String serverHost;

    private static Integer serverPort;

    private static String proxyContent;

    private static boolean sslServerEnable;

    private static String sslServerHost;

    private static Integer sslServerPort;

    private static String nginxHome;

    private static String nginxConf;

    private static String domainName;

    private static String serialNo;


    public static boolean isSslServerEnable() {
        return sslServerEnable;
    }


    public static String getSslServerHost() {
        return sslServerHost;
    }


    public static Integer getSslServerPort() {
        return sslServerPort;
    }


    public static String getDomainName() {
        return domainName;
    }


    public static String getNginxHome() {
        return nginxHome;
    }


    public static String getNginxConf() {
        return nginxConf;
    }


    public static String getServerDomain() {
        return serverDomain;
    }


    public static String getNginxBin() {
        StringBuilder nginxBin = new StringBuilder();
        nginxBin.append(nginxHome).append(File.separatorChar);
        if (ServerConst.OS_TYPE.contains("win")) {
            nginxBin.append("nginx.exe");
            return nginxBin.toString();
        }
        nginxBin.append("sbin").append(File.separatorChar).append("nginx");
        return nginxBin.toString();
    }


    public static String getHolerConfDir() {
        StringBuilder confDir = new StringBuilder();
        confDir.append(nginxConf).append(File.separatorChar).append("holer").append(File.separatorChar);
        return confDir.toString();
    }


    public static Integer getWebServerPort() {
        return webServerPort;
    }


    public static Integer getServerPort() {
        return serverPort;
    }


    public static String getServerHost() {
        return serverHost;
    }


    public static String getProxyContent() {
        return proxyContent;
    }


    public static String getSerialNo() {
        return serialNo;
    }


    public static Map<String, HolerChannel> holerChannels() {
        return holerChannels;
    }


    public static Map<String, HolerClient> trialClients() {
        return trialClients;
    }


    public static String extraClientId() {
        return String.valueOf(extraClientId.incrementAndGet());
    }


    public static void initCfg() {
        serverDomain = ServerUtil.property(ServerConst.SERVER_DOMAIN);
        if (StringUtils.isBlank(serverDomain)) {
            serverDomain = "wdom.net";
        }

        serverHost = ServerUtil.property(ServerConst.SERVER_HOST);
        serverPort = Integer.valueOf(ServerUtil.property(ServerConst.SERVER_PORT));

        sslServerEnable = Boolean.valueOf(ServerUtil.property(ServerConst.SSLSERVER_ENABLE));
        sslServerHost = ServerUtil.property(ServerConst.SSLSERVER_HOST);
        sslServerPort = Integer.valueOf(ServerUtil.property(ServerConst.SSLSERVER_PORT));

        domainName = ServerUtil.property(ServerConst.HOLER_DOMAIN_NAME);
        serialNo = ServerUtil.property(ServerConst.HOLER_SERIAL_NO);

        nginxHome = StringUtils.replaceChars(ServerUtil.property(ServerConst.HOLER_NGINX_HOME), '/', File.separatorChar);
        nginxConf = nginxHome + File.separatorChar + "conf";
        webServerPort = 8080;

        String httpPort = ServerUtil.property(ServerConst.WEB_SERVER_PORT);
        if (StringUtils.isNotBlank(httpPort) && StringUtils.isNumeric(httpPort)) {
            webServerPort = Integer.valueOf(httpPort);
        }


        try {
            InputStream is = ServerMgr.class.getClassLoader().getResourceAsStream("conf/proxy.conf");
            proxyContent = IOUtils.toString(is, "UTF-8");
            HolerUtil.close(is);


            String sslSource = "conf" + File.separatorChar + "nginx-server.";
            String sslDest = nginxConf + File.separatorChar + "nginx-server.";
            File sslFile = new File(sslDest + "pem");
            if (!sslFile.exists()) {

                is = ServerMgr.class.getClassLoader().getResourceAsStream(sslSource + "pem");
                FileUtils.copyInputStreamToFile(is, new File(sslDest + "pem"));
                HolerUtil.close(is);
            }

            sslFile = new File(sslDest + "key");
            if (!sslFile.exists()) {

                is = ServerMgr.class.getClassLoader().getResourceAsStream(sslSource + "key");
                FileUtils.copyInputStreamToFile(is, new File(sslDest + "key"));
                HolerUtil.close(is);
            }


            File confDir = new File(getHolerConfDir());
            if (!confDir.exists()) {
                FileUtils.forceMkdir(confDir);
            }

            File confFile = new File(getHolerConfDir() + "holer.conf");
            if (!confFile.exists()) {

                HolerPort hp = new HolerPort();
                hp.setDomain(ServerUtil.defaultDomain(0));
                hp.setPortNum(webServerPort);
                hp.setType(HolerType.HTTP);
                newNginxConf("holer.conf", hp);

                confFile = new File(nginxConf + File.separatorChar + "nginx.conf");
                FileUtils.copyFile(confFile, new File(confFile.getAbsolutePath() + ".orig"));

                String content = FileUtils.readFileToString(confFile, "UTF-8");
                content = StringUtils.removeEnd(content.trim(), "}") + "    server_names_hash_bucket_size 256;\n    include    holer/*.conf;\n}";
                FileUtils.write(confFile, content, "UTF-8");
            }


            HolerUtil.exec(nginxReload());
        } catch (Exception e) {

            log.error("Failed to initialize configuration.", e);
        }
    }


    public static void initHoler() {
        DBClientService clientService = ServerUtil.getInstance().getClientService();
        DBPortService portService = ServerUtil.getInstance().getPortService();

        List<HolerClient> clients = clientService.findAll();
        if (CollectionUtils.isEmpty(clients)) {
            return;
        }


        String accesskey = "";
        Map<Integer, HolerPort> portMap = null;
        Map<String, Map<Integer, HolerPort>> portTable = new HashMap<String, Map<Integer, HolerPort>>();

        List<HolerPort> ports = portService.findAll();
        if (CollectionUtils.isNotEmpty(ports)) {
            for (HolerPort port : ports) {

                accesskey = port.getAccessKey();
                portMap = (Map) portTable.get(accesskey);
                if (null == portMap) {
                    portMap = new HashMap<Integer, HolerPort>();
                }
                portMap.put(port.getPortNum(), port);
                portTable.put(accesskey, portMap);
            }
        }

        holerChannels.clear();
        for (HolerClient client : clients) {

            client.setPorts(null);

            portMap = (Map) portTable.get(client.getAccessKey());
            if (null == portMap) {
                portMap = new HashMap<Integer, HolerPort>();
            }

            HolerChannel hc = new HolerChannel();
            hc.setClient(client);
            hc.setPorts(portMap);

            holerChannels.put(client.getAccessKey(), hc);
        }
    }


    public static void saveClient(HolerClient client) {
        if (null == client) {
            return;
        }


        HolerChannel hc = (HolerChannel) holerChannels.get(client.getAccessKey());
        if (null == hc) {

            hc = new HolerChannel();
        } else {

            client.setChannel(hc.getClient().getChannel());
        }

        hc.setClient(client);
        holerChannels.put(client.getAccessKey(), hc);
    }


    public static void savePort(HolerPort port) {
        if (null == port) {
            return;
        }
        HolerChannel hc = (HolerChannel) holerChannels.get(port.getAccessKey());
        if (null == hc) {
            return;
        }
        if (null == hc.getPorts()) {
            hc.setPorts(new HashMap<>());
        }
        hc.getPorts().put(port.getPortNum(), port);
        addProxy(port);
        ServerContainer.getContainer().listenPort(port);
    }


    public static void deleteClient(HolerClient client) {
        if (null == client) {
            return;
        }


        String key = client.getAccessKey();
        if (!holerChannels.containsKey(key)) {
            return;
        }


        HolerChannel hc = (HolerChannel) holerChannels.get(key);
        if (null != hc && MapUtils.isNotEmpty(hc.getPorts())) {

            for (HolerPort port : hc.getPorts().values()) {

                deleteProxy(port);
                unbind(port.getPortNum());
            }
            hc.getPorts().clear();
        }

        removeExtraChannels(key);
        HolerUtil.close(hc.getClient().getChannel());
        holerChannels.remove(key);
    }



    public static void deletePort(HolerPort port) {
        if (null == port) {
            return;
        }

        HolerChannel hc = (HolerChannel) holerChannels.get(port.getAccessKey());
        if (null == hc || MapUtils.isEmpty(hc.getPorts())) {
            return;
        }

        Integer portNum = port.getPortNum();
        if (!hc.getPorts().containsKey(portNum)) {
            return;
        }
        port = (HolerPort) hc.getPorts().get(portNum);
        hc.getPorts().remove(portNum);
        deleteProxy(port);
        unbind(port.getPortNum());
    }


    public static HolerChannel holerChannel(String key) {
        return (HolerChannel) holerChannels.get(key);
    }


    public static HolerClient holerClient(String key) {
        if (StringUtils.isEmpty(key)) {
            return null;
        }
        HolerChannel hc = holerChannel(key);
        if (null == hc) {
            return null;
        }
        return hc.getClient();
    }


    public static Integer port(Channel channel) {
        if (null == channel) {
            return null;
        }
        InetSocketAddress addr = (InetSocketAddress) channel.localAddress();
        return addr.getPort();
    }


    public static String host(Channel channel) {
        if (null == channel) {
            return "";
        }
        InetSocketAddress addr = (InetSocketAddress) channel.remoteAddress();
        return addr.getHostName();
    }


    public static HolerChannel holerChannel(Integer portNum) {
        Set<Map.Entry<String, HolerChannel>> entries = holerChannels.entrySet();
        for (Map.Entry<String, HolerChannel> entry : entries) {

            if (((HolerChannel) entry.getValue()).getPorts().containsKey(portNum)) {
                return (HolerChannel) entry.getValue();
            }
        }
        return null;
    }


    public static HolerPort holerPort(String key, Integer portNum) {
        if (StringUtils.isEmpty(key)) {
            return null;
        }

        HolerChannel hc = holerChannel(key);
        if (null == hc || MapUtils.isEmpty(hc.getPorts())) {
            return null;
        }
        return (HolerPort) hc.getPorts().get(portNum);
    }


    public static HolerPort holerPort(String uri) {
        if (StringUtils.isEmpty(uri) || !StringUtils.isNumeric(uri)) {
            return null;
        }

        Integer portNum = Integer.valueOf(uri);
        HolerChannel hc = holerChannel(portNum);
        if (null == hc || MapUtils.isEmpty(hc.getPorts()) || !hc.getPorts().containsKey(portNum)) {
            return null;
        }
        return (HolerPort) hc.getPorts().get(portNum);
    }


    public static void removeHolerChannel(String key) {
        if (StringUtils.isEmpty(key)) {
            return;
        }


        removeExtraChannels(key);
        HolerClient client = holerClient(key);
        if (null == client) {
            return;
        }


        Channel holerChannel = client.getChannel();
        if (null == holerChannel) {
            return;
        }


        HolerUtil.close(holerChannel);
        client.setChannel(null);
    }


    public static void removeHolerChannel(Channel holerChannel) {
        if (null == holerChannel) {
            return;
        }


        String key = (String) holerChannel.attr(HolerConst.HOLER_KEY).get();
        removeHolerChannel(key);
        HolerUtil.close(holerChannel);
        log.info("Disconnect holer channel {}", holerChannel);
    }


    public static void notify(Channel holerChannel, byte msgType, Long serialNum) {
        if (!HolerUtil.isActive(holerChannel)) {
            return;
        }


        HolerMsg msg = new HolerMsg();
        if (null != serialNum) {
            msg.setSerialNumber(serialNum);
        }
        msg.setType(msgType);
        holerChannel.writeAndFlush(msg);
    }


    public static boolean isEnabledClient(String accessKey) {
        if (StringUtils.isEmpty(accessKey)) {
            return false;
        }
        HolerChannel hc = holerChannel(accessKey);
        if (null == hc || null == hc.getClient()) {
            return false;
        }
        return hc.getClient().getEnabled();
    }

    public static boolean isOveruse(HolerClient client) {
        if (null == client || null == client.getOnlineAt()) {
            return false;
        }
        long time = System.currentTimeMillis() - client.getOnlineAt().getTime();
        if (time >= 1200000L) {
            return true;
        }
        return false;
    }


    public static boolean isOveruse(Channel channel, String accessKey) {
        if (null == channel || StringUtils.isEmpty(accessKey)) {
            return false;
        }

        String hostName = host(channel);
        String key = hostName + accessKey;
        HolerClient client = (HolerClient) trialClients.get(key);
        return isOveruse(client);
    }

    public static boolean afterOneDay(HolerClient client) {
        if (null == client) {
            return false;
        }
        long time = System.currentTimeMillis() - client.getOnlineAt().getTime();
        if (time >= 86400000L) {
            return true;
        }
        return false;
    }


    public static void bind(ServerBootstrap strap, Integer port) {
        if (null == strap || null == port) {
            return;
        }


        try {
            ChannelFuture cf = strap.bind(port.intValue());
            cf.get();
            bindChannels.put(port, cf.channel());
        } catch (Exception e) {


            if (!(e.getCause() instanceof java.net.BindException)) {

                log.error("Failed to bind port " + port, e);
                throw new RuntimeException(e);
            }
        }
    }


    public static void unbind(Integer port) {
        if (null == port) {
            return;
        }

        HolerUtil.close((Channel) bindChannels.get(port));
        bindChannels.remove(port);
    }

    public static boolean isbind(Integer port) {
        if (null == port) {
            return false;
        }
        return bindChannels.containsKey(port);
    }


    public static void deleteProxy(HolerPort hp) {
        if (!HolerType.HTTP.equals(hp.getType()) && !HolerType.HTTPS.equals(hp.getType())) {
            return;
        }
        if (StringUtils.isEmpty(hp.getDomain())) {
            return;
        }
        String proxyName = StringUtils.replace(hp.getDomain(), " ", "-") + "-" + hp.getPortNum() + ".conf";
        try {
            FileUtils.forceDelete(new File(getHolerConfDir() + proxyName));
        } catch (Exception exception) {
        }
        HolerUtil.exec(nginxReload());
    }

    public static void newNginxConf(String proxyName, HolerPort hp) {
        String content = getProxyContent();
        content = StringUtils.replaceOnce(content, "@DOMAIN@", hp.getDomain());
        content = StringUtils.replaceOnce(content, "@PORT@", String.valueOf(hp.getPortNum()));
        if ("https".equalsIgnoreCase(hp.getType().value())) { //判断类型是否是https
            content = StringUtils.replaceOnce(content, "@PROTOCOL@", "https"); //增加前缀
            content = StringUtils.replaceOnce(content, "@LISTEN_PORT@", "443 ssl");
            content = StringUtils.replace(content, "#ssl_certificate", "ssl_certificate");
        } else {
            content = StringUtils.replaceOnce(content, "@PROTOCOL@", "http");
            content = StringUtils.replaceOnce(content, "@LISTEN_PORT@", "80");
        }
        try {
            FileUtils.write(new File(getHolerConfDir() + proxyName), content, "UTF-8");
        } catch (Exception exception) { }
    }

    public static void addProxy(HolerPort hp) {
        if (!HolerType.HTTP.equals(hp.getType()) && !HolerType.HTTPS.equals(hp.getType())) {
            return;
        }
        if (!hp.getEnabled() || hp.getExpireAt() <= System.currentTimeMillis()) {
            return;
        }
        String proxyName = StringUtils.replace(hp.getDomain(), " ", "-") + "-" + hp.getPortNum() + ".conf";
        newNginxConf(proxyName, hp);
        HolerUtil.exec(nginxReload());
    }


    public static Channel removeExtraChannel(String key, String extraId) {
        HolerChannel hc = (HolerChannel) holerChannels.get(key);
        if (null == hc) {
            return null;
        }
        return removeExtraChannel(hc, extraId);
    }


    public static Channel removeExtraChannel(HolerChannel hc, String extraId) {
        if (null == hc || null == hc.getClient() || null == hc.getClient().getChannel()) {
            return null;
        }

        return removeExtraChannel(hc.getClient().getChannel(), extraId);
    }

    public static Channel removeExtraChannel(Channel holerChannel, String extraId) {
        if (null == holerChannel) {
            return null;
        }

        Map<String, Channel> extraClients = (Map) holerChannel.attr(ServerConst.EXTRA_CLIENTS).get();
        if (MapUtils.isEmpty(extraClients) || !extraClients.containsKey(extraId)) {
            return null;
        }

        synchronized (holerChannel) {

            return (Channel) extraClients.remove(extraId);
        }
    }


    public static void removeExtraChannels(String key) {
        Channel holerChannel = getHolerChannel(key);
        removeExtraChannels(holerChannel);
    }


    public static void removeExtraChannels(Channel holerChannel) {
        if (null == holerChannel) {
            return;
        }


        Map<String, Channel> extraClients = (Map) holerChannel.attr(ServerConst.EXTRA_CLIENTS).get();
        if (MapUtils.isEmpty(extraClients)) {
            return;
        }

        for (Channel extraChannel : extraClients.values()) {
            HolerUtil.close(extraChannel);
        }
        extraClients.clear();
    }


    public static Channel getHolerChannel(String key) {
        if (StringUtils.isEmpty(key)) {
            return null;
        }

        HolerChannel hc = (HolerChannel) holerChannels.get(key);
        return getHolerChannel(hc);
    }


    public static Channel getHolerChannel(HolerChannel hc) {
        if (null == hc || null == hc.getClient()) {
            return null;
        }
        return hc.getClient().getChannel();
    }


    public static Channel getExtraChannel(Channel hc, String extraId) {
        if (null == hc || StringUtils.isEmpty(extraId)) {
            return null;
        }

        Map<String, Channel> extraClients = (Map) hc.attr(ServerConst.EXTRA_CLIENTS).get();
        if (MapUtils.isEmpty(extraClients) || !extraClients.containsKey(extraId)) {
            return null;
        }

        return (Channel) extraClients.get(extraId);
    }

    public static Channel getExtraChannel(String key, String extraId) {
        if (StringUtils.isEmpty(key) || StringUtils.isEmpty(extraId)) {
            return null;
        }

        Channel hc = getHolerChannel(key);
        return getExtraChannel(hc, extraId);
    }

    public static Integer clientStatus(String key) {
        Channel ch = getHolerChannel(key);
        if (HolerUtil.isActive(ch)) {
            return 1;
        }
        return 0;
    }


    public static boolean isOnline(String key) {
        int status = clientStatus(key);
        if (1 == status) {
            return true;
        }
        return false;
    }


    public static Long countOnlineClient() {
        Long online = 0L;
        if (MapUtils.isEmpty(holerChannels)) {
            return online;
        }

        Set<Map.Entry<String, HolerChannel>> entries = holerChannels.entrySet();
        for (Map.Entry<String, HolerChannel> entry : entries) {
            if (1 == clientStatus((String) entry.getKey())) {
                Long long1, long2 = online + 1L;
//                long1;
            }
        }
        return online;
    }

    public static String nginxReload() {
        return getNginxBin() + String.format(" -p %s -s reload", new Object[]{nginxHome});
    }
}
