/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.
 */
package org.apache.nifi.web.api.dto;

import io.swagger.v3.oas.annotations.media.Schema;

import jakarta.xml.bind.annotation.XmlType;
import java.nio.charset.StandardCharsets;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;

/**
 * The contents of a flow snippet.
 */
@XmlType(name = "flowSnippet")
public class FlowSnippetDTO {

    private static final long LSB = 0;

    private Set<ProcessGroupDTO> processGroups = new LinkedHashSet<>();
    private Set<RemoteProcessGroupDTO> remoteProcessGroups = new LinkedHashSet<>();
    private Set<ProcessorDTO> processors = new LinkedHashSet<>();
    private Set<PortDTO> inputPorts = new LinkedHashSet<>();
    private Set<PortDTO> outputPorts = new LinkedHashSet<>();
    private Set<ConnectionDTO> connections = new LinkedHashSet<>();
    private Set<LabelDTO> labels = new LinkedHashSet<>();
    private Set<FunnelDTO> funnels = new LinkedHashSet<>();
    private Set<ControllerServiceDTO> controllerServices = new LinkedHashSet<>();

    private final boolean newTemplate;
    private Set<String> convertedUuids = new HashSet<>();

    public FlowSnippetDTO() {
        this(false);
    }

    public FlowSnippetDTO(boolean newTemplate) {
        this.newTemplate = newTemplate;
    }
    /**
     * @return connections in this flow snippet
     */
    @Schema(description = "The connections in this flow snippet."
    )
    public Set<ConnectionDTO> getConnections() {
        return connections;
    }

    public void setConnections(Set<ConnectionDTO> connections) {
        this.removeInstanceIdentifierIfNecessary(connections);
        this.connections = this.orderedById(connections);
    }

    /**
     * @return input ports in this flow snippet
     */
    @Schema(description = "The input ports in this flow snippet."
    )
    public Set<PortDTO> getInputPorts() {
        return inputPorts;
    }

    public void setInputPorts(Set<PortDTO> inputPorts) {
        this.removeInstanceIdentifierIfNecessary(inputPorts);
        this.inputPorts = this.orderedById(inputPorts);
    }

    /**
     * @return labels in this flow snippet
     */
    @Schema(description = "The labels in this flow snippet."
    )
    public Set<LabelDTO> getLabels() {
        return labels;
    }

    public void setLabels(Set<LabelDTO> labels) {
        this.removeInstanceIdentifierIfNecessary(labels);
        this.labels = this.orderedById(labels);
    }

    /**
     * @return funnels in this flow snippet
     */
    @Schema(description = "The funnels in this flow snippet."
    )
    public Set<FunnelDTO> getFunnels() {
        return funnels;
    }

    public void setFunnels(Set<FunnelDTO> funnels) {
        this.removeInstanceIdentifierIfNecessary(funnels);
        this.funnels = this.orderedById(funnels);
    }

    /**
     * @return output ports in this flow snippet
     */
    @Schema(description = "The output ports in this flow snippet."
    )
    public Set<PortDTO> getOutputPorts() {
        return outputPorts;
    }

    public void setOutputPorts(Set<PortDTO> outputPorts) {
        this.removeInstanceIdentifierIfNecessary(outputPorts);
        this.outputPorts = this.orderedById(outputPorts);
    }

    /**
     * @return process groups in this flow snippet
     */
    @Schema(description = "The process groups in this flow snippet."
    )
    public Set<ProcessGroupDTO> getProcessGroups() {
        return processGroups;
    }

    public void setProcessGroups(Set<ProcessGroupDTO> processGroups) {
        this.removeInstanceIdentifierIfNecessary(processGroups);
        this.processGroups = this.orderedById(processGroups);
    }

    /**
     * @return processors in this flow group
     */
    @Schema(description = "The processors in this flow snippet."
    )
    public Set<ProcessorDTO> getProcessors() {
        return processors;
    }

    public void setProcessors(Set<ProcessorDTO> processors) {
        this.removeInstanceIdentifierIfNecessary(processors);
        this.processors = this.orderedById(processors);
    }

    /**
     * @return remote process groups in this flow snippet
     */
    @Schema(description = "The remote process groups in this flow snippet."
    )
    public Set<RemoteProcessGroupDTO> getRemoteProcessGroups() {
        return remoteProcessGroups;
    }

    public void setRemoteProcessGroups(Set<RemoteProcessGroupDTO> remoteProcessGroups) {
        this.removeInstanceIdentifierIfNecessary(remoteProcessGroups);
        this.remoteProcessGroups = this.orderedById(remoteProcessGroups);
    }

    /**
     * @return the Controller Services in this flow snippet
     */
    @Schema(description = "The controller services in this flow snippet."
    )
    public Set<ControllerServiceDTO> getControllerServices() {
        return controllerServices;
    }

    public void setControllerServices(Set<ControllerServiceDTO> controllerServices) {
        this.removeInstanceIdentifierIfNecessary(controllerServices);
        this.controllerServices = this.orderedById(controllerServices);
    }

    private <T extends ComponentDTO> Set<T> orderedById(Set<T> dtos) {
        TreeSet<T> components = new TreeSet<>(new Comparator<ComponentDTO>() {
            @Override
            public int compare(ComponentDTO c1, ComponentDTO c2) {
                return c1.getId().compareTo(c2.getId());
            }
        });
        components.addAll(dtos);
        return components;
    }

    private long generateMsb(String id) {
        final UUID temp;
        if (convertedUuids.contains(id)) {
            temp = UUID.fromString(id);
        } else {
            temp = UUID.nameUUIDFromBytes(id.getBytes(StandardCharsets.UTF_8));

            // record what the converted uuid will be so we do not re-convert a previously converted uuid
            convertedUuids.add(new UUID(temp.getMostSignificantBits(), LSB).toString());
        }
        return temp.getMostSignificantBits();
    }

    private void removeInstanceIdentifierIfNecessary(Set<? extends ComponentDTO> componentDtos) {
        if (this.newTemplate) {
            for (ComponentDTO componentDto : componentDtos) {
                UUID id = new UUID(this.generateMsb(componentDto.getId()), LSB);
                componentDto.setId(id.toString());

                id = new UUID(this.generateMsb(componentDto.getParentGroupId()), LSB);
                componentDto.setParentGroupId(id.toString());
                if (componentDto instanceof ControllerServiceDTO) {
                    ControllerServiceDTO csDTO = (ControllerServiceDTO) componentDto;
                    Map<String, PropertyDescriptorDTO> map = csDTO.getDescriptors();
                    Map<String, String> props = csDTO.getProperties();
                    for (Entry<String, PropertyDescriptorDTO> entry : map.entrySet()) {
                        if (entry.getValue().getIdentifiesControllerService() != null && props.get(entry.getKey()) != null) {
                            String key = entry.getKey();
                            String value = props.get(key);
                            id = new UUID(this.generateMsb(value), LSB);
                            props.put(key, id.toString());
                        }
                    }
                } else if (componentDto instanceof ProcessorDTO) {
                    ProcessorDTO processorDTO = (ProcessorDTO) componentDto;
                    Map<String, PropertyDescriptorDTO> map = processorDTO.getConfig().getDescriptors();
                    Map<String, String> props = processorDTO.getConfig().getProperties();
                    for (Entry<String, PropertyDescriptorDTO> entry : map.entrySet()) {
                        if (entry.getValue().getIdentifiesControllerService() != null && props.get(entry.getKey()) != null) {
                            String key = entry.getKey();
                            String value = props.get(key);
                            id = new UUID(this.generateMsb(value), LSB);
                            props.put(key, id.toString());
                        }
                    }
                } else if (componentDto instanceof ConnectionDTO) {
                    ConnectionDTO connectionDTO = (ConnectionDTO) componentDto;

                    ConnectableDTO cdto = connectionDTO.getSource();
                    if (!cdto.getType().equals("REMOTE_INPUT_PORT") && !cdto.getType().equals("REMOTE_OUTPUT_PORT")) {
                        id = new UUID(this.generateMsb(cdto.getId()), LSB);
                        cdto.setId(id.toString());
                    }

                    id = new UUID(this.generateMsb(cdto.getGroupId()), LSB);
                    cdto.setGroupId(id.toString());

                    cdto = connectionDTO.getDestination();
                    if (!cdto.getType().equals("REMOTE_INPUT_PORT") && !cdto.getType().equals("REMOTE_OUTPUT_PORT")) {
                        id = new UUID(this.generateMsb(cdto.getId()), LSB);
                        cdto.setId(id.toString());
                    }

                    id = new UUID(this.generateMsb(cdto.getGroupId()), LSB);
                    cdto.setGroupId(id.toString());
                } else if (componentDto instanceof ProcessGroupDTO) {
                    FlowSnippetDTO fsDTO = ((ProcessGroupDTO) componentDto).getContents();

                    this.removeInstanceIdentifierIfNecessary(fsDTO.getConnections());
                    fsDTO.connections = this.orderedById(fsDTO.getConnections());

                    this.removeInstanceIdentifierIfNecessary(fsDTO.getControllerServices());
                    fsDTO.controllerServices = this.orderedById(fsDTO.getControllerServices());

                    this.removeInstanceIdentifierIfNecessary(fsDTO.getFunnels());
                    fsDTO.funnels = this.orderedById(fsDTO.getFunnels());

                    this.removeInstanceIdentifierIfNecessary(fsDTO.getInputPorts());
                    fsDTO.inputPorts = this.orderedById(fsDTO.getInputPorts());

                    this.removeInstanceIdentifierIfNecessary(fsDTO.getLabels());
                    fsDTO.labels = this.orderedById(fsDTO.getLabels());

                    this.removeInstanceIdentifierIfNecessary(fsDTO.getOutputPorts());
                    fsDTO.outputPorts = this.orderedById(fsDTO.getOutputPorts());

                    this.removeInstanceIdentifierIfNecessary(fsDTO.getProcessGroups());
                    fsDTO.processGroups = this.orderedById(fsDTO.getProcessGroups());

                    this.removeInstanceIdentifierIfNecessary(fsDTO.getProcessors());
                    fsDTO.processors = this.orderedById(fsDTO.getProcessors());

                    this.removeInstanceIdentifierIfNecessary(fsDTO.getRemoteProcessGroups());
                    fsDTO.remoteProcessGroups = this.orderedById(fsDTO.getRemoteProcessGroups());
                } else if (componentDto instanceof RemoteProcessGroupDTO) {
                    RemoteProcessGroupContentsDTO contentsDTO = ((RemoteProcessGroupDTO) componentDto).getContents();
                    contentsDTO.setInputPorts(this.orderedRemotePortsById(contentsDTO.getInputPorts()));
                    contentsDTO.setOutputPorts(this.orderedRemotePortsById(contentsDTO.getOutputPorts()));
                }
            }
        }
    }

    private <T extends RemoteProcessGroupPortDTO> Set<T> orderedRemotePortsById(Set<T> dtos) {
        TreeSet<T> components = new TreeSet<>(new Comparator<RemoteProcessGroupPortDTO>() {
            @Override
            public int compare(RemoteProcessGroupPortDTO c1, RemoteProcessGroupPortDTO c2) {
                return UUID.fromString(c1.getId()).compareTo(UUID.fromString(c2.getId()));
            }
        });
        components.addAll(dtos);
        return components;
    }
}
