package io.cattle.platform.engine.server.impl;

import io.cattle.platform.archaius.util.ArchaiusUtil;
import io.cattle.platform.async.utils.TimeoutException;
import io.cattle.platform.engine.manager.ProcessManager;
import io.cattle.platform.engine.manager.ProcessNotFoundException;
import io.cattle.platform.engine.manager.impl.ProcessRecordDao;
import io.cattle.platform.engine.process.ExitReason;
import io.cattle.platform.engine.process.ProcessInstance;
import io.cattle.platform.engine.process.ProcessInstanceException;
import io.cattle.platform.engine.process.impl.ProcessCancelException;
import io.cattle.platform.engine.process.util.ProcessEngineUtils;
import io.cattle.platform.engine.server.ProcessInstanceDispatcher;
import io.cattle.platform.engine.server.ProcessInstanceExecutor;
import io.cattle.platform.engine.server.ProcessInstanceReference;
import io.cattle.platform.engine.server.ProcessServer;
import io.cattle.platform.lock.LockCallback;
import io.cattle.platform.lock.LockManager;
import io.cattle.platform.metrics.util.MetricsUtil;
import io.cattle.platform.util.exception.ExceptionUtils;
import io.cattle.platform.util.exception.LoggableException;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.RejectedExecutionException;

import javax.annotation.PostConstruct;
import javax.inject.Inject;
import javax.inject.Named;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.codahale.metrics.Counter;

public class ProcessInstanceDispatcherImpl implements ProcessInstanceDispatcher, ProcessInstanceExecutor {
    private static final Logger log = LoggerFactory.getLogger(ProcessInstanceDispatcherImpl.class);

    private static Counter EVENT = MetricsUtil.getRegistry().counter("process_execution.event");
    private static Counter CANCELED = MetricsUtil.getRegistry().counter("process_execution.cancel");
    private static Counter DONE = MetricsUtil.getRegistry().counter("process_execution.done");
    private static Counter NOT_FOUND = MetricsUtil.getRegistry().counter("process_execution.not_found");
    private static Counter EXCEPTION = MetricsUtil.getRegistry().counter("process_execution.unknown_exception");
    private static Counter TIMEOUT = MetricsUtil.getRegistry().counter("process_execution.timeout");

    @Inject @Named("ProcessEventExecutorService")
    ExecutorService eventExecutor;
    @Inject @Named("ProcessBlockingExecutorService")
    ExecutorService blockingExecutor;
    @Inject @Named("ProcessNonBlockingExecutorService")
    ExecutorService nonBlockingExecutor;
    @Inject
    @Named("ProcessBlockingExtraExecutorService")
    ExecutorService blockingExtraExecutor;
    @Inject
    @Named("ProcessBlockingSystemExecutorService")
    ExecutorService blockingSystemExecutor;
    @Inject
    ProcessRecordDao processRecordDao;
    @Inject
    LockManager lockManager;
    @Inject
    ProcessManager processManager;
    @Inject
    ProcessServer processServer;
    Map<ExitReason, Counter> counters = new HashMap<ExitReason, Counter>();

    @Override
    public void dispatch(ProcessInstanceReference ref) {
        if (!ProcessEngineUtils.enabled()) {
            return;
        }

        ref.setExecutor(this);

        boolean submitted = false;
        if (ref.isEvent()) {
            try {
                eventExecutor.execute(ref);
                submitted = true;
            } catch (RejectedExecutionException e) {
                //
            }
        }
        if (!submitted) {
            if (isBlocking(ref)) {
                if (ref.isSystem()) {
                    blockingSystemExecutor.execute(ref);
                } else {
                    blockingExecutor.execute(ref);
                }
            } else if (isBlockingExtra(ref)) {
                blockingExtraExecutor.execute(ref);
            }else {
                nonBlockingExecutor.execute(ref);
            }
        }
    }

    protected boolean isBlockingExtra(ProcessInstanceReference ref) {
        if (ArchaiusUtil.getBoolean("process." + ref.getName().split("[.]")[0] + ".blockingextra").get()) {
            return true;
        }
        return ArchaiusUtil.getBoolean("process." + ref.getName() + ".blockingextra").get();
    }

    protected boolean isBlocking(ProcessInstanceReference ref) {
        if (ArchaiusUtil.getBoolean("process." + ref.getName().split("[.]")[0] + ".blocking").get()) {
            return true;
        }
        return ArchaiusUtil.getBoolean("process." + ref.getName() + ".blocking").get();
    }

    @Override
    public void execute(final long processId) {
        Long nextId = processId;
        while (nextId != null) {
            final long currentId = nextId;
            nextId = lockManager.tryLock(new ProcessDispatchLock(currentId), new LockCallback<Long>() {
                @Override
                public Long doWithLock() {
                    return processExecuteWithLock(currentId);
                }
            });
        }
    }

    public Long processExecuteWithLock(long processId) {
        EVENT.inc();

        boolean runRemaining = false;
        ProcessInstance instance = null;
        try {
            instance = processManager.loadProcess(processId);
            instance.execute();
            runRemaining = true;
            DONE.inc();
        } catch (ProcessNotFoundException e) {
            NOT_FOUND.inc();
            log.debug("Failed to find process for id [{}]", processId);
        } catch (ProcessInstanceException e) {
            counters.get(e.getExitReason()).inc();
            if (e.getExitReason().isError()) {
                log.error("Process [{}:{}] on [{}] failed, exit [{}] : {}", instance.getName(), processId, instance.getResourceId(), e
                        .getExitReason(), e.getMessage());
            }
        } catch (TimeoutException e) {
            TIMEOUT.inc();
            log.info("Timeout on process [{}:{}] on [{}] : {}", instance.getName(), processId, instance.getResourceId(), e
                    .getMessage());
        } catch (ProcessCancelException e) {
            CANCELED.inc();
            runRemaining = true;
            log.debug("Process canceled [{}:{}] on [{}] : {}", instance.getName(), processId, instance.getResourceId(), e.getMessage());
        } catch (Throwable e) {
            if (e instanceof LoggableException) {
                EXCEPTION.inc();
                ((LoggableException) e).log(log);
            } else {
                Throwable cause = ExceptionUtils.getRootCause(e);
                if (cause instanceof ProcessCancelException) {
                    CANCELED.inc();
                    log.error("Unknown exception running process [{}:{}] on [{}], canceled by [{}]",
                            instance == null ? null : instance.getName(), processId,
                            instance == null ? null : instance.getResourceId(), cause.getMessage());
                } else {
                    EXCEPTION.inc();
                    log.error("Unknown exception running process [{}:{}] on [{}]",
                            instance == null ? null : instance.getName(), processId,
                            instance == null ? null : instance.getResourceId(), e);
                }
            }
        }

        if (runRemaining) {
            return processServer.getNextTask(instance);
        }

        return null;
    }

    @PostConstruct
    public void init() {
        for (ExitReason e : ExitReason.values()) {
            switch (e) {
            case DONE:
            case ALREADY_DONE:
                break;
            default:
                counters.put(e, MetricsUtil.getRegistry().counter("process_execution." + e.toString().toLowerCase()));
            }
        }
    }

}