package com.fitbank.fitpatch;

import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.prefs.Preferences;

import org.apache.commons.beanutils.WrapDynaBean;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.Transformer;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.tmatesoft.svn.core.SVNCommitInfo;
import org.tmatesoft.svn.core.SVNDepth;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.auth.BasicAuthenticationManager;
import org.tmatesoft.svn.core.auth.SVNAuthentication;
import org.tmatesoft.svn.core.auth.SVNSSHAuthentication;
import org.tmatesoft.svn.core.internal.io.svn.SVNRepositoryFactoryImpl;
import org.tmatesoft.svn.core.wc.ISVNStatusHandler;
import org.tmatesoft.svn.core.wc.SVNClientManager;
import org.tmatesoft.svn.core.wc.SVNCommitPacket;
import org.tmatesoft.svn.core.wc.SVNInfo;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.core.wc.SVNRevisionRange;
import org.tmatesoft.svn.core.wc.SVNStatus;
import org.tmatesoft.svn.core.wc.SVNStatusType;
import org.tmatesoft.svn.core.wc.SVNWCClient;
import org.tmatesoft.svn.core.wc.SVNWCUtil;

import com.fitbank.fitpatch.MantisQueries.ConnectionException;
import com.fitbank.util.Debug;

/**
 * Maneja un parche
 *
 * @author FitBank CI
 */
public final class PatchHandler {

    public static final List<String> PROPERTIES =
            Arrays.asList("controlSources", "mantis", "username", "privateKey",
            "passphrase", "port");

    private static Preferences preferences =
            Preferences.userNodeForPackage(PatchHandler.class);

    private MantisQueries mq;

    private SVNClientManager svn;

    private Map<String, Set<Integer>> revisionMap;

    public static String pad(Integer patchInt) {
        return StringUtils.leftPad(patchInt.toString(), 7, "0");
    }

    public static void main(String[] args) throws NumberFormatException,
            SVNException, ConnectionException {
        PatchHandler patchHandler = new PatchHandler();

        if (args.length < 1 || args[0].equals("help")) {
            System.err.println(
                    "Uso: fitpatch [comando]");

            System.err.println("");
            System.err.println("Comandos:");
            System.err.println("\thelp: muestra esta ayuda");
            System.err.println("\tconfig list: muestra las propiedades");
            System.err.println(
                    "\tconfig set [propiedad] [valor]: cambia un valor de propiedad");
            System.err.println(
                    "\tmerge [incidencia]: hace merge de la incidencia");
            System.err.println(
                    "\tcommit: hace commit de la incidencia actual");
            System.err.println(
                    "\tcleanup: limpia la copia de trabajo");

            System.exit(1);
        }

        String comando = args[0];

        if ("config".equals(comando)) {
            patchHandler.config(args);
            System.exit(0);
        }

        patchHandler.connect();


        if ("merge".equals(comando)) {
            patchHandler.merge(pad(Integer.parseInt(args[1])));
        } else if ("commit".equals(comando)) {
            patchHandler.commit();
        } else if ("cleanup".equals(comando)) {
            patchHandler.cleanup();
        }
    }

    private void config(String[] args) {
        String action = args[1];

        if ("list".equals(action)) {
            WrapDynaBean bean = new WrapDynaBean(this);
            for (String s : PROPERTIES) {
                System.out.println(s + "=" + bean.get(s));
            }
        } else if ("set".equals(action)) {
            String key = args[2];
            String value = args[3];

            if (!PROPERTIES.contains(key)) {
                System.err.println("Propiedad no encontrada: " + key);
                System.exit(1);
            }

            if ("port".equals(action)) {
                preferences.putInt(key, Integer.parseInt(value));
            } else {
                preferences.put(key, value);
            }
        } else {
            System.err.println("Comando de configuración incorrecto: " + action);
            System.exit(1);
        }
    }

    public String getControlSources() {
        return preferences.get("controlSources", "");
    }

    public String getMantis() {
        return preferences.get("mantis", "192.168.1.13:3306");
    }

    public String getUsername() {
        return preferences.get("username", "");
    }

    public String getPrivateKey() {
        return preferences.get("privateKey", "");
    }

    public String getPassphrase() {
        return preferences.get("passphrase", "");
    }

    public int getPort() {
        return preferences.getInt("port", 22);
    }

    public Map<String, Set<Integer>> getRevisionMap() {
        return Collections.unmodifiableMap(revisionMap);
    }

    public void connect() throws ConnectionException {
        SVNSSHAuthentication auth = new SVNSSHAuthentication(getUsername(),
                new File(getPrivateKey()), getPassphrase(), getPort(), true);
        BasicAuthenticationManager authMgr =
                new BasicAuthenticationManager(new SVNAuthentication[] { auth });
        svn = SVNClientManager.newInstance(SVNWCUtil.createDefaultOptions(
                false), authMgr);
        SVNRepositoryFactoryImpl.setup();

        mq = new MantisQueries(getMantis(), "fitpatch", "fitpatch");
    }

    public void query(String incidencia) {
        Debug.info("Usando incidencia #" + incidencia);

        revisionMap = mq.revisionMap(Long.parseLong(incidencia), "fitbank");
    }

    public void merge(String incidencia) throws SVNException {
        if (loadIncidencia() != null) {
            throw new RuntimeException(
                    "Actualmente se está trabajando en otra incidencia");
        }

        query(incidencia);

        File controlDir = new File(getControlSources());

        Debug.info("Revisando url de copia de trabajo...");
        SVNInfo info = svn.getWCClient().doInfo(controlDir, SVNRevision.WORKING);

        if (!info.getURL().getPath().endsWith("/trunk")) {
            throw new Error("Carpeta seleccionada no corresponde con trunk");
        } else {
            Debug.info("OK");
        }

        Debug.info("Revisando cambios en copia de trabajo...");
        Debug.info("OK");

        Debug.info("Actualizando a la última revisión...");
        Debug.info("OK");

        for (String branch : revisionMap.keySet()) {
            Collection<SVNRevisionRange> revisions = CollectionUtils.collect(
                    revisionMap.get(branch), new Transformer() {

                public Object transform(Object input) {
                    int n = (Integer) input;
                    return new SVNRevisionRange(SVNRevision.create(n - 1),
                            SVNRevision.create(n));
                }

            });

            SVNURL branchURL = svn.getWCClient().doInfo(controlDir,
                    SVNRevision.WORKING).getRepositoryRootURL().appendPath(
                    branch, true);

            Debug.info(String.format("Haciendo merge de %s...", branch));
            svn.getDiffClient().doMerge(branchURL, SVNRevision.HEAD, revisions,
                    controlDir, SVNDepth.INFINITY, true, false, false, false);
            Debug.info("OK");
        }

        saveIncidencia(incidencia);

        Debug.info("Revisando conflictos...");
        Debug.info("OK");
    }

    public void commit() throws SVNException {
        String incidencia = loadIncidencia();
        query(incidencia);

        File controlDir = new File(getControlSources());

        Debug.info("Haciendo commit...");
        SVNCommitPacket cp =
                svn.getCommitClient().doCollectCommitItems(
                new File[] { controlDir }, false, false, SVNDepth.INFINITY, null);
        SVNCommitInfo info = svn.getCommitClient().doCommit(cp, false, String.
                format("Paso a control de la incidencia #%s", incidencia));

        if (info.getErrorMessage() != null) {
            throw new Error("Error al hacer commit: " + info.getErrorMessage());
        } else {
            Debug.info("OK");
        }

        deleteIncidencia();
    }

    public void cleanup() throws SVNException {
        Debug.info("Limpiando copia de trabajo...");
        File controlDir = new File(getControlSources());
        SVNWCClient wc = svn.getWCClient();

        wc.setRevertMissingDirectories(true);
        wc.doCleanup(controlDir, true);
        wc.doRevert(new File[] { controlDir }, SVNDepth.INFINITY,
                Collections.EMPTY_LIST);

        svn.getStatusClient().doStatus(controlDir, SVNRevision.WORKING,
                SVNDepth.INFINITY, false, true, false, false,
                new CleanStatusHandler(), Collections.EMPTY_LIST);
    }

    private void saveIncidencia(String incidencia) {
        File file = new File(getControlSources(), ".fitpatch");

        try {
            FileWriter writer = new FileWriter(file);
            IOUtils.write(incidencia, writer);
            writer.close();
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
    }

    private String loadIncidencia() {
        File file = new File(getControlSources(), ".fitpatch");

        if (!file.exists()) {
            return null;
        }
        try {
            return IOUtils.toString(new FileReader(file)).trim();
        } catch (IOException ex) {
            return null;
        }
    }

    private void deleteIncidencia() {
        File file = new File(getControlSources(), ".fitpatch");

        if (file.exists()) {
            file.delete();
        }
    }

    private static class CleanStatusHandler implements ISVNStatusHandler {

        public void handleStatus(SVNStatus status) throws
                SVNException {
            if (status.getContentsStatus() == SVNStatusType.STATUS_UNVERSIONED) {
                status.getFile().delete();
            }
        }

    }

}
