package com.atlassian.stash.internal.plugin;

import com.atlassian.sal.core.pluginsettings.AbstractStringPluginSettings;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.Scope;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.UncategorizedDataAccessException;
import org.springframework.stereotype.Component;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.transaction.support.TransactionTemplate;

/**
 * Implementation of {@link com.atlassian.sal.api.pluginsettings.PluginSettings} backed directly by Hibernate objects.
 */
@Component("pluginSettingsPrototype")
@Scope("prototype")
public class DefaultPluginSettings extends AbstractStringPluginSettings {

    private final PluginSettingDao dao;
    private final String namespace;
    private final TransactionTemplate readOnlyTransaction;
    private final TransactionTemplate readWriteTransaction;

    public DefaultPluginSettings(PluginSettingDao dao, String namespace, TransactionTemplate readOnlyTransaction,
                                 TransactionTemplate readWriteTransaction) {
        this.dao = dao;
        this.namespace = namespace;
        this.readOnlyTransaction = readOnlyTransaction;
        this.readWriteTransaction = readWriteTransaction;
    }

    @Override
    protected String getActual(String key) {
        /*
         * Note that if the plugin setting does not exist and the following returns null, then nothing will be cached
         * in Hibernate's L1 or L2 caches (it does not implement negative caching of natural id lookups).  This may
         * potentially result in a lot of database queries if negative lookups are frequent.  See STASHDEV-9902.
         */
        return readOnlyTransaction.execute(status -> dao.get(namespace, key));
    }

    @Override
    protected void putActual(String key, String val) {
        if (StringUtils.isEmpty(val)) {
            // This intentionally removes the key, see BSERVDEV-3029 and BSERVDEV-11124.
            removeActual(key);
        } else {
            PluginSetting setting;
            // Due to the inherent race condition in HibernatePluginSettingDao.put() when two or more threads INSERT a
            // new plugin setting record, it is possible to get spurious DataIntegrityViolationException's here.
            for (int tries = 1;; tries++) {
                try {
                    setting = readWriteTransaction.execute(status -> dao.put(namespace, key, val));
                    break;
                } catch (DataIntegrityViolationException e) {
                    // Another thread probably did an INSERT of the same namespace and key.  Retrying should reload the
                    // new record from the database and then succeed on the UPDATE path.
                    if (tries >= 5) {
                        throw e;
                    }
                }
            }
            // A PluginSetting object was just modified in an inner transaction, but a PluginSetting object for the same
            // namespace and key may still remain cached in the Hibernate session associated with an outer transaction.
            // Attach it to the outer transaction so that the Hibernate L1 cache in the associated session does not
            // cache the old (stale) value.
            if (TransactionSynchronizationManager.isActualTransactionActive()) {
                dao.attach(setting);
            }
        }
    }

    @Override
    protected void removeActual(String key) {
        try {
            doInTransaction(() -> dao.remove(namespace, key));
        } catch (UncategorizedDataAccessException e) {
            // Another thread probably did a DELETE of the record around the same time.  Ignore.
        }
        if (TransactionSynchronizationManager.isActualTransactionActive()) {
            // A PluginSetting object was just removed in an inner transaction, but a PluginSetting object for the same
            // namespace and key may still remain cached in the Hibernate session associated with the outer transaction.
            // Evict it to make sure the stale value does not remain cached.
            dao.evict(namespace, key);
        }
    }

    private void doInTransaction(Runnable runnable) {
        readWriteTransaction.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                runnable.run();
            }
        });
    }

}
