package org.zstack.compute.vm;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.zstack.core.CoreGlobalProperty;
import org.zstack.core.cloudbus.CloudBus;
import org.zstack.core.cloudbus.EventFacade;
import org.zstack.core.db.DatabaseFacade;
import org.zstack.core.notification.N;
import org.zstack.core.thread.SyncTask;
import org.zstack.core.thread.ThreadFacade;
import org.zstack.header.exception.CloudRuntimeException;
import org.zstack.header.vm.*;
import org.zstack.utils.Utils;
import org.zstack.utils.logging.CLogger;

import javax.persistence.Tuple;
import javax.persistence.TypedQuery;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.zstack.utils.CollectionDSL.list;

/**
 */
public abstract class VmTracer {
    private static final CLogger logger = Utils.getLogger(VmTracer.class);

    @Autowired
    private CloudBus bus;
    @Autowired
    private DatabaseFacade dbf;
    @Autowired
    private ThreadFacade thdf;
    @Autowired
    private EventFacade evtf;

    private class Tracer {
        String hostUuid;
        Map<String, VmInstanceState> hostSideStates;
        Map<String, VmInstanceState> mgmtSideStates;

        @Transactional(readOnly = true)
        private void buildManagementServerSideVmStates() {
            mgmtSideStates = new HashMap<String, VmInstanceState>();
            String sql = "select vm.uuid, vm.state from VmInstanceVO vm where vm.hostUuid = :huuid or (vm.hostUuid is null and vm.lastHostUuid = :huuid)" +
                    " and vm.state not in (:vmstates)";
            TypedQuery<Tuple> q = dbf.getEntityManager().createQuery(sql, Tuple.class);
            q.setParameter("huuid", hostUuid);
            q.setParameter("vmstates", list(VmInstanceState.Destroyed, VmInstanceState.Destroying));
            List<Tuple> ts = q.getResultList();

            for (Tuple t : ts) {
                mgmtSideStates.put(t.get(0, String.class), t.get(1, VmInstanceState.class));
                logger.debug(String.format("ManagementServerSideVmStates vm %s, state %s", t.get(0, String.class), t.get(1, VmInstanceState.class).toString()));
            }
        }

        private void checkFromHostSide() {
            for (Map.Entry<String, VmInstanceState> e : hostSideStates.entrySet()) {
                String vmUuid = e.getKey();
                VmInstanceState actualState = e.getValue();

                VmInstanceState expectedState = mgmtSideStates.get(vmUuid);
                if (expectedState == null) {
                    // an anonymous vm showing on this host
                    handleAnonymousVm(vmUuid, actualState, expectedState);
                } else if (actualState != expectedState) {
                    // vm state changed on host side
                    handleStateChangeOnHostSide(vmUuid, actualState, expectedState);
                }
            }
        }

        private void handleStateChangeOnHostSide(final String vmUuid, final VmInstanceState actualState, VmInstanceState expected) {
            VmStateChangedOnHostMsg msg = new VmStateChangedOnHostMsg(logger, "handleStateChangeOnHostSide send VmStateChangedOnHostMsg");
            msg.setVmStateAtTracingMoment(expected);
            msg.setVmInstanceUuid(vmUuid);
            msg.setStateOnHost(actualState);
            msg.setHostUuid(hostUuid);
            bus.makeTargetServiceIdByResourceUuid(msg, VmInstanceConstant.SERVICE_ID, vmUuid);
            bus.send(msg);
        }

        private void handleAnonymousVm(final String vmUuid, final VmInstanceState actualState, VmInstanceState expected) {
            final VmInstanceVO vm = dbf.findByUuid(vmUuid, VmInstanceVO.class);
            if (vm == null) {
                logger.debug(String.format("[Vm Tracer] detects stranger vm[identity:%s, state:%s]", vmUuid, actualState));
                VmTracerCanonicalEvents.StrangerVmFoundData data = new VmTracerCanonicalEvents.StrangerVmFoundData();
                data.setVmIdentity(vmUuid);
                data.setVmState(actualState);
                data.setHostUuid(hostUuid);
                evtf.fire(VmTracerCanonicalEvents.STRANGER_VM_FOUND_PATH, data);

                N.New(VmInstanceVO.class, vmUuid).warn_("A strange vm[%s] was found on the host[%s], May cause problems, Please manually clean this vm", vmUuid, hostUuid);

                return;
            }

            handleStateChangeOnHostSide(vmUuid, actualState, expected);
        }

        private void checkFromManagementServerSide() {
            // from mgmt server we only check missing vm, vm state change has been updated by host side check
            for (Map.Entry<String, VmInstanceState> e : mgmtSideStates.entrySet()) {
                String vmUuid = e.getKey();
                VmInstanceState expectedState = e.getValue();
                if (expectedState != VmInstanceState.Stopped && !hostSideStates.containsKey(vmUuid)) {
                    handleMissingVm(vmUuid, expectedState);
                }
            }
        }

        private void handleMissingVm(final String vmUuid, final VmInstanceState expectedState) {
            VmStateChangedOnHostMsg msg = new VmStateChangedOnHostMsg(logger, "handleMissingVm send VmStateChangedOnHostMsg (vm: %s, host: %s, expect state %s, setState Stopped)");
            msg.setVmStateAtTracingMoment(expectedState);
            msg.setHostUuid(hostUuid);
            msg.setVmInstanceUuid(vmUuid);
            msg.setStateOnHost(VmInstanceState.Stopped);
            bus.makeTargetServiceIdByResourceUuid(msg, VmInstanceConstant.SERVICE_ID, vmUuid);
            bus.send(msg);
        }

        void trace() {
            buildManagementServerSideVmStates();
            checkFromHostSide();
            checkFromManagementServerSide();
        }
    }

    protected void reportVmState(final String hostUuid, final Map<String, VmInstanceState> vmStates) {
        for (Map.Entry<String, VmInstanceState> e : vmStates.entrySet()) {
            logger.debug(String.format("reportVmState vm: %s, statte", e.getKey(), e.getValue().toString()));
        }

        for (VmInstanceState state : vmStates.values()) {
            if (state != VmInstanceState.Running && state != VmInstanceState.Stopped && state != VmInstanceState.Paused) {
                throw new CloudRuntimeException(String.format("host can only report vm state as Running, Stopped or Paused, got %s", state));
            }
        }

        if (!CoreGlobalProperty.VM_TRACER_ON) {
            logger.debug(String.format("vm tracer is off, skip reporting vm state on host[uuid:%s]", hostUuid));
            return;
        }

        thdf.syncSubmit(new SyncTask<Object>() {
            @Override
            public String getSyncSignature() {
                return String.format("trace-vm-state-on-host-%s", hostUuid);
            }

            @Override
            public int getSyncLevel() {
                return 1;
            }

            @Override
            public String getName() {
                return String.format("trace-vm-state-on-host-%s", hostUuid);
            }

            @Override
            public Object call() throws Exception {
                Tracer t = new Tracer();
                t.hostUuid = hostUuid;
                t.hostSideStates = vmStates;
                t.trace();
                return null;
            }
        });
    }

}

@@ -6117,18 +6117,21 @@
 %25s, stat
-t
 e
+: %25s
 %22, e.get

Fix a typo and format string in reportVmState