/*
 * Copyright (c) 2019 Of Him Code Technology Studio
 * Jpom is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 * 			http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */
package org.dromara.jpom.plugin;

import cn.hutool.core.collection.CollStreamUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.comparator.VersionComparator;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.lang.Tuple;
import cn.hutool.core.util.StrUtil;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import lombok.Lombok;
import lombok.extern.slf4j.Slf4j;
import org.dromara.jpom.common.i18n.I18nMessageUtil;
import org.eclipse.jgit.api.*;
import org.eclipse.jgit.api.errors.CheckoutConflictException;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.api.errors.NoHeadException;
import org.eclipse.jgit.api.errors.TransportException;
import org.eclipse.jgit.errors.NoRemoteRepositoryException;
import org.eclipse.jgit.errors.NotSupportedException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.SubmoduleConfig.FetchRecurseSubmodulesMode;
import org.eclipse.jgit.merge.MergeStrategy;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.*;
import org.eclipse.jgit.util.FS;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.*;
import java.util.stream.Collectors;

/**
 * git工具
 * <p>
 * <a href="https://developer.aliyun.com/ask/275691">https://developer.aliyun.com/ask/275691</a>
 * <p>
 * <a href="https://github.com/centic9/jgit-cookbook">https://github.com/centic9/jgit-cookbook</a>
 *
 * @author bwcx_jzy
 * @author Hotstrip
 * add git with ssh key to visit repository
 * @since 2019/7/15
 **/
@Slf4j
public class JGitUtil {

    /**
     * 检查本地的remote是否存在对应的url
     *
     * @param url  要检查的url
     * @param file 本地仓库文件
     * @return true 存在对应url
     * @throws IOException     IO
     * @throws GitAPIException E
     */
    public static boolean checkRemoteUrl(String url, File file) throws IOException, GitAPIException {
        try (Git git = Git.open(file)) {
            RemoteListCommand remoteListCommand = git.remoteList();
            boolean urlTrue = false;
            List<RemoteConfig> list = remoteListCommand.call();
            end:
            for (RemoteConfig remoteConfig : list) {
                for (URIish urIish : remoteConfig.getURIs()) {
                    if (urIish.toString().equals(url)) {
                        urlTrue = true;
                        break end;
                    }
                }
            }
            return urlTrue;
        } catch (NoRemoteRepositoryException ex) {
            log.warn("JGit: No remote repository found for url: {}", url);
            return false;
        }
    }

    /**
     * 检查本地的仓库是否存在对应的分支
     *
     * @param branchName 要检查的 branchName
     * @param file       本地仓库文件
     * @return true 存在对应url
     * @throws IOException     IO
     * @throws GitAPIException E
     */
    private static boolean checkBranchName(String branchName, File file) throws IOException, GitAPIException {
        try (Git pullGit = Git.open(file)) {
            // 判断本地是否存在对应分支
            List<Ref> list = pullGit.branchList().call();
            for (Ref ref : list) {
                String name = ref.getName();
                if (StrUtil.equals(name, Constants.R_HEADS + branchName)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * 删除重新clone
     *
     * @param parameter   参数
     * @param branchName  分支
     * @param tagName     标签
     * @param printWriter 日志流
     * @param file        文件
     * @return git
     * @throws GitAPIException api
     * @throws IOException     删除文件失败
     */
    private static Git reClone(Map<String, Object> parameter, String branchName, String tagName, File file, PrintWriter printWriter) throws GitAPIException, IOException {
        println(printWriter, StrUtil.EMPTY);
        String string = I18nMessageUtil.get("i18n.auto_reclone_repository.60f6");
        println(printWriter, "JGit: " + string);
        if (!FileUtil.clean(file)) {
            FileUtil.del(file.toPath());
        }
        CloneCommand cloneCommand = Git.cloneRepository();
        if (printWriter != null) {
            Integer progressRatio = (Integer) parameter.get("reduceProgressRatio");
            cloneCommand.setProgressMonitor(new SmallTextProgressMonitor(printWriter, progressRatio));
        }
        if (branchName != null) {
            cloneCommand.setBranch(Constants.R_HEADS + branchName);
            cloneCommand.setBranchesToClone(Collections.singletonList(Constants.R_HEADS + branchName));
        }
        if (tagName != null) {
            cloneCommand.setBranch(Constants.R_TAGS + tagName);
        }
        String url = (String) parameter.get("url");
        CloneCommand command = cloneCommand.setURI(url)
            .setDirectory(file)
            .setCloneSubmodules(true);
        // 设置凭证
        setCredentials(command, parameter);
        return command.call();
    }

    /**
     * 设置仓库凭证
     *
     * @param transportCommand git 相关操作
     * @param parameter        参数
     */
    public static void setCredentials(TransportCommand<?, ?> transportCommand, Map<String, Object> parameter) {
        // 设置超时时间 秒
        Integer timeout = (Integer) parameter.get("timeout");
        // 设置账号密码
        Integer protocol = (Integer) parameter.get("protocol");
        String username = (String) parameter.get("username");
        String password = StrUtil.emptyToDefault((String) parameter.get("password"), StrUtil.EMPTY);
        if (protocol == 0) {
            // http
            CredentialsProvider credentialsProvider = new SslVerifyUsernamePasswordCredentialsProvider(username, password);
            transportCommand.setCredentialsProvider(credentialsProvider);
            //
            Optional.ofNullable(timeout)
                .map(integer -> integer <= 0 ? null : integer)
                .ifPresent(transportCommand::setTimeout);
        } else if (protocol == 1) {
            // ssh
            //File rsaFile = BuildUtil.getRepositoryRsaFile(repositoryModel);
            File rsaFile = (File) parameter.get("rsaFile");
            transportCommand.setTransportConfigCallback(transport -> {
                SshTransport sshTransport = (SshTransport) transport;
                sshTransport.setSshSessionFactory(new JschConfigSessionFactory() {
                    @Override
                    protected void configure(OpenSshConfig.Host hc, Session session) {
                        session.setConfig("StrictHostKeyChecking", "no");
                        // ssh 需要单独设置超时
                        Optional.ofNullable(timeout)
                            .map(integer -> integer <= 0 ? null : integer)
                            .ifPresent(integer -> {
                                try {
                                    session.setTimeout(integer * 1000);
                                } catch (JSchException e) {
                                    throw Lombok.sneakyThrow(e);
                                }
                            });
                    }

                    @Override
                    protected JSch createDefaultJSch(FS fs) throws JSchException {
                        JSch jSch = super.createDefaultJSch(fs);
                        if (rsaFile == null) {
                            return jSch;
                        }
                        // 添加私钥文件
                        //String rsaPass = repositoryModel.getPassword();
                        if (StrUtil.isEmpty(password)) {
                            jSch.addIdentity(rsaFile.getPath());
                        } else {
                            jSch.addIdentity(rsaFile.getPath(), password);
                        }
                        return jSch;
                    }
                });
            });
        } else {
            throw new IllegalStateException(I18nMessageUtil.get("i18n.protocol_type_not_supported.7a66"));
        }
    }

    private static Git initGit(Map<String, Object> parameter, String branchName, String tagName, File file, PrintWriter printWriter) {
        String url = (String) parameter.get("url");
        return Optional.of(file).flatMap(file12 -> {
                // 文件信息
                if (FileUtil.file(file12, Constants.DOT_GIT).exists()) {
                    return Optional.of(true);
                }
                return Optional.empty();
            }).flatMap(status -> {
                try {
                    // 远程地址
                    if (checkRemoteUrl(url, file)) {
                        return Optional.of(true);
                    }
                } catch (IOException | GitAPIException e) {
                    throw Lombok.sneakyThrow(e);
                }
                return Optional.empty();
            }).flatMap(aBoolean -> {
                if (StrUtil.isEmpty(tagName)) {
                    // 分支模式，继续验证
                    return Optional.of(true);
                }
                // 标签模式直接中断
                return Optional.empty();
            })
            .flatMap(status -> {
                // 本地分支
                try {
                    // 远程地址
                    if (checkBranchName(branchName, file)) {
                        return Optional.of(true);
                    }
                } catch (IOException | GitAPIException e) {
                    throw Lombok.sneakyThrow(e);
                }
                return Optional.empty();
            }).map(aBoolean -> {
                try {
                    return aBoolean ? Git.open(file) : reClone(parameter, branchName, tagName, file, printWriter);
                } catch (IOException | GitAPIException e) {
                    throw Lombok.sneakyThrow(e);
                }
            }).orElseGet(() -> {
                try {
                    return reClone(parameter, branchName, tagName, file, printWriter);
                } catch (GitAPIException | IOException e) {
                    throw Lombok.sneakyThrow(e);
                }
            });
    }

    /**
     * 获取仓库远程的所有分支
     *
     * @param parameter 参数
     * @return Tuple
     * @throws GitAPIException api
     */
    public static Tuple getBranchAndTagList(Map<String, Object> parameter) throws Exception {

        String url = (String) parameter.get("url");
        try {
            LsRemoteCommand lsRemoteCommand = Git.lsRemoteRepository()
                .setRemote(url);
            // 更新凭证
            setCredentials(lsRemoteCommand, parameter);
            //
            Collection<Ref> call = lsRemoteCommand
                .setHeads(true)
                .setTags(true)
                .call();
            if (CollUtil.isEmpty(call)) {
                return null;
            }
            Map<String, List<Ref>> refMap = CollStreamUtil.groupByKey(call, ref -> {
                String name = ref.getName();
                if (name.startsWith(Constants.R_TAGS)) {
                    return Constants.R_TAGS;
                } else if (name.startsWith(Constants.R_HEADS)) {
                    return Constants.R_HEADS;
                }
                return null;
            });

            // branch list
            List<Ref> branchListRef = refMap.get(Constants.R_HEADS);
            if (branchListRef == null) {
                return null;
            }
            List<String> branchList = branchListRef.stream()
                .map(ref -> {
                    String name = ref.getName();
                    if (name.startsWith(Constants.R_HEADS)) {
                        return name.substring((Constants.R_HEADS).length());
                    }
                    return null;
                })
                .filter(Objects::nonNull)
                .sorted((o1, o2) -> VersionComparator.INSTANCE.compare(o2, o1))
                .collect(Collectors.toList());

            // list tag
            List<Ref> tagListRef = refMap.get(Constants.R_TAGS);
            List<String> tagList = tagListRef == null ? new ArrayList<>() : tagListRef.stream()
                .map(ref -> {
                    String name = ref.getName();
                    if (name.startsWith(Constants.R_TAGS)) {
                        return name.substring((Constants.R_TAGS).length());
                    }
                    return null;
                })
                .filter(Objects::nonNull)
                .sorted((o1, o2) -> VersionComparator.INSTANCE.compare(o2, o1))
                .collect(Collectors.toList());
            return new Tuple(branchList, tagList);
        } catch (Exception t) {
            checkTransportException(t, null, null);
            return null;
        }
    }

    /**
     * 拉取对应分支最新代码
     *
     * @param parameter  参数
     * @param file       仓库路径
     * @param branchName 分支名
     * @return 返回最新一次提交信息
     * @throws IOException     IO
     * @throws GitAPIException api
     */
    public static String[] checkoutPull(Map<String, Object> parameter, File file, String branchName, PrintWriter printWriter) throws Exception {
        String url = (String) parameter.get("url");
        String path = FileUtil.getAbsolutePath(file);
        synchronized (StrUtil.concat(false, url, path).intern()) {
            try (Git git = initGit(parameter, branchName, null, file, printWriter)) {
                // 拉取代码
                PullResult pull = pull(git, parameter, branchName, printWriter);
                // 最后一次提交记录
                return getLastCommitMsg(file, false, branchName);
            } catch (Exception t) {
                checkTransportException(t, file, printWriter);
            }
        }
        return new String[]{StrUtil.EMPTY, StrUtil.EMPTY};
    }

    /**
     * 拉取远程最新代码
     *
     * @param git         仓库对象
     * @param branchName  分支
     * @param parameter   参数
     * @param printWriter 日志流
     * @return pull result
     * @throws Exception 异常
     */
    private static PullResult pull(Git git, Map<String, Object> parameter, String branchName, PrintWriter printWriter) throws Exception {
        Integer progressRatio = (Integer) parameter.get("reduceProgressRatio");
        SmallTextProgressMonitor progressMonitor = new SmallTextProgressMonitor(printWriter, progressRatio);
        // 放弃本地修改
        git.checkout().setName(branchName).setForced(true).setProgressMonitor(progressMonitor).call();
        //
        PullCommand pull = git.pull();
        //
        setCredentials(pull, parameter);
        //
        PullResult call = pull
            .setRemoteBranchName(branchName)
            .setProgressMonitor(progressMonitor)
            .setRecurseSubmodules(FetchRecurseSubmodulesMode.YES)
            .call();
        // 输出拉取结果
        if (call != null) {
            String fetchedFrom = call.getFetchedFrom();
            FetchResult fetchResult = call.getFetchResult();
            MergeResult mergeResult = call.getMergeResult();
            RebaseResult rebaseResult = call.getRebaseResult();
            if (mergeResult != null) {
                String string = I18nMessageUtil.get("i18n.merge_result.454b");
                println(printWriter, string + ": {}", mergeResult);
            }
            if (rebaseResult != null) {
                String string = I18nMessageUtil.get("i18n.rebase_result.55d4");
                println(printWriter, string + "：{}", rebaseResult);
            }
            if (fetchedFrom != null) {
                String string = I18nMessageUtil.get("i18n.source.d6c1");
                println(printWriter, string + ": {}", fetchedFrom);
            }
            //			if (fetchResult != null) {
            //				println(printWriter, "fetchResult {}", fetchResult);
            //			}
        }
        //
        SubmoduleUpdateCommand subUpdate = git.submoduleUpdate();
        setCredentials(subUpdate, parameter);
        Collection<String> rst = subUpdate
            .setProgressMonitor(progressMonitor)
            .setFetch(true)
            .setStrategy(MergeStrategy.THEIRS)
            .call();
        println(printWriter, String.join("\n", rst));
        return call;
    }

    /**
     * 拉取对应分支最新代码
     *
     * @param printWriter 日志输出流
     * @param parameter   参数
     * @param file        仓库路径
     * @param tagName     标签名
     * @throws IOException     IO
     * @throws GitAPIException api
     */
    public static String[] checkoutPullTag(Map<String, Object> parameter, File file, String tagName, PrintWriter printWriter) throws Exception {
        String url = (String) parameter.get("url");
        String path = FileUtil.getAbsolutePath(file);
        synchronized (StrUtil.concat(false, url, path).intern()) {
            try (Git git = initGit(parameter, null, tagName, file, printWriter)) {
                // 获取最后提交信息
                return getLastCommitMsg(file, true, tagName);
            } catch (Exception t) {
                checkTransportException(t, file, printWriter);
            }
        }
        return new String[]{StrUtil.EMPTY, StrUtil.EMPTY};
    }

    /**
     * 检查异常信息
     *
     * @param ex          异常信息
     * @param gitFile     仓库地址
     * @param printWriter 日志流
     * @throws TransportException 非账号密码异常
     */
    public static void checkTransportException(Exception ex, File gitFile, PrintWriter printWriter) throws Exception {
        println(printWriter, "");
        Throwable causedBy = ExceptionUtil.getCausedBy(ex, NotSupportedException.class);
        if (causedBy != null) {
            println(printWriter, I18nMessageUtil.get("i18n.current_address_may_not_be_git.41c6") + causedBy.getMessage());
            throw new IllegalStateException(I18nMessageUtil.get("i18n.current_address_may_not_be_git.41c6") + causedBy.getMessage(), ex);
        }
        causedBy = ExceptionUtil.getCausedBy(ex, NoRemoteRepositoryException.class);
        if (causedBy != null) {
            println(printWriter, I18nMessageUtil.get("i18n.remote_repository_does_not_exist.7009") + causedBy.getMessage());
            throw new IllegalStateException(I18nMessageUtil.get("i18n.remote_repository_does_not_exist.7009") + causedBy.getMessage(), ex);
        }
        causedBy = ExceptionUtil.getCausedBy(ex, RepositoryNotFoundException.class);
        if (causedBy != null) {
            println(printWriter, I18nMessageUtil.get("i18n.current_address_no_repository.db31") + causedBy.getMessage());
            throw new IllegalStateException(I18nMessageUtil.get("i18n.current_address_no_repository.db31") + causedBy.getMessage(), ex);
        }
        if (ex instanceof TransportException) {
            String msg = ex.getMessage();
            if (StrUtil.containsAny(msg, JGitText.get().notAuthorized, JGitText.get().authenticationNotSupported)) {
                throw new IllegalArgumentException(I18nMessageUtil.get("i18n.incorrect_account_credentials_or_unsupported_auth.1ef9") + msg, ex);
            }
            throw ex;
        } else if (ex instanceof NoHeadException) {
            println(printWriter, I18nMessageUtil.get("i18n.pull_code_exception_with_cleanup.a887") + ex.getMessage());
            if (gitFile == null) {
                throw ex;
            } else {
                FileUtil.del(gitFile);
            }
            throw ex;
        } else if (ex instanceof CheckoutConflictException) {
            println(printWriter, I18nMessageUtil.get("i18n.code_pull_conflict.6e8e") + ex.getMessage());
            throw ex;
        } else {
            println(printWriter, I18nMessageUtil.get("i18n.unknown_exception_on_pull_code.2b2e") + ex.getMessage());
            throw ex;
        }
    }

    /**
     * 输出日志信息
     *
     * @param printWriter 日志流
     * @param template    日志内容模版
     * @param params      参数
     */
    private static void println(PrintWriter printWriter, CharSequence template, Object... params) {
        if (printWriter == null) {
            return;
        }
        printWriter.println(StrUtil.format(template, params));
        // 需要 flush 让输出立即生效
        IoUtil.flush(printWriter);
    }

    /**
     * 解析仓库指定分支最新提交
     *
     * @param git        仓库
     * @param branchName 分支
     * @return objectID
     * @throws GitAPIException 异常
     */
    private static ObjectId getBranchAnyObjectId(Git git, String branchName) throws GitAPIException {
        List<Ref> list = git.branchList().call();
        for (Ref ref : list) {
            String name = ref.getName();
            if (name.startsWith(Constants.R_HEADS + branchName)) {
                return ref.getObjectId();
            }
        }
        return null;
    }

    /**
     * 解析仓库指定分支最新提交
     *
     * @param git     仓库
     * @param tagName 标签
     * @return objectID
     * @throws GitAPIException 异常
     */
    private static ObjectId getTagAnyObjectId(Git git, String tagName) throws GitAPIException {
        List<Ref> list = git.tagList().call();
        for (Ref ref : list) {
            String name = ref.getName();
            if (name.startsWith(Constants.R_TAGS + tagName)) {
                return ref.getObjectId();
            }
        }
        return null;
    }

    /**
     * 获取对应分支的最后一次提交记录
     *
     * @param file    仓库文件夹
     * @param refName 名称
     * @return String[] 第一个元素为最后一次 hash 值， 第二个元素为描述
     * @throws IOException     IO
     * @throws GitAPIException api
     */
    public static String[] getLastCommitMsg(File file, boolean tag, String refName) throws IOException, GitAPIException {
        try (Git git = Git.open(file)) {
            ObjectId anyObjectId = tag ? getTagAnyObjectId(git, refName) : getBranchAnyObjectId(git, refName);
            Objects.requireNonNull(anyObjectId, StrUtil.format(I18nMessageUtil.get("i18n.no_branch_or_tag_message.8ae3"), refName));
            //System.out.println(anyObjectId.getName());
            String lastCommitMsg = getLastCommitMsg(file, refName, anyObjectId);
            return new String[]{anyObjectId.getName(), lastCommitMsg};
        }
    }

    /**
     * 解析提交信息
     *
     * @param file     仓库文件夹
     * @param desc     描述
     * @param objectId 提交信息
     * @return 描述
     * @throws IOException IO
     */
    private static String getLastCommitMsg(File file, String desc, ObjectId objectId) throws IOException {
        try (Git git = Git.open(file)) {
            RevWalk walk = new RevWalk(git.getRepository());
            RevCommit revCommit = walk.parseCommit(objectId);
            String time = new DateTime(revCommit.getCommitTime() * 1000L).toString();
            PersonIdent personIdent = revCommit.getAuthorIdent();
            return StrUtil.format("{} {} {}[{}] {} {}",
                desc,
                revCommit.getShortMessage(),
                personIdent.getName(),
                personIdent.getEmailAddress(),
                time,
                revCommit.getParentCount());
        }
    }
}
