/*
 *   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.dromara.soul.sync.data.http;

import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.dromara.soul.common.concurrent.SoulThreadFactory;
import org.dromara.soul.common.constant.HttpConstants;
import org.dromara.soul.common.dto.AppAuthData;
import org.dromara.soul.common.dto.ConfigData;
import org.dromara.soul.common.dto.MetaData;
import org.dromara.soul.common.dto.PluginData;
import org.dromara.soul.common.dto.RuleData;
import org.dromara.soul.common.dto.SelectorData;
import org.dromara.soul.common.enums.ConfigGroupEnum;
import org.dromara.soul.common.exception.SoulException;
import org.dromara.soul.sync.data.api.AuthDataSubscriber;
import org.dromara.soul.sync.data.api.MetaDataSubscriber;
import org.dromara.soul.sync.data.api.PluginDataSubscriber;
import org.dromara.soul.sync.data.api.SyncDataService;
import org.dromara.soul.sync.data.http.config.HttpConfig;
import org.dromara.soul.sync.data.http.handler.HttpSyncDataHandler;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.client.OkHttp3ClientHttpRequestFactory;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;

import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * HTTP long polling implementation.
 *
 * @author huangxiaofeng
 * @author xiaoyu
 */
@SuppressWarnings("all")
@Slf4j
public class HttpSyncDataService extends HttpSyncDataHandler implements SyncDataService, AutoCloseable {
    
    private static final AtomicBoolean RUNNING = new AtomicBoolean(false);
    
    /**
     * cache group config with md5 info.
     */
    private static final ConcurrentMap<ConfigGroupEnum, ConfigData> GROUP_CACHE = new ConcurrentHashMap<>();
    
    private static final Gson GSON = new Gson();
    
    /**
     * default: 10s.
     */
    private Duration connectionTimeout = Duration.ofSeconds(10);
    
    /**
     * only use for http long polling.
     */
    private RestTemplate httpClient;
    
    private ExecutorService executor;
    
    private HttpConfig httpConfig;
    
    private List<String> serverList;
    
    public HttpSyncDataService(final HttpConfig httpConfig, final PluginDataSubscriber pluginDataSubscriber,
                               final List<MetaDataSubscriber> metaDataSubscribers, final List<AuthDataSubscriber> authDataSubscribers) {
        super(pluginDataSubscriber, metaDataSubscribers, authDataSubscribers);
        this.httpConfig = httpConfig;
        serverList = Lists.newArrayList(Splitter.on(",").split(httpConfig.getUrl()));
        start(httpConfig);
    }
    
    private void start(final HttpConfig httpConfig) {
        // init RestTemplate
        OkHttp3ClientHttpRequestFactory factory = new OkHttp3ClientHttpRequestFactory();
        factory.setConnectTimeout((int) this.connectionTimeout.toMillis());
        factory.setReadTimeout((int) HttpConstants.CLIENT_POLLING_READ_TIMEOUT);
        this.httpClient = new RestTemplate(factory);
        // It could be initialized multiple times, so you need to control that.
        if (RUNNING.compareAndSet(false, true)) {
            // fetch all group configs.
            this.fetchGroupConfig(ConfigGroupEnum.values());
            // one thread for listener, another one for fetch configuration data.
            this.executor = new ThreadPoolExecutor(3, 3, 0L, TimeUnit.MILLISECONDS,
                    new LinkedBlockingQueue<>(),
                    SoulThreadFactory.create("http-long-polling", true));
            // start long polling.
            this.executor.execute(new HttpLongPollingTask());
        } else {
            log.info("soul http long polling was started, executor=[{}]", executor);
        }
    }
    
    private void fetchGroupConfig(final ConfigGroupEnum... groups) throws SoulException {
        StringBuilder params = new StringBuilder();
        for (ConfigGroupEnum groupKey : groups) {
            params.append("groupKeys").append("=").append(groupKey.name()).append("&");
        }
        SoulException ex = null;
        for (String server : serverList) {
            String url = server + "/configs/fetch?" + StringUtils.removeEnd(params.toString(), "&");
            log.info("request configs: [{}]", url);
            String json = this.httpClient.getForObject(url, String.class);
            log.info("get latest configs: [{}]", json);
            updateCacheWithJson(json);
            return;
        }
        if (ex != null) {
            throw ex;
        }
    }
    
    private void updateCacheWithJson(final String json) {
        JsonObject jsonObject = GSON.fromJson(json, JsonObject.class);
        JsonObject data = jsonObject.getAsJsonObject("data");
        // plugin
        JsonObject pluginData = data.getAsJsonObject(ConfigGroupEnum.PLUGIN.name());
        if (pluginData != null) {
            ConfigData<PluginData> result = GSON.fromJson(pluginData, new TypeToken<ConfigData<PluginData>>() {
            }.getType());
            GROUP_CACHE.put(ConfigGroupEnum.PLUGIN, result);
            this.flushAllPlugin(result.getData());
        }
        // rule
        JsonObject ruleData = data.getAsJsonObject(ConfigGroupEnum.RULE.name());
        if (ruleData != null) {
            ConfigData<RuleData> result = GSON.fromJson(ruleData, new TypeToken<ConfigData<RuleData>>() {
            }.getType());
            GROUP_CACHE.put(ConfigGroupEnum.RULE, result);
            this.flushAllRule(result.getData());
        }
        // selector
        JsonObject selectorData = data.getAsJsonObject(ConfigGroupEnum.SELECTOR.name());
        if (selectorData != null) {
            ConfigData<SelectorData> result = GSON.fromJson(selectorData, new TypeToken<ConfigData<SelectorData>>() {
            }.getType());
            GROUP_CACHE.put(ConfigGroupEnum.SELECTOR, result);
            this.flushAllSelector(result.getData());
        }
        // appAuth
        JsonObject appAuthData = data.getAsJsonObject(ConfigGroupEnum.APP_AUTH.name());
        if (appAuthData != null) {
            ConfigData<AppAuthData> result = GSON.fromJson(appAuthData, new TypeToken<ConfigData<AppAuthData>>() {
            }.getType());
            GROUP_CACHE.put(ConfigGroupEnum.APP_AUTH, result);
            this.flushAllAppAuth(result.getData());
        }
        // metaData
        JsonObject metaData = data.getAsJsonObject(ConfigGroupEnum.META_DATA.name());
        if (metaData != null) {
            ConfigData<MetaData> result = GSON.fromJson(metaData, new TypeToken<ConfigData<MetaData>>() {
            }.getType());
            GROUP_CACHE.put(ConfigGroupEnum.META_DATA, result);
            this.flushMetaData(result.getData());
        }
    }
    
    @SuppressWarnings("unchecked")
    private void doLongPolling() {
        MultiValueMap<String, String> params = new LinkedMultiValueMap<>(16);
        for (ConfigGroupEnum group : ConfigGroupEnum.values()) {
            ConfigData<?> cacheConfig = GROUP_CACHE.get(group);
            String value = String.join(",", cacheConfig.getMd5(), String.valueOf(cacheConfig.getLastModifyTime()));
            params.put(group.name(), Lists.newArrayList(value));
        }
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        HttpEntity httpEntity = new HttpEntity(params, headers);
        for (String server : serverList) {
            String listenerUrl = server + "/configs/listener";
            log.debug("request listener configs: [{}]", listenerUrl);
            try {
                String json = this.httpClient.postForEntity(listenerUrl, httpEntity, String.class).getBody();
                log.debug("listener result: [{}]", json);
                JsonArray groupJson = GSON.fromJson(json, JsonObject.class).getAsJsonArray("data");
                if (groupJson != null) {
                    // fetch group configuration async.
                    ConfigGroupEnum[] changedGroups = GSON.fromJson(groupJson, ConfigGroupEnum[].class);
                    if (ArrayUtils.isNotEmpty(changedGroups)) {
                        log.info("Group config changed: {}", Arrays.toString(changedGroups));
                        this.fetchGroupConfig(changedGroups);
                    }
                }
                break;
            } catch (RestClientException e) {
                log.error("listener configs fail, can not connection this server:[{}]", listenerUrl);
                /*  ex = new SoulException("Init cache error, serverList:" + serverList, e);*/
                // try next server, if have another one.
            }
        }
    }
    
    @Override
    public void close() throws Exception {
        RUNNING.set(false);
        if (executor != null) {
            executor.shutdownNow();
            // help gc
            executor = null;
        }
    }
    
    class HttpLongPollingTask implements Runnable {
        @Override
        public void run() {
            while (RUNNING.get()) {
                doLongPolling();
            }
            log.warn("Stop http long polling.");
        }
    }
}