package info.knacki.pass.git;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.security.InvalidParameterException;
import java.util.ArrayDeque;
import java.util.logging.Level;
import java.util.logging.Logger;

import info.knacki.pass.git.entities.GitCommit;
import info.knacki.pass.git.entities.GitObject;
import info.knacki.pass.git.entities.GitRef;
import info.knacki.pass.io.AppendableInputStream;
import info.knacki.pass.io.CharsetHelper;
import info.knacki.pass.io.FileUtils;
import info.knacki.pass.io.OnErrorListener;
import info.knacki.pass.io.OnResponseListener;
import info.knacki.pass.io.OnStreamResponseListener;
import info.knacki.pass.io.ssh.SSHConnection;
import info.knacki.pass.io.ssh.SSHFactory;
import info.knacki.pass.settings.SettingsManager;

public class SSHGitProtocol extends BaseGitProtocol {
    private final static Logger log = Logger.getLogger(SSHGitProtocol.class.getName());
    private final SSHUrl fRepoUrl;
    private GitRef[] fRefsCache = null;
    private final class ActiveSSHWrapper {
        final SSHConnection fConnection;
        final OutputStream fStdin;
        final InputStream fStdout;
        final InputStream fStderr;

        ActiveSSHWrapper(SSHConnection con, OutputStream stdin, InputStream stdout, InputStream stderr) {
            fConnection = con;
            fStdin = stdin;
            fStdout = stdout;
            fStderr = stderr;
        }
    }

    private final class SSHUrl implements SSHConnection.ConnectionParams {
        private final String repoHost;
        final String repoName;

        SSHUrl(String url) {
            if (url.contains("://"))
                url = url.substring(url.indexOf("://"));
            int repoSep = url.indexOf(':');
            if (repoSep < 0)
                throw new InvalidParameterException("Cannot find repository name");
            repoHost = url.substring(0, repoSep);
            repoName = url.substring(repoSep + 1);
        }

        @Override
        public String GetHostname() {
            return repoHost;
        }

        @Override
        public boolean HasAuthentication() {
            return fConfig.HasAuthentication();
        }

        @Override
        public String GetUser() {
            return fConfig.GetUser();
        }

        @Override
        public String GetPassword() {
            return fConfig.GetPassword();
        }

        @Override
        public String GetPrivateKey() {
            return fConfig.GetPrivateKey();
        }
    }

    SSHGitProtocol(SettingsManager.Git config) {
        super(config);
        fRepoUrl = new SSHUrl(config.GetUrl());
    }

    private byte[] ReadLine(InputStream in) throws IOException {
        byte[] lineLenBytes = new byte[4];
        if (in.read(lineLenBytes, 0, 4) < 4) {
            return null;
        }
        int lineLen;
        try {
            lineLen = Integer.parseInt(CharsetHelper.ByteArrayToString(lineLenBytes), 16);
        }
        catch (NumberFormatException e) {
            ByteArrayOutputStream stream = new ByteArrayOutputStream();
            stream.write(lineLenBytes);
            byte[] tmp = new byte[1024];
            int read = in.read(tmp);
            stream.write(tmp, 0, read);
            String error = CharsetHelper.ByteArrayToString(stream.toByteArray());
            throw new NumberFormatException(error);
        }
        if (lineLen == 0) {
            return null;
        }

        lineLen -= 4;
        if (lineLen == 0)
            return new byte[] {};
        byte[] line = new byte[lineLen];
        if (in.read(line) != lineLen) {
            return null;
        }
        for (int i =0; i < lineLen; ++i)
            if (line[i] == 0)
                lineLen = i;
        return line;
    }

    private GitRef[] GetRefs(ActiveSSHWrapper sshConnection, OnErrorListener errorListener) {
        ArrayDeque<GitRef> references = new ArrayDeque<>();

        try {
            byte[] line;
            while ((line = ReadLine(sshConnection.fStdout)) != null) {
                GitRef ref = new GitRef(CharsetHelper.ByteArrayToString(line, 0, 40), CharsetHelper.ByteArrayToString(line, 41, line.length -42));

                if (ref.GetBranch().startsWith("refs/heads/")) {
                    references.add(ref);
                    log.info("Found ref: " + ref);
                }
            }
        }
        catch (IOException | NumberFormatException e) {
            final String errorMsg = "Cannot read from ssh command: " +e.getMessage();
            log.log(Level.SEVERE, errorMsg, e);
            errorListener.OnError(errorMsg, e);
            return null;
        }
        GitRef[] refs = new GitRef[references.size()];
        return references.toArray(refs);
    }

    private void ListenForErrors(InputStream stderr, OnErrorListener onError) {
        ListenForErrors(stderr, onError, true);
    }

    private void ListenForErrors(InputStream stderr, OnErrorListener onError, boolean separateThread) {
        Thread thread = (new Thread() {
            @Override
            public void run() {
                BufferedReader reader = new BufferedReader(new InputStreamReader(stderr));
                String line;

                try {
                    while ((line = reader.readLine()) != null) {
                        onError.OnError(line, null);
                    }
                }
                catch (IOException e) {
                    log.log(Level.SEVERE, "Cannot read from stderr", e);
                }
            }
        });

        if (separateThread)
            thread.start();
        else
            thread.run();
    }

    @Override
    public void GetRefs(OnResponseListener<GitRef[]> callback) {
        if (fRefsCache != null) {
            callback.OnResponse(fRefsCache);
            return;
        }
        AppendableInputStream stdin = new AppendableInputStream();

        SSHFactory
                .createInstance(fRepoUrl, "git-upload-pack " + fRepoUrl.repoName)
                .SetOnConnectionReadyListener((connection, stdout, stderr) -> {
                    ListenForErrors(stderr, (msg, exc) -> log.severe(msg));
                    fRefsCache = GetRefs(new ActiveSSHWrapper(connection, stdin.GetWriter(), stdout, stderr), callback);
                    connection.disconnect();
                    if (fRefsCache != null)
                        callback.OnResponse(fRefsCache);
                })
                .connect();
    }

    private byte[] PullPackData(ActiveSSHWrapper sshWrapper, String hash, OnStreamResponseListener<byte[]> listener) {
        try {
            ListenForErrors(sshWrapper.fStderr, (msg, e) -> listener.OnMsg(msg));
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            out.write(GitLine("want " +hash));
            out.write(GitLine("deepen 1"));
            out.write(GitLine(null));
            out.write(GitLine("done"));
            sshWrapper.fStdin.write(out.toByteArray());
            String line;
            do {
                byte[] b = ReadLine(sshWrapper.fStdout);
                line = b == null ? null : CharsetHelper.ByteArrayToString(b);
            } while (!"NAK\n".equals(line));
            return FileUtils.ReadAllStream(sshWrapper.fStdout);
        }
        catch (IOException e) {
            log.log(Level.SEVERE, "Cannot get hash " +hash +": " +e.getMessage(), e);
            return null;
        }
    }

    private void PullPackData(String hash, OnStreamResponseListener<byte[]> response) {
        AppendableInputStream stdin = new AppendableInputStream();

        SSHFactory
                .createInstance(fRepoUrl, "git-upload-pack " + fRepoUrl.repoName)
                .SetOnConnectionReadyListener((connection, stdout, stderr) -> {
                    ListenForErrors(stderr, (msg, exc) -> log.severe(msg));
                    byte[] data = PullPackData(new ActiveSSHWrapper(connection, stdin.GetWriter(), stdout, stderr), hash, response);
                    connection.disconnect();
                    response.OnResponse(data);
                })
                .connect();
    }

    @Override
    public void FetchCommit(GitRef headRef, OnStreamResponseListener<GitCommit> response) {
        PullPackData(headRef.GetHash(), new OnStreamResponseListener<byte[]>() {
            @Override
            public void OnMsg(String message) {
                response.OnMsg(message);
            }

            @Override
            public void OnResponse(byte[] packData) {
                try {
                    Pacman pacman = new PacmanBuilder().build(packData);
                    OnMsg("Read " +pacman.fPacked.size() +" objects from pack file");
                    response.OnResponse(pacman.BuildCommit(headRef.GetHash()));
                }
                catch (PacmanBuilder.InvalidPackException e) {
                    response.OnError("Cannot read pack object: " +e.getMessage(), e);
                }
            }

            @Override
            public void OnError(String msg, Throwable e) {
                response.OnError(msg, e);
            }
        });
    }

    @Override
    protected void PushPack(GitCommit commit, Pacman pack, String branch, OnStreamResponseListener<Void> resp) {
        AppendableInputStream in = new AppendableInputStream();
        SSHFactory
            .createInstance(fRepoUrl, "git-receive-pack " +fRepoUrl.repoName)
            .SetOnConnectionReadyListener((connection, stdout, stderr) -> {
                ActiveSSHWrapper activeSSHWrapper = new ActiveSSHWrapper(connection, in.GetWriter(), stdout, stderr);
                fRefsCache = null;
                GitRef ref = GetRefFromBranchName(GetRefs(activeSSHWrapper, resp), branch);
                ListenForErrors(stderr, (msg, e) -> resp.OnMsg(msg));
                try {
                    OutputStream w = in.GetWriter();
                    String prev = ref == null ? "0000000000000000000000000000000000000000" : ref.GetHash();
                    w.write(GitLine(prev + " " + GitSha1.BytesToString(GitSha1.getRawSha1OfPackable(commit)) + " " + branch));
                    w.write(GitLine(null));
                    if (!pack.Write(w)) {
                        resp.OnError("Pack error", null);
                        return;
                    }
                }
                catch (IOException e) {
                    resp.OnError("Cannot write pack: " +e.getMessage(), e);
                }
                ListenForErrors(stdout, (msg, e) -> resp.OnMsg(msg), false);
                resp.OnResponse(null);
            })
            .connect(in);
    }

    @Override
    public void FetchBlob(GitObject.GitBlob blob, OnStreamResponseListener<byte[]> response) {
        if (blob instanceof GitObject.GitRawData)
            response.OnResponse(((GitObject.GitRawData) blob).GetData());
        else
            response.OnError("Unsupported object " +CharsetHelper.ByteArrayToString(blob.GetHash()), new InvalidParameterException());
    }

    @Override
    public void FetchHead(OnStreamResponseListener<GitCommit> response) {
        AppendableInputStream stdin = new AppendableInputStream();

        SSHFactory
            .createInstance(fRepoUrl, "git-upload-pack " +fRepoUrl.repoName)
            .SetOnConnectionReadyListener((connection, stdout, stderr) -> {
                ActiveSSHWrapper SSHWrapper = new ActiveSSHWrapper(connection, stdin.GetWriter(), stdout, stderr);
                GitRef ref = GetHeadRef(GetRefs(SSHWrapper, response));
                if (ref == null)
                    return;
                GetHeadCommitFromRef(ref, new OnStreamResponseListener<GitCommit>() {
                    @Override
                    public void OnMsg(String message) {
                        response.OnMsg(message);
                    }

                    @Override
                    public void OnResponse(GitCommit result) {
                        response.OnResponse(result);
                    }

                    @Override
                    public void OnError(String msg, Throwable e) {
                        response.OnError(msg, e);
                    }
                });
            })
            .connect(stdin);
    }

    @Override
    protected void OnBranchCreated() {
        fRefsCache = null;
    }
}
