package com.thinkbiganalytics.nifi.rest.client;

/*-
 * #%L
 * thinkbig-nifi-rest-client-api
 * %%
 * 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.feedmgr.TemplateCreationHelper;
import com.thinkbiganalytics.nifi.rest.model.flow.NifiFlowProcessGroup;
import com.thinkbiganalytics.nifi.rest.model.visitor.NifiFlowBuilder;
import com.thinkbiganalytics.nifi.rest.model.visitor.NifiVisitableProcessGroup;
import com.thinkbiganalytics.nifi.rest.model.visitor.NifiVisitableProcessor;
import com.thinkbiganalytics.nifi.rest.support.NifiConstants;
import com.thinkbiganalytics.nifi.rest.support.NifiProcessUtil;
import com.thinkbiganalytics.nifi.rest.visitor.NifiConnectionOrderVisitor;
import com.thinkbiganalytics.nifi.rest.visitor.NifiConnectionOrderVisitorCache;
import com.thinkbiganalytics.support.FeedNameUtil;

import org.apache.nifi.web.api.dto.ProcessGroupDTO;
import org.apache.nifi.web.api.dto.ProcessorDTO;
import org.apache.nifi.web.api.dto.TemplateDTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;

/**
 */
public class DefaultNiFiFlowVisitorClient implements NiFiFlowVisitorClient {

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


    private NiFiRestClient restClient;

    public DefaultNiFiFlowVisitorClient(NiFiRestClient restClient) {
        this.restClient = restClient;
    }


    public NifiVisitableProcessGroup getFlowOrder(String processGroupId, NifiConnectionOrderVisitorCache cache) throws NifiComponentNotFoundException {
        ProcessGroupDTO processGroupEntity = restClient.processGroups().findById(processGroupId, true, true).orElse(null);
        return getFlowOrder(processGroupEntity, cache);
    }


    public NifiVisitableProcessGroup getFlowOrder(ProcessGroupDTO processGroupEntity, NifiConnectionOrderVisitorCache cache) throws NifiComponentNotFoundException {
        return getFlowOrder(processGroupEntity, cache, true);
    }

    /**
     * @param quiet dont throw any NotFound exceptions from the REST API
     */
    public NifiVisitableProcessGroup getFlowOrder(ProcessGroupDTO processGroupEntity, NifiConnectionOrderVisitorCache cache, boolean logRestAccessErrors) throws NifiComponentNotFoundException {
        if (cache == null) {
            cache = new NifiConnectionOrderVisitorCache();
        }

        final NifiConnectionOrderVisitorCache finalCache = cache;
        Optional<ProcessGroupDTO> cachedProcessGroup = cache.getProcessGroup(processGroupEntity.getId());
        if (!cachedProcessGroup.isPresent() && processGroupEntity.getContents() != null) {
            NifiProcessUtil.getProcessGroups(processGroupEntity).stream().forEach(processGroupDTO -> finalCache.add(processGroupDTO));
        }

        NifiVisitableProcessGroup group = null;
        if (processGroupEntity != null) {
            group = new NifiVisitableProcessGroup(processGroupEntity);
            NifiConnectionOrderVisitor orderVisitor = new NifiConnectionOrderVisitor(restClient, group, finalCache);
            try {
                Optional<ProcessGroupDTO> parent = cache.getProcessGroup(processGroupEntity.getParentGroupId());
                if (!parent.isPresent()) {
                    parent = restClient.processGroups().findById(processGroupEntity.getParentGroupId(), false, false, logRestAccessErrors);
                }
                if (parent.isPresent()) {
                    group.setParentProcessGroup(parent.get());
                }
            } catch (NifiComponentNotFoundException e) {
                //cant find the parent
            }
            group.accept(orderVisitor);
            finalCache.add(orderVisitor.toCachedItem());
        }
        return group;
    }


    public NifiFlowProcessGroup getFeedFlow(String processGroupId) {
        return getFeedFlow(processGroupId, null);
    }

    public NifiFlowProcessGroup getFeedFlow(String processGroupId, NifiConnectionOrderVisitorCache cache) {
        NifiVisitableProcessGroup visitableGroup = getFlowOrder(processGroupId, cache);
        NifiFlowProcessGroup flow = new NifiFlowBuilder().build(visitableGroup);
        String categoryName = flow.getParentGroupName();
        String feedName = flow.getName();
        feedName = FeedNameUtil.fullName(categoryName, feedName);
        //if it is a versioned feed then strip the version to get the correct feed name
        feedName = TemplateCreationHelper.parseVersionedProcessGroupName(feedName);
        flow.setFeedName(feedName);
        return flow;
    }

    public Set<ProcessorDTO> getProcessorsForFlow(String processGroupId) throws NifiComponentNotFoundException {
        NifiVisitableProcessGroup group = getFlowOrder(processGroupId, null);
        Set<ProcessorDTO> processors = new HashSet<>();
        for (NifiVisitableProcessor p : group.getStartingProcessors()) {
            processors.addAll(p.getProcessors());
        }
        return processors;
    }


    public NifiFlowProcessGroup getFeedFlowForCategoryAndFeed(String categoryAndFeedName) {
        NifiFlowProcessGroup flow = null;
        String category = FeedNameUtil.category(categoryAndFeedName);
        String feed = FeedNameUtil.feed(categoryAndFeedName);
        //1 find the ProcessGroup under "root" matching the name category
        ProcessGroupDTO processGroupEntity = restClient.processGroups().findRoot();
        ProcessGroupDTO root = processGroupEntity;
        ProcessGroupDTO categoryGroup = root.getContents().getProcessGroups().stream().filter(group -> category.equalsIgnoreCase(group.getName())).findAny().orElse(null);
        if (categoryGroup != null) {
            ProcessGroupDTO feedGroup = categoryGroup.getContents().getProcessGroups().stream().filter(group -> feed.equalsIgnoreCase(group.getName())).findAny().orElse(null);
            if (feedGroup != null) {
                flow = getFeedFlow(feedGroup.getId());
            }
        }
        return flow;
    }

    public List<NifiFlowProcessGroup> getFeedFlows(Collection<String> feedNames) {
        log.info("get Graph of Nifi Flows looking for {} ", feedNames == null ? "ALL Feeds " : feedNames);
        long start = System.currentTimeMillis();
        NifiConnectionOrderVisitorCache cache = new NifiConnectionOrderVisitorCache();
        List<NifiFlowProcessGroup> feedFlows = new ArrayList<>();
        ProcessGroupDTO processGroupEntity = restClient.processGroups().findRoot();
        ProcessGroupDTO root = processGroupEntity;
        //first level is the category
        root.getContents().getProcessGroups().stream().sorted(new Comparator<ProcessGroupDTO>() {
            @Override
            public int compare(ProcessGroupDTO o1, ProcessGroupDTO o2) {
                if (TemplateCreationHelper.REUSABLE_TEMPLATES_PROCESS_GROUP_NAME.equalsIgnoreCase(o1.getName())) {
                    return -1;
                }
                if (TemplateCreationHelper.REUSABLE_TEMPLATES_PROCESS_GROUP_NAME.equalsIgnoreCase(o2.getName())) {
                    return -1;
                }
                return o1.getName().compareTo(o2.getName());
            }
        }).forEach(category -> {
            for (ProcessGroupDTO feedProcessGroup : category.getContents().getProcessGroups()) {

                //second level is the feed
                String feedName = FeedNameUtil.fullName(category.getName(), feedProcessGroup.getName());
                //if it is a versioned feed then strip the version to get the correct feed name
                feedName = TemplateCreationHelper.parseVersionedProcessGroupName(feedName);
                //if feednames are sent in, only add those that match or those in the reusable group
                if ((feedNames == null || feedNames.isEmpty()) || (feedNames != null && (feedNames.contains(feedName) || TemplateCreationHelper.REUSABLE_TEMPLATES_PROCESS_GROUP_NAME
                    .equalsIgnoreCase(category.getName())))) {
                    NifiFlowProcessGroup feedFlow = getFeedFlow(feedProcessGroup.getId(), cache);
                    feedFlow.setFeedName(feedName);
                    feedFlows.add(feedFlow);
                }
            }
        });
        long end = System.currentTimeMillis();
        log.info("finished Graph of Nifi Flows.  Returning {} flows, {} ", feedFlows.size(), (end - start) + " ms");
        return feedFlows;
    }


    //walk entire graph
    public List<NifiFlowProcessGroup> getFeedFlows() {
        return getFeedFlows(null);
    }


    public NifiFlowProcessGroup getTemplateFeedFlow(TemplateDTO template) {
        ProcessGroupDTO parentProcessGroup = new ProcessGroupDTO();
        parentProcessGroup.setId(UUID.randomUUID().toString());
        parentProcessGroup.setParentGroupId(UUID.randomUUID().toString());
        parentProcessGroup.setName(template.getName());
        parentProcessGroup.setContents(template.getSnippet());
        NifiConnectionOrderVisitorCache cache = new NifiConnectionOrderVisitorCache();
        Collection<ProcessGroupDTO> groups = NifiProcessUtil.getProcessGroups(parentProcessGroup);
        cache.add(parentProcessGroup);
        //add the snippet as its own process group
        if (template.getSnippet().getProcessors() != null) {
            //find the first processor and get its parent group id
            Optional<ProcessorDTO> firstProcessor = template.getSnippet().getProcessors().stream().findFirst();
            if (firstProcessor.isPresent()) {
                String groupId = firstProcessor.get().getParentGroupId();
                ProcessGroupDTO snippetGroup = new ProcessGroupDTO();
                snippetGroup.setId(groupId);
                snippetGroup.setParentGroupId(template.getGroupId());
                snippetGroup.setContents(template.getSnippet());
                cache.add(snippetGroup);
            }

        }
        if (groups != null) {
            groups.stream().forEach(group -> cache.add(group));
        }
        NifiVisitableProcessGroup visitableGroup = getFlowOrder(parentProcessGroup, cache, false);
        NifiFlowProcessGroup flow = new NifiFlowBuilder().build(visitableGroup);
        return flow;
    }

    public NifiFlowProcessGroup getTemplateFeedFlow(String templateId) {
        TemplateDTO template = restClient.templates().findById(templateId).orElseThrow(() -> new NifiComponentNotFoundException(templateId, NifiConstants.NIFI_COMPONENT_TYPE.TEMPLATE, null));
        return getTemplateFeedFlow(template);
    }


}
