package io.cattle.platform.systemstack.listener;

import static io.cattle.platform.core.model.tables.ServiceTable.*;
import static io.cattle.platform.core.model.tables.StackTable.*;

import io.cattle.platform.archaius.util.ArchaiusUtil;
import io.cattle.platform.async.utils.TimeoutException;
import io.cattle.platform.configitem.events.ConfigUpdate;
import io.cattle.platform.configitem.model.Client;
import io.cattle.platform.configitem.version.ConfigItemStatusManager;
import io.cattle.platform.core.addon.CatalogTemplate;
import io.cattle.platform.core.constants.AccountConstants;
import io.cattle.platform.core.constants.CommonStatesConstants;
import io.cattle.platform.core.constants.ProjectTemplateConstants;
import io.cattle.platform.core.constants.ServiceConstants;
import io.cattle.platform.core.dao.HostDao;
import io.cattle.platform.core.model.Account;
import io.cattle.platform.core.model.ExternalHandler;
import io.cattle.platform.core.model.ProjectTemplate;
import io.cattle.platform.core.model.Service;
import io.cattle.platform.core.model.Stack;
import io.cattle.platform.db.jooq.dao.impl.AbstractJooqDao;
import io.cattle.platform.eventing.EventService;
import io.cattle.platform.eventing.annotation.AnnotatedEventListener;
import io.cattle.platform.eventing.annotation.EventHandler;
import io.cattle.platform.eventing.lock.EventLock;
import io.cattle.platform.json.JsonMapper;
import io.cattle.platform.object.ObjectManager;
import io.cattle.platform.object.meta.ObjectMetaDataManager;
import io.cattle.platform.object.process.ObjectProcessManager;
import io.cattle.platform.object.process.StandardProcess;
import io.cattle.platform.object.resource.ResourceMonitor;
import io.cattle.platform.object.util.DataAccessor;
import io.cattle.platform.systemstack.catalog.CatalogService;
import io.cattle.platform.systemstack.process.SystemStackTrigger;
import io.github.ibuildthecloud.gdapi.condition.Condition;
import io.github.ibuildthecloud.gdapi.condition.ConditionType;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.inject.Inject;

import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.netflix.config.DynamicBooleanProperty;

public class SystemStackUpdate extends AbstractJooqDao implements AnnotatedEventListener {

    private static final DynamicBooleanProperty LAUNCH_COMPOSE_EXECUTOR = ArchaiusUtil.getBoolean("compose.executor.execute");

    private static final Logger log = LoggerFactory.getLogger(SystemStackUpdate.class);

    public static final String VIRTUAL_MACHINE = "virtualMachine";
    public static final List<String> ORC_PRIORITY = Arrays.asList(
            AccountConstants.ORC_KUBERNETES,
            AccountConstants.ORC_SWARM,
            AccountConstants.ORC_MESOS,
            AccountConstants.ORC_WINDOWS
            );
    public static final Set<String> ORCS = new HashSet<>(ORC_PRIORITY);

    @Inject
    ConfigItemStatusManager itemManager;
    @Inject
    EventService eventService;
    @Inject
    ObjectManager objectManager;
    @Inject
    HostDao hostDao;
    @Inject
    ObjectProcessManager processManager;
    @Inject
    JsonMapper jsonMapper;
    @Inject
    CatalogService catalogService;
    @Inject
    ResourceMonitor resourceMonitor;

    @EventHandler(lock=EventLock.class)
    public void globalServiceUpdate(ConfigUpdate update) {
        if (update.getResourceId() == null) {
            return;
        }

        final Client client = new Client(Account.class, new Long(update.getResourceId()));
        itemManager.runUpdateForEvent(SystemStackTrigger.STACKS, update, client, new Runnable() {
            @Override
            public void run() {
                try {
                    process(client.getResourceId());
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        });
    }

    protected void startStacks(Account account) throws IOException {
        if (account.getRemoved() != null) {
            return;
        }

        List<Long> createdStackIds = DataAccessor.fieldLongList(account, AccountConstants.FIELD_CREATED_STACKS);
        List<Long> startedStackIds = DataAccessor.fieldLongList(account, AccountConstants.FIELD_STARTED_STACKS);
        if (!startedStackIds.isEmpty() || createdStackIds.isEmpty()) {
            return;
        }

        if (!hostDao.hasActiveHosts(account.getId())) {
            return;
        }

        startedStackIds = new ArrayList<>();
        for (Long stackId : createdStackIds) {
            Stack stack = objectManager.loadResource(Stack.class, stackId);
            if (stack == null) {
                continue;
            }

            stack = resourceMonitor.waitForNotTransitioning(stack);
            if (CommonStatesConstants.ACTIVE.equals(stack.getState())) {
                for (Service service : objectManager.find(Service.class, SERVICE.STACK_ID, stackId, SERVICE.REMOVED, null)) {
                    processManager.scheduleStandardProcess(StandardProcess.ACTIVATE, service, null);
                }
            }

            startedStackIds.add(stackId);
        }

        objectManager.setFields(account, AccountConstants.FIELD_STARTED_STACKS, startedStackIds);
    }

    public static String chooseOrchestration(List<String> externalIds) {
        String orchestration = "cattle";
        Set<String> installedOrcs = new HashSet<>();
        for (String externalId : externalIds) {
            String orcType = getStackTypeFromExternalId(externalId);
            if (ORCS.contains(orcType)) {
                installedOrcs.add(orcType);
            }
        }

        for (String orc : ORC_PRIORITY) {
            if (installedOrcs.contains(orc)) {
                if (AccountConstants.ORC_KUBERNETES.equals(orc)) {
                    orchestration = AccountConstants.ORC_KUBERNETES_DISPLAY;
                } else {
                    orchestration = orc;
                }
            }
        }

        return orchestration;
    }

    protected void process(long accountId) throws IOException {
        Account account = objectManager.loadResource(Account.class, accountId);
        if (account == null || account.getRemoved() != null) {
            return;
        }

        createStacks(account);
        startStacks(account);

        List<Stack> stacks = objectManager.find(Stack.class,
                STACK.ACCOUNT_ID, account.getId(),
                STACK.REMOVED, null,
                STACK.EXTERNAL_ID, new Condition(ConditionType.LIKE, "%:infra*%"));

        boolean virtualMachine = false;
        List<String> externalIds = new ArrayList<>();
        for (Stack stack : stacks) {
            if (!ServiceConstants.isSystem(stack) || CommonStatesConstants.REMOVING.equals(stack.getState())) {
                continue;
            }

            externalIds.add(stack.getExternalId());
            String orcType = getStackTypeFromExternalId(stack.getExternalId());
            if (VIRTUAL_MACHINE.equals(orcType)) {
                virtualMachine = true;
            }
        }

        String orchestration = chooseOrchestration(externalIds);

        boolean oldVm = DataAccessor.fieldBool(account, AccountConstants.FIELD_VIRTUAL_MACHINE);
        String oldOrch = DataAccessor.fieldString(account, AccountConstants.FIELD_ORCHESTRATION);
        if (oldVm != virtualMachine || !ObjectUtils.equals(oldOrch, orchestration)) {
            objectManager.setFields(account,
                    AccountConstants.FIELD_ORCHESTRATION, orchestration,
                    AccountConstants.FIELD_VIRTUAL_MACHINE, virtualMachine);
            io.cattle.platform.object.util.ObjectUtils.publishChanged(eventService, account.getId(),
                    account.getId(), AccountConstants.TYPE);
        }
    }

    public static String getStackTypeFromExternalId(String externalId) {
        externalId = StringUtils.removeStart(externalId, "catalog://");
        String[] parts = externalId.split(":");
        if (parts.length < 2) {
            return null;
        }
        return StringUtils.removeStart(parts[1], "infra*");
    }

    public List<Long> createStacks(Account account) throws IOException {
        List<Long> createdStackIds = DataAccessor.fieldLongList(account, AccountConstants.FIELD_CREATED_STACKS);
        if (!createdStackIds.isEmpty()) {
            return createdStackIds;
        }

        ProjectTemplate projectTemplate = objectManager.loadResource(ProjectTemplate.class, account.getProjectTemplateId());
        if (projectTemplate == null) {
            return Collections.emptyList();
        }

        List<CatalogTemplate> templates = DataAccessor.fieldObjectList(projectTemplate, ProjectTemplateConstants.FIELD_STACKS,
                CatalogTemplate.class, jsonMapper);
        Map<String, CatalogTemplate> templatesById = catalogService.resolvedExternalIds(templates);
        createdStackIds = new ArrayList<>();

        boolean executorRunning = false;
        for (Map.Entry<String, CatalogTemplate> entry : templatesById.entrySet()) {
            String externalId = entry.getKey();
            Stack stack = objectManager.findAny(Stack.class,
                    STACK.ACCOUNT_ID, account.getId(),
                    STACK.EXTERNAL_ID, externalId,
                    STACK.REMOVED, null);

            if (stack == null) {
                executorRunning = waitForExecutor(executorRunning);
                stack = catalogService.deploy(account.getId(), entry.getValue());
            }

            createdStackIds.add(stack.getId());
        }

        objectManager.reload(account);
        objectManager.setFields(account, AccountConstants.FIELD_CREATED_STACKS, createdStackIds);
        return createdStackIds;
    }

    private synchronized boolean waitForExecutor(boolean executorRunning) {
        if (!LAUNCH_COMPOSE_EXECUTOR.get()) {
            return true;
        }

        if (executorRunning) {
            return executorRunning;
        }

        for (int i = 0; i < 120; i++) {
            ExternalHandler handler = objectManager.findAny(ExternalHandler.class,
                    ObjectMetaDataManager.NAME_FIELD, "rancher-compose-executor",
                    ObjectMetaDataManager.STATE_FIELD, CommonStatesConstants.ACTIVE);
            if (handler != null) {
                return true;
            }
            log.info("Waiting for rancher-compose-executor");

            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new IllegalStateException(e);
            }
        }

        throw new TimeoutException("Failed to find rancher-compose-executor");
    }

}