package com.atlassian.stash.internal.auth.trusted;

import com.atlassian.bitbucket.Product;
import com.atlassian.bitbucket.util.MoreCollectors;
import com.atlassian.bitbucket.util.MoreStreams;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageRequestImpl;
import com.atlassian.plugin.spring.AvailableToPlugins;
import com.atlassian.security.auth.trustedapps.*;
import com.atlassian.stash.internal.annotation.Unsecured;
import com.google.common.base.Function;
import com.google.common.collect.Iterables;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.spec.InvalidKeySpecException;

@AvailableToPlugins(interfaces = {TrustedApplicationsManager.class, TrustedApplicationsConfigurationManager.class})
@Service("trustedApplicationsManager")
@SuppressWarnings("deprecation")
@Transactional
public class TrustedApplicationsManagerImpl
        implements ApplicationListener<ContextRefreshedEvent>, TrustedApplicationsManager, TrustedApplicationsConfigurationManager {

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

    private final EncryptionProvider encryptionProvider;
    private final TrustedApplicationDao trustedApplicationDao;
    private final CurrentApplicationDao currentApplicationDao;

    @Autowired
    public TrustedApplicationsManagerImpl(EncryptionProvider encryptionProvider,
                                          TrustedApplicationDao trustedApplicationDao,
                                          CurrentApplicationDao currentApplicationDao) {
        this.encryptionProvider = encryptionProvider;
        this.trustedApplicationDao = trustedApplicationDao;
        this.currentApplicationDao = currentApplicationDao;
    }

    @Override
    @PreAuthorize("hasGlobalPermission('SYS_ADMIN')")
    public TrustedApplication addTrustedApplication(Application app, RequestConditions conditions) {
        InternalTrustedApplication application = trustedApplicationDao.getByApplicationId(app.getID());
        InternalTrustedApplication.Builder builder;
        if (application == null) {
            builder = new InternalTrustedApplication.Builder()
                .applicationId(app.getID())
                .publicKey(app.getPublicKey().getEncoded());
        } else {
            builder = application.copy();
        }

        if (conditions != null) {
            builder.certificateTimeout(conditions.getCertificateTimeout());
            builder.ipPatterns(conditions.getIPPatterns());
            builder.urlPatterns(conditions.getURLPatterns());
        }

        if (application == null) {
            // new application
            application = trustedApplicationDao.create(builder.build());
        } else {
            // updated conditions
            application = trustedApplicationDao.update(builder.build());
        }
        return toTrustedApplication.apply(application);
    }

    @Override
    @PreAuthorize("hasGlobalPermission('SYS_ADMIN')")
    public boolean deleteApplication(String id) {
        final InternalTrustedApplication application = trustedApplicationDao.getByApplicationId(id);
        if (application != null) {
            trustedApplicationDao.delete(application);
        }
        return application != null;
    }

    @Override
    @Transactional(readOnly = true)
    @Unsecured("harmless utility method for retrieving public remote certificates")
    public Iterable<TrustedApplication> getTrustedApplications() {
        return MoreStreams.streamIterable(trustedApplicationDao.findAll())
                .map(toTrustedApplication::apply)
                .collect(MoreCollectors.toImmutableList());
    }

    @Override
    @Transactional(readOnly = true)
    @Unsecured("harmless utility method for retrieving public remote certificates")
    public TrustedApplication getTrustedApplication(String id) {
        return toTrustedApplication.apply(trustedApplicationDao.getByApplicationId(id));
    }

    @Override
    @Unsecured("harmless utility method for retrieving public remote certificates")
    public CurrentApplication getCurrentApplication() {
        final InternalCurrentApplication internalCurrentApplication = getInternalCurrentApplication();
        try {
            return new DefaultCurrentApplication(
                    encryptionProvider,
                    encryptionProvider.toPublicKey(internalCurrentApplication.getPublicKey()),
                    encryptionProvider.toPrivateKey(internalCurrentApplication.getPrivateKey()),
                    internalCurrentApplication.getApplicationId()
            );
        } catch (NoSuchAlgorithmException | InvalidKeySpecException | NoSuchProviderException e) {
            throw new IllegalStateException(e);
        }
    }

    @Override
    @Unsecured("harmless utility method for retrieving public remote certificates")
    public Application getApplicationCertificate(String baseUrl) throws ApplicationRetriever.RetrievalException {
        return encryptionProvider.getApplicationCertificate(baseUrl);
    }

    /**
     * Make sure a {@link CurrentApplication} exists before we do anything.
     * This avoids a race condition where we can end up with >1 because
     * {@link #getInternalCurrentApplication()} will only commit the new
     * currentApplication when its surrounding transaction completes.
     */
    //Must be done using ApplicationListener, not InternalEventListener, because Spring AOP is proxying
    //this bean by interfaces and this method is not present on the implemented interfaces (SPR-13526)
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        Page<InternalCurrentApplication> all = currentApplicationDao.findAll(new PageRequestImpl(0, 1));
        if (!all.getIsLastPage()) {
            log.warn("Multiple certificates found for this {} instance: please see https://confluence.atlassian.com/x/awiaEQ", Product.NAME);
        } else if (all.getSize() == 0) {
            getInternalCurrentApplication();
        }
    }

    private InternalCurrentApplication getInternalCurrentApplication() {
        InternalCurrentApplication currentApplication = currentApplicationDao.get();
        if (currentApplication == null) {
            final KeyPair keyPair;
            try {
                keyPair = encryptionProvider.generateNewKeyPair();
            } catch (NoSuchAlgorithmException | NoSuchProviderException e) {
                throw new IllegalStateException(e);
            }
            currentApplication = new InternalCurrentApplication(
                    encryptionProvider.generateUID(),
                    keyPair.getPublic().getEncoded(),
                    keyPair.getPrivate().getEncoded()
            );
            currentApplication = currentApplicationDao.create(currentApplication);
        }
        return currentApplication;
    }

    private final Function<InternalTrustedApplication, TrustedApplication> toTrustedApplication =
            new Function<InternalTrustedApplication, TrustedApplication>() {
        @Override
        public TrustedApplication apply(InternalTrustedApplication input) {
            if (input != null) {
                try {
                    return new DefaultTrustedApplication(
                            encryptionProvider,
                            encryptionProvider.toPublicKey(input.getPublicKey()),
                            input.getApplicationId(),
                            RequestConditions.builder()
                                .addIPPattern(Iterables.toArray(input.getIPPatterns(), String.class))
                                .addURLPattern(Iterables.toArray(input.getUrlPatterns(), String.class))
                                .setCertificateTimeout(input.getCertificateTimeout())
                                .build()
                    );
                } catch (NoSuchAlgorithmException | InvalidKeySpecException | NoSuchProviderException e) {
                    throw new IllegalStateException(e);
                }
            } else {
            return null;
        }
        }
    };

}
