/*
 * Copyright 1999-2018 Alibaba Group Holding Ltd.
 *
 * Licensed 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.alibaba.csp.sentinel.dashboard.controller.cluster;

import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;

import com.alibaba.csp.sentinel.cluster.ClusterStateManager;
import com.alibaba.csp.sentinel.dashboard.client.CommandNotFoundException;
import com.alibaba.csp.sentinel.dashboard.discovery.AppManagement;
import com.alibaba.csp.sentinel.util.StringUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

import com.alibaba.csp.sentinel.dashboard.datasource.entity.SentinelVersion;
import com.alibaba.csp.sentinel.dashboard.domain.cluster.request.ClusterClientModifyRequest;
import com.alibaba.csp.sentinel.dashboard.domain.cluster.request.ClusterModifyRequest;
import com.alibaba.csp.sentinel.dashboard.domain.cluster.request.ClusterServerModifyRequest;
import com.alibaba.csp.sentinel.dashboard.domain.cluster.state.AppClusterClientStateWrapVO;
import com.alibaba.csp.sentinel.dashboard.domain.cluster.state.AppClusterServerStateWrapVO;
import com.alibaba.csp.sentinel.dashboard.domain.cluster.state.ClusterUniversalStatePairVO;
import com.alibaba.csp.sentinel.dashboard.domain.cluster.state.ClusterUniversalStateVO;
import com.alibaba.csp.sentinel.dashboard.service.ClusterConfigService;
import com.alibaba.csp.sentinel.dashboard.util.ClusterEntityUtils;
import com.alibaba.csp.sentinel.dashboard.util.VersionUtils;
import com.alibaba.csp.sentinel.dashboard.domain.Result;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author Eric Zhao
 * @since 1.4.0
 */
@RestController
@RequestMapping(value = "/cluster")
public class ClusterConfigController {

	private final Logger logger = LoggerFactory.getLogger(ClusterConfigController.class);

	private final SentinelVersion version140 = new SentinelVersion().setMajorVersion(1).setMinorVersion(4);

	@Autowired
	private AppManagement appManagement;

	@Autowired
	private ClusterConfigService clusterConfigService;

	@PostMapping("/config/modify_single")
	public Result<Boolean> apiModifyClusterConfig(@RequestBody String payload) {
		if (StringUtil.isBlank(payload)) {
			return Result.ofFail(-1, "empty request body");
		}
		try {
			JSONObject body = JSON.parseObject(payload);
			if (body.containsKey(KEY_MODE)) {
				int mode = body.getInteger(KEY_MODE);
				switch (mode) {
				case ClusterStateManager.CLUSTER_CLIENT:
					ClusterClientModifyRequest data = JSON.parseObject(payload, ClusterClientModifyRequest.class);
					Result<Boolean> res = checkValidRequest(data);
					if (res != null) {
						return res;
					}
					clusterConfigService.modifyClusterClientConfig(data).get();
					return Result.ofSuccess(true);
				case ClusterStateManager.CLUSTER_SERVER:
					ClusterServerModifyRequest d = JSON.parseObject(payload, ClusterServerModifyRequest.class);
					Result<Boolean> r = checkValidRequest(d);
					if (r != null) {
						return r;
					}
					// TODO: bad design here, should refactor!
					clusterConfigService.modifyClusterServerConfig(d).get();
					return Result.ofSuccess(true);
				default:
					return Result.ofFail(-1, "invalid mode");
				}
			}
			return Result.ofFail(-1, "invalid parameter");
		}
		catch (ExecutionException ex) {
			logger.error("Error when modifying cluster config", ex.getCause());
			return errorResponse(ex);
		}
		catch (Throwable ex) {
			logger.error("Error when modifying cluster config", ex);
			return Result.ofFail(-1, ex.getMessage());
		}
	}

	private <T> Result<T> errorResponse(ExecutionException ex) {
		if (isNotSupported(ex.getCause())) {
			return unsupportedVersion();
		}
		else {
			return Result.ofThrowable(-1, ex.getCause());
		}
	}

	@GetMapping("/state_single")
	public Result<ClusterUniversalStateVO> apiGetClusterState(@RequestParam String app, @RequestParam String ip,
			@RequestParam Integer port) {
		if (StringUtil.isEmpty(app)) {
			return Result.ofFail(-1, "app cannot be null or empty");
		}
		if (StringUtil.isEmpty(ip)) {
			return Result.ofFail(-1, "ip cannot be null or empty");
		}
		if (port == null || port <= 0) {
			return Result.ofFail(-1, "Invalid parameter: port");
		}
		if (!checkIfSupported(app, ip, port)) {
			return unsupportedVersion();
		}
		try {
			return clusterConfigService.getClusterUniversalState(app, ip, port).thenApply(Result::ofSuccess).get();
		}
		catch (ExecutionException ex) {
			logger.error("Error when fetching cluster state", ex.getCause());
			return errorResponse(ex);
		}
		catch (Throwable throwable) {
			logger.error("Error when fetching cluster state", throwable);
			return Result.ofFail(-1, throwable.getMessage());
		}
	}

	@GetMapping("/server_state/{app}")
	public Result<List<AppClusterServerStateWrapVO>> apiGetClusterServerStateOfApp(@PathVariable String app) {
		if (StringUtil.isEmpty(app)) {
			return Result.ofFail(-1, "app cannot be null or empty");
		}
		try {
			return clusterConfigService.getClusterUniversalState(app)
					.thenApply(ClusterEntityUtils::wrapToAppClusterServerState).thenApply(Result::ofSuccess).get();
		}
		catch (ExecutionException ex) {
			logger.error("Error when fetching cluster server state of app: " + app, ex.getCause());
			return errorResponse(ex);
		}
		catch (Throwable throwable) {
			logger.error("Error when fetching cluster server state of app: " + app, throwable);
			return Result.ofFail(-1, throwable.getMessage());
		}
	}

	@GetMapping("/client_state/{app}")
	public Result<List<AppClusterClientStateWrapVO>> apiGetClusterClientStateOfApp(@PathVariable String app) {
		if (StringUtil.isEmpty(app)) {
			return Result.ofFail(-1, "app cannot be null or empty");
		}
		try {
			return clusterConfigService.getClusterUniversalState(app)
					.thenApply(ClusterEntityUtils::wrapToAppClusterClientState).thenApply(Result::ofSuccess).get();
		}
		catch (ExecutionException ex) {
			logger.error("Error when fetching cluster token client state of app: " + app, ex.getCause());
			return errorResponse(ex);
		}
		catch (Throwable throwable) {
			logger.error("Error when fetching cluster token client state of app: " + app, throwable);
			return Result.ofFail(-1, throwable.getMessage());
		}
	}

	@GetMapping("/state/{app}")
	public Result<List<ClusterUniversalStatePairVO>> apiGetClusterStateOfApp(@PathVariable String app) {
		if (StringUtil.isEmpty(app)) {
			return Result.ofFail(-1, "app cannot be null or empty");
		}
		try {
			return clusterConfigService.getClusterUniversalState(app).thenApply(Result::ofSuccess).get();
		}
		catch (ExecutionException ex) {
			logger.error("Error when fetching cluster state of app: " + app, ex.getCause());
			return errorResponse(ex);
		}
		catch (Throwable throwable) {
			logger.error("Error when fetching cluster state of app: " + app, throwable);
			return Result.ofFail(-1, throwable.getMessage());
		}
	}

	private boolean isNotSupported(Throwable ex) {
		return ex instanceof CommandNotFoundException;
	}

	private boolean checkIfSupported(String app, String ip, int port) {
		try {
			return Optional.ofNullable(appManagement.getDetailApp(app)).flatMap(e -> e.getMachine(ip, port))
					.flatMap(m -> VersionUtils.parseVersion(m.getVersion()).map(v -> v.greaterOrEqual(version140)))
					.orElse(true);
			// If error occurred or cannot retrieve machine info, return true.
		}
		catch (Exception ex) {
			return true;
		}
	}

	private Result<Boolean> checkValidRequest(ClusterModifyRequest request) {
		if (StringUtil.isEmpty(request.getApp())) {
			return Result.ofFail(-1, "app cannot be empty");
		}
		if (StringUtil.isEmpty(request.getIp())) {
			return Result.ofFail(-1, "ip cannot be empty");
		}
		if (request.getPort() == null || request.getPort() < 0) {
			return Result.ofFail(-1, "invalid port");
		}
		if (request.getMode() == null || request.getMode() < 0) {
			return Result.ofFail(-1, "invalid mode");
		}
		if (!checkIfSupported(request.getApp(), request.getIp(), request.getPort())) {
			return unsupportedVersion();
		}
		return null;
	}

	private <R> Result<R> unsupportedVersion() {
		return Result.ofFail(4041,
				"Sentinel client not supported for cluster flow control (unsupported version or dependency absent)");
	}

	private static final String KEY_MODE = "mode";

}
