package com.vackosar.gitflowincrementalbuild.control.jgit;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Vector;

import org.eclipse.jgit.transport.JschConfigSessionFactory;
import org.eclipse.jgit.transport.OpenSshConfig.Host;
import org.eclipse.jgit.util.FS;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.jcraft.jsch.Identity;
import com.jcraft.jsch.IdentityRepository;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.agentproxy.AgentProxyException;
import com.jcraft.jsch.agentproxy.Connector;
import com.jcraft.jsch.agentproxy.ConnectorFactory;
import com.jcraft.jsch.agentproxy.RemoteIdentityRepository;

/**
 * Sets to each created {@link Session} a {@link IdentityRepository} that contains (unencrypted) {@link Identity Identities} retrieved from a running SSH agent
 * like {@code ssh-agent} or {@code PAGEANT}. The communication with the agent is provided by {@link RemoteIdentityRepository} ({@code jsch-agent-proxy}).
 * <p>
 * Additionally, this factory also adds all unencrypted(!) {@code Identites} from the {@link JSch#getIdentityRepository() default JSch IdentityRepository}
 * (after the {@code Identites} from the agent).
 * </p>
 * <p>
 * Design note: This factory does <b>not</b> simply set {@link RemoteIdentityRepository} via {@link JSch#setIdentityRepository(IdentityRepository)} as
 * <a href="https://github.com/ymnk/jsch-agent-proxy/blob/master/examples/src/main/java/com/jcraft/jsch/agentproxy/examples/JSchWithAgentProxy.java#L32">
 * advertised</a> by {@code jsch-agent-proxy}. The reason for that is the following special handling of default {@code Identites} (like {@code ~/.ssh/id_rsa})
 * <i>in case those {@code Identites} are encrypted</i> (which is typically the case when agent access is needed):
 * <ul>
 * <li>{@link IdentityRepository.Wrapper}: contains a cache that is consulted <i>before</i> the {@code Identites} from the wrapped repo (unencrpyted, from the
 * agent) and since the cache contains encrypted {@code Identites}, authentication will fail</li>
 * <li>{@link JSch#addIdentity(Identity, byte[])}: created the {@code Wrapper} on demand</li>
 * </ul>
 * Therefore this factory sets an individual {@link IdentityRepository} for each {@link Session} (for which no such wrapping happens). This repository is also
 * read-only to prevent undesired write access to the agent.
  */
public class AgentProxyAwareJschConfigSessionFactory extends JschConfigSessionFactory {

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

    @Override
    protected void configure(Host hc, Session session) {
        // nothing to do
    }

    @Override
    protected Session createSession(Host hc, String user, String host, int port, FS fs)
            throws JSchException {
        JSch jSch = getJSch(hc, fs);

        // assumption: identities from agent are always unencrypted
        final Collection<Identity> allUnencryptedIdentities = getIdentitiesFromAgentProxy();

        @SuppressWarnings("unchecked")
        Collection<Identity> identities = ((Collection<Identity>) jSch.getIdentityRepository().getIdentities());
        identities.stream()
                .filter(id -> !id.isEncrypted())
                .forEach(allUnencryptedIdentities::add);
        
        Session session = jSch.getSession(user, host, port);
        session.setIdentityRepository(new ReadOnlyIdentityRepository(allUnencryptedIdentities));
        return session;
    }

    private Collection<Identity> getIdentitiesFromAgentProxy() {

        Connector con = null;
        try {
            con = ConnectorFactory.getDefault().createConnector();
        } catch(AgentProxyException e) {
            logger.warn("AgentProxy setup failed, cannot read identities from agent", e);
        }
        return con != null ? new RemoteIdentityRepository(con).getIdentities() : new ArrayList<>();
    }

    private static class ReadOnlyIdentityRepository implements IdentityRepository {

        private final Collection<Identity> allUnencryptedIdentities;

        private ReadOnlyIdentityRepository(Collection<Identity> allUnencryptedIdentities) {
            this.allUnencryptedIdentities = allUnencryptedIdentities;
        }

        @Override
        public void removeAll() {
        }

        @Override
        public boolean remove(byte[] blob) {
            return false;
        }

        @Override
        public int getStatus() {
            return IdentityRepository.RUNNING;
        }

        @Override
        public String getName() {
            return getClass().getName();
        }

        @Override
        public Vector<Identity> getIdentities() {
            return new Vector<>(allUnencryptedIdentities);
        }

        @Override
        public boolean add(byte[] identity) {
            return false;
        }
    }
}