/*
 * 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.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.details.ActionDetails;
import org.apache.nifi.action.details.FlowChangeConfigureDetails;
import org.apache.nifi.authorization.user.NiFiUserUtils;
import org.apache.nifi.connectable.ConnectableType;
import org.apache.nifi.connectable.Port;
import org.apache.nifi.controller.ScheduledState;
import org.apache.nifi.remote.PublicPort;
import org.apache.nifi.authorization.user.NiFiUser;
import org.apache.nifi.web.api.dto.PortDTO;
import org.apache.nifi.web.dao.PortDAO;
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 PortAuditor extends NiFiAuditor {

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

    /**
     * Audits the creation of a port.
     *
     * @param proceedingJoinPoint join point
     * @return port
     * @throws Throwable ex
     */
    @Around("within(org.apache.nifi.web.dao.PortDAO+) && "
            + "execution(org.apache.nifi.connectable.Port createPort(java.lang.String, org.apache.nifi.web.api.dto.PortDTO))")
    public Port createPortAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        // perform the underlying operation
        Port port = (Port) proceedingJoinPoint.proceed();

        // audit the port creation
        final Action action = generateAuditRecord(port, Operation.Add);

        // save the actions
        if (action != null) {
            saveAction(action, logger);
        }

        return port;
    }

    /**
     * Audits the update of a port.
     *
     * @param proceedingJoinPoint join point
     * @param portDTO port dto
     * @param portDAO port dao
     * @return port
     * @throws Throwable ex
     */
    @Around("within(org.apache.nifi.web.dao.PortDAO+) && "
            + "execution(org.apache.nifi.connectable.Port updatePort(org.apache.nifi.web.api.dto.PortDTO)) && "
            + "args(portDTO) && "
            + "target(portDAO)")
    public Port updatePortAdvice(ProceedingJoinPoint proceedingJoinPoint, PortDTO portDTO, PortDAO portDAO) throws Throwable {
        final Port port = portDAO.getPort(portDTO.getId());
        final ScheduledState scheduledState = port.getScheduledState();
        final String name = port.getName();
        final String comments = port.getComments();
        final int maxConcurrentTasks = port.getMaxConcurrentTasks();

        final Set<String> existingUsers = new HashSet<>();
        final Set<String> existingGroups = new HashSet<>();
        boolean isPublicPort = false;
        if (port instanceof PublicPort) {
            isPublicPort = true;
            final PublicPort publicPort = (PublicPort) port;
            existingUsers.addAll(publicPort.getUserAccessControl());
            existingGroups.addAll(publicPort.getGroupAccessControl());
        }

        // perform the underlying operation
        final Port updatedPort = (Port) proceedingJoinPoint.proceed();

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

        // ensure the user was found
        if (user != null) {
            Collection<ActionDetails> configurationDetails = new ArrayList<>();

            // see if the name has changed
            if (name != null && portDTO.getName() != null && !name.equals(updatedPort.getName())) {
                // create the config details
                FlowChangeConfigureDetails configDetails = new FlowChangeConfigureDetails();
                configDetails.setName("Name");
                configDetails.setValue(updatedPort.getName());
                configDetails.setPreviousValue(name);

                configurationDetails.add(configDetails);
            }

            // see if the comments has changed
            if (comments != null && portDTO.getComments() != null && !comments.equals(updatedPort.getComments())) {
                // create the config details
                FlowChangeConfigureDetails configDetails = new FlowChangeConfigureDetails();
                configDetails.setName("Comments");
                configDetails.setValue(updatedPort.getComments());
                configDetails.setPreviousValue(comments);

                configurationDetails.add(configDetails);
            }

            // if this is a root group port, consider concurrent tasks
            if (isPublicPort) {
                if (portDTO.getConcurrentlySchedulableTaskCount() != null && updatedPort.getMaxConcurrentTasks() != maxConcurrentTasks) {
                    // create the config details
                    FlowChangeConfigureDetails configDetails = new FlowChangeConfigureDetails();
                    configDetails.setName("Concurrent Tasks");
                    configDetails.setValue(String.valueOf(updatedPort.getMaxConcurrentTasks()));
                    configDetails.setPreviousValue(String.valueOf(maxConcurrentTasks));

                    configurationDetails.add(configDetails);
                }

                // if user access control was specified in the request
                if (portDTO.getUserAccessControl() != null) {
                    final Set<String> newUsers = new HashSet<>(portDTO.getUserAccessControl());
                    newUsers.removeAll(existingUsers);

                    final Set<String> removedUsers = new HashSet<>(existingUsers);
                    removedUsers.removeAll(portDTO.getUserAccessControl());

                    // if users were added/removed
                    if (newUsers.size() > 0 || removedUsers.size() > 0) {
                        // create the config details
                        FlowChangeConfigureDetails configDetails = new FlowChangeConfigureDetails();
                        configDetails.setName("User Access Control");
                        configDetails.setValue(StringUtils.join(portDTO.getUserAccessControl(), ", "));
                        configDetails.setPreviousValue(StringUtils.join(existingUsers, ", "));

                        configurationDetails.add(configDetails);
                    }
                }

                // if group access control was specified in the request
                if (portDTO.getGroupAccessControl() != null) {
                    final Set<String> newGroups = new HashSet<>(portDTO.getGroupAccessControl());
                    newGroups.removeAll(existingGroups);

                    final Set<String> removedGroups = new HashSet<>(existingGroups);
                    removedGroups.removeAll(portDTO.getGroupAccessControl());

                    // if groups were added/removed
                    if (newGroups.size() > 0 || removedGroups.size() > 0) {
                        // create the config details
                        FlowChangeConfigureDetails configDetails = new FlowChangeConfigureDetails();
                        configDetails.setName("Group Access Control");
                        configDetails.setValue(StringUtils.join(portDTO.getGroupAccessControl(), ", "));
                        configDetails.setPreviousValue(StringUtils.join(existingGroups, ", "));

                        configurationDetails.add(configDetails);
                    }
                }
            }

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

            // determine the type of port
            Component componentType = Component.OutputPort;
            if (ConnectableType.INPUT_PORT == updatedPort.getConnectableType()) {
                componentType = Component.InputPort;
            }

            // add each configuration detail
            if (!configurationDetails.isEmpty()) {
                // create the timestamp for the update
                Date timestamp = new Date();

                // create the actions
                for (ActionDetails detail : configurationDetails) {
                    // create the port action for updating the name
                    FlowChangeAction portAction = new FlowChangeAction();
                    portAction.setUserIdentity(user.getIdentity());
                    portAction.setOperation(Operation.Configure);
                    portAction.setTimestamp(timestamp);
                    portAction.setSourceId(updatedPort.getIdentifier());
                    portAction.setSourceName(updatedPort.getName());
                    portAction.setSourceType(componentType);
                    portAction.setActionDetails(detail);

                    actions.add(portAction);
                }
            }

            // determine the new executing state
            final ScheduledState updatedScheduledState = updatedPort.getScheduledState();

            // determine if the running state has changed
            if (scheduledState != updatedScheduledState) {
                // create a processor action
                FlowChangeAction processorAction = new FlowChangeAction();
                processorAction.setUserIdentity(user.getIdentity());
                processorAction.setTimestamp(new Date());
                processorAction.setSourceId(updatedPort.getIdentifier());
                processorAction.setSourceName(updatedPort.getName());
                processorAction.setSourceType(componentType);

                // set the operation accordingly
                if (ScheduledState.RUNNING.equals(updatedScheduledState)) {
                    processorAction.setOperation(Operation.Start);
                } else if (ScheduledState.DISABLED.equals(updatedScheduledState)) {
                    processorAction.setOperation(Operation.Disable);
                } else {
                    // state is now stopped... consider the previous state
                    if (ScheduledState.RUNNING.equals(scheduledState)) {
                        processorAction.setOperation(Operation.Stop);
                    } else if (ScheduledState.DISABLED.equals(scheduledState)) {
                        processorAction.setOperation(Operation.Enable);
                    }
                }
                actions.add(processorAction);
            }

            // ensure there are actions to record
            if (!actions.isEmpty()) {
                // save the actions
                saveActions(actions, logger);
            }
        }

        return updatedPort;
    }

    /**
     * Audits the removal of a processor via deleteProcessor().
     *
     * @param proceedingJoinPoint join point
     * @param portId port id
     * @param portDAO port dao
     * @throws Throwable ex
     */
    @Around("within(org.apache.nifi.web.dao.PortDAO+) && "
            + "execution(void deletePort(java.lang.String)) && "
            + "args(portId) && "
            + "target(portDAO)")
    public void removePortAdvice(ProceedingJoinPoint proceedingJoinPoint, String portId, PortDAO portDAO) throws Throwable {
        // get the port before removing it
        Port port = portDAO.getPort(portId);

        // remove the port
        proceedingJoinPoint.proceed();

        // if no exceptions were thrown, add removal actions...
        final Action action = generateAuditRecord(port, Operation.Remove);

        // save the actions
        if (action != null) {
            saveAction(action, logger);
        }

    }

    /**
     * Generates an audit record for the creation of the specified port.
     *
     * @param port port
     * @param operation operation
     * @return action
     */
    public Action generateAuditRecord(Port port, Operation operation) {
        return generateAuditRecord(port, operation, null);
    }

    /**
     * Generates an audit record for the creation of the specified port.
     *
     * @param port port
     * @param operation operation
     * @param actionDetails details
     * @return action
     */
    public Action generateAuditRecord(Port port, Operation operation, ActionDetails actionDetails) {
        FlowChangeAction action = null;

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

        // ensure the user was found
        if (user != null) {
            // determine the type of port
            Component componentType = Component.OutputPort;
            if (ConnectableType.INPUT_PORT == port.getConnectableType()) {
                componentType = Component.InputPort;
            }

            // create the port action for adding this processor
            action = new FlowChangeAction();
            action.setUserIdentity(user.getIdentity());
            action.setOperation(operation);
            action.setTimestamp(new Date());
            action.setSourceId(port.getIdentifier());
            action.setSourceName(port.getName());
            action.setSourceType(componentType);

            if (actionDetails != null) {
                action.setActionDetails(actionDetails);
            }
        }

        return action;
    }
}
