package nu.studer.gradle.credentials;

import nu.studer.gradle.credentials.domain.CredentialsContainer;
import nu.studer.gradle.credentials.domain.CredentialsEncryptor;
import nu.studer.gradle.credentials.domain.CredentialsPersistenceManager;
import nu.studer.gradle.util.AlwaysFalseSpec;
import nu.studer.gradle.util.MD5;
import org.gradle.api.Action;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.initialization.Settings;
import org.gradle.api.invocation.Gradle;
import org.gradle.api.plugins.ExtensionAware;
import org.gradle.api.plugins.ExtraPropertiesExtension;
import org.gradle.api.tasks.TaskContainer;
import org.gradle.util.GradleVersion;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.util.function.Function;

/**
 * Plugin to store and access encrypted credentials using password-based encryption (PBE). The credentials are stored in the Gradle home directory in a separate file for each
 * passphrase. If no passphrase is provided, a default passphrase is used and the credentials are stored in the default credentials file 'gradle.encrypted.properties'. While
 * running a build, only one passphrase is active per project.
 * <p>
 * The plugin provides a credentials container through the 'credentials' property that is available from the Gradle project. This allows access to credentials in the form of
 * <code>project.myCredentialKey</code>. The already persisted credentials can be accessed through the credentials container, and new credentials can be added to the container
 * ad-hoc while the build is executed. Credentials added ad-hoc are not available beyond the lifetime of the build.
 * <p>
 * The plugin adds a task to add credentials and a task to remove credentials.
 */
public class CredentialsPlugin implements Plugin<ExtensionAware> {

    public static final String DEFAULT_PASSPHRASE_CREDENTIALS_FILE = "gradle.encrypted.properties";
    public static final String DEFAULT_PASSPHRASE = ">>Default passphrase to encrypt passwords!<<";

    public static final String CREDENTIALS_CONTAINER_PROPERTY = "credentials";

    public static final String CREDENTIALS_LOCATION_PROPERTY = "credentialsLocation";
    public static final String CREDENTIALS_PASSPHRASE_PROPERTY = "credentialsPassphrase";
    public static final String CREDENTIALS_KEY_PROPERTY = "credentialsKey";
    public static final String CREDENTIALS_VALUE_PROPERTY = "credentialsValue";

    public static final String ADD_CREDENTIALS_TASK_NAME = "addCredentials";
    public static final String REMOVE_CREDENTIALS_TASK_NAME = "removeCredentials";

    private static final Action<Pair> NOOP = (Pair p) -> {
    };

    private static final Logger LOGGER = LoggerFactory.getLogger(CredentialsPlugin.class);

    @SuppressWarnings("NullableProblems")
    @Override
    public void apply(ExtensionAware extensionAware) {
        // abort if old Gradle version is not supported
        if (GradleVersion.current().getBaseVersion().compareTo(GradleVersion.version("5.0")) < 0) {
            throw new IllegalStateException("This version of the credentials plugin is not compatible with Gradle < 5.0");
        }

        // handle plugin application to settings file and project file
        if (extensionAware instanceof Settings) {
            Settings settings = (Settings) extensionAware;
            init(settings.getGradle(), settings, (String loc) -> settings.getSettingsDir().toPath().resolve(loc).toFile(), NOOP);
        } else if (extensionAware instanceof Project) {
            Project project = (Project) extensionAware;
            init(project.getGradle(), project, project::file, (Pair creds) -> addTasks(creds, project.getTasks()));
        } else {
            throw new IllegalStateException("The credentials plugin can only be applied to Settings and Project instances");
        }
    }

    private void init(Gradle gradle, ExtensionAware extensionAware, Function<String, File> locationResolver, Action<Pair> customizations) {
        // get the passphrase from the project properties, otherwise use the default passphrase
        String passphrase = getStringProperty(CREDENTIALS_PASSPHRASE_PROPERTY, DEFAULT_PASSPHRASE, extensionAware);

        // derive the name of the credentials file from the passphrase
        String credentialsFileName = deriveFileNameFromPassphrase(passphrase);

        // create credentials encryptor for the given passphrase
        CredentialsEncryptor credentialsEncryptor = CredentialsEncryptor.withPassphrase(passphrase.toCharArray());

        // create a credentials persistence manager that operates on the credentials file, possibly located in a user-configured folder
        String credentialsLocation = getStringProperty(CREDENTIALS_LOCATION_PROPERTY, null, extensionAware);
        File credentialsLocationDir = credentialsLocation != null ? locationResolver.apply(credentialsLocation) : gradle.getGradleUserHomeDir();
        File credentialsFile = new File(credentialsLocationDir, credentialsFileName);
        CredentialsPersistenceManager credentialsPersistenceManager = new CredentialsPersistenceManager(credentialsFile);

        // add a new 'credentials' property and transiently store the persisted credentials for access in build scripts
        CredentialsContainer credentialsContainer = new CredentialsContainer(credentialsEncryptor, credentialsPersistenceManager.readCredentials());
        setProperty(CREDENTIALS_CONTAINER_PROPERTY, credentialsContainer, extensionAware);
        LOGGER.debug("Registered property '" + CREDENTIALS_CONTAINER_PROPERTY + "'");

        // allow further ExtensionAware-specific customization
        customizations.execute(new Pair(credentialsEncryptor, credentialsPersistenceManager));
    }

    private String getStringProperty(String key, String defaultValue, ExtensionAware extensionAware) {
        ExtraPropertiesExtension properties = extensionAware.getExtensions().getExtraProperties();
        return properties.has(key) ? (String) properties.get(key) : defaultValue;
    }

    private void setProperty(String key, Object value, ExtensionAware extensionAware) {
        ExtraPropertiesExtension properties = extensionAware.getExtensions().getExtraProperties();
        properties.set(key, value);
    }

    private void addTasks(Pair result, TaskContainer tasks) {
        CredentialsEncryptor credentialsEncryptor = result.credentialsEncryptor;
        CredentialsPersistenceManager credentialsPersistenceManager = result.credentialsPersistenceManager;

        // add a task instance that stores new credentials through the credentials persistence manager
        AddCredentialsTask addCredentials = tasks.create(ADD_CREDENTIALS_TASK_NAME, AddCredentialsTask.class);
        addCredentials.setDescription("Adds the credentials specified through the project properties 'credentialsKey' and 'credentialsValue'.");
        addCredentials.setGroup("Credentials");
        addCredentials.setCredentialsEncryptor(credentialsEncryptor);
        addCredentials.setCredentialsPersistenceManager(credentialsPersistenceManager);
        addCredentials.getOutputs().upToDateWhen(AlwaysFalseSpec.INSTANCE);
        LOGGER.debug(String.format("Registered task '%s'", addCredentials.getName()));

        // add a task instance that removes some credentials through the credentials persistence manager
        RemoveCredentialsTask removeCredentials = tasks.create(REMOVE_CREDENTIALS_TASK_NAME, RemoveCredentialsTask.class);
        removeCredentials.setDescription("Removes the credentials specified through the project property 'credentialsKey'.");
        removeCredentials.setGroup("Credentials");
        removeCredentials.setCredentialsPersistenceManager(credentialsPersistenceManager);
        removeCredentials.getOutputs().upToDateWhen(AlwaysFalseSpec.INSTANCE);
        LOGGER.debug(String.format("Registered task '%s'", removeCredentials.getName()));
    }

    private String deriveFileNameFromPassphrase(String passphrase) {
        // derive the name of the file that contains the credentials from the given passphrase
        String credentialsFileName;
        if (passphrase.equals(DEFAULT_PASSPHRASE)) {
            credentialsFileName = DEFAULT_PASSPHRASE_CREDENTIALS_FILE;
            LOGGER.debug("No explicit passphrase provided. Using default credentials file name: " + credentialsFileName);
        } else {
            credentialsFileName = "gradle." + MD5.generateMD5Hash(passphrase) + ".encrypted.properties";
            LOGGER.debug("Custom passphrase provided. Using credentials file name: " + credentialsFileName);
        }
        return credentialsFileName;
    }

    private static final class Pair {

        private final CredentialsEncryptor credentialsEncryptor;
        private final CredentialsPersistenceManager credentialsPersistenceManager;

        private Pair(CredentialsEncryptor credentialsEncryptor, CredentialsPersistenceManager credentialsPersistenceManager) {
            this.credentialsEncryptor = credentialsEncryptor;
            this.credentialsPersistenceManager = credentialsPersistenceManager;
        }

    }

}