package com.atlassian.bitbucket.internal.mirroring.upstream;

import com.atlassian.bitbucket.NoSuchEntityException;
import com.atlassian.bitbucket.auth.AuthenticationContext;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.internal.mirroring.MirroringRequest;
import com.atlassian.bitbucket.internal.mirroring.MirroringRequestState;
import com.atlassian.bitbucket.internal.mirroring.upstream.dao.AoMirroringRequest;
import com.atlassian.bitbucket.internal.mirroring.upstream.dao.MirroringRequestDao;
import com.atlassian.bitbucket.mirroring.upstream.*;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.permission.PermissionValidationService;
import com.atlassian.bitbucket.server.ApplicationPropertiesService;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.bitbucket.user.UserService;
import com.atlassian.bitbucket.util.Chainable;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageRequest;
import com.atlassian.bitbucket.util.PagedIterable;
import com.atlassian.event.api.EventPublisher;
import com.atlassian.sal.api.transaction.TransactionCallback;
import com.atlassian.sal.api.transaction.TransactionTemplate;
import com.google.common.collect.Maps;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.Nonnull;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import static com.atlassian.bitbucket.internal.mirroring.MirroringRequestState.*;
import static com.atlassian.bitbucket.util.PageUtils.asPageOf;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.slf4j.LoggerFactory.getLogger;

@Component
public class DefaultMirroringRequestService implements MirroringRequestService {
    private static final Logger log = getLogger(DefaultMirroringRequestService.class);

    public static final int MAX_PAGE_REQUESTS = 100;

    private final ApplicationPropertiesService applicationPropertiesService;
    private final AuthenticationContext authenticationContext;
    private final EventPublisher eventPublisher;
    private final I18nService i18nService;
    private final MirrorAddonManager mirrorAddonManager;
    private final MirroringRequestDao mirrorRequestDao;
    private final InternalMirrorService mirrorService;
    private final SmartMirroringFeature mirroringFeature;
    private final PermissionValidationService permissionValidationService;
    private final TransactionTemplate transactionTemplate;
    private final UserService userService;

    @Autowired
    public DefaultMirroringRequestService(ApplicationPropertiesService applicationPropertiesService,
                                          AuthenticationContext authenticationContext, EventPublisher eventPublisher,
                                          I18nService i18nService, MirrorAddonManager mirrorAddonManager,
                                          MirroringRequestDao mirrorRequestDao, InternalMirrorService mirrorService,
                                          SmartMirroringFeature mirroringFeature,
                                          PermissionValidationService permissionValidationService,
                                          TransactionTemplate transactionTemplate, UserService userService) {
        this.applicationPropertiesService = applicationPropertiesService;
        this.authenticationContext = authenticationContext;
        this.eventPublisher = eventPublisher;
        this.i18nService = i18nService;
        this.mirrorAddonManager = mirrorAddonManager;
        this.mirrorRequestDao = mirrorRequestDao;
        this.mirrorService = mirrorService;
        this.mirroringFeature = mirroringFeature;
        this.permissionValidationService = permissionValidationService;
        this.transactionTemplate = transactionTemplate;
        this.userService = userService;
    }

    @Override
    public void delete(@Nonnull MirroringRequest request) {
        checkEnabled();
        checkNotNull(request, "request");

        permissionValidationService.validateForGlobal(Permission.SYS_ADMIN);

        aroundStateChange(request, () -> {
            //noinspection ConstantConditions
            mirrorRequestDao.delete(request.getId());
            return null;
        });
    }

    @Nonnull
    @Override
    public Page<MirroringRequest> findAll(@Nonnull PageRequest pageRequest) {
        checkEnabled();
        checkNotNull(pageRequest, "pageRequest");

        permissionValidationService.validateForGlobal(Permission.SYS_ADMIN);

        return transactionTemplate.execute(() -> asPageOf(MirroringRequest.class,
                initResolvers(mirrorRequestDao.findAll(pageRequest.buildRestrictedPageRequest(MAX_PAGE_REQUESTS)))));
    }

    @Nonnull
    @Override
    public Page<MirroringRequest> findAll(@Nonnull MirroringRequestSearchRequest searchRequest, @Nonnull PageRequest pageRequest) {
        checkEnabled();
        checkNotNull(pageRequest, "pageRequest");

        permissionValidationService.validateForGlobal(Permission.SYS_ADMIN);

        return transactionTemplate.execute(() -> asPageOf(MirroringRequest.class,
                initResolvers(mirrorRequestDao.findAll(searchRequest, pageRequest.buildRestrictedPageRequest(MAX_PAGE_REQUESTS)))));
    }

    @Override
    public MirroringRequest getById(int id) {
        checkEnabled();
        permissionValidationService.validateForGlobal(Permission.SYS_ADMIN);

        return transactionTemplate.execute(() -> mirrorRequestDao.getById(id));
    }

    @Nonnull
    @Override
    public MirroringRequest register(@Nonnull MirroringRequest request) {
        checkEnabled();
        checkNotNull(request, "request");

        //Note: no permission checking - this must be open to unauthenticated users

        return aroundStateChange(request, () -> {
            checkMirrorNotAlreadyConnected(request.getMirrorId());

            Iterable<AoMirroringRequest> existingMirroringRequests = new PagedIterable<>(pageRequest ->
                    mirrorRequestDao.findByMirrorId(request.getMirrorId(), pageRequest), 100);

            for (AoMirroringRequest existingMirroringRequest : existingMirroringRequests) {
                if (existingMirroringRequest.getState() == PENDING) {
                    if (equivalent(existingMirroringRequest, request)) {
                        return existingMirroringRequest;
                    }

                    throw new ConflictingMirroringRequestException(
                            i18nService.createKeyedMessage("bitbucket.mirroring.requests.pendingDiffer", request.getMirrorId()));
                }
                // note that we don't worry about previously accepted requests here. If we get here, a previously
                // accepted mirror has been removed
            }

            if (mirrorRequestDao.countInState(PENDING) >= getMaxPendingRequests()) {
                throw new MirroringRequestLimitExceededException(
                        i18nService.createKeyedMessage("bitbucket.mirroring.requests.maxPendingExceeded"));
            }

            AoMirroringRequest newRequest = mirrorRequestDao.create(request);
            eventPublisher.publish(new MirroringRequestCreatedEvent(DefaultMirroringRequestService.this, newRequest));
            return newRequest;
        });
    }

    @Nonnull
    @Override
    public MirroringRequest reject(@Nonnull MirroringRequest request) {
        checkEnabled();
        checkNotNull(request, "request");

        permissionValidationService.validateForGlobal(Permission.SYS_ADMIN);
        return aroundStateChange(request, () -> {
            AoMirroringRequest storedRequest = getRequestOrFail(request);

            if (storedRequest.getState() == REJECTED) {
                return storedRequest;
            }

            if (storedRequest.getState() == ACCEPTED) {
                throw new IllegalMirroringRequestStateException(
                        i18nService.createKeyedMessage("bitbucket.mirroring.requests.alreadyAccepted"));
            }

            MirroringRequest rejected = resolve(storedRequest, REJECTED);
            eventPublisher.publish(new MirroringRequestRejectedEvent(DefaultMirroringRequestService.this, rejected));
            return rejected;
        });
    }

    @Nonnull
    @Override
    public MirrorServer accept(@Nonnull MirroringRequest request) {
        checkEnabled();
        checkNotNull(request, "request");

        permissionValidationService.validateForGlobal(Permission.SYS_ADMIN);
        return aroundStateChange(request, () -> {
            AoMirroringRequest storedRequest = getRequestOrFail(request);

            if (storedRequest.getState() == ACCEPTED) {
                return mirrorService.getById(storedRequest.getMirrorId());
            }

            checkMirrorNotAlreadyConnected(storedRequest.getMirrorId());

            // install the connect add-on for the  new mirror
            MirrorAddon addon = mirrorAddonManager.install(storedRequest);
            MirrorServer mirror = mirrorService.install(storedRequest, addon.getKey());

            MirroringRequest accepted = resolve(storedRequest, ACCEPTED);
            eventPublisher.publish(new MirroringRequestAcceptedEvent(DefaultMirroringRequestService.this, accepted));
            return mirror;
        });
    }

    private <T> T aroundStateChange(MirroringRequest request, TransactionCallback<T> change) {
        try {
            return transactionTemplate.execute(change);
        } catch (RuntimeException e) {
            if (request.getId() != null) {
                log.error("Failed to change state of mirroring request for {}", request.getBaseUrl(), e);
            } else {
                log.error("Failed to create mirroring request for {}", request.getBaseUrl(), e);
            }
            throw e;
        }
    }

    private void checkEnabled() {
        mirroringFeature.checkEnabled();
    }

    private void checkMirrorNotAlreadyConnected(@Nonnull String mirrorId) {
        MirrorServer mirror = mirrorService.getById(mirrorId);
        if (mirror != null) {
            throw new MirrorAlreadyRegisteredException(i18nService.createKeyedMessage("bitbucket.mirroring.alreadyRegistered", mirrorId));
        }
    }

    private boolean equivalent(AoMirroringRequest existingRequest, MirroringRequest request) {
        return existingRequest.getMirrorId().equals(request.getMirrorId())
                && existingRequest.getMirrorName().equals(request.getMirrorName())
                && existingRequest.getProductType().equals(request.getProductType())
                && existingRequest.getProductVersion().equals(request.getProductVersion())
                && existingRequest.getAddonDescriptorUri().equals(request.getAddonDescriptorUri());
    }

    private int getMaxPendingRequests() {
        return applicationPropertiesService.getPluginProperty(
                UpstreamConstants.PROPERTY_MIRRORING_REGISTRATION_MAX_PENDING,
                UpstreamConstants.DEFAULT_MIRRORING_REGO_REQUEST_MAX_PENDING);
    }

    private AoMirroringRequest getRequestOrFail(MirroringRequest request) {
        //noinspection ConstantConditions
        AoMirroringRequest storedRequest = mirrorRequestDao.getById(request.getId());
        if (storedRequest == null) {
            throw new NoSuchEntityException(i18nService.createKeyedMessage("bitbucket.mirroring.requests.notFound", request.getId()));
        }
        return storedRequest;
    }

    private AoMirroringRequest initResolver(AoMirroringRequest request, ApplicationUser currentUser) {
        request.initResolver(currentUser);
        return request;
    }

    private Page<AoMirroringRequest> initResolvers(Page<AoMirroringRequest> requests) {
        if (requests.getSize() == 0) {
            return requests;
        }

        Set<Integer> userIds = Chainable.chain(requests.getValues())
                .transform(AoMirroringRequest::getResolverId)
                .filter(Objects::nonNull)
                .toSet();

        if (userIds.isEmpty()) {
            return requests;
        }

        Map<Integer, ApplicationUser> map = Maps.uniqueIndex(userService.getUsersById(userIds), ApplicationUser::getId);
        for (AoMirroringRequest request : requests.getValues()) {
            if (request.getResolverId() != null) {
                ApplicationUser user = map.get(request.getResolverId());
                if (user != null) {
                    request.initResolver(user);
                }
            }
        }

        return requests;
    }

    private MirroringRequest resolve(AoMirroringRequest request, MirroringRequestState state) {
        ApplicationUser currentUser = authenticationContext.getCurrentUser();
        //noinspection ConstantConditions
        return initResolver(
                mirrorRequestDao.resolve(request.getId(), state, currentUser.getId()),
                currentUser);
    }
}
