/*
 * 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.audit;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.action.Action;
import org.apache.nifi.action.Component;
import org.apache.nifi.action.FlowChangeAction;
import org.apache.nifi.action.Operation;
import org.apache.nifi.action.component.details.FlowChangeExtensionDetails;
import org.apache.nifi.action.component.details.FlowChangeRemoteProcessGroupDetails;
import org.apache.nifi.action.details.ConnectDetails;
import org.apache.nifi.action.details.FlowChangeConnectDetails;
import org.apache.nifi.authorization.user.NiFiUserUtils;
import org.apache.nifi.connectable.ConnectableType;
import org.apache.nifi.connectable.Connection;
import org.apache.nifi.connectable.Funnel;
import org.apache.nifi.connectable.Port;
import org.apache.nifi.controller.ProcessorNode;
import org.apache.nifi.controller.Snippet;
import org.apache.nifi.controller.label.Label;
import org.apache.nifi.groups.ProcessGroup;
import org.apache.nifi.groups.RemoteProcessGroup;
import org.apache.nifi.authorization.user.NiFiUser;
import org.apache.nifi.web.api.dto.ConnectableDTO;
import org.apache.nifi.web.api.dto.ConnectionDTO;
import org.apache.nifi.web.api.dto.FlowSnippetDTO;
import org.apache.nifi.web.api.dto.FunnelDTO;
import org.apache.nifi.web.api.dto.PortDTO;
import org.apache.nifi.web.api.dto.ProcessGroupDTO;
import org.apache.nifi.web.api.dto.ProcessorDTO;
import org.apache.nifi.web.api.dto.RemoteProcessGroupDTO;
import org.apache.nifi.web.api.dto.SnippetDTO;
import org.apache.nifi.web.dao.ConnectionDAO;
import org.apache.nifi.web.dao.FunnelDAO;
import org.apache.nifi.web.dao.LabelDAO;
import org.apache.nifi.web.dao.PortDAO;
import org.apache.nifi.web.dao.ProcessGroupDAO;
import org.apache.nifi.web.dao.ProcessorDAO;
import org.apache.nifi.web.dao.RemoteProcessGroupDAO;
import org.apache.nifi.web.dao.SnippetDAO;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;

/**
 *
 */
@Aspect
public class SnippetAuditor extends NiFiAuditor {

    private static final Logger logger = LoggerFactory.getLogger(SnippetAuditor.class);

    private PortDAO inputPortDAO;
    private PortDAO outputPortDAO;
    private RemoteProcessGroupDAO remoteProcessGroupDAO;
    private ProcessorDAO processorDAO;
    private FunnelDAO funnelDAO;
    private ConnectionDAO connectionDAO;
    private LabelDAO labelDAO;

    private PortAuditor portAuditor;
    private RemoteProcessGroupAuditor remoteProcessGroupAuditor;
    private ProcessGroupAuditor processGroupAuditor;
    private ProcessorAuditor processorAuditor;
    private FunnelAuditor funnelAuditor;
    private RelationshipAuditor relationshipAuditor;
    private LabelAuditor labelAuditor;

    /**
     * Audits copy/paste.
     *
     * @param proceedingJoinPoint join point
     * @return dto
     * @throws Throwable ex
     */
    @Around("within(org.apache.nifi.web.dao.SnippetDAO+) && "
            + "execution(org.apache.nifi.web.api.dto.FlowSnippetDTO copySnippet(java.lang.String, java.lang.String, java.lang.Double, java.lang.Double, java.lang.String))")
    public FlowSnippetDTO copySnippetAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        // perform the underlying operation
        FlowSnippetDTO snippet = (FlowSnippetDTO) proceedingJoinPoint.proceed();
        auditSnippet(snippet);
        return snippet;
    }

    /**
     * Audits the instantiation of a template.
     *
     * @param proceedingJoinPoint join point
     * @return dto
     * @throws Throwable ex
     */
    @Around("within(org.apache.nifi.web.dao.TemplateDAO+) && "
            + "execution(org.apache.nifi.web.api.dto.FlowSnippetDTO instantiateTemplate("
            + "java.lang.String, java.lang.Double, java.lang.Double, java.lang.String, org.apache.nifi.web.api.dto.FlowSnippetDTO, java.lang.String))")
    public FlowSnippetDTO instantiateTemplateAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        // perform the underlying operation
        FlowSnippetDTO snippet = (FlowSnippetDTO) proceedingJoinPoint.proceed();
        auditSnippet(snippet);
        return snippet;
    }

    /**
     * Audits the specified snippet.
     */
    private void auditSnippet(final FlowSnippetDTO snippet) {
        final Collection<Action> actions = new ArrayList<>();
        final Date timestamp = new Date();

        // input ports
        for (final PortDTO inputPort : snippet.getInputPorts()) {
            actions.add(generateAuditRecord(inputPort.getId(), inputPort.getName(), Component.InputPort, Operation.Add, timestamp));
        }

        // output ports
        for (final PortDTO outputPort : snippet.getOutputPorts()) {
            actions.add(generateAuditRecord(outputPort.getId(), outputPort.getName(), Component.OutputPort, Operation.Add, timestamp));
        }

        // remote processor groups
        for (final RemoteProcessGroupDTO remoteProcessGroup : snippet.getRemoteProcessGroups()) {
            FlowChangeRemoteProcessGroupDetails remoteProcessGroupDetails = new FlowChangeRemoteProcessGroupDetails();
            remoteProcessGroupDetails.setUri(remoteProcessGroup.getTargetUri());

            final FlowChangeAction action = generateAuditRecord(remoteProcessGroup.getId(), remoteProcessGroup.getName(), Component.RemoteProcessGroup, Operation.Add, timestamp);
            action.setComponentDetails(remoteProcessGroupDetails);
            actions.add(action);
        }

        // processor groups
        for (final ProcessGroupDTO processGroup : snippet.getProcessGroups()) {
            actions.add(generateAuditRecord(processGroup.getId(), processGroup.getName(), Component.ProcessGroup, Operation.Add, timestamp));
        }

        // processors
        for (final ProcessorDTO processor : snippet.getProcessors()) {
            final FlowChangeExtensionDetails processorDetails = new FlowChangeExtensionDetails();
            processorDetails.setType(StringUtils.substringAfterLast(processor.getType(), "."));

            final FlowChangeAction action = generateAuditRecord(processor.getId(), processor.getName(), Component.Processor, Operation.Add, timestamp);
            action.setComponentDetails(processorDetails);
            actions.add(action);
        }

        // funnels
        for (final FunnelDTO funnel : snippet.getFunnels()) {
            actions.add(generateAuditRecord(funnel.getId(), StringUtils.EMPTY, Component.Funnel, Operation.Add, timestamp));
        }

        // connections
        for (final ConnectionDTO connection : snippet.getConnections()) {
            final ConnectableDTO source = connection.getSource();
            final ConnectableDTO destination = connection.getDestination();

            // determine the relationships and connection name
            final String relationships = CollectionUtils.isEmpty(connection.getSelectedRelationships()) ? StringUtils.EMPTY : StringUtils.join(connection.getSelectedRelationships(), ", ");
            final String name = StringUtils.isBlank(connection.getName()) ? relationships : connection.getName();

            // create the connect details
            FlowChangeConnectDetails connectDetails = new FlowChangeConnectDetails();
            connectDetails.setSourceId(source.getId());
            connectDetails.setSourceName(source.getName());
            connectDetails.setSourceType(determineConnectableType(source));
            connectDetails.setRelationship(relationships);
            connectDetails.setDestinationId(destination.getId());
            connectDetails.setDestinationName(destination.getName());
            connectDetails.setDestinationType(determineConnectableType(destination));

            // create the audit record
            final FlowChangeAction action = generateAuditRecord(connection.getId(), name, Component.Connection, Operation.Connect, timestamp);
            action.setActionDetails(connectDetails);
            actions.add(action);
        }

        // save the actions
        if (!actions.isEmpty()) {
            saveActions(actions, logger);
        }
    }

    /**
     * Determines the type of component the specified connectable is.
     */
    private Component determineConnectableType(ConnectableDTO connectable) {
        Component component = Component.Controller;

        final String connectableType = connectable.getType();
        if (ConnectableType.PROCESSOR.name().equals(connectableType)) {
            component = Component.Processor;
        } else if (ConnectableType.INPUT_PORT.name().equals(connectableType)) {
            component = Component.InputPort;
        } else if (ConnectableType.OUTPUT_PORT.name().equals(connectableType)) {
            component = Component.OutputPort;
        } else if (ConnectableType.FUNNEL.name().equals(connectableType)) {
            component = Component.Funnel;
        } else {
            component = Component.RemoteProcessGroup;
        }

        return component;
    }

    /**
     * Generates an audit record for the creation of the specified funnel.
     */
    private FlowChangeAction generateAuditRecord(String id, String name, Component type, Operation operation, Date timestamp) {
        FlowChangeAction action = null;

        // get the current user
        NiFiUser user = NiFiUserUtils.getNiFiUser();

        // ensure the user was found
        if (user != null) {
            // create the action for adding this funnel
            action = new FlowChangeAction();
            action.setUserIdentity(user.getIdentity());
            action.setOperation(operation);
            action.setTimestamp(timestamp);
            action.setSourceId(id);
            action.setSourceName(name);
            action.setSourceType(type);
        }

        return action;
    }

    /**
     * Audits a bulk move.
     *
     * @param proceedingJoinPoint join point
     * @param snippetDTO dto
     * @param snippetDAO dao
     * @return snippet
     * @throws Throwable ex
     */
    @Around("within(org.apache.nifi.web.dao.SnippetDAO+) && "
            + "execution(org.apache.nifi.controller.Snippet updateSnippetComponents(org.apache.nifi.web.api.dto.SnippetDTO)) && "
            + "args(snippetDTO) && "
            + "target(snippetDAO)")
    public Snippet updateSnippetAdvice(ProceedingJoinPoint proceedingJoinPoint, SnippetDTO snippetDTO, SnippetDAO snippetDAO) throws Throwable {
        // get the snippet before removing it
        Snippet snippet = snippetDAO.getSnippet(snippetDTO.getId());
        final String previousGroupId = snippet.getParentGroupId();

        // perform the underlying operation
        snippet = (Snippet) proceedingJoinPoint.proceed();

        // if this snippet is linked and its parent group id has changed
        final String groupId = snippetDTO.getParentGroupId();
        if (!previousGroupId.equals(groupId)) {

            // create move audit records for all items in this snippet
            final Collection<Action> actions = new ArrayList<>();

            for (String id : snippet.getProcessors().keySet()) {
                final ProcessorNode processor = processorDAO.getProcessor(id);
                final Action action = processorAuditor.generateAuditRecord(processor, Operation.Move, createMoveDetails(previousGroupId, groupId, logger));
                if (action != null) {
                    actions.add(action);
                }
            }

            for (String id : snippet.getFunnels().keySet()) {
                final Funnel funnel = funnelDAO.getFunnel(id);
                final Action action = funnelAuditor.generateAuditRecord(funnel, Operation.Move, createMoveDetails(previousGroupId, groupId, logger));
                if (action != null) {
                    actions.add(action);
                }
            }

            for (String id : snippet.getInputPorts().keySet()) {
                final Port port = inputPortDAO.getPort(id);
                final Action action = portAuditor.generateAuditRecord(port, Operation.Move, createMoveDetails(previousGroupId, groupId, logger));
                if (action != null) {
                    actions.add(action);
                }
            }

            for (String id : snippet.getOutputPorts().keySet()) {
                final Port port = outputPortDAO.getPort(id);
                final Action action = portAuditor.generateAuditRecord(port, Operation.Move, createMoveDetails(previousGroupId, groupId, logger));
                if (action != null) {
                    actions.add(action);
                }
            }

            for (String id : snippet.getRemoteProcessGroups().keySet()) {
                final RemoteProcessGroup remoteProcessGroup = remoteProcessGroupDAO.getRemoteProcessGroup(id);
                final Action action = remoteProcessGroupAuditor.generateAuditRecord(remoteProcessGroup, Operation.Move, createMoveDetails(previousGroupId, groupId, logger));
                if (action != null) {
                    actions.add(action);
                }
            }

            for (String id : snippet.getProcessGroups().keySet()) {
                final ProcessGroupDAO processGroupDAO = getProcessGroupDAO();
                final ProcessGroup processGroup = processGroupDAO.getProcessGroup(id);
                final Action action = processGroupAuditor.generateAuditRecord(processGroup, Operation.Move, createMoveDetails(previousGroupId, groupId, logger));
                if (action != null) {
                    actions.add(action);
                }
            }

            for (String id : snippet.getConnections().keySet()) {
                final Connection connection = connectionDAO.getConnection(id);
                final Action action = relationshipAuditor.generateAuditRecordForConnection(connection, Operation.Move, createMoveDetails(previousGroupId, groupId, logger));
                if (action != null) {
                    actions.add(action);
                }
            }

            for (String id : snippet.getLabels().keySet()) {
                final Label label = labelDAO.getLabel(id);
                final Action action = labelAuditor.generateAuditRecord(label, Operation.Move, createMoveDetails(previousGroupId, groupId, logger));
                if (action != null) {
                    actions.add(action);
                }
            }

            // save the actions
            if (CollectionUtils.isNotEmpty(actions)) {
                saveActions(actions, logger);
            }
        }

        return snippet;
    }

    /**
     * Audits bulk delete.
     *
     * @param proceedingJoinPoint join point
     * @param snippetId snippet id
     * @param snippetDAO dao
     * @throws Throwable ex
     */
    @Around("within(org.apache.nifi.web.dao.SnippetDAO+) && "
            + "execution(void deleteSnippetComponents(java.lang.String)) && "
            + "args(snippetId) && "
            + "target(snippetDAO)")
    public void removeSnippetAdvice(ProceedingJoinPoint proceedingJoinPoint, String snippetId, SnippetDAO snippetDAO) throws Throwable {
        // get the snippet before removing it
        final Snippet snippet = snippetDAO.getSnippet(snippetId);

        // locate all the components being removed
        final Set<Funnel> funnels = new HashSet<>();
        for (String id : snippet.getFunnels().keySet()) {
            funnels.add(funnelDAO.getFunnel(id));
        }

        final Set<Port> inputPorts = new HashSet<>();
        for (String id : snippet.getInputPorts().keySet()) {
            inputPorts.add(inputPortDAO.getPort(id));
        }

        final Set<Port> outputPorts = new HashSet<>();
        for (String id : snippet.getOutputPorts().keySet()) {
            outputPorts.add(outputPortDAO.getPort(id));
        }

        final Set<RemoteProcessGroup> remoteProcessGroups = new HashSet<>();
        for (String id : snippet.getRemoteProcessGroups().keySet()) {
            remoteProcessGroups.add(remoteProcessGroupDAO.getRemoteProcessGroup(id));
        }

        final Set<ProcessGroup> processGroups = new HashSet<>();
        final ProcessGroupDAO processGroupDAO = getProcessGroupDAO();
        for (String id : snippet.getProcessGroups().keySet()) {
            processGroups.add(processGroupDAO.getProcessGroup(id));
        }

        final Set<ProcessorNode> processors = new HashSet<>();
        for (String id : snippet.getProcessors().keySet()) {
            processors.add(processorDAO.getProcessor(id));
        }

        final Set<Connection> connections = new HashSet<>();
        for (String id : snippet.getConnections().keySet()) {
            connections.add(connectionDAO.getConnection(id));
        }

        final Set<Label> labels = new HashSet<>();
        for (String id : snippet.getLabels().keySet()) {
            labels.add(labelDAO.getLabel(id));
        }

        // remove the snippet and components
        proceedingJoinPoint.proceed();

        final Collection<Action> actions = new ArrayList<>();

        // audit funnel removal
        for (Funnel funnel : funnels) {
            final Action action = funnelAuditor.generateAuditRecord(funnel, Operation.Remove);
            if (action != null) {
                actions.add(action);
            }
        }

        for (Port inputPort : inputPorts) {
            final Action action = portAuditor.generateAuditRecord(inputPort, Operation.Remove);
            if (action != null) {
                actions.add(action);
            }
        }

        for (Port outputPort : outputPorts) {
            final Action action = portAuditor.generateAuditRecord(outputPort, Operation.Remove);
            if (action != null) {
                actions.add(action);
            }
        }

        for (RemoteProcessGroup remoteProcessGroup : remoteProcessGroups) {
            final Action action = remoteProcessGroupAuditor.generateAuditRecord(remoteProcessGroup, Operation.Remove);
            if (action != null) {
                actions.add(action);
            }
        }

        for (ProcessGroup processGroup : processGroups) {
            final Action action = processGroupAuditor.generateAuditRecord(processGroup, Operation.Remove);
            if (action != null) {
                actions.add(action);
            }
        }

        for (ProcessorNode processor : processors) {
            final Action action = processorAuditor.generateAuditRecord(processor, Operation.Remove);
            if (action != null) {
                actions.add(action);
            }
        }

        for (Connection connection : connections) {
            final ConnectDetails connectDetails = relationshipAuditor.createConnectDetails(connection, connection.getRelationships());
            final Action action = relationshipAuditor.generateAuditRecordForConnection(connection, Operation.Disconnect, connectDetails);
            if (action != null) {
                actions.add(action);
            }
        }

        for (Label label : labels) {
            final Action action = labelAuditor.generateAuditRecord(label, Operation.Remove);
            if (action != null) {
                actions.add(action);
            }
        }

        // save the actions
        if (CollectionUtils.isNotEmpty(actions)) {
            saveActions(actions, logger);
        }
    }

    /* setters */
    public void setFunnelDAO(FunnelDAO funnelDAO) {
        this.funnelDAO = funnelDAO;
    }

    public void setInputPortDAO(PortDAO inputPortDAO) {
        this.inputPortDAO = inputPortDAO;
    }

    public void setOutputPortDAO(PortDAO outputPortDAO) {
        this.outputPortDAO = outputPortDAO;
    }

    public void setPortAuditor(PortAuditor portAuditor) {
        this.portAuditor = portAuditor;
    }

    public void setFunnelAuditor(FunnelAuditor funnelAuditor) {
        this.funnelAuditor = funnelAuditor;
    }

    public void setProcessGroupAuditor(ProcessGroupAuditor processGroupAuditor) {
        this.processGroupAuditor = processGroupAuditor;
    }

    public void setRemoteProcessGroupAuditor(RemoteProcessGroupAuditor remoteProcessGroupAuditor) {
        this.remoteProcessGroupAuditor = remoteProcessGroupAuditor;
    }

    public void setLabelAuditor(LabelAuditor labelAuditor) {
        this.labelAuditor = labelAuditor;
    }

    public void setRemoteProcessGroupDAO(RemoteProcessGroupDAO remoteProcessGroupDAO) {
        this.remoteProcessGroupDAO = remoteProcessGroupDAO;
    }

    public void setConnectionDAO(ConnectionDAO connectionDAO) {
        this.connectionDAO = connectionDAO;
    }

    public void setProcessorAuditor(ProcessorAuditor processorAuditor) {
        this.processorAuditor = processorAuditor;
    }

    public void setProcessorDAO(ProcessorDAO processorDAO) {
        this.processorDAO = processorDAO;
    }

    public void setLabelDAO(LabelDAO labelDAO) {
        this.labelDAO = labelDAO;
    }

    public void setRelationshipAuditor(RelationshipAuditor relationshipAuditor) {
        this.relationshipAuditor = relationshipAuditor;
    }

}
