package com.thinkbiganalytics.feedmgr.nifi;

/*-
 * #%L
 * thinkbig-feed-manager-controller
 * %%
 * Copyright (C) 2017 ThinkBig Analytics
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */

import com.thinkbiganalytics.nifi.rest.client.LegacyNifiRestClient;
import com.thinkbiganalytics.nifi.rest.client.layout.AlignProcessGroupComponents;
import com.thinkbiganalytics.nifi.rest.client.layout.ProcessGroupAndConnections;
import com.thinkbiganalytics.nifi.rest.model.NiFiPropertyDescriptorTransform;
import com.thinkbiganalytics.nifi.rest.support.NifiConstants;
import com.thinkbiganalytics.nifi.rest.support.NifiTemplateNameUtil;

import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.web.api.dto.PortDTO;
import org.apache.nifi.web.api.dto.ProcessGroupDTO;
import org.apache.nifi.web.api.dto.status.ProcessGroupStatusDTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

/**
 * This code will find all Process groups matching the Version name of  <Name> - {13 digit timestamp} and if the group doesn't have anything in its Queue, it will delete it from NiFi
 */
public class CleanupStaleFeedRevisions {

    private static final Logger log = LoggerFactory.getLogger(CleanupStaleFeedRevisions.class);
    LegacyNifiRestClient restClient;
    /**
     * the name of the parent group to look at or the word 'all' to access everything
     **/
    private String processGroupId;
    private Set<PortDTO> stoppedPorts = new HashSet<>();

    private Set<ProcessGroupDTO> deletedProcessGroups = new HashSet<>();
    private NiFiPropertyDescriptorTransform propertyDescriptorTransform;


    /**
     * Cleanup versioned process groups that are no longer active
     *
     * @param restClient                  the nifi rest client
     * @param processGroupId              a parent process group (i.e. a category) to look at, or the word 'all' to clean up everything
     * @param propertyDescriptorTransform the transformation bean
     */
    public CleanupStaleFeedRevisions(LegacyNifiRestClient restClient, String processGroupId, NiFiPropertyDescriptorTransform propertyDescriptorTransform) {
        this.processGroupId = processGroupId;
        this.restClient = restClient;
        this.propertyDescriptorTransform = propertyDescriptorTransform;
    }


    /**
     * Cleanup all versioned feed process groups
     * if the {@link this#processGroupId} == 'all' then it will clean up everything, otherwise it will cleanup just the children under the {@link this#processGroupId}
     */
    public void cleanup() {
        deletedProcessGroups.clear();
        Set<String> categoriesToCleanup = new HashSet<>();
        if ("all".equalsIgnoreCase(processGroupId)) {
            ProcessGroupDTO root = restClient.getNiFiRestClient().processGroups().findRoot();

            root.getContents().getProcessGroups().stream().forEach(categoryGroup -> {
                categoriesToCleanup.add(categoryGroup.getId());
            });
        } else {
            categoriesToCleanup.add(processGroupId);
        }
        categoriesToCleanup.stream().forEach(categoryProcessGroupId -> doCleanup(categoryProcessGroupId));
        log.info("Successfully Cleaned up versioned ProcessGroups, deleting {} groups ", deletedProcessGroups.size());


    }


    /**
     * Return the list of groups that were deleted as part of the {@link this#cleanup()} activity
     *
     * @return the list of groups that were deleted as part of the {@link this#cleanup()} activity
     */
    public Set<ProcessGroupDTO> getDeletedProcessGroups() {
        return deletedProcessGroups;
    }

    private void doCleanup(String processGroupId) {
        stoppedPorts.clear();
        AlignProcessGroupComponents alignProcessGroupComponents = new AlignProcessGroupComponents(restClient.getNiFiRestClient(), processGroupId);
        alignProcessGroupComponents.groupItems();
        final Map<String, ProcessGroupAndConnections> groups = alignProcessGroupComponents.getProcessGroupWithConnectionsMap();
        Set<ProcessGroupAndConnections> deletedItems = new HashSet<>();
        groups.values().stream()
            .filter(groupAndConnections -> NifiTemplateNameUtil.isVersionedProcessGroup(groupAndConnections.getProcessGroup().getName()))
            .filter(groupAndConnections -> canDelete(groupAndConnections.getProcessGroup()))
            .forEach(groupAndConnections -> {
                cleanup(groupAndConnections);
                deletedItems.add(groupAndConnections);
            });
        startPorts();
        //relayout the group
        if (!deletedItems.isEmpty()) {
            new AlignProcessGroupComponents(restClient.getNiFiRestClient(), processGroupId).autoLayout();
        }
    }

    private void cleanup(ProcessGroupAndConnections groupAndConnections) {
        //stop the  ports
        stopPorts(groupAndConnections.getPorts());
        groupAndConnections.getConnections().stream().forEach(connectionDTO -> restClient.deleteConnection(connectionDTO, false));
        log.info("About to delete {}", groupAndConnections.getProcessGroup().getName());
        restClient.deleteProcessGroup(groupAndConnections.getProcessGroup());
        deletedProcessGroups.add(groupAndConnections.getProcessGroup());

    }

    private void stopPorts(Set<PortDTO> ports) {
        if (!ports.isEmpty()) {
            ports.stream()
                .filter(portDTO -> !stoppedPorts.contains(portDTO))
                .forEach(portDTO1 -> {
                    if (isInputPort(portDTO1)) {
                        restClient.stopInputPort(portDTO1.getParentGroupId(), portDTO1.getId());
                        stoppedPorts.add(portDTO1);
                    } else {
                        restClient.stopOutputPort(portDTO1.getParentGroupId(), portDTO1.getId());
                        stoppedPorts.add(portDTO1);
                    }
                });
        }
    }

    private void startPorts() {
        Set<PortDTO> startedPorts = new HashSet<>();
        if (!stoppedPorts.isEmpty()) {
            stoppedPorts.stream()
                .forEach(portDTO1 -> {
                    if (isInputPort(portDTO1)) {
                        restClient.startInputPort(portDTO1.getParentGroupId(), portDTO1.getId());
                        startedPorts.add(portDTO1);
                    } else {
                        restClient.startOutputPort(portDTO1.getParentGroupId(), portDTO1.getId());
                        startedPorts.add(portDTO1);
                    }
                });
        }
        stoppedPorts.removeAll(startedPorts);
    }

    private boolean isInputPort(PortDTO portDTO) {
        return NifiConstants.NIFI_PORT_TYPE.INPUT_PORT.name().equalsIgnoreCase(portDTO.getType());
    }

    private boolean isOutputPort(PortDTO portDTO) {
        return NifiConstants.NIFI_PORT_TYPE.OUTPUT_PORT.name().equalsIgnoreCase(portDTO.getType());
    }


    private boolean canDelete(ProcessGroupDTO groupDTO) {

        Optional<ProcessGroupStatusDTO> statusDTO = restClient.getNiFiRestClient().processGroups().getStatus(groupDTO.getId());
        if (statusDTO.isPresent()) {
            return !hasItemsInQueue(statusDTO.get());
        } else {
            return false;
        }
    }

    private boolean hasItemsInQueue(ProcessGroupStatusDTO statusDTO) {
        String queuedCount = propertyDescriptorTransform.getQueuedCount(statusDTO);
        return StringUtils.isNotBlank(queuedCount) && !queuedCount.equalsIgnoreCase("0");
    }


}
