#include "git_client.h"
#include "update.h"
#include "worker.h"
#include "utils.h"

#include <QRegularExpression>
#include <QDebug>
#include <QDir>
#include <QStringList>


GitClient::GitClient(QString const &customerNrStr,
                     QString const &customerRepository,
                     QString const &workingDirectory,
                     QString const &branchName,
                     QObject *parent)
  : QObject(parent)
  , m_worker(qobject_cast<Worker *>(parent))
  , m_repositoryPath(QString("https://git.mimbach49.de/GerhardHoffmann/%1.git").arg(customerNrStr))
  , m_customerNr(customerNrStr)
  , m_workingDirectory(workingDirectory)
  , m_branchName(branchName)
  , m_customerRepository(customerRepository) {
    if (!m_worker) {
        qCritical() << "ERROR CASTING PARENT TO WORKER FAILED";
    }
}

bool GitClient::gitCloneCustomerRepository() {
    /*  Blobless clone
        ==============

        When using the --filter=blob:none option, the initial git clone will
        download all reachable commits and trees, and only download the blobs
        for commits when you do a git checkout. This includes the first checkout
        inside the git clone operation.

        The important thing to notice is that we have a copy of every blob at
        HEAD but the blobs in the history are not present. If your repository
        has a deep history full of large blobs, then this option can
        significantly reduce your git clone times. The commit and tree data is
        still present, so any subsequent git checkout only needs to download
        the missing blobs. The Git client knows how to batch these requests to
        ask the server only for the missing blobs.

        Further, when running git fetch in a blobless clone, the server only
        sends the new commits and trees. The new blobs are downloaded only
        after a git checkout. Note that git pull runs git fetch and then git
        merge, so it will download the necessary blobs during the git merge
        command.

        When using a blobless clone, you will trigger a blob download whenever
        you need the contents of a file, but you will not need one if you only
        need the OID (object-id) of a file. This means that git log can detect
        which commits changed a given path without needing to download extra
        data.

        This means that blobless clones can perform commands like git
        merge-base, git log, or even git log -- <path> with the same performance
        as a full clone.

        Commands like git diff or git blame <path> require the contents of the
        paths to compute diffs, so these will trigger blob downloads the first
        time they are run. However, the good news is that after that you will
        have those blobs in your repository and do not need to download them a
        second time. Most developers only need to run git blame on a small
        number of files, so this tradeoff of a slightly slower git blame command
        is worth the faster clone and fetch times.

        Note: git v2.18 does not support treeless clones: --filter=tree:0.
     */

    // Note: for some reason it is necessary to pass "--progress ---v",
    // otherwise QProcess returns an error of 128 = 0x80 for the command.

    QString gitCommand("git clone --progress -vvv --filter=blob:none ");
    gitCommand += m_repositoryPath;
    Command c(gitCommand);

    qInfo() << "IN CURRENT WD" << m_workingDirectory
            << "CLONE VIA COMMAND" << gitCommand;

    if (c.execute(m_workingDirectory)) { // execute the command in wd
        QString const result = c.getCommandResult();
        if (!result.isEmpty()) {
            // Cloning into 'customer_281'...\n
            int customer = -1;
            int cloning = result.indexOf("Cloning", 0, Qt::CaseInsensitive);
            if (cloning != -1) {
                customer = result.indexOf("customer_", cloning, Qt::CaseInsensitive);
                if (customer != -1) {
                    QString customerNr = result.mid(customer);
                    static constexpr char const ch = '\'';
                    int i = customerNr.indexOf(QChar(ch));
                    if (i != -1) {
                        if ((customerNr = customerNr.mid(0, i)) == m_customerNr) {
                            qInfo() << "CLONING" << m_repositoryPath << "OK";
                            return true;
                        }
                        Utils::printCriticalErrorMsg(
                            QString("ERROR CLONE RESULT HAS WRONG CUSTOMER-NR. (%1 != %2) CLONE_RESULT=%3")
                                .arg(customerNr)
                                .arg(m_customerNr)
                                .arg(result));
                        return false;
                    }
                }
            }
            Utils::printCriticalErrorMsg(
                QString("ERROR CLONE RESULT HAS WRONG FORMAT. CLONING=%1 CUSTOMER=%2 CLONE_RESULT=%3")
                    .arg(cloning)
                    .arg(customer)
                    .arg(result));
            return false;
        }
        Utils::printCriticalErrorMsg("ERROR CLONE RESULT IS EMPTY");
    }
    return false;
}

bool GitClient::copyGitConfigFromMaster() { // only allowed when called in
                                            // master branch (???)
    if (QDir(m_customerRepository).exists()) {
        QString const cp = QString("cp .gitconfig .git/config");
        Command c("bash");
        if (c.execute(m_customerRepository, QStringList() << "-c" << cp)) {
            qInfo() << "cp .gitconfig .git/config OK";
            return true;
        }
        qCritical() << "ERROR cp .gitconfig .git/config";
    }
    return false;
}

QStringList GitClient::gitBranchNames() {
    // git config --global pager.branch false
    QStringList bNames;
    if (QDir(m_customerRepository).exists()) {
        QString gitCommand("git branch -a");
        Command c(gitCommand);
        if (c.execute(m_customerRepository)) {
            QString const result = c.getCommandResult();
            return result.split('\n');
        }
    }
    return bNames;
}

bool GitClient::gitCheckoutBranch() {
    // TODO: nachsehen, ob der Branch ueberhaupt existiert

    if (QDir(m_customerRepository).exists()) {
        int zoneNr = Utils::read1stLineOfFile("/mnt/system_data/zone_nr");
        m_branchName = (zoneNr != 0)
            ? QString("zg1/zone%1").arg(zoneNr) : "master";

        QString gitCommand("git checkout ");
        gitCommand += m_branchName;

        Command c(gitCommand);
        return c.execute(m_customerRepository); // execute command in customerRepo
    }
    Utils::printCriticalErrorMsg(QString("ERROR ") + m_customerRepository + " DOES NOT EXIST");
    return false;
}

bool GitClient::gitCloneAndCheckoutBranch() {
    qInfo() << "CLONE" << m_repositoryPath << "AND CHECKOUT" << m_branchName;
    if (gitCloneCustomerRepository()) {
        //if (copyGitConfigFromMaster()) {
            if (gitCheckoutBranch()) {
                return true;
            } else {
                // TODO
            }
        //}
    }
    Utils::printCriticalErrorMsg(QString("CLONE ") + m_repositoryPath + " AND CHECKOUT FAILED");
    return false;
}

QStringList GitClient::gitShowReason(QString branchName) {
    QStringList lst;
    if (QDir(m_customerRepository).exists()) {
        // %h: commit (short form)
        // %s: commit message
        // %cI: commit date, strict ISO 8601 format
        // Note: branch with branchName has to exist: format zg1/zone1
        Command c(QString("git show origin/%1 -s --format=\"c=%h m=%s d=%cI\"").arg(branchName));
        if (c.execute(m_customerRepository)) {
            QString const s = c.getCommandResult().trimmed();
            int const c = s.indexOf("c=");
            int const m = s.indexOf("m=");
            int const d = s.indexOf("d=");

            QString msg{""}; // IsmasClient::getReasonForLastSendVersion();
            QString commit{""}, date{""};

            if (c != -1) {
                int start = c + 2;
                if (m >= start) {
                    int length = m - start;
                    commit = s.mid(start, length).trimmed();

                    start = m + 2;
                    if (d >= start) {
                        length = d - start;
                        // msg = " (";
                        msg = s.mid(start, length).trimmed();
                        // msg += ")";

                        start = d + 2;
                        date = s.mid(start);
                    }
                }

                if (!commit.isEmpty() && !msg.isEmpty() && !date.isEmpty()) {
                    lst << commit << msg << date;
                }
            }
        }
    } else {
        qCritical() << "CUSTOMER_REPOSITORY" << m_customerRepository
                    << "DOES NOT EXIST";
    }
    return lst;
}

/*
 Zu beachten: wird eine datei neu hinzugefuegt (git add/commit) dann aber gleich
 wieder geloscht, so wird sie im diff nicht angezeigt.
 */
std::optional<QStringList> GitClient::gitDiff(QString const &commits) {
    if (QDir(m_customerRepository).exists()) {
        // 409f198..6c22726
        QString gitCommand("git diff --compact-summary ");
        gitCommand += commits;

        Command c(gitCommand);
        if (c.execute(m_customerRepository)) { // execute command in local customerRepo
            QString s = c.getCommandResult().trimmed();
            Utils::printInfoMsg("GIT DIFF RESULT " + s);

            QStringList lines = Update::split(s, '\n');
            QStringList fileNames;
            // each line has the format "etc/psa_config/DC2C_print01.json | 1 +
            // or the format            "etc/psa_config/DC2C_print01.json (new) | 1 +
            // the filenames are relativ to the repository
            for (int i = 0; i < lines.size(); ++i) {
                QString const &line = lines.at(i);
                int newIndex = line.indexOf("(new");    // for new files
                int goneIndex = line.indexOf("(gone");  // for removed files
                int modeIndex = line.indexOf("(mode");
                int pipeIndex = line.indexOf('|');
                if (newIndex != -1) {
                    QString file = line.left(newIndex).trimmed();
                    qInfo() << "FILE (NEW)" << file;
                    fileNames << file;
                } else
                if (modeIndex != -1) {
                    QString const file = line.left(modeIndex).trimmed();
                    qInfo() << "FILE (MODE)" << file;
                    fileNames << file;
                } else
                if (goneIndex != -1) {
                    QString const file = line.left(goneIndex).trimmed();
                    qCritical() << "FILE (GONE)" << file;
                } else
                if (pipeIndex != -1) {
                    QString const file = line.left(pipeIndex).trimmed();
                    qInfo() << "FILE (PIPE)" << file;
                    fileNames << file;
                }
            }
            if (!fileNames.isEmpty()) {
                return fileNames;
            }
        }
    }
    return std::nullopt;
}

bool GitClient::gitFsck() {
    bool r = false;
    if (QDir(m_customerRepository).exists()) {
        Command c("git fsck");
        if ((r = c.execute(m_customerRepository)) == false) {
            QString const &s = c.getCommandResult().trimmed();
            Utils::printCriticalErrorMsg(QString("GIT FSCK FAILED: %1").arg(s));
        }
    }
    return r;
}

bool GitClient::branchExistsRemotely() {
    bool remoteBranchExists = false;
    if (QDir(m_customerRepository).exists()) {
        qInfo() << "BRANCH NAME" << m_branchName;
        QString const cmd = QString("git ls-remote --exit-code --heads origin %1").arg(m_branchName);
        Command c(cmd);
        if (c.execute(m_customerRepository)) {
            // expected result: c16c833c8778c1b3691a74afee5a469177e4e69b refs/heads/zg1/zone1000
            QString const s = c.getCommandResult().trimmed();


            if (!s.isEmpty()) {
                // the result is only one line
                if ((remoteBranchExists = s.contains(m_branchName)) == true) {
                    qCritical() << "(" << __func__ << ":" << __LINE__ << ") branch"
                                << m_branchName << "EXISTS REMOTELY. (" << s << ")";
                }
            } else {
                Utils::printCriticalErrorMsg(QString("EMPTY RESULT FOR CMD %1").arg(cmd));
            }
        } else {
            Utils::printCriticalErrorMsg(QString("FAILED TO EXEC '%1'").arg(cmd));
        }
    }
    return remoteBranchExists;
}

bool GitClient::branchExistsLocally() {
    Command c("git branch -l");
    if (c.execute(m_customerRepository)) {
        QString const s = c.getCommandResult().trimmed();
        if (!s.isEmpty()) {
            QStringList lines = Update::split(s, '\n');
            if (!lines.empty()) {
                for (int i=0; i < lines.size(); ++i) {
                    QString line = lines.at(i);
                    // expected: * [new branch]      zg1/zone12 -> origin/zg1/zone12"
                    if (line.contains(m_branchName)) {
                        if (m_worker) {
                            QStringList lst(QString("BRANCH-NAME %1 CONTAINED IN RESULT %2").arg(m_branchName).arg(s));
                            m_worker->CONSOLE(lst) << Worker::UPDATE_STEP::PULL_NEW_BRANCH;
                        }
                        return true;
                    }
                }

                if (m_worker) {
                    QStringList lst(QString("BRANCH-NAME %1 NOT CONTAINED IN RESULT %2").arg(m_branchName).arg(s));
                    m_worker->CONSOLE(lst) << Worker::UPDATE_STEP::PULL_NEW_BRANCH_FAILURE;
                }

            } else {
                if (m_worker) {
                    QStringList lst(QString("'git branch -l' RETURNED NO LINES"));
                    m_worker->CONSOLE(lst) << Worker::UPDATE_STEP::PULL_NEW_BRANCH_FAILURE;
                }
            }
        } else {
            if (m_worker) {
                QStringList lst(QString("'git branch -l' RETURNED EMPTY RESULT"));
                m_worker->CONSOLE(lst) << Worker::UPDATE_STEP::PULL_NEW_BRANCH_FAILURE;
            }
        }
    } else {
        if (m_worker) {
            QStringList lst(QString("FAILED TO EXEC 'git branch -l'"));
            m_worker->CONSOLE(lst) << Worker::UPDATE_STEP::PULL_NEW_BRANCH_FAILURE;
        }
    }

    return false;
}

bool GitClient::gitPullNewBranches() {

    if (QDir(m_customerRepository).exists()) {
        Command c("git pull");
        if (c.execute(m_customerRepository)) {
            QString const s = c.getCommandResult().trimmed();
            // expected: Already up-to-date.
            if (!s.isEmpty()) {
                QStringList lst;
                QString msg(QString("GIT-PULL-NEW-BRANCH. RESULT=%1").arg(s));
                if (m_worker) {
                    m_worker->CONSOLE(lst) << Worker::UPDATE_STEP::PULL_NEW_BRANCH;
                }
                return true;
            }
        }
    }
    return false;
}

/*
 Hat sich nichts geaendert, so werden auch keine Commits <>..<> angezeigt
 */
std::optional<QString> GitClient::gitPull() {
    if (QDir(m_customerRepository).exists()) {
        qInfo() << "BRANCH NAME" << m_branchName;

        Command c("git pull");
        if (c.execute(m_customerRepository)) {
            QString const s = c.getCommandResult().trimmed();
            if (!s.isEmpty()) {
                QStringList lines = Update::split(s, '\n');
                worker()->CONSOLE(lines) << Worker::UPDATE_STEP::UPDATE_REPOSITORY;
                if (!lines.empty()) {
                    static const QRegularExpression alreadyUpToDate("^\\s*Already\\s+up\\s+to\\s+date.*$");
                    if (std::none_of(lines.cbegin(), lines.cend(),
                        [](QString const &s) { return s.contains(alreadyUpToDate); })) {
                        int zoneNr = Utils::read1stLineOfFile("/mnt/system_data/zone_nr");
                        m_branchName = (zoneNr != 0) ? QString("zg1/zone%1").arg(zoneNr) : "master";
                        // lines can look like this:
                        // From https://git.mimbach49.de/GerhardHoffmann/customer_281
                        //   41ec581..5d25ac3  master     -> origin/master
                        //   ff10f57..43530a1  zg1/zone1  -> origin/zg1/zone1
                        //   6ed893f..5d9882c  zg1/zone2  -> origin/zg1/zone2
                        //   4384d17..77045d8  zg1/zone3  -> origin/zg1/zone3
                        //   89d2812..36a0d74  zg1/zone5  -> origin/zg1/zone5
                        //
                        // More exactly:
                        // remote: Counting objects: 382, done.
                        // remote: Compressing objects: 100% (203/203), done.
                        // remote: Total 278 (delta 177), reused 103 (delta 59)
                        // Receiving objects: 100% (278/278), 4.89 MiB | 539 KiB/s, done.
                        // Resolving deltas: 100% (177/177), completed with 40 local objects.
                        // From ssh://longair@pacific.mpi-cbg.de/srv/git/fiji
                        // 3036acc..9eb5e40  debian-release-20081030 -> origin/debian-release-20081030
                        // * [new branch]      debian-release-20081112 -> origin/debian-release-20081112
                        // * [new branch]      debian-release-20081112.1 -> origin/debian-release-20081112.1
                        // 3d619e7..6260626  master     -> origin/master
                        //
                        // The most important bits here are the lines like these:
                        //
                        //     3036acc..9eb5e40  debian-release-20081030 -> origin/debian-release-20081030
                        //   * [new branch]      debian-release-20081112 -> origin/debian-release-20081112
                        //
                        // The first line of these two shows that your remote-tracking branch
                        // origin/debian-release-20081030 has been advanced from the commit 3036acc to 9eb5e40.
                        // The bit before the arrow is the name of the branch in the remote repository.
                        // The second line similarly show that since we last did this, a new remote tracking
                        // branch has been created. (git fetch may also fetch new tags if they have appeared
                        // in the remote repository.)

                        bool found = false;
                        for (int i=0; i < lines.size(); ++i) {
                            if (lines.at(i).contains(m_branchName)) {
                                found = true;
                                // 409f198..6c22726  zg1/zone1  -> origin/zg1/zone1
                                static QRegularExpression re("(^\\s*)([0-9A-Fa-f]+..[0-9A-Fa-f]+)(.*$)");
                                QRegularExpressionMatch match = re.match(lines.at(i));
                                if (match.hasMatch()) {
                                    if (re.captureCount() == 3) { // start with full match (0), then the other 3 matches
                                        QString const matchCaptured = match.captured(2);
                                        worker()->CONSOLE(QStringList(matchCaptured)) << Worker::UPDATE_STEP::UPDATE_REPOSITORY;
                                        return matchCaptured;
                                    } else {
                                        QStringList lst(QString("(wrong capture count (%1)").arg(re.captureCount()));
                                        worker()->GUI(lst) << (worker()->CONSOLE(lst) << Worker::UPDATE_STEP::UPDATE_REPOSITORY_FAILURE);
                                    }
                                } else {
                                    QStringList lst("no regex-match for commits");
                                    worker()->GUI(lst) << (worker()->CONSOLE(lst) << Worker::UPDATE_STEP::UPDATE_REPOSITORY_FAILURE);
                                }
                            }
                        }
                        if (!found) {
                            QStringList lst(QString("unknown branch name ") + m_branchName);
                            worker()->GUI(lst) << (worker()->CONSOLE(lst) << Worker::UPDATE_STEP::UPDATE_REPOSITORY_FAILURE);
                        }
                    } else {
                        return "Already up to date";
                    }
                } else {
                    QStringList lst(QString("WRONG FORMAT FOR RESULT OF 'GIT PULL' ") + s);
                    worker()->GUI(lst) << (worker()->CONSOLE(lst) << Worker::UPDATE_STEP::UPDATE_REPOSITORY_FAILURE);
                }
            } else {
                QStringList lst("EMPTY RESULT FOR 'GIT PULL'");
                worker()->GUI(lst) << (worker()->CONSOLE(lst) << Worker::UPDATE_STEP::UPDATE_REPOSITORY_FAILURE);
            }
        }
    } else {
        QStringList lst(QString("REPOSITORY ") + m_customerRepository + " DOES NOT EXIST");
        worker()->GUI(lst) << (worker()->CONSOLE(lst) << Worker::UPDATE_STEP::UPDATE_REPOSITORY_FAILURE);
    }
    return std::nullopt;
}

std::optional<QStringList> GitClient::gitMerge() {
    Command c("git merge");
    if (c.execute(m_workingDirectory)) {
        QString s = c.getCommandResult();
        QStringList lst = Update::split(s, '\n');
        return lst;
    }
    return std::nullopt;
}

QString GitClient::gitLastCommit(QString fileName) {
    if (QDir(m_customerRepository).exists()) {
        QString const filePath
            = QDir::cleanPath(m_customerRepository + QDir::separator() + fileName);
        if (QFile(filePath).exists()) {
            QString const gitCommand = QString("git log %1 | head -n 1").arg(fileName);
            Command c("bash");
            if (c.execute(m_customerRepository, QStringList() << "-c" << gitCommand)) {
                QString const r = c.getCommandResult();
                int const idx = r.indexOf("commit ");
                if (idx != -1) {
                    return r.mid(idx + 8).trimmed();
                }
            }
        }
    }
    return "";
}

// fileName has to an absolute path
QString GitClient::gitBlob(QString fileName) {
    QFileInfo fi(fileName);
    if (fi.exists()) {
        QString const gitCommand = QString("git hash-object %1").arg(fileName);
        Command c(gitCommand);
        if (c.execute("/tmp")) {
            return c.getCommandResult().trimmed();
        }
    }
    return "N/A";
}

QString GitClient::gitCommitForBlob(QString blob) {
    if (QDir(m_customerRepository).exists()) {
        QString const gitCommand
            = QString("git whatchanged --all --find-object=%1 | head -n 1").arg(blob);
        Command c(gitCommand);
        if (c.execute(m_customerRepository)) {
            return c.getCommandResult();
        }
    }
    return "";
}

bool GitClient::gitIsFileTracked(QString fName) {
    if (QDir(m_customerRepository).exists()) {
        QString const gitCommand
            = QString("git ls-files --error-unmatch %1").arg(fName);
        Command c(gitCommand);
        return c.execute(m_customerRepository);
    }
    return false;
}


//get_commit_for_blob () {
//    # search for the blob in all commits for the file(name) $1
//    echo $(git log --all --pretty=format:%H -- $2   |
//           xargs -I{} bash -c "git ls-tree {} -- $2 |
//           grep -q $1 && echo -n {} && head -n 1")
//}