package com.atlassian.stash.internal.bootstrap;

import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.user.SecurityService;
import com.atlassian.bitbucket.util.UncheckedOperation;
import com.atlassian.license.SIDManager;
import com.atlassian.stash.internal.ApplicationConstants;
import com.atlassian.stash.internal.concurrent.InternalLockService;
import com.atlassian.stash.internal.server.InternalApplicationPropertiesService;
import com.atlassian.stash.internal.spring.AbstractSmartLifecycle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

/**
 * Listens for {@link org.springframework.context.event.ContextRefreshedEvent} and creates the initial configuration
 * for the application.
 * Checks whether the bootstrap operation must actually be performed and delegate to an operation.
 */
@Component("bootstrapper")
public class Bootstrapper extends AbstractSmartLifecycle {

    private static final Logger log = LoggerFactory.getLogger(Bootstrapper.class);

    private final InternalApplicationPropertiesService propertiesService;
    private final SecurityService securityService;
    private final SIDManager sidManager;
    private final UncheckedOperation bootstrapOperation;
    private final InternalLockService lockService;

    @Autowired
    public Bootstrapper(InternalApplicationPropertiesService propertiesService,
                        SecurityService securityService,
                        SIDManager sidManager,
                        @Qualifier("bootstrapOperation") UncheckedOperation bootstrapOperation,
                        InternalLockService lockService) {
        this.propertiesService = propertiesService;
        this.securityService = securityService;
        this.sidManager = sidManager;
        this.bootstrapOperation = bootstrapOperation;
        this.lockService = lockService;
    }

    /**
     * Bootstrapper should run after all other beans have finished initializing, but before anything else in
     * the lifecycle. This ensures the server is bootstrapped before plugins are started (for example).
     */
    @Override
    public int getPhase() {
        return ApplicationConstants.LIFECYCLE_PHASE_BOOTSTRAP;
    }

    /**
     * Runs the injected {@link #bootstrapOperation bootstrap operation} with {@link Permission#SYS_ADMIN SYS_ADMIN}
     * permission.
     * <p>
     * Note: Any transaction created by the {@link #bootstrapOperation} must be committed before the
     * {@link com.atlassian.stash.internal.concurrent.BootstrapLock BootstrapLock} is released, so it
     * is important that no transaction exists before invoking it.
     */
    @Override
    public void start() {
        securityService.withPermission(Permission.SYS_ADMIN, "Bootstrap application").call(() -> {
            if (propertiesService.isBootstrapped()) {
                log.debug("Application already setup, skipping bootstrap");
            } else {
                // Critical section: only one instance at a time can perform the bootstrap operation
                lockService.getBootstrapLock().withLock(() -> {
                    // Don't bootstrap if the app is already setup
                    if (propertiesService.isBootstrapped()) {
                        log.debug("Application already setup, skipping bootstrap");
                    } else {
                        log.debug("Performing bootstrap operation");
                        bootstrapOperation.perform();
                        log.debug("Finished bootstrap operation");
                    }

                    return null;
                });
            }

            ensureMinimalApplicationProperties();

            return null;
        });

        super.start();
    }

    protected void ensureMinimalApplicationProperties() {
        if (propertiesService.getServerId() == null) {
            // Critical section: only one instance at a time can perform this
            lockService.getBootstrapLock().withLock(() -> {
                if (propertiesService.getServerId() == null) {
                    log.info("Generating unique server ID (SID)");
                    propertiesService.setServerId(sidManager.generateSID());
                }

                return null;
            });
        }
    }
}