package com.atlassian.bitbucket.mesh.credentials;

import com.atlassian.bitbucket.mesh.execution.ExecutionManager;
import com.atlassian.bitbucket.mesh.git.exception.InternalFailureException;
import com.atlassian.bitbucket.mesh.git.exception.UnsupportedArgumentException;
import com.atlassian.bitbucket.mesh.rpc.v1.RpcPrivateKeyCredentials;
import com.atlassian.bitbucket.mesh.rpc.v1.RpcRemoteRepository;
import com.atlassian.bitbucket.mesh.rpc.v1.RpcUnsupportedArgument;
import com.atlassian.bitbucket.mesh.rpc.v1.RpcUsernamePasswordCredentials;
import com.google.protobuf.ByteString;

import javax.annotation.Nonnull;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

import static com.atlassian.bitbucket.mesh.io.MoreFiles.deleteNowOrOnExit;
import static java.util.Objects.requireNonNull;

public final class SimpleCredentialsHelper implements CredentialsHelper {

    private final ExecutionManager executionManager;

    public SimpleCredentialsHelper(ExecutionManager executionManager) {
        this.executionManager = executionManager;
    }

    @SuppressWarnings("ResultOfMethodCallIgnored")
    public static void securePrivateKey(File keyFile) {
        // Deny read, write and execute for everyone
        keyFile.setReadable(false, false);
        keyFile.setWritable(false, false);
        keyFile.setExecutable(false, false);
        // Allow read and write for the owner
        keyFile.setReadable(true, true);
        keyFile.setWritable(true, true);
    }

    /**
     * Writes the provided {@code privateKeyBytes} on disk in a secured temporary file and registers it for cleanup
     * when the current RPC finished
     *
     * @param privateKeyBytes the private key bytes
     */
    @Nonnull
    @Override
    public Path createTemporaryPrivateKey(@Nonnull ByteString privateKeyBytes) {
        requireNonNull(privateKeyBytes, "privateKeyBytes");

        Path keyPath = null;
        try {
            Path p = keyPath = Files.createTempFile("", ".key");
            Files.write(keyPath, privateKeyBytes.toByteArray());
            securePrivateKey(keyPath.toFile());
            executionManager.getContextOrThrow().addCleanupCallback(() -> deleteNowOrOnExit(p));
            return keyPath;
        } catch (IOException e) {
            if (keyPath != null) {
                deleteNowOrOnExit(keyPath);
            }
            throw new InternalFailureException("Failed to write private key to disk", e);
        }
    }

    /**
     * Creates a {@link Credentials} instance for the provided repository if credentials were specified on the
     * {@link RpcRemoteRepository} and invokes the provided {@code action}.
     *
     * @param repository the remote repository
     * @return the {@link Credentials} or {@code null} if no credentials were specified on the request
     */
    @Override
    public Credentials getCredentials(@Nonnull RpcRemoteRepository repository) {
        requireNonNull(repository, "repository");

        switch (repository.getCredentialsOneofCase()) {
            case CREDENTIALSONEOF_NOT_SET:
                return null;
            case PRIVATE_KEY:
                RpcPrivateKeyCredentials credentials = repository.getPrivateKey();
                Path keyPath = createTemporaryPrivateKey(credentials.getPrivateKey());
                return convert(credentials, keyPath);
            case USERNAME_PASSWORD:
                return convert(repository.getUsernamePassword());
            default:
                throw new UnsupportedArgumentException(RpcUnsupportedArgument.Reason.UNRECOGNIZED,
                        "Credentials type not recognized: " + repository.getCredentialsOneofCase().name());
        }
    }

    /**
     * Converts RPC model private key credentials to internal {@link PrivateKeyCredentials}
     *
     * @param credentials the RPC credentials entity
     * @param privateKeyPath the (temporary) path where the private key file has been stored
     * @return the {@link PrivateKeyCredentials}
     */
    private static PrivateKeyCredentials convert(RpcPrivateKeyCredentials credentials, Path privateKeyPath) {
        return new PrivateKeyCredentials.Builder(credentials.getUsername().toStringUtf8(), privateKeyPath)
                .passphrase(credentials.getPassword().toStringUtf8())
                .build();
    }

    /**
     * Converts RPC model username/password credentials to the internal {@link UsernamePasswordCredentials}
     *
     * @param credentials the RPC credentials entity
     * @return the {@link UsernamePasswordCredentials}
     */
    private static UsernamePasswordCredentials convert(RpcUsernamePasswordCredentials credentials) {
        return new UsernamePasswordCredentials(credentials.getUsername().toStringUtf8(),
                credentials.getPassword().toStringUtf8());
    }
}
