package org.bench4q.master.infrastructure.highavailable.impl;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Future;
import org.apache.log4j.Logger;
import org.bench4q.master.domain.RunningAgentInterface;
import org.bench4q.master.domain.RunningScriptInterface;
import org.bench4q.master.domain.entity.Agent;
import org.bench4q.master.domain.factory.RunningAgentFactory;
import org.bench4q.master.domain.repository.AgentRepository;
import org.bench4q.master.infrastructure.communication.AgentMessenger;
import org.bench4q.master.infrastructure.highavailable.CurrentLoadSubject;
import org.bench4q.master.infrastructure.highavailable.HighAvailablePool;
import org.bench4q.share.enums.master.AgentStatus;
import org.bench4q.share.models.agent.RunScenarioResultModel;
import org.bench4q.share.models.agent.ServerStatusModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class HighAvailablePoolImpl extends CurrentLoadSubject implements
		HighAvailablePool {
	private AgentRepository agentRepository;
	private AgentMessenger agentMessenger;
	private RunningAgentFactory runningAgentFactory;
	private Map<String, Agent> pool;
	private Map<String, ServerStatusModel> agentStatusOfPreviousBeatMap;
	private Map<UUID, RunningAgentInterface> agentRunBlotters;
	private List<UUID> agentRunIdShouldBeSubstitute;
	private long maxAvailableLoad;
	private int currentAvailableLoad;
	static Logger logger = Logger.getLogger(HighAvailablePoolImpl.class);

	public Map<String, Agent> getPool() {
		return this.pool;
	}

	private void setPool(Map<String, Agent> pool) {
		this.pool = pool;
	}

	private AgentRepository getAgentRepository() {
		return agentRepository;
	}

	private void setAgentRepository(AgentRepository agentRepository) {
		this.agentRepository = agentRepository;
	}

	private AgentMessenger getAgentMessenger() {
		return agentMessenger;
	}

	@Autowired
	private void setAgentMessenger(AgentMessenger agentMessenger) {
		this.agentMessenger = agentMessenger;
	}

	private RunningAgentFactory getRunningAgentFactory() {
		return runningAgentFactory;
	}

	@Autowired
	private void setRunningAgentFactory(RunningAgentFactory runningAgentFactory) {
		this.runningAgentFactory = runningAgentFactory;
	}

	public Long getMaxAvailableLoad() {
		this.calculateHAPoolLoadStatusInMonopolize();
		return maxAvailableLoad;
	}

	private void setMaxAvailableLoad(Long maxAvailableLoad) {
		this.maxAvailableLoad = maxAvailableLoad;
	}

	public synchronized void add(Agent agent) {
		this.getPool().put(agent.getHostName(), agent);
	}

	public synchronized void remove(Agent agent) {
		this.getPool().remove(agent.getHostName());
	}

	public int getCurrentAvailableLoad() {
		this.calculateHAPoolLoadStatusInMonopolize();
		return currentAvailableLoad;
	}

	private void setCurrentAvailableLoad(int currentAvailableLoad) {
		this.currentAvailableLoad = currentAvailableLoad;
	}

	private void setAgentStatusOfPreviousBeatMap(
			Map<String, ServerStatusModel> agentStatusOfPreviousBeatMap) {
		this.agentStatusOfPreviousBeatMap = agentStatusOfPreviousBeatMap;
	}

	private List<UUID> getAgentRunIdShouldBeSubstitute() {
		return agentRunIdShouldBeSubstitute;
	}

	public Map<UUID, RunningAgentInterface> getAgentRunBlotters() {
		return agentRunBlotters;
	}

	private void setAgentRunBlotters(
			Map<UUID, RunningAgentInterface> agentRunBlotters) {
		this.agentRunBlotters = agentRunBlotters;
	}

	private void setAgentRunIdShouldBeSubstitute(
			List<UUID> agentRunIdShouldBeSubstitute) {
		this.agentRunIdShouldBeSubstitute = agentRunIdShouldBeSubstitute;
	}

	@Autowired
	public HighAvailablePoolImpl(AgentRepository agentRepository) {
		this.setPool(new HashMap<String, Agent>());
		this.setAgentRunBlotters(new HashMap<UUID, RunningAgentInterface>());
		this.setAgentStatusOfPreviousBeatMap(new HashMap<String, ServerStatusModel>());
		this.setCurrentAvailableLoad(0);
		this.setAgentRunIdShouldBeSubstitute(new LinkedList<UUID>());
		this.setAgentRepository(agentRepository);
		initialPool();
	}

	private void initialPool() {
		for (Agent agent : this.getAgentRepository().loadEntities()) {
			this.add(agent);
		}
	}

	@Scheduled(cron = "0,30 */1 * * * *")
	public synchronized void checkAllHeartBeat() {
		heartBeatsAndUpdateHAPool();
		doSubstituteIfRequired();
	}

	private void heartBeatsAndUpdateHAPool() {
		Map<Agent, Future<ServerStatusModel>> map = new HashMap<Agent, Future<ServerStatusModel>>();
		for (Agent agent : this.getPool().values()) {
			map.put(agent, this.getAgentMessenger().getStatusAsync(agent));
		}
		this.updateAgentPoolByHeart(map);
	}

	private void updateAgentPoolByHeart(
			Map<Agent, Future<ServerStatusModel>> map) {
		this.setCurrentAvailableLoad(0);
		this.setMaxAvailableLoad((long) 0);
		for (Agent agent : map.keySet()) {
			ServerStatusModel model;
			try {
				model = map.get(agent).get();
				if (model == null) {
					doForBreakDown(agent);
				} else {
					doForHealth(model, agent);
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}

		this.getAgentRepository().Update(this.getPool().values());
	}

	private void doSubstituteIfRequired() {
		for (UUID agentRunId : this.getAgentRunIdShouldBeSubstitute()) {
			RunningAgentInterface runningAgent = this.getAgentRunBlotters()
					.get(agentRunId);
			if (runningAgent == null) {
				// TODO:Actually this is an error
				continue;
			}
			runningAgent.substituteOnBoard();
			this.getAgentRunIdShouldBeSubstitute().remove(agentRunId);
		}
	}

	private synchronized void calculateHAPoolLoadStatusInMonopolize() {
		int maxLoad = 0, availableLoad = 0;
		for (Agent agent : this.getPool().values()) {
			if (agent.getCurrentEnumStatus() == AgentStatus.BreakDown) {
				continue;
			} else if (agent.getCurrentEnumStatus() == AgentStatus.InIdle) {
				maxLoad += agent.getMaxLoad();
				availableLoad += agent.getRemainLoad();
			} else if (agent.getCurrentEnumStatus() == AgentStatus.InRunning) {
				maxLoad += agent.getMaxLoad();
			}
		}
		this.setMaxAvailableLoad((long) maxLoad);
		this.setCurrentAvailableLoad(availableLoad);
	}

	private void addABlotter(RunningAgentInterface runningAgent) {
		this.getAgentRunBlotters().put(runningAgent.getAgentRunId(),
				runningAgent);
	}

	private void removeABlotter(UUID agentRunId) {
		this.getAgentRunBlotters().remove(agentRunId);
	}

	public int blotterSize() {
		return this.getAgentRunBlotters().size();
	}

	private void doForBreakDown(Agent agent) {
		agent.setCurrentEnumStatus(AgentStatus.BreakDown);
		List<UUID> runIDs = queryUnfinishedTest(null, agent.getHostName());
		if (runIDs == null) {
			return;
		}
		this.getAgentRunIdShouldBeSubstitute().addAll(runIDs);
	}

	private void doForHealth(ServerStatusModel newModel, Agent agent) {
		List<UUID> agentUnfinishedRunIds = newModel.getRunningTests();
		if (agentUnfinishedRunIds == null || agentUnfinishedRunIds.size() == 0) {
			doForInIdle(agent);
			return;
		}
		doForInRunning(agent, newModel);
	}

	private void doForInRunning(Agent agent, ServerStatusModel newModel) {
		agent.updateStatus(AgentStatus.InRunning);
		this.agentStatusOfPreviousBeatMap.put(agent.getHostName(), newModel);
	}

	private void doForInIdle(Agent agent) {
		agent.updateStatus(AgentStatus.InIdle);
		agent.resetLoad();
	}

	private List<UUID> queryUnfinishedTest(ServerStatusModel newModel,
			String hostName) {
		if (newModel == null) {
			ServerStatusModel model = this.agentStatusOfPreviousBeatMap
					.get(hostName);
			return model == null ? null : model.getRunningTests();
		}
		return newModel.getRunningTests();
	}

	public List<RunningAgentInterface> applyFor(int load) {
		List<RunningAgentInterface> result = new ArrayList<RunningAgentInterface>();
		int loadForRunCurrent = -1;
		RunScenarioResultModel runScenarioResultModel = null;
		if (load >= this.getCurrentAvailableLoad()) {
			logger.info("currentAvailableLoad not enough for substitute");
			return null;
		}
		for (Agent agent : this.getPool().values()) {
			if (allocationFinish(load)) {
				break;
			}
			if (agent.isInUse()) {
				continue;
			}
			loadForRunCurrent = getMin(load, agent.getRemainLoad());
			agent.bookTest(loadForRunCurrent);
			runScenarioResultModel = this.getAgentMessenger().bookTest(agent,
					loadForRunCurrent);
			if (runScenarioResultModel == null
					|| runScenarioResultModel.getRunId() == null) {
				logger.error(runScenarioResultModel == null ? "runScenarioResultModel is null"
						: "runScenarioResultModel.getRunId()"
								+ runScenarioResultModel.getRunId());
				agent.backLoad(loadForRunCurrent);
				continue;
			}
			load -= loadForRunCurrent;
			RunningAgentInterface runningAgentWithouRunningScript = this
					.getRunningAgentFactory()
					.buildRunningAgentWithOutIdAndRunningScript(agent,
							loadForRunCurrent,
							runScenarioResultModel.getRunId());
			result.add(runningAgentWithouRunningScript);
			this.addABlotter(runningAgentWithouRunningScript);
			this.getAgentRepository().update(agent);
		}
		if (!allocationFinish(load)) {
			logger.error("allocationUnfinished, remain " + load
					+ " unallocated");
			rollbackForApply(result);
			return null;
		}
		return result;
	}

	private void rollbackForApply(List<RunningAgentInterface> runningAgents) {
		for (RunningAgentInterface runningAgent : runningAgents) {
			runningAgent.stop();
			this.removeABlotter(runningAgent.getAgentRunId());
		}
	}

	private int getMin(int load, int remainLoad) {
		return load >= remainLoad ? remainLoad : load;
	}

	private boolean allocationFinish(int load) {
		return load == 0;
	}

	public void cleanUpAboutTestPlan(
			final Collection<? extends RunningScriptInterface> runningScripts) {

		for (RunningScriptInterface runningScript : runningScripts) {
			for (RunningAgentInterface runningAgent : runningScript
					.getRunningAgents()) {
				this.getAgentRunBlotters().remove(runningAgent.getAgentRunId());
				logger.info("see the agentBlotter after run : "
						+ this.getAgentRunBlotters().get(
								runningAgent.getAgentRunId()));
			}
		}
	}

	public void checkHeartBeat(Agent agent) {
		ServerStatusModel model = queryAgentStatus(agent);
		if (model == null) {
			doForBreakDown(agent);
		} else {
			doForHealth(model, agent);
		}
	}

	@Override
	public ServerStatusModel queryAgentStatus(Agent agent) {
		return this.getAgentMessenger().getStatus(agent);
	}

}
