package com.atlassian.stash.internal.audit;

import com.atlassian.bitbucket.audit.AuditEntry;
import com.atlassian.bitbucket.audit.AuditEntryBuilder;
import com.atlassian.bitbucket.audit.Priority;
import com.atlassian.event.api.EventPublisher;
import com.atlassian.bitbucket.event.audit.AuditEvent;
import com.atlassian.bitbucket.request.RequestManager;
import com.atlassian.bitbucket.request.RequestMetadata;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.bitbucket.auth.AuthenticationContext;
import org.codehaus.jackson.map.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.Collections;
import java.util.Date;
import java.util.Set;

/**
 * Base class for classes which construct an publish {@link AuditEvent audit events}
 *
 * @since 2.6
 */
public abstract class AbstractAuditEventPublisher {

    private static final ObjectMapper objectMapperInstance = new ObjectMapper();

    protected final AuditEntryLoggingService auditLoggingService;
    protected final AuthenticationContext authContext;
    protected final EventPublisher eventPublisher;
    protected final Logger log;
    protected final RequestManager requestManager;

    protected AbstractAuditEventPublisher(AuditEntryLoggingService auditLoggingService, RequestManager requestManager,
                                          AuthenticationContext authContext, EventPublisher eventPublisher) {
        this.auditLoggingService = auditLoggingService;
        this.authContext = authContext;
        this.eventPublisher = eventPublisher;
        this.requestManager = requestManager;

        log = LoggerFactory.getLogger(getClass());
    }

    protected String convertToJsonString(Object dataValues) throws IOException {
        return objectMapperInstance.writeValueAsString(dataValues);
    }

    protected ApplicationUser getCurrentStashUser() {
        return authContext.getCurrentUser();
    }

    protected <T extends Exception> T logAndRethrow(Object event, T ex) throws T {
        AuditEntryBuilder builder = new AuditEntryBuilder()
                .action("Publish AuditEvent")
                .target(event.getClass().getSimpleName())
                .timestamp(new Date())
                .details("Failed to publish @Audited annotated event");

        setSourceIp(builder);
        auditLoggingService.log(builder.build());
        log.error("Failed to convert and publish @Audited annotated event {} of type {} due to {}. Note that " +
                "AuditEntryConverter require a default no-args constructor.", event, event.getClass(), ex);
        throw ex;
    }

    protected void publish(Object event, AuditEntry auditEntry, Priority priority) throws Exception {
        publish(event, auditEntry, Collections.<String>emptySet(), priority);
    }

    protected void publish(Object event, AuditEntry auditEntry, Set<String> channels, Priority priority) throws Exception {
        try {
            eventPublisher.publish(new AuditEvent(this, auditEntry, channels, priority));
        } catch (Exception ex) {
            auditLoggingService.log(auditEntry);
            throw logAndRethrow(event, ex);
        }
    }

    protected AuditEntryBuilder setCommonFields(Object event, AuditEntryBuilder builder) {
        return setSourceIp(builder)
                .action(event.getClass())
                .timestamp(new Date())
                .user(getCurrentStashUser());
    }

    protected AuditEntryBuilder setSourceIp(AuditEntryBuilder builder) {
        RequestMetadata context = requestManager.getRequestMetadata();
        if (context != null) {
            builder.sourceIpAddress(context.getRemoteAddress());
        }
        return builder;
    }
}
