package com.atlassian.stash.internal.audit;

import com.atlassian.bitbucket.audit.*;
import com.atlassian.bitbucket.event.repository.*;
import com.atlassian.event.api.EventListener;
import com.atlassian.event.api.EventPublisher;
import com.atlassian.bitbucket.event.hook.RepositoryHookEvent;
import com.atlassian.bitbucket.event.hook.RepositoryHookSettingsChangedEvent;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.request.RequestManager;
import com.atlassian.bitbucket.auth.AuthenticationContext;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

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

@Component
public class RepositoryEventListener extends AbstractAuditEventListener {

    @Autowired
    public RepositoryEventListener(AuditEntryLoggingService auditLoggingService,
                                   RequestManager requestManager,
                                   AuthenticationContext authContext,
                                   EventPublisher eventPublisher) {
        super(auditLoggingService, requestManager, authContext, eventPublisher);
    }

    @EventListener
    public void onRepositoryModificationRequested(RepositoryModificationRequestedEvent event) throws Exception {
        auditRepositoryModificationEvent(event, Collections.<String>emptySet(), Priority.LOW);
    }

    @EventListener
    public void onRepositoryModified(RepositoryModifiedEvent event) throws Exception {
        auditRepositoryModificationEvent(event, Sets.newHashSet(Channels.REPOSITORY_UI), Priority.HIGH);
    }

    @EventListener
    public void onRepositoryAccessed(RepositoryAccessedEvent event) throws Exception {
        auditRepositoryEvent(event);
    }

    @EventListener
    public void onRepositoryCreated(RepositoryCreatedEvent event) throws Exception {
        ImmutableMap.Builder<String, Object> builder = getMapBuilder();
        Repository repository = event.getRepository();
        addProjectAndRepositoryToMap(repository, builder);
        if (repository.getOrigin() != null) {
            builder.put("origin", toProjectAndRepositoryMap(repository.getOrigin()));
        }
        auditRepositoryEvent(event, Sets.newHashSet(Channels.PROJECT_UI, Channels.REPOSITORY_UI), Priority.MEDIUM,
                convertToJsonString(builder.build()));
    }

    @EventListener
    public void onRepositoryCreationFailed(RepositoryCreationFailedEvent event) throws Exception {
        auditRepositoryEvent(event);
    }

    @EventListener
    public void onRepositoryDefaultBranchModified(RepositoryDefaultBranchModifiedEvent event) throws Exception {
        auditRepositoryEvent(event, Collections.<String>emptySet(), Priority.LOW,
                convertToJsonString(getMapBuilder().put("defaultBranch", event.getNewValue()).build()));
    }

    @EventListener
    public void onRepositoryDeleted(RepositoryDeletedEvent event) throws Exception {
        auditRepositoryEvent(event, Sets.newHashSet(Channels.PROJECT_UI), Priority.HIGH,
                convertToJsonString(addProjectAndRepositoryToMap(event.getRepository(), getMapBuilder()).build()));
    }

    @EventListener
    public void onRepositoryDeletionRequested(RepositoryDeletionRequestedEvent event) throws Exception {
        auditRepositoryEvent(event);
    }

    @EventListener
    public void onRepositoryOtherRead(RepositoryOtherReadEvent event) throws Exception {
        auditRepositoryEvent(event);
    }

    @EventListener
    public void onRepositoryOtherWrite(RepositoryOtherWriteEvent event) throws Exception {
        auditRepositoryEvent(event);
    }

    @EventListener
    public void onRepositoryPull(RepositoryPullEvent event) throws Exception {
        auditRepositoryEvent(event);
    }

    @EventListener
    public void onRepositoryHook(RepositoryHookEvent event) throws Exception {
        if (event instanceof RepositoryHookSettingsChangedEvent) {
            auditRepositoryHookSettingsChanged((RepositoryHookSettingsChangedEvent) event);
            return;
        }
        AuditEntry auditEntry = createHookAuditEntryBuilder(event).build();
        publish(event, auditEntry, Sets.newHashSet(Channels.REPOSITORY_UI), Priority.HIGH);
    }

    @EventListener
    public void onRepositoryPush(RepositoryPushEvent event) throws Exception {
        Repository repository = event.getRepository();
        AuditEntry auditEntry = createAuditEntryBuilder(event)
                .target(toProjectAndRepositoryString(repository))
                .details(convertToJsonString(toMaps(event.getRefChanges())))
                .build();
        publish(event, auditEntry, Priority.LOW);
    }

    private static Map<String, Object> diffRepositories(Repository old, Repository updated) {
        ImmutableMap.Builder<String, Object> builder = ImmutableMap.builder();
        if (!StringUtils.equals(old.getName(), updated.getName())) {
            builder.put("old.name", old.getName());
            builder.put("new.name", updated.getName());
        }
        if (old.isForkable() != updated.isForkable()) {
            builder.put("old.isForkable", old.isForkable());
            builder.put("new.isForkable", updated.isForkable());
        }
        if (old.isPublic() != updated.isPublic()) {
            builder.put("old.isPublic", old.isPublic());
            builder.put("new.isPublic", updated.isPublic());
        }
        if (!old.getProject().equals(updated.getProject())) {
            builder.put("old.project", old.getProject().getKey());
            builder.put("new.project", updated.getProject().getKey());
        }
        return builder.build();
    }

    private void auditRepositoryEvent(RepositoryEvent event) throws Exception {
        auditRepositoryEvent(event, Collections.<String>emptySet(), Priority.LOW, null);
    }

    private void auditRepositoryEvent(RepositoryEvent event, Set<String> channels, Priority priority,
                                      String details) throws Exception {
        Repository repository = event.getRepository();
        AuditEntry auditEntry = createAuditEntryBuilder(event)
                .repository(repository)
                .target(toProjectAndRepositoryString(repository))
                .details(details)
                .build();
        publish(event, auditEntry, channels, priority);
    }

    private void auditRepositoryHookSettingsChanged(RepositoryHookSettingsChangedEvent event) throws Exception {
        String hookSettingsDetails = convertToJsonString(
                addProjectAndRepositoryToMap(event.getRepository(), getMapBuilder())
                        .put("hook", event.getRepositoryHookKey())
                        .put("settings", event.getSettings().asMap())
                        .build());
        AuditEntry auditEntry = createHookAuditEntryBuilder(event)
                .details(hookSettingsDetails)
                .build();
        publish(event, auditEntry, Sets.newHashSet(Channels.REPOSITORY_UI), Priority.HIGH);
    }

    private void auditRepositoryModificationEvent(AbstractRepositoryModificationEvent event, Set<String> channels,
                                                  Priority priority) throws Exception {
        Repository repository = event.getRepository();
        Map<String, Object> repositoryDifferences = diffRepositories(event.getOldValue(), event.getNewValue());
        AuditEntry auditEntry = createAuditEntryBuilder(event)
                .repository(repository)
                .target(toProjectAndRepositoryString(repository))
                .details(convertToJsonString(repositoryDifferences))
                .build();
        if (repositoryDifferences.size() > 0) {
            publish(event, auditEntry, channels, priority);
        } else {
            publish(event, auditEntry, Collections.<String>emptySet(), Priority.LOW);
        }
    }

    private AuditEntryBuilder createHookAuditEntryBuilder(RepositoryHookEvent event) throws IOException {
        String hookDetails = convertToJsonString(
                addProjectAndRepositoryToMap(event.getRepository(), getMapBuilder())
                        .put("hook", event.getRepositoryHookKey())
                        .build());
        return createAuditEntryBuilder(event)
                .repository(event.getRepository())
                .target(event.getRepositoryHookKey())
                .details(hookDetails);
    }
}