package org.bench4q.master.domain.service;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import org.apache.log4j.Logger;
import org.bench4q.master.Main;
import org.bench4q.master.domain.entity.TestPlan;
import org.bench4q.master.domain.entity.User;
import org.bench4q.master.domain.factory.TestPlanFactory;
import org.bench4q.master.domain.repository.TestPlanRepository;
import org.bench4q.master.domain.valueobject.schedulscript.TaskCompleteCallback;
import org.bench4q.master.exception.ExceptionLog;
import org.bench4q.master.exception.ExceptionUtils.IllegalParameterException;
import org.bench4q.master.infrastructure.highavailable.CurrentLoadObserver;
import org.bench4q.master.infrastructure.highavailable.HighAvailablePool;
import org.bench4q.share.enums.master.TestPlanStatus;
import org.bench4q.share.models.master.TestPlanModel;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Restrictions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class TestPlanEngine implements TaskCompleteCallback,
		CurrentLoadObserver {
	private HighAvailablePool haPool;
	private TestPlanRepository testPlanRepository;
	private TestPlanFactory testPlanFactory;
	private ScheduledExecutorService scheduleExecutor;

	public static Logger logger = Logger.getLogger(TestPlanEngine.class);

	private TestPlanRepository getTestPlanRepository() {
		return testPlanRepository;
	}

	@Autowired
	private void setTestPlanRepository(TestPlanRepository testPlanRepository) {
		this.testPlanRepository = testPlanRepository;
	}

	private TestPlanFactory getTestPlanFactory() {
		return testPlanFactory;
	}

	@Autowired
	private void setTestPlanFactory(TestPlanFactory testPlanFactory) {
		this.testPlanFactory = testPlanFactory;
	}

	private HighAvailablePool getHaPool() {
		return haPool;
	}

	@Autowired
	private void setHaPool(HighAvailablePool haPool) {
		this.haPool = haPool;
		this.getHaPool().setObserver(this);
	}

	private ScheduledExecutorService getScheduleExecutor() {
		return scheduleExecutor;
	}

	private void setScheduleExecutor(ScheduledExecutorService scheduleExecutor) {
		this.scheduleExecutor = scheduleExecutor;
	}

	public TestPlanEngine() {
		init();
	}

	private void init() {
		this.setScheduleExecutor(new ScheduledThreadPoolExecutor(1));
		this.getScheduleExecutor().scheduleAtFixedRate(new Runnable() {
			public void run() {
				TestPlan testPlan = pickATestPlan();
				if (testPlan == null) {
					logger.info("testPlan by pick is null");
					return;
				}
				logger.info(testPlan.getTestPlanRunId() + " start to run");
				doRunTestPlan(UUID.fromString(testPlan.getTestPlanRunId()));
			}
		}, 0, Main.PICK_CYCLE_IN_SECONDS, TimeUnit.SECONDS);
	}

	public UUID runWith(final TestPlanModel testPlanModel, User user) {
		ExecutorService executorService = Executors.newCachedThreadPool();
		final UUID testPlanId = UUID.randomUUID();
		logger.info("name:" + testPlanModel.getName() + " start to run");
		logger.info(testPlanId + " start to run");
		if (!submitTestPlan(testPlanModel, user, testPlanId)) {
			return null;
		}
		Runnable runnable = new Runnable() {
			public void run() {
				doRunTestPlan(testPlanId);
			}
		};
		executorService.execute(runnable);
		executorService.shutdown();
		return testPlanId;
	}

	public boolean stop(final UUID testPlanRunId) {
		TestPlan testPlan = this.getTestPlanRepository().getRunningTestPlanBy(
				testPlanRunId);
		if (testPlan == null) {
			return false;
		}
		boolean result = testPlan.stop();
		this.doTaskComplete(testPlanRunId);
		logger.info("The user abort this testPlan!");
		return result;
	}

	public boolean submitTestPlan(final TestPlanModel testPlanBusinessModel,
			final User user, final UUID testPlanRunId) {
		try {
			TestPlan testPlan = this.getTestPlanFactory()
					.createATestPlanWithoutId(testPlanBusinessModel, user,
							testPlanRunId);
			Logger.getLogger(TestPlanService.class).info(
					"test plan name:" + testPlan.getName());
			return this.getTestPlanRepository().attach(testPlan);
		} catch (IllegalParameterException e) {
			// TODO: give back the message
			Logger.getLogger(TestPlanService.class).error(
					ExceptionLog.getStackTrace(e) + e.getMessage());
			return false;
		}
	}

	public void doRunTestPlan(final UUID testPlanId) {
		TestPlan testPlan = getTestPlanRepository().getTestPlanInDomainBy(
				testPlanId);
		if (testPlan == null) {
			logger.error("can't get the testplan with testPlanId:"
					+ testPlanId.toString());
			return;
		}
		testPlan.run();
		this.getTestPlanRepository().attachRunningTestPlan(testPlan);
	}

	public void doTaskComplete(UUID testPlanID) {
		TestPlan testPlan = this.getTestPlanRepository().getRunningTestPlanBy(
				testPlanID);
		if (testPlan == null || !testPlan.isFinish()) {
			return;
		}
		cleanUpTestPlan(testPlan);
		commitComplete(testPlan);
		logger.info("Test plan with id " + testPlanID.toString() + " finishes!");
	}

	private void cleanUpTestPlan(TestPlan testPlan) {
		this.getHaPool().cleanUpAboutTestPlan(testPlan.getTestPlanScripts());
		this.getTestPlanRepository().detachRunningTestPlan(
				UUID.fromString(testPlan.getTestPlanRunId()));
	}

	private void commitComplete(TestPlan testPlan) {
		testPlan.setCurrentStatus(TestPlanStatus.Complete.name());
		this.getTestPlanRepository().updateEntity(testPlan);
	}

	public synchronized TestPlan pickATestPlan() {
		logger.info("enter pick");
		try {
			List<Criterion> criterions = new ArrayList<Criterion>();
			Criterion notComplete = Restrictions.not(Restrictions.eq(
					"currentStatus", TestPlanStatus.Complete.name()));
			criterions.add(notComplete);
			Criterion notInRunning = Restrictions.not(Restrictions.eq(
					"currentStatus", TestPlanStatus.InRunning.name()));
			criterions.add(notInRunning);
			Criterion lessThanPoolAvailbleLoad = Restrictions.lt(
					"requiredLoad", this.getHaPool().getCurrentAvailableLoad());
			criterions.add(lessThanPoolAvailbleLoad);
			Criterion lessThanMaxFailTimes = Restrictions.lt("failTimes",
					Main.MAX_FAIL_TIMES);
			criterions.add(lessThanMaxFailTimes);
			Criterion intervalGTMin_Interval = Restrictions.lt(
					"lastRunningTime", new Date(System.currentTimeMillis()
							- Main.MIN_EXECUTE_INTERVAL_IN_SECONDS * 1000));
			criterions.add(intervalGTMin_Interval);
			List<TestPlan> testPlans = this.getTestPlanRepository()
					.loadTestPlansBy(criterions, Order.desc("createDateTime"));
			return testPlans.size() > 0 ? testPlans.get(0) : null;
		} catch (Exception e) {
			logger.error(e, e);
			return null;
		}
	}

	@Override
	public void executePendingTestPlan() {
		pickATestPlan();
	}
}
