package com.vackosar.gitflowincrementalbuild.control.jgit;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.jgit.errors.UnsupportedCredentialItem;
import org.eclipse.jgit.transport.CredentialItem;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.URIish;
import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.TemporaryBuffer;
import org.eclipse.jgit.util.FS.ExecutionResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * JGit-{@link CredentialsProvider} for HTTP(S) that is delegating all credential requests to native Git via {@code git credential fill}. This will consult
 * all configured credential helpers, if any (for the repo, the user and the system). Such a helper might query the user for the credentials in case it
 * cannot yet provide them. However, the assumption here is that the credentials should already exist. Therefore this provider does <i>not</i> give feedback
 * to native Git via {@code git credential approve} or {@code git credential verify}.
 * <p>
 * This provider will suppress any console input requests (see
 * <a href="https://git-scm.com/docs/git#Documentation/git.txt-codeGITTERMINALPROMPTcode">GIT_TERMINAL_PROMPT</a>).
 * </p>
 *
 * @see <a href="https://git-scm.com/docs/git-credential">Git documentation: git credential</a>
 */
public class HttpDelegatingCredentialsProvider extends CredentialsProvider {

    private Logger logger = LoggerFactory.getLogger(HttpDelegatingCredentialsProvider.class);

    private final Path projectDir;
    private final Map<String, String> additionalNativeGitEnvironment;

    private final Map<URIish, CredentialsPair> credentials = new HashMap<>();

    public HttpDelegatingCredentialsProvider(Path projectDir, Map<String, String> additionalNativeGitEnvironment) {
        this.projectDir = projectDir;
        this.additionalNativeGitEnvironment = additionalNativeGitEnvironment;
    }

    @Override
    public boolean isInteractive() {
        // possibly interactive in case some credential helper asks for input
        return true;
    }

    @Override
    public boolean supports(CredentialItem... items) {
        return Arrays.stream(items)
                .allMatch(item -> item instanceof CredentialItem.Username || item instanceof CredentialItem.Password);
    }

    @Override
    public boolean get(URIish uri, CredentialItem... items) throws UnsupportedCredentialItem {

        // only handle HTTP(s)
        if (uri.getScheme() != null && !uri.getScheme().startsWith("http")) {
            return false;
        }

        CredentialsPair credentialsPair = credentials.computeIfAbsent(uri, u -> {
            try {
                return lookupCredentials(uri);
            } catch (IOException | InterruptedException | RuntimeException e) {
                logger.warn("Failed to look up credentials via 'git credential fill' for: " + uri, e);
                return null;
            }
        });
        if (credentialsPair == null) {
            return false;
        }

        // map extracted credentials to CredentialItems, see also: org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider
        for (CredentialItem item : items) {
            if (item instanceof CredentialItem.Username) {
                ((CredentialItem.Username) item).setValue(credentialsPair.username);
            } else if (item instanceof CredentialItem.Password) {
                ((CredentialItem.Password) item).setValue(credentialsPair.password);
            } else if (item instanceof CredentialItem.StringType && item.getPromptText().equals("Password: ")) {
                ((CredentialItem.StringType) item).setValue(new String(credentialsPair.password));
            } else {
                throw new UnsupportedCredentialItem(uri, item.getClass().getName() + ":" + item.getPromptText());
            }
        }

        return true;
    }

    @Override
    // see also: org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider.clear()
    public void reset(URIish uri) {
        Optional.ofNullable(credentials.remove(uri))
                .ifPresent(credPair -> {
                    credPair.username = null;
                    Arrays.fill(credPair.password, (char) 0);
                    credPair.password = null;
                });
    }

    public void resetAll() {
        new HashSet<>(credentials.keySet()).forEach(this::reset);
    }

    private CredentialsPair lookupCredentials(URIish uri) throws IOException, InterruptedException {
        // utilize JGit command execution capabilities
        FS fs = FS.detect();
        ProcessBuilder procBuilder = fs.runInShell("git", new String[] {"credential", "fill"});

        // prevent native git from requesting console input (not implemented)
        procBuilder.environment().put("GIT_TERMINAL_PROMPT", "0");

        // add additional environment entries, if present (test only)
        if (!additionalNativeGitEnvironment.isEmpty()) {
            procBuilder.environment().putAll(additionalNativeGitEnvironment);
        }
        procBuilder.directory(projectDir.toFile());

        ExecutionResult result = fs.execute(procBuilder, new ByteArrayInputStream(buildGitCommandInput(uri).getBytes(Charset.defaultCharset())));
        if (result.getRc() != 0) {
            logger.info(bufferToString(result.getStdout()));
            logger.error(bufferToString(result.getStderr()));
            throw new IllegalStateException("Native Git invocation failed with return code " + result.getRc()
                    + ". See previous log output for more details.");
        }

        return extractCredentials(bufferToString(result.getStdout()));
    }

    // build input for "git credential fill" as per https://git-scm.com/docs/git-credential#_typical_use_of_git_credential
    private String buildGitCommandInput(URIish uri) {
        StringBuilder builder = new StringBuilder();
        builder.append("protocol=").append(uri.getScheme()).append("\n");
        builder.append("host=").append(uri.getHost());
        if (uri.getPort() != -1) {
            builder.append(":").append(uri.getPort());
        }
        builder.append("\n");
        Optional.ofNullable(uri.getPath())
                .map(path -> path.startsWith("/") ? path.substring(1) : path)
                .ifPresent(path -> builder.append("path=").append(path).append("\n"));
        Optional.ofNullable(uri.getUser())
                .ifPresent(user -> builder.append("username=").append(user).append("\n"));
        return builder.toString();
    }

    private String bufferToString(TemporaryBuffer buffer) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        buffer.writeTo(baos, null);
        return baos.toString(Charset.defaultCharset().name());
    }

    private CredentialsPair extractCredentials(String nativeGitOutput) {
        Matcher matcher = Pattern.compile("(?<=username=).+|(?<=password=).+").matcher(nativeGitOutput);
        if (!matcher.find()) {
            throw new IllegalStateException("Could not find username in native Git output");
        }
        String username = matcher.group();
        if (!matcher.find()) {
            throw new IllegalStateException("Could not find password in native Git output");
        }
        char[] password = matcher.group().toCharArray();

        CredentialsPair credPair = new CredentialsPair();
        credPair.username = username;
        credPair.password = password;
        return credPair;
    }

    private static class CredentialsPair {
        private String username;
        private char[] password;
    }
}