package com.thinkbiganalytics.nifi.v1.rest.client;

/*-
 * #%L
 * thinkbig-nifi-rest-client-v1
 * %%
 * 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.google.common.collect.ImmutableMap;
import com.thinkbiganalytics.nifi.rest.client.AbstractNiFiProcessGroupsRestClient;
import com.thinkbiganalytics.nifi.rest.client.NiFiComponentState;
import com.thinkbiganalytics.nifi.rest.client.NiFiProcessGroupsRestClient;
import com.thinkbiganalytics.nifi.rest.client.NifiComponentNotFoundException;
import com.thinkbiganalytics.nifi.rest.support.NifiConstants;

import org.apache.nifi.web.api.dto.ConnectableDTO;
import org.apache.nifi.web.api.dto.ConnectionDTO;
import org.apache.nifi.web.api.dto.ControllerServiceDTO;
import org.apache.nifi.web.api.dto.FlowSnippetDTO;
import org.apache.nifi.web.api.dto.PortDTO;
import org.apache.nifi.web.api.dto.ProcessGroupDTO;
import org.apache.nifi.web.api.dto.RevisionDTO;
import org.apache.nifi.web.api.dto.flow.FlowDTO;
import org.apache.nifi.web.api.dto.status.ProcessGroupStatusDTO;
import org.apache.nifi.web.api.entity.ConnectionEntity;
import org.apache.nifi.web.api.entity.ConnectionsEntity;
import org.apache.nifi.web.api.entity.ControllerServiceEntity;
import org.apache.nifi.web.api.entity.ControllerServicesEntity;
import org.apache.nifi.web.api.entity.FlowEntity;
import org.apache.nifi.web.api.entity.FunnelEntity;
import org.apache.nifi.web.api.entity.InputPortsEntity;
import org.apache.nifi.web.api.entity.InstantiateTemplateRequestEntity;
import org.apache.nifi.web.api.entity.LabelEntity;
import org.apache.nifi.web.api.entity.OutputPortsEntity;
import org.apache.nifi.web.api.entity.PortEntity;
import org.apache.nifi.web.api.entity.ProcessGroupEntity;
import org.apache.nifi.web.api.entity.ProcessGroupFlowEntity;
import org.apache.nifi.web.api.entity.ProcessGroupsEntity;
import org.apache.nifi.web.api.entity.ProcessorEntity;
import org.apache.nifi.web.api.entity.RemoteProcessGroupEntity;
import org.apache.nifi.web.api.entity.ScheduleComponentsEntity;

import java.util.Collections;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import javax.annotation.Nonnull;
import javax.ws.rs.ClientErrorException;
import javax.ws.rs.NotFoundException;

/**
 * Implements a {@link NiFiProcessGroupsRestClient} for communicating with NiFi v1.0.
 */
public class NiFiProcessGroupsRestClientV1 extends AbstractNiFiProcessGroupsRestClient {

    /**
     * Base path for process group requests
     */
    private static final String BASE_PATH = "/process-groups/";

    /**
     * REST client for communicating with NiFi
     */
    private final NiFiRestClientV1 client;

    /**
     * Constructs a {@code NiFiProcessGroupsRestClientV1} with the specified NiFi REST client.
     *
     * @param client the REST client
     */
    public NiFiProcessGroupsRestClientV1(@Nonnull final NiFiRestClientV1 client) {
        this.client = client;
    }

    @Nonnull
    @Override
    public ProcessGroupDTO create(@Nonnull String parentProcessGroupId, @Nonnull String name) {
        final ProcessGroupEntity entity = new ProcessGroupEntity();

        final ProcessGroupDTO processGroup = new ProcessGroupDTO();
        processGroup.setName(name);
        entity.setComponent(processGroup);

        final RevisionDTO revision = new RevisionDTO();
        revision.setVersion(0L);
        entity.setRevision(revision);

        try {
            return client.post(BASE_PATH + parentProcessGroupId + "/process-groups", entity, ProcessGroupEntity.class).getComponent();
        } catch (final NotFoundException e) {
            throw new NifiComponentNotFoundException(parentProcessGroupId, NifiConstants.NIFI_COMPONENT_TYPE.PROCESS_GROUP, e);
        }
    }

    @Nonnull
    @Override
    public ConnectionDTO createConnection(@Nonnull final String processGroupId, @Nonnull final ConnectableDTO source, @Nonnull final ConnectableDTO dest) {
        final ConnectionEntity entity = new ConnectionEntity();

        final ConnectionDTO connection = new ConnectionDTO();
        connection.setDestination(dest);
        connection.setName(source.getName() + "-" + dest.getName());
        connection.setSource(source);
        entity.setComponent(connection);

        final RevisionDTO revision = new RevisionDTO();
        revision.setVersion(0L);
        entity.setRevision(revision);

        try {
            return client.post(BASE_PATH + processGroupId + "/connections", entity, ConnectionEntity.class).getComponent();
        } catch (final NotFoundException e) {
            throw new NifiComponentNotFoundException(processGroupId, NifiConstants.NIFI_COMPONENT_TYPE.PROCESS_GROUP, e);
        }
    }

    @Nonnull
    @Override
    public ControllerServiceDTO createControllerService(@Nonnull final String processGroupId, @Nonnull final ControllerServiceDTO controllerService) {
        final ControllerServiceEntity entity = new ControllerServiceEntity();
        entity.setComponent(controllerService);

        final RevisionDTO revision = new RevisionDTO();
        revision.setVersion(0L);
        entity.setRevision(revision);

        try {
            return client.post(BASE_PATH + processGroupId + "/controller-services", entity, ControllerServiceEntity.class).getComponent();
        } catch (final NotFoundException e) {
            throw new NifiComponentNotFoundException(processGroupId, NifiConstants.NIFI_COMPONENT_TYPE.PROCESS_GROUP, e);
        }
    }

    @Nonnull
    @Override
    public PortDTO createInputPort(@Nonnull final String processGroupId, @Nonnull final PortDTO inputPort) {
        final PortEntity entity = new PortEntity();
        entity.setComponent(inputPort);

        final RevisionDTO revision = new RevisionDTO();
        revision.setVersion(0L);
        entity.setRevision(revision);

        try {
            return client.post(BASE_PATH + processGroupId + "/input-ports", entity, PortEntity.class).getComponent();
        } catch (final NotFoundException e) {
            throw new NifiComponentNotFoundException(processGroupId, NifiConstants.NIFI_COMPONENT_TYPE.PROCESS_GROUP, e);
        }
    }

    @Nonnull
    @Override
    public PortDTO createOutputPort(@Nonnull final String processGroupId, @Nonnull final PortDTO outputPort) {
        final PortEntity entity = new PortEntity();
        entity.setComponent(outputPort);

        final RevisionDTO revision = new RevisionDTO();
        revision.setVersion(0L);
        entity.setRevision(revision);

        try {
            return client.post(BASE_PATH + processGroupId + "/output-ports", entity, PortEntity.class).getComponent();
        } catch (final NotFoundException e) {
            throw new NifiComponentNotFoundException(processGroupId, NifiConstants.NIFI_COMPONENT_TYPE.PROCESS_GROUP, e);
        }
    }

    @Nonnull
    @Override
    public Set<ProcessGroupDTO> findAll(@Nonnull final String parentGroupId) {
        try {
            return client.get(BASE_PATH + parentGroupId + "/process-groups", null, ProcessGroupsEntity.class)
                .getProcessGroups().stream()
                .map(ProcessGroupEntity::getComponent)
                .collect(Collectors.toSet());
        } catch (final NotFoundException e) {
            throw new NifiComponentNotFoundException(parentGroupId, NifiConstants.NIFI_COMPONENT_TYPE.PROCESS_GROUP, e);
        }
    }

    @Nonnull
    @Override
    public Optional<ProcessGroupDTO> findById(@Nonnull final String processGroupId, final boolean recursive, final boolean verbose) {
        return findById(processGroupId, recursive, verbose, true);
    }

    public Optional<ProcessGroupDTO> findById(@Nonnull final String processGroupId, final boolean recursive, final boolean verbose, boolean logRestAccessErrors) {
        return findEntityById(processGroupId, logRestAccessErrors).filter(processGroupEntity -> processGroupEntity != null)
            .map(ProcessGroupEntity::getComponent)
            .map(processGroup -> {
                if (verbose) {
                    processGroup.setContents(getFlowSnippet(processGroupId, recursive));
                }
                return processGroup;
            });
    }

    /**
     * Gets a process group entity.
     *
     * @param processGroupId the process group id
     * @return the process group entity, if found
     */
    @Nonnull
    public Optional<ProcessGroupEntity> findEntityById(@Nonnull final String processGroupId) {
        return findEntityById(processGroupId, true);
    }

    /**
     * Gets a process group entity.
     *
     * @param processGroupId      the process group id
     * @param logRestAccessErrors true to log any REST exceptions.  false will not log exceptions
     * @return the process group entity, if found
     */
    @Nonnull
    public Optional<ProcessGroupEntity> findEntityById(@Nonnull final String processGroupId, boolean logRestAccessErrors) {
        try {
            return Optional.ofNullable(client.get(BASE_PATH + processGroupId, null, ProcessGroupEntity.class, logRestAccessErrors));
        } catch (final NotFoundException e) {
            return Optional.empty();
        }
    }

    public Optional<ProcessGroupStatusDTO> getStatus(String processGroupId) {
        return Optional.ofNullable(findEntityById(processGroupId).map(processGroupEntity -> processGroupEntity.getStatus()).orElse(null));
    }


    @Nonnull
    @Override
    public Set<ConnectionDTO> getConnections(@Nonnull final String processGroupId) {
        try {
            return client.get(BASE_PATH + processGroupId + "/connections", null, ConnectionsEntity.class)
                .getConnections().stream()
                .map(ConnectionEntity::getComponent)
                .collect(Collectors.toSet());
        } catch (final NotFoundException e) {
            throw new NifiComponentNotFoundException(processGroupId, NifiConstants.NIFI_COMPONENT_TYPE.PROCESS_GROUP, e);
        }
    }

    @Nonnull
    @Override
    public Set<ControllerServiceDTO> getControllerServices(@Nonnull final String processGroupId) {
        try {
            return client.get("/flow/process-groups/" + processGroupId + "/controller-services", null, ControllerServicesEntity.class)
                .getControllerServices().stream()
                .map(ControllerServiceEntity::getComponent)
                .collect(Collectors.toSet());
        } catch (final NotFoundException e) {
            throw new NifiComponentNotFoundException(processGroupId, NifiConstants.NIFI_COMPONENT_TYPE.PROCESS_GROUP, e);
        }
    }

    @Nonnull
    @Override
    public Set<PortDTO> getInputPorts(@Nonnull final String processGroupId) {
        try {
            return client.get(BASE_PATH + processGroupId + "/input-ports", null, InputPortsEntity.class)
                .getInputPorts().stream()
                .map(PortEntity::getComponent)
                .collect(Collectors.toSet());
        } catch (final NotFoundException e) {
            throw new NifiComponentNotFoundException(processGroupId, NifiConstants.NIFI_COMPONENT_TYPE.PROCESS_GROUP, e);
        }
    }

    @Nonnull
    @Override
    public Set<PortDTO> getOutputPorts(@Nonnull final String processGroupId) {
        try {
            return client.get(BASE_PATH + processGroupId + "/output-ports", null, OutputPortsEntity.class)
                .getOutputPorts().stream()
                .map(PortEntity::getComponent)
                .collect(Collectors.toSet());
        } catch (final NotFoundException e) {
            throw new NifiComponentNotFoundException(processGroupId, NifiConstants.NIFI_COMPONENT_TYPE.PROCESS_GROUP, e);
        }
    }

    @Nonnull
    @Override
    public FlowSnippetDTO instantiateTemplate(@Nonnull final String processGroupId, @Nonnull final String templateId) {
        final InstantiateTemplateRequestEntity entity = new InstantiateTemplateRequestEntity();
        entity.setOriginX(10.0);
        entity.setOriginY(10.0);
        entity.setTemplateId(templateId);

        try {
            final FlowEntity flow = client.post(BASE_PATH + processGroupId + "/template-instance", entity, FlowEntity.class);
            return toFlowSnippet(flow.getFlow());
        } catch (final NotFoundException e) {
            throw new NifiComponentNotFoundException(processGroupId, NifiConstants.NIFI_COMPONENT_TYPE.PROCESS_GROUP, e);
        } catch (final ClientErrorException e) {
            final String msg = e.getResponse().readEntity(String.class);
            throw new NifiComponentNotFoundException("Unable to create Template instance for templateId: " + templateId + " under Process Group " + processGroupId + ". " + msg);
        }
    }

    @Override
    public void schedule(@Nonnull final String processGroupId, @Nonnull final String parentGroupId, @Nonnull final NiFiComponentState state) {
        final ScheduleComponentsEntity entity = new ScheduleComponentsEntity();
        entity.setId(processGroupId);
        entity.setState(state.toString());

        try {
            client.put("/flow/process-groups/" + processGroupId, entity, ScheduleComponentsEntity.class);
        } catch (final NotFoundException e) {
            throw new NifiComponentNotFoundException(processGroupId, NifiConstants.NIFI_COMPONENT_TYPE.PROCESS_GROUP, e);
        }
    }

    @Nonnull
    @Override
    public ProcessGroupDTO update(@Nonnull final ProcessGroupDTO processGroup) {
        return findEntityById(processGroup.getId())
            .flatMap(current -> {
                final ProcessGroupEntity entity = new ProcessGroupEntity();
                entity.setComponent(processGroup);

                final RevisionDTO revision = new RevisionDTO();
                revision.setVersion(current.getRevision().getVersion());
                entity.setRevision(revision);

                try {
                    return Optional.of(client.put(BASE_PATH + processGroup.getId(), entity, ProcessGroupEntity.class).getComponent());
                } catch (final NotFoundException e) {
                    return Optional.empty();
                }
            })
            .orElseThrow(() -> new NifiComponentNotFoundException(processGroup.getId(), NifiConstants.NIFI_COMPONENT_TYPE.PROCESS_GROUP, null));
    }

    @Nonnull
    @Override
    protected Optional<ProcessGroupDTO> doDelete(@Nonnull final ProcessGroupDTO processGroup) {
        return findEntityById(processGroup.getId())
            .flatMap(entity -> {
                final Long version = entity.getRevision().getVersion();
                try {
                    return Optional.of(client.delete(BASE_PATH + processGroup.getId(), ImmutableMap.of("version", version), ProcessGroupEntity.class).getComponent());
                } catch (final NotFoundException e) {
                    return Optional.empty();
                }
            });
    }


    /**
     * Fetches the flow snippet for the specified process group.
     *
     * @param processGroupId the process group id
     * @param recursive      {@code true} to include flows for child process groups, or {@code false} to leave child process group flows empty
     * @return the flow for the process group
     */
    @Nonnull
    private FlowSnippetDTO getFlowSnippet(@Nonnull final String processGroupId, final boolean recursive) {
        // Fetch the flow
        final FlowSnippetDTO snippet;
        try {
            snippet = toFlowSnippet(client.get("/flow/process-groups/" + processGroupId, null, ProcessGroupFlowEntity.class).getProcessGroupFlow().getFlow());
        } catch (final NotFoundException e) {
            throw new NifiComponentNotFoundException(processGroupId, NifiConstants.NIFI_COMPONENT_TYPE.PROCESS_GROUP, e);
        }

        // Add flow for child process groups
        if (recursive) {
            for (final ProcessGroupDTO processGroup : snippet.getProcessGroups()) {
                processGroup.setContents(getFlowSnippet(processGroup.getId(), true));
            }
        }

        // Return flow
        return snippet;
    }

    private Set<ProcessGroupEntity> findAllEntities(@Nonnull final String parentGroupId) {
        try {
            return client.get(BASE_PATH + parentGroupId + "/process-groups", null, ProcessGroupsEntity.class)
                .getProcessGroups();
        } catch (final NotFoundException e) {
            throw new NifiComponentNotFoundException(parentGroupId, NifiConstants.NIFI_COMPONENT_TYPE.PROCESS_GROUP, e);
        }
    }

    /**
     * Converts the specified flow to a flow snippet.
     *
     * @param flow the flow
     * @return the flow snippet
     */
    @Nonnull
    private FlowSnippetDTO toFlowSnippet(@Nonnull final FlowDTO flow) {
        final FlowSnippetDTO snippet = new FlowSnippetDTO();
        snippet.setConnections(flow.getConnections().stream().map(ConnectionEntity::getComponent).collect(Collectors.toSet()));
        snippet.setControllerServices(Collections.emptySet());
        snippet.setFunnels(flow.getFunnels().stream().map(FunnelEntity::getComponent).collect(Collectors.toSet()));
        snippet.setInputPorts(flow.getInputPorts().stream().map(PortEntity::getComponent).collect(Collectors.toSet()));
        snippet.setLabels(flow.getLabels().stream().map(LabelEntity::getComponent).collect(Collectors.toSet()));
        snippet.setOutputPorts(flow.getOutputPorts().stream().map(PortEntity::getComponent).collect(Collectors.toSet()));
        snippet.setProcessGroups(flow.getProcessGroups().stream().map(ProcessGroupEntity::getComponent).collect(Collectors.toSet()));
        snippet.setProcessors(flow.getProcessors().stream().map(ProcessorEntity::getComponent).collect(Collectors.toSet()));
        snippet.setRemoteProcessGroups(flow.getRemoteProcessGroups().stream().map(RemoteProcessGroupEntity::getComponent).collect(Collectors.toSet()));
        return snippet;
    }
}
