package cn.reghao.autodop.dmaster.app.service.bd.tools.repo;

import cn.reghao.autodop.dmaster.app.model.constant.build.RepoAuthType;
import cn.reghao.autodop.dmaster.app.model.po.config.build.RepoAuthConfig;
import cn.reghao.jdkutil.text.TextFile;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import org.eclipse.jgit.api.CloneCommand;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.PullCommand;
import org.eclipse.jgit.api.TransportConfigCallback;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.internal.storage.file.FileRepository;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.*;
import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.eclipse.jgit.util.FS;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

/**
 * Git 实现
 *
 * @author reghao
 * @date 2019-10-12 22:22:00
 */
public class GitImpl implements CodeUpdater {
    private UsernamePasswordCredentialsProvider credentials;
    private SshSessionFactory sshSessionFactory;

    public GitImpl(RepoAuthConfig repoAuthConfig) {
        switch (RepoAuthType.valueOf(repoAuthConfig.getAuthType())) {
            case http:
                httpAuth(repoAuthConfig.getUsername(), repoAuthConfig.getPassword());
                break;
            case ssh:
                sshAuth(repoAuthConfig.getRsaPrikey());
                break;
            default:
        }
    }

    private void httpAuth(String username, String password) {
        this.credentials = new UsernamePasswordCredentialsProvider(username, password);
    }

    private void sshAuth(String prikeyFile) {
        String prikeyPath = System.getProperty("java.io.tmpdir") + "/rsa_prikey";
        File prikey = new File(prikeyPath);
        try {
            new TextFile().write(prikey, prikeyFile);
        } catch (IOException e) {
            // TODO 处理异常
            e.printStackTrace();
        }

        this.sshSessionFactory = new JschConfigSessionFactory() {
            @Override
            protected void configure(OpenSshConfig.Host hc, Session session) {
                // 不检查 RSA 公钥
                // TODO 后期使用 jsch 实现 ssh-keyscan 命令，动态生成 fingerprint
                Properties properties = new Properties();
                properties.put("StrictHostKeyChecking", "no");
                session.setConfig(properties);
            }

            @Override
            protected JSch createDefaultJSch(FS fs) throws JSchException {
                JSch defaultJSch = new JSch();
                JSch.setConfig("ssh-rsa", JSch.getConfig("signature.rsa"));
                defaultJSch.addIdentity(prikeyPath);
                return defaultJSch;
            }
        };
    }

    /**
     * 不存在则 clone，存在则 pull
     *
     * @param
     * @return
     * @date 2020-05-13 下午9:22
     */
    @Override
    public CommitInfo update(String remote, String branch, String local) throws Exception {
        String localRepoDir = local + dirname(remote);
        File file = new File(localRepoDir);
        if (!file.exists() && !file.mkdirs()) {
            throw new Exception("创建 " + localRepoDir + " 目录失败");
        }

        File localRepo = new File(localRepoDir + "/.git");
        CommitInfo commitInfo;
        if (!localRepo.exists()) {
            commitInfo = clone(remote, branch, localRepoDir);
        } else {
            try (Repository repo = new FileRepository(localRepo.getAbsolutePath())) {
                if (sshSessionFactory != null) {
                    PullCommand gitPull = new Git(repo).pull().setTransportConfigCallback(new TransportConfigCallback() {
                        @Override
                        public void configure(Transport transport) {
                            SshTransport sshTransport = (SshTransport) transport;
                            sshTransport.setSshSessionFactory(sshSessionFactory);
                        }
                    });
                    gitPull.call();
                } else {
                    PullCommand gitPull = new Git(repo).pull().setCredentialsProvider(credentials);
                    gitPull.call();
                }

                commitInfo = commitInfo(repo, branch);
                commitInfo.setChangedFiles(changedFileList(repo, branch));
            }
        }
        commitInfo.setRemote(remote);
        commitInfo.setBranch(branch);
        return commitInfo;
    }

    private CommitInfo clone(String remote, String branch, String local) throws Exception {
        CloneCommand gitClone;
        if (sshSessionFactory != null) {
            gitClone = Git.cloneRepository()
                    .setURI(remote)
                    .setBranch(branch)
                    .setDirectory(new File(local))
                    .setTransportConfigCallback(new TransportConfigCallback() {
                        @Override
                        public void configure(Transport transport) {
                            SshTransport sshTransport = (SshTransport) transport;
                            sshTransport.setSshSessionFactory(sshSessionFactory);
                        }
                    });
        } else {
            gitClone = Git.cloneRepository()
                    .setURI(remote)
                    .setBranch(branch)
                    .setDirectory(new File(local))
                    .setCredentialsProvider(credentials);
        }

        try (Git git = gitClone.call()) {
            Repository repo = git.getRepository();
            CommitInfo commitInfo = commitInfo(repo, branch);
            commitInfo.setChangedFiles(changedFileList(repo, branch));
            return commitInfo;
        }
    }

    private CommitInfo commitInfo(Repository repo, String branch) throws Exception {
        Ref head = repo.findRef("refs/heads/" + branch);
        ObjectId objectId = head.getObjectId();
        try (RevWalk walk = new RevWalk(repo)) {
            // 最近一次提交的信息
            RevCommit lastCommit = walk.parseCommit(objectId);
            long commitTime = lastCommit.getCommitTime();
            String commitAuthor = lastCommit.getAuthorIdent().getName();
            String commitMsg = lastCommit.getFullMessage();

            CommitInfo commitInfo = new CommitInfo();
            commitInfo.setCommitId(objectId.name().substring(0, 8));
            commitInfo.setMsCommitTime(commitTime*1000);
            commitInfo.setCommitAuthor(commitAuthor);
            if (commitMsg.length() > 1000) {
                commitInfo.setCommitMsg(commitMsg.substring(0, 1000));
            } else {
                commitInfo.setCommitMsg(commitMsg);
            }
            return commitInfo;
        }
    }

    private List<ChangedFile> changedFileList(Repository repo, String branch) throws Exception {
        // TODO 查找指定的 branch
        Ref headMaster = repo.findRef("refs/heads/" + branch);
        ObjectId objectId = headMaster.getObjectId();
        try (RevWalk walk = new RevWalk(repo)) {
            // 最近一次提交
            RevCommit newCommit = walk.parseCommit(objectId);
            walk.markStart(newCommit);
            // 遍历两次找到 newCommit 的前一次提交，newCommit 不是第一次提交
            walk.next();
            RevCommit oldCommit = walk.next();

            ObjectId newHead = newCommit.getTree().getId();
            ObjectId oldHead = oldCommit.getTree().getId();

            try (ObjectReader reader = repo.newObjectReader()) {
                CanonicalTreeParser newTreeIter = new CanonicalTreeParser();
                newTreeIter.reset(reader, newHead);
                CanonicalTreeParser oldTreeIter = new CanonicalTreeParser();
                oldTreeIter.reset(reader, oldHead);
                try (Git git = new Git(repo)) {
                    List<DiffEntry> diffs= git.diff()
                            .setNewTree(newTreeIter)
                            .setOldTree(oldTreeIter)
                            .call();

                    List<ChangedFile> changedFiles = new ArrayList<>();
                    for (DiffEntry entry : diffs) {
                        ChangedFile changedFile = new ChangedFile();
                        changedFile.setChangeType(entry.getChangeType().name());
                        changedFile.setOldFilePath(entry.getOldPath());
                        changedFile.setNewFilePath(entry.getNewPath());
                        changedFiles.add(changedFile);
                    }

                    return changedFiles;
                }
            }
        }
    }

    @Override
    public CommitInfo update(String remote, String branch, String local, String commitId) throws Exception {
        return null;
    }

    @Override
    public CommitInfo latestCommitInfo(String local, String branch) throws Exception {
        File localRepo = new File(local + "/.git");
        if (!localRepo.exists()) {
            return null;
        }

        try (Repository repo = new FileRepository(localRepo.getAbsolutePath())) {
            return commitInfo(repo, branch);
        }
    }

    private String dirname(String gitUrl) {
        int lastIndex = gitUrl.lastIndexOf("/");
        return gitUrl.substring(lastIndex).split(".git")[0];
    }
}
