/*
 * Copyright 2009- ECMWF.
 *
 * This software is licensed under the terms of the Apache Licence version 2.0
 * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
 * In applying this licence, ECMWF does not waive the privileges and immunities
 * granted to it by virtue of its status as an intergovernmental organisation
 * nor does it submit to any jurisdiction.
 */

#include "VFileTransfer.hpp"

#include <QFileInfo>
#include <QMap>
#include <QProcess>
#include <QString>
#include <QtGlobal>

#include "DirectoryHandler.hpp"
#include "UiLog.hpp"
#include "VConfig.hpp"
#include "VProperty.hpp"

static QMap<QProcess::ProcessError, QString> errorStr;

// #define UI_FILETRANSFER_DEBUG_

VFileTransferCore::VFileTransferCore(QObject* parent) : QObject(parent), scriptName_("ecflow_ui_transfer_file.sh") {
    if (errorStr.isEmpty()) {
        errorStr[QProcess::FailedToStart] = "failed to start";
        errorStr[QProcess::Crashed]       = "crashed";
        errorStr[QProcess::Timedout]      = "timed out";
        errorStr[QProcess::WriteError]    = "failed with write error";
        errorStr[QProcess::ReadError]     = "failed with read error";
        errorStr[QProcess::UnknownError]  = "failed";
    }
}

VFileTransferCore::~VFileTransferCore() = default;

bool VFileTransferCore::isActive() const {
    return (proc_ && proc_->state() != QProcess::NotRunning);
}

void VFileTransferCore::clear() {
    if (isActive()) {
        stopTransfer(false, false);
    }

    remoteUser_.clear();
    remoteHost_.clear();
    remoteUserAndHost_.clear();
    sourceFile_.clear();
    fResult_.reset();
    transferDuration_ = 0;
    socksPort_.clear();
    useSocks_ = false;
}

void VFileTransferCore::stopTransfer(bool broadcast) {
    stopTransfer(broadcast, true);
}

// Use doClear carefully to avoid infinite recurso
void VFileTransferCore::stopTransfer(bool broadcast, bool doClear) {
    if (proc_) {
        proc_->kill();
        if (broadcast) {
            // on Mac OS (Big Sur) with Qt6 the slotProcFinished() is not
            // called automatically so we need to call it here!
            slotProcFinished(1, QProcess::CrashExit);
        }
        else {
            if (proc_) {
                proc_->disconnect(this);
                proc_->deleteLater();
                proc_ = nullptr;
            }
        }
        if (doClear) {
            clear();
        }
    }
}

void VFileTransferCore::transferIt() {
    if (proc_) {
        proc_->disconnect(this);
        proc_->kill();
        proc_->deleteLater();
        proc_ = nullptr;
    }
    Q_ASSERT(proc_ == nullptr);

    if (!proc_) {
        proc_ = new QProcess(this);

        connect(proc_,
                SIGNAL(finished(int, QProcess::ExitStatus)),
                this,
                SLOT(slotProcFinished(int, QProcess::ExitStatus)));

#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)
        connect(proc_,
                SIGNAL(errorOccurred(QProcess::ProcessError)),
                this,
                SLOT(slotErrorOccurred(QProcess::ProcessError)));
#endif
    }

    Q_ASSERT(proc_->state() == QProcess::NotRunning);

    fResult_ = VFile::createTmpFile(true); // we will delete the file from disk

    // If there is an exe dir we check if it is added to the PATH env
    // variable.
    QString exeDir = QString::fromStdString(DirectoryHandler::etcDir());
    if (!exeDir.isEmpty()) {
        QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
        QString envPath         = env.value("PATH");
        if (!envPath.contains(exeDir)) {
            env.insert("PATH", exeDir + ":" + envPath);
            proc_->setProcessEnvironment(env);
        }
    }
    /*
        if(remoteUid.isEmpty() || remoteUid == "$USER")
           remoteUid = "__USER__";

        remoteUserAndHost_ = remoteUid + "@" + host*/
    ;

    QString command = buildCommand();

#ifdef UI_FILETRANSFER_DEBUG_
    UiLog().dbg() << UI_FN_INFO;
    UiLog().dbg() << " command=" << command;
    UiLog().dbg() << " exeDir=" << exeDir;
    UiLog().dbg() << " program=" << proc_->program();
    UiLog().dbg() << " arguments=" << proc_->arguments();
#endif

    stopper_.start();

#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
    proc_->start("/bin/sh", QStringList() << "-c" << command);
#else
    proc_->start("/bin/sh -c \"" + command + "\"");
#endif
}

void VFileTransferCore::slotProcFinished(int exitCode, QProcess::ExitStatus exitStatus) {
#ifdef UI_FILETRANSFER_DEBUG_
    UI_FN_DBG
#endif

    if (!proc_) {
        return;
    }

    transferDuration_ = stopper_.elapsed();

    QString stdErrTxt = stdErr();
    QString stdOutTxt(proc_->readAllStandardOutput());

    stdErrTxt = stdErrTxt.trimmed();
    stdOutTxt = stdOutTxt.trimmed();

#ifdef UI_FILETRANSFER_DEBUG_
    UiLog().dbg() << " exitCode=" << exitCode << " exitStatus=" << exitStatus;
    UiLog().dbg() << " errorString=" << proc_->errorString();
    UiLog().dbg() << " stdout=" << stdOutTxt;
    UiLog().dbg() << " stderr=" << stdErrTxt;
#endif

    QString errTxt = stdOutTxt;
    if (!errTxt.isEmpty()) {
        errTxt += "\n\n";
    }
    errTxt += stdErrTxt;
    QString errPreTxt = "Script <b>" + scriptName_ + "</b> ";
    errPreTxt += errorStr.value(proc_->error(), "failed") + "!\n\n";
    errTxt.prepend(errPreTxt);

    if (exitCode == 0 && exitStatus == QProcess::NormalExit) {
        if (checkResults()) {
            Q_EMIT transferFinished();
            return;
        }
        else {
            Q_EMIT transferFailed(errTxt);
        }
    }

    Q_EMIT transferFailed(errTxt);

    if (proc_) {
        proc_->deleteLater();
        proc_ = nullptr;
    }
}

// It is called when on MacOs with Qt6 and the process fails to start!
#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)
void VFileTransferCore::slotErrorOccurred(QProcess::ProcessError) {
    #ifdef UI_FILETRANSFER_DEBUG_
    UI_FN_DBG
    if (proc_) {
        UiLog().dbg() << "errorString=" << proc_->errorString();
    }
    #endif
    slotProcFinished(1, QProcess::CrashExit);
}
#endif

QString VFileTransferCore::stdErr() {
    if (ignoreSetX_) {
        QString res;
        QString txt(proc_->readAllStandardError());
        Q_FOREACH (QString s, txt.split("\n")) {
            if (!s.startsWith("+ ")) {
                res += s + "\n";
            }
        }
        return res;
    }

    return QString(proc_->readAllStandardError());
}

QString VFileTransferCore::buildSocksProxyJump() {
    QString s;
#if 0
    if (VConfig::instance()->proxychainsUsed()) {
        auto p1 = VConfig::instance()->find("network.sshJump.proxyJumpUser");
        auto p2 = VConfig::instance()->find("network.sshJump.proxyJumpHost");
        if (p1 && p2) {
            QString user = p1->valueAsString().simplified();
            QString host = p2->valueAsString().simplified();
            if (!host.isEmpty()) {
                if (user == "$USER") {
                    if (char* ch = getenv("USER")) {
                        user = QString(ch);
                    }
                }
                s = user + "@" + p2->valueAsString();
            }
        }
    }
#endif
    return s;
}

QString VFileTransferCore::socksPort() {
    if (VConfig::instance()->proxychainsUsed()) {
        auto p = VConfig::instance()->find("network.socks.port");
        if (p) {
            return p->valueAsString();
        }
    }
    return {};
}

QString VFileTransferCore::socksUser() {
    if (VConfig::instance()->proxychainsUsed()) {
        auto p = VConfig::instance()->find("network.socks.uid");
        if (p) {
            auto user = p->valueAsString().simplified();
            if (user == "$USER") {
                user.clear();
            }
            return user;
        }
    }
    return {};
}

void VFileTransferCore::socksRemoteUserAndHost(QString& user, QString& host) {
    user.clear();
    host.clear();
#if 0
    if (VConfig::instance()->proxychainsUsed()) {
        auto p1 = VConfig::instance()->find("network.socks.remoteUser");
        auto p2 = VConfig::instance()->find("network.socks.remoteHost");
        if (p1 && p2) {
            user = p1->valueAsString();
            if (user.isEmpty() || user == "$USER") {
                if (char* ch = getenv("USER")) {
                    user = QString(ch);
                }
            }
            host = p2->valueAsString();
        }
    }
#endif
}

//============================================
//
// VFileTransfer
//
//============================================

void VFileTransfer::clear() {
    VFileTransferCore::clear();
    byteMode_    = AllBytes;
    byteVal_     = 0;
    useMetaData_ = false;
    fMetaRes_.reset();
    modTime_ = 0;
    checkSum_.clear();
}

void VFileTransfer::transferLocalViaSocks(QString sourceFile, ByteMode byteMode, size_t byteVal) {
    transferLocalViaSocks(sourceFile, byteMode, byteVal, false, 0, {});
}

void VFileTransfer::transferLocalViaSocks(QString sourceFile,
                                          ByteMode byteMode,
                                          size_t byteVal,
                                          size_t remoteModTime,
                                          const std::string& remoteCheckSum) {
    transferLocalViaSocks(sourceFile, byteMode, byteVal, true, remoteModTime, remoteCheckSum);
}

void VFileTransfer::transferLocalViaSocks(QString sourceFile,
                                          ByteMode byteMode,
                                          size_t byteVal,
                                          bool useMetaData,
                                          size_t remoteModTime,
                                          const std::string& remoteCheckSum) {
    clear();

    sourceFile_  = sourceFile;
    byteMode_    = byteMode;
    byteVal_     = byteVal;
    useMetaData_ = useMetaData;
    modTime_     = remoteModTime;
    checkSum_    = remoteCheckSum;
    remoteUser_  = socksUser();
    socksPort_   = socksPort();
    useSocks_    = true;

    transferIt();
}

void VFileTransfer::transfer(QString sourceFile, QString host, QString remoteUid, ByteMode byteMode, size_t byteVal) {
    clear();

    remoteUser_  = remoteUid;
    remoteHost_  = host;
    sourceFile_  = sourceFile;
    byteMode_    = byteMode;
    byteVal_     = byteVal;
    useMetaData_ = false;
    useSocks_    = false;

    transferIt();
}

QString VFileTransfer::buildCommand() {
    QString byteModeStr = "all";
    if (byteMode_ == BytesFromPos) {
        byteModeStr = "pos";
    }
    else if (byteMode_ == LastBytes) {
        byteModeStr = "last";
    }

    QString proxyJump = buildSocksProxyJump();

    QString command = scriptName_;

    if (useMetaData_) {
        command += " -m file_md ";
    }
    else {
        command += " -m file ";
    }

    auto targetFile = QString::fromStdString(fResult_->path());

    command += "-s \'" + sourceFile_ + "\' -t " + " \'" + targetFile + "\' -b " + byteModeStr + " -v " +
               QString::number(byteVal_);

    if (proxyJump != "__NOJUMP__" && !proxyJump.isEmpty()) {
        command += " -j " + proxyJump;
    }

    auto u = remoteUser_;
    if (u.isEmpty() || u == "$USER") {
        u = "__USER__";
    }
    command += " -u " + u;

    if (useSocks_) {
        command += " -p " + socksPort_;
    }
    else {
        command += " -h " + remoteHost_;
    }

    //    command += "-s \'" + sourceFile_ + "\' -u " + remoteUid + " -h " + host +
    //            " -t " + " \'" + targetFile + "\' -j " + proxyJump +
    //            " -b " + byteModeStr + " -v " +  QString::number(byteVal_);

    if (useMetaData_) {
        fMetaRes_ = VFile::createTmpFile(true); // we will delete the file from disk
        Q_ASSERT(fMetaRes_);
        command += " -x \'" + QString::fromStdString(fMetaRes_->path()) + "\'  -o " + QString::number(modTime_) +
                   " -c " + ((!checkSum_.empty()) ? QString::fromStdString(checkSum_) : "x");
    }

    return command;
}

bool VFileTransfer::checkResults() {
    if (!fResult_) {
        return false;
    }

    if (useMetaData_) {
        // this sets the delta contents
        if (!readMetaData()) {
            return false;
        }
    }
    else {
        fResult_->setDeltaContents(byteMode_ == BytesFromPos);
    }

    fResult_->setSourcePath(sourceFile_.toStdString());
    fResult_->setFetchMode(VFile::TransferFetchMode);
    std::string method;
    if (!remoteUserAndHost_.isEmpty()) {
        method = "from " + remoteUserAndHost_.toStdString() + " via ssh";
    }
    else {
        method = "via ssh";
    }
    fResult_->setFetchModeStr(method);
    fResult_->setTransferDuration(transferDuration_);
    fResult_->setFetchDate(QDateTime::currentDateTime());

#ifdef UI_FILETRANSFER_DEBUG_
    if (fResult) {
        UiLog().dbg() << UI_FN_INFO << "result is null";
    }
    else {
        UiLog().dbg() << UI_FN_INFO << "result size=" << fResult_->sizeInBytes();
    }
#endif
    return (!fResult_->isEmpty() || fResult_->hasDeltaContents() || (byteMode_ == LastBytes && byteVal_ != 0));
}

bool VFileTransfer::readMetaData() {
    //  format:
    //      retCode:modTime:checkSum:padding
    //
    //      where:
    //          - retCode is a 1-digit number
    //          - modeTime/checkSum can be empty
    //          - padding can be empty
    //      note:
    //          - in AllBytes mode the retCode can only be "'0"
    //          - in delta (=BytesFromPos) mode:
    //                retCode = "0" : delta transferred
    //                retCode = "1" : the whole file transferred
    //          - in delta mode a single "1" message is allowed
    //
    //  e.g.:
    //      0:1662369142:8fssaf:abcd
    //      0:1662369142::abcd
    //      1:::
    //      0
    if (useMetaData_) {
        if (!fMetaRes_) {
            return false;
        }
        QFile f(fMetaRes_->path().c_str());
        if (f.open(QFile::ReadOnly | QFile::Text)) {
            QString ds = f.readAll();
            ds         = ds.simplified();
#ifdef UI_FILETRANSFER_DEBUG_
            UiLog().dbg() << UI_FN_INFO << "ds=" << ds;
#endif
            if (byteMode_ == BytesFromPos && ds == "1") {
                fResult_->setDeltaContents(true);
                fResult_->setSourceModTime(modTime_);
                fResult_->setSourceCheckSum(checkSum_);
                return true;
            }

            auto lst = ds.split(":");
#ifdef UI_FILETRANSFER_DEBUG_
            UiLog().dbg() << " lst=" << lst;
#endif
            if (lst.count() >= 3) {
                if (byteMode_ == AllBytes) {
                    fResult_->setDeltaContents(false);
                    if (lst[0] != "0") {
                        return false;
                    }
                }
                else if (byteMode_ == BytesFromPos) {
                    // all file
                    if (lst[0] == "0") {
                        fResult_->setDeltaContents(false);
                        // delta
                    }
                    else if (lst[0] == "1") {
                        fResult_->setDeltaContents(true);
                    }
                    else {
                        return false;
                    }
                }
                else {
                    return false;
                }
                fResult_->setSourceModTime(lst[1].toUInt());
                fResult_->setSourceCheckSum(lst[2].toStdString());
                return true;
            }
        }
        return false;
    }
    return true;
}

//============================================
//
// VDirTransfer
//
//============================================

void VDirTransfer::transfer(QString sourceFile, QString host, QString remoteUid) {
    clear();

    remoteHost_ = host;
    remoteUser_ = remoteUid;
    sourceFile_ = sourceFile;

    transferIt();
}

void VDirTransfer::transferLocalViaSocks(QString sourceFile) {
    clear();

    sourceFile_ = sourceFile;
    remoteUser_ = socksUser();
    socksPort_  = socksPort();
    useSocks_   = true;

    transferIt();
}

QString VDirTransfer::buildCommand() {
    QString proxyJump = buildSocksProxyJump();

    auto targetFile = QString::fromStdString(fResult_->path());

    QString command = scriptName_ + " -m dir -s \'" + sourceFile_ + "\' -t " + " \'" + targetFile + "\' ";

    if (proxyJump != "__NOJUMP__" && !proxyJump.isEmpty()) {
        command += " -j " + proxyJump;
    }

    auto u = remoteUser_;
    if (u.isEmpty() || u == "$USER") {
        u = "__USER__";
    }
    command += " -u " + u;

    if (useSocks_) {
        command += " -p " + socksPort_;
    }
    else {
        command += " -h " + remoteHost_;
    }

    return command;
}

bool VDirTransfer::checkResults() {
    if (fResult_) {
        fResult_->setFetchMode(VFile::TransferFetchMode);
        std::string method;
        if (!remoteUserAndHost_.isEmpty()) {
            method = "from " + remoteUserAndHost_.toStdString() + " via ssh";
        }
        else {
            method = "via ssh";
        }
        fResult_->setFetchModeStr(method);
        fResult_->setTransferDuration(transferDuration_);
        fResult_->setFetchDate(QDateTime::currentDateTime());
        return (!fResult_->isEmpty());
    }
    return false;
}
