package com.flowable.local.work.events;

import com.flowable.audit.api.AuditService;
import com.flowable.core.common.api.security.SecurityScope;
import com.flowable.core.spring.security.SecurityUtils;
import com.flowable.dataobject.api.event.FlowableDataObjectEventType;
import com.flowable.dataobject.api.runtime.DataObjectInstanceVariableContainer;
import com.flowable.dataobject.engine.delegate.event.impl.*;
import org.flowable.common.engine.api.delegate.event.FlowableEvent;
import org.flowable.common.engine.api.delegate.event.FlowableEventListener;
import org.flowable.common.engine.api.delegate.event.FlowableEventType;
import org.flowable.common.engine.impl.cfg.TransactionState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.util.*;

/*
    Writes audit trail entries on changes of data objects.
    These entries must be retrieved by a specific REST endpoint (implemented in AuditTrailController),
    because the flowable audit-trail API supports only audit trail entries with scopeType cmmn and bpmn.
    The class writes the following data into the table FLW_AUDIT_INSTANCE:
    - ID_                   autogenerated ID
    - CREATION_TIME_        now
    - CREATOR_ID_           current user ID
    - TYPE_                 "user" or "system"
    - SUB_TYPE_             "created" / "updated" / "deleted"
    - SCOPE_DEFINITION_ID_  data object model key
    - SCOPE_ID_             data object instance ID
    - SCOPE_TYPE_           "dataObject"
    - PAYLOAD_              {
                                <data object column name 1>: <content of data object field in column 1>
                                <data object column name 2>: <content of data object field in column 1>
                                etc.
                            }
    - TENANT_ID_            tenant ID
    - SUB_SCOPE_ID_         content of data object field in column "key", if column "key" exists
                            -> this mapping makes "key" filterable in a data table
 */
@Component
public class GenericDataObjectAuditTrailEventHandler implements FlowableEventListener {
    private static final Logger LOGGER = LoggerFactory.getLogger(GenericDataObjectAuditTrailEventHandler.class);
    protected final AuditService auditService;

    public GenericDataObjectAuditTrailEventHandler(AuditService auditService) {
        this.auditService = auditService;
    }

    @Override
    public void onEvent(FlowableEvent flowableEvent) {
        DataObjectInstanceVariableContainer dataObjectInstance = null;
        String scopeDefinitionId = null;
        String operation = null;
        Map<String, Object> modificationData = Map.of();
        String subType;
        if (flowableEvent instanceof FlowableDataObjectInstanceCreatedEventImpl) {
            dataObjectInstance = ((FlowableDataObjectInstanceCreatedEventImpl) flowableEvent).getDataObjectInstance();
            subType = "created";
        } else if (flowableEvent instanceof FlowableDataObjectInstanceUpdatedEventImpl) {
            dataObjectInstance = ((FlowableDataObjectInstanceUpdatedEventImpl) flowableEvent).getDataObjectInstance();
            subType = "updated";
        } else if (flowableEvent instanceof FlowableDataObjectInstanceDeletedEventImpl) {
            dataObjectInstance = ((FlowableDataObjectInstanceDeletedEventImpl) flowableEvent).getDataObjectInstance();
            subType = "deleted";
        } else if (flowableEvent instanceof FlowableDataObjectInstancesBulkUpdatedEventImpl) {
            scopeDefinitionId = ((FlowableDataObjectInstancesBulkUpdatedEventImpl) flowableEvent).getDataObjectDefinition().getKey();
            operation = ((FlowableDataObjectInstancesBulkUpdatedEventImpl) flowableEvent).getOperation();
            modificationData = ((FlowableDataObjectInstancesBulkUpdatedEventImpl) flowableEvent).getModificationData();
            subType = "updatedBulk";
        } else if (flowableEvent instanceof FlowableDataObjectInstancesBulkDeletedEventImpl) {
            scopeDefinitionId = ((FlowableDataObjectInstancesBulkDeletedEventImpl) flowableEvent).getDataObjectDefinition().getKey();
            operation = ((FlowableDataObjectInstancesBulkDeletedEventImpl) flowableEvent).getOperation();
            subType = "deletedBulk";
        } else {
            LOGGER.warn("Unhandled FlowableDataObjectEvent type: {}", flowableEvent.getClass().getName());
            return;
        }

        Map<String, Object> payload = new HashMap<>();
        String scopeId = null;
        String subScopeId = null;
        SecurityScope currentUserSecurityScope = SecurityUtils.getCurrentUserSecurityScopeSafe();

        if (subType.equals("created") || subType.equals("updated") || subType.equals("deleted")) {
            HashMap<String, Object> data = (HashMap<String, Object>) dataObjectInstance.getData();
            if (data == null) {
                LOGGER.warn("FlowableDataObjectEvent: No data");
                return;
            }

            scopeDefinitionId = dataObjectInstance.getDefinitionKey();

            for (Map.Entry<String, Object> entry : data.entrySet()) {
                if (entry.getKey().equals("id")) {
                    scopeId = (String) entry.getValue();
                } else if (entry.getKey().equals("key")) {
                    // If there is a "key" in the data object, expose it as subScopeId for making it filterable.
                    subScopeId = (String) entry.getValue();
                } else {
                    payload.put(entry.getKey(), entry.getValue());
                }
            }
        } else {
            payload.put("operation", operation);
            payload.putAll(modificationData);
        }
        if (currentUserSecurityScope == null) {
            auditService.createAuditInstanceBuilder()
                    .type("system")
                    .subType(subType)
                    .scopeType("dataObject")
                    .scopeDefinitionId(scopeDefinitionId)
                    .scopeId(scopeId)
                    .subScopeId(subScopeId)
                    .payload(payload)
                    .create();
        } else {
            auditService.createAuditInstanceBuilder()
                    .type("user")
                    .creatorId(currentUserSecurityScope.getUserId())
                    .tenantId(currentUserSecurityScope.getTenantId())
                    .subType(subType)
                    .scopeType("dataObject")
                    .scopeDefinitionId(scopeDefinitionId)
                    .scopeId(scopeId)
                    .subScopeId(subScopeId)
                    .payload(payload)
                    .create();
        }
    }

    @Override
    public boolean isFailOnException() {
        return true;
    }

    @Override
    public boolean isFireOnTransactionLifecycleEvent() {
        return false;
    }

    @Override
    public String getOnTransaction() {
        return TransactionState.COMMITTING.name();
    }

    @Override
    public Collection<? extends FlowableEventType> getTypes() {
        List<FlowableEventType> eventTypes = new ArrayList<>();
        eventTypes.add(FlowableDataObjectEventType.DATA_OBJECT_INSTANCE_CREATED);
        eventTypes.add(FlowableDataObjectEventType.DATA_OBJECT_INSTANCE_UPDATED);
        eventTypes.add(FlowableDataObjectEventType.DATA_OBJECT_INSTANCE_DELETED);
        return eventTypes;
    }
}
