/*
 * 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 "ServerComThread.hpp"

#include <algorithm>

#include "ServerComQueue.hpp"
#include "ServerDefsAccess.hpp"
#include "ServerHandler.hpp"
#include "SuiteFilter.hpp"
#include "UiLog.hpp"
#include "ecflow/client/ClientInvoker.hpp"
#include "ecflow/core/CommandLine.hpp"
#include "ecflow/core/Converter.hpp"
#include "ecflow/node/Defs.hpp"
#include "ecflow/node/Suite.hpp"

#define _UI_SERVERCOMTHREAD_DEBUG

ServerComThread::ServerComThread(ServerHandler* server, ClientInvoker* ci) : server_(server), ci_(ci) {
    assert(server_);
}

ServerComThread::~ServerComThread() {
    detach();
}

void ServerComThread::task(VTask_ptr task) {
    // do not execute thread if already running

    if (isRunning()) {
        UiLog(serverName_).err() << "ComThread::task - thread already running, will not execute command";
    }
    else {
        // if(!server_ && server)
        //     initObserver(server);

        // We set the parameters needed to run the task. These members are not protected by
        // a mutex, because apart from this function only run() can access them!!
        serverName_ = server_->longName();
        taskType_   = task->type();

        if (taskType_ != VTask::LogOutTask) {

            command_      = task->command();
            commandAsStr_ = task->commandAsStr();
            params_       = task->params();
            contents_     = task->contents();
            vars_         = task->vars();
            nodePath_.clear();
            nodePath_ = task->targetPath();
            zombie_   = task->zombie();

            // Suite filter
            hasSuiteFilter_   = server_->suiteFilter()->isEnabled();
            autoAddNewSuites_ = server_->suiteFilter()->autoAddNewSuites();
            if (hasSuiteFilter_)
                filteredSuites_ = server_->suiteFilter()->filter();
            else
                filteredSuites_.clear();

            maxLineNum_ = server_->conf()->intValue(VServerSettings::MaxOutputFileLines);
        }

        // Start the thread execution
        start();
    }
}

void ServerComThread::run() {
    UiLog(serverName_).dbg() << "ComThread::run --> path=" << nodePath_;

    // Init flags
    rescanNeed_    = false;
    bool isMessage = false;
    std::string errorString;

    try {
        // define the tasks that will explicitly/implicitly call ci_->sync_local()
        std::set<VTask::Type> sync_tasks = {VTask::CommandTask,
                                            VTask::SyncTask,
                                            VTask::WhySyncTask,
                                            VTask::ScriptSubmitTask,
                                            VTask::ZombieCommandTask,
                                            VTask::JobStatusTask,
                                            VTask::PlugTask};

        // this is called during reset - it requires a special treatment
        if (taskType_ == VTask::ResetTask) {
            UiLog(serverName_).dbg() << " RESET";
            reset();
        }

        // logout is also special!!!
        else if (taskType_ == VTask::LogOutTask) {
            UiLog(serverName_).dbg() << " LOGOUT";
            detach();
            if (ci_->client_handle() > 0) {
                ci_->ch1_drop();
            }
        }

        // this is very important to exclude non-compatible servers
        else if (taskType_ == VTask::ServerVersionTask) {
            UiLog(serverName_).dbg() << " SERVER_VERSION";
            ci_->server_version();
            UiLog(serverName_).dbg() << " SERVER_VERSION FINISHED";
        }
        // tasks that will explicitly/implicitly call ci_->sync_local()
        else if (sync_tasks.find(taskType_) != sync_tasks.end()) {
            // we need to lock the mutex on the defs
            ServerDefsAccess defsAccess(server_);

            switch (taskType_) {
                case VTask::CommandTask: {
                    // will automatically call ci_->sync_local() after the
                    // command finished!!!

                    UiLog(serverName_).dbg() << " COMMAND";

                    // Depending on the source of the action, the command might be:
                    //  - the 'literal' command string, or
                    //  - the 'tokenised' command
                    // Once the CommandLine is created, the handling is the same.
                    CommandLine cl = commandAsStr_.empty() ? CommandLine(command_) : CommandLine(commandAsStr_);
#ifdef _UI_SERVERCOMTHREAD_DEBUG
                    UiLog(serverName_).dbg() << " args=" << cl;
#endif
                    ci_->invoke(cl);
                    break;
                }

                case VTask::SyncTask: {
                    UiLog(serverName_).dbg() << " SYNC";
                    UiLog(serverName_).dbg() << " sync begin";
                    ci_->sync_local();
                    UiLog(serverName_).dbg() << " sync end";
                    break;
                }

                case VTask::WhySyncTask: {
                    UiLog(serverName_).dbg() << " WHYSYNC";
                    UiLog(serverName_).dbg() << "sync begin";
                    ci_->sync_local(true); // sync_suite_clock
                    UiLog(serverName_).dbg() << " sync end";
                    break;
                }

                case VTask::ScriptSubmitTask: {
                    UiLog(serverName_).dbg() << " SCRIP SUBMIT";
                    ci_->edit_script_submit(nodePath_,
                                            vars_,
                                            contents_,
                                            (params_["alias"] == "1") ? true : false,
                                            (params_["run"] == "1") ? true : false);
                    break;
                }

                case VTask::ZombieCommandTask: {
                    std::string cmd = params_["command"];
                    UiLog(serverName_).dbg() << " ZOMBIE COMMAND " << cmd << " path=" << zombie_.path_to_task();
                    if (cmd == "zombie_fob")
                        ci_->zombieFob(zombie_);
                    else if (cmd == "zombie_fail")
                        ci_->zombieFail(zombie_);
                    else if (cmd == "zombie_adopt")
                        ci_->zombieAdopt(zombie_);
                    else if (cmd == "zombie_block")
                        ci_->zombieBlock(zombie_);
                    else if (cmd == "zombie_remove")
                        ci_->zombieRemove(zombie_);
                    else if (cmd == "zombie_kill")
                        ci_->zombieKill(zombie_);

                    break;
                }
                case VTask::JobStatusTask: {
                    UiLog(serverName_).dbg() << " JOB STATUS";
                    ci_->status(nodePath_);
                    break;
                }
                case VTask::PlugTask: {
                    UiLog(serverName_).dbg() << " PLUG";
                    ci_->plug(params_["source"], params_["target"]);
                    break;
                }

                default: {
                    Q_ASSERT(0);
                    exit(1);
                }
            }

            // ci_->sync_local() was called!!
            // If a rescan or fullscan is needed we have either added/remove nodes or deleted the defs.
            // So there were significant changes.

            // We detach the nodes currently available in defs, then we attach them again. We can still have nodes
            // that were removed from the defs but are still attached. These will be detached when in ServerHandler we
            // clear the tree. This tree contains shared pointers to the nodes, so when the tree is cleared
            // the shared pointer are reset, the node descturctor is called and finally update_delete is called and
            // we can detach the node.

            if (rescanNeed_ || ci_->server_reply().full_sync()) {
                UiLog(serverName_).dbg() << " rescan needed!";
                detach(defsAccess.defs());
                attach(defsAccess.defs());
            }
        }

        // all the other tasks - these will not call ci_->sync_local(()
        else {

            switch (taskType_) {

                case VTask::NewsTask: {
                    UiLog(serverName_).dbg() << " NEWS";
                    ci_->news_local();
                    break;
                }

                case VTask::JobTask:
                case VTask::ManualTask:
                case VTask::ScriptTask:
                case VTask::OutputTask:
                case VTask::JobStatusFileTask: {
                    UiLog(serverName_).dbg() << " FILE"
                                             << " " << params_["clientPar"];
                    if (maxLineNum_ < 0)
                        ci_->file(nodePath_, params_["clientPar"]);
                    else
                        ci_->file(nodePath_, params_["clientPar"], ecf::convert_to<std::string>(maxLineNum_));

                    break;
                }

                case VTask::MessageTask:
                    UiLog(serverName_).dbg() << " EDIT HISTORY";
                    ci_->edit_history(nodePath_);
                    break;

                case VTask::StatsTask:
                    UiLog(serverName_).dbg() << " STATS";
                    ci_->stats();
                    break;
                    ;

                case VTask::HistoryTask:
                    UiLog(serverName_).dbg() << " SERVER LOG";
                    ci_->getLog(100);
                    break;

                case VTask::ScriptPreprocTask:
                    UiLog(serverName_).dbg() << " SCRIP PREPROCESS";
                    ci_->edit_script_preprocess(nodePath_);
                    break;

                case VTask::ScriptEditTask:
                    UiLog(serverName_).dbg() << " SCRIP EDIT";
                    ci_->edit_script_edit(nodePath_);
                    break;

                case VTask::SuiteListTask:
                    UiLog(serverName_).dbg() << " SUITES";
                    ci_->suites();
                    break;

                case VTask::SuiteAutoRegisterTask: {
                    UiLog(serverName_).dbg() << " SUITE AUTO REGISTER";
                    if (hasSuiteFilter_) {
                        ci_->ch1_auto_add(autoAddNewSuites_);
                    }
                    break;
                }

                case VTask::ZombieListTask:
                    UiLog(serverName_).dbg() << " ZOMBIES";
                    ci_->zombieGet();
                    break;

                default:
                    break;

            } // switch

        } // if

    } // try

    catch (std::exception& e) {
        isMessage   = true;
        errorString = e.what();
    }

    // we can get an error string in two ways - either an exception is raised, or
    // the get_string() of the server reply is non-empty.
    if (!isMessage && (taskType_ == VTask::CommandTask) && !(ci_->server_reply().get_string().empty())) {
        isMessage   = true;
        errorString = ci_->server_reply().get_string();
    }

    // we  failed or we have a string returned
    if (isMessage) {
        // note that we need to emit a signal rather than directly call a message function
        // because we can't call Qt widgets from a worker thread

        UiLog(serverName_).dbg() << " thread failed: " << errorString;
        if (taskType_ == VTask::LogOutTask) {
            Q_EMIT logoutDone();
            return;
        }

        Q_EMIT failed(errorString);

        // Reset flags
        rescanNeed_ = false;

        // This will stop the thread.
        return;
    }

    if (taskType_ == VTask::LogOutTask) {
        Q_EMIT logoutDone();
        return;
    }

    // Reset flags
    rescanNeed_ = false;

    // Can we use it? We are in the thread!!!
    // UserMessage::message(UserMessage::DBG, false, std::string("  ServerComThread::run finished"));
}

#if 0
void ServerComThread::sync_local(bool sync_suite_clock)
{
    //For this part we need to lock the mutex on defs
    {
        ServerDefsAccess defsAccess(server_);

        UiLog(serverName_).dbg() << "ComThread::sync_local --> sync begin";
        ci_->sync_local(sync_suite_clock);
        UiLog(serverName_).dbg() << " sync end";

        //If a rescan or fullscan is needed we have either added/remove nodes or deleted the defs.
        //So there were significant changes.

        //We detach the nodes currently available in defs, then we attach them again. We can still have nodes
        //that were removed from the defs but are still attached. These will be detached when in ServerHandler we
        //clear the tree. This tree contains shared pointers to the nodes, so when the tree is cleared
        //the shared pointer are reset, the node descturctor is called and finally update_delete is called and
        //we can detach the node.

        if(rescanNeed_ || ci_->server_reply().full_sync())
        {
            UiLog(serverName_).dbg() << " rescan needed!";
            detach(defsAccess.defs());
            attach(defsAccess.defs());
        }
    }
}
#endif

void ServerComThread::reset() {
    UiLog(serverName_).dbg() << "ComThread::reset -->";

    // Lock the mutex on defs
    ServerDefsAccess defsAccess(server_);

    // Detach the defs and the nodes from the observer
    detach(defsAccess.defs());

    if (hasSuiteFilter_) {
        if (!filteredSuites_.empty()) {
            UiLog(serverName_).dbg() << " register suites=" << filteredSuites_;

            // This will DROP existing handle(when using ch1_register(..)) and RESET defs, and return a new handle, and
            // defs to the client
            ci_->ch1_register(autoAddNewSuites_, filteredSuites_);
        }
        // If the suite filter is empty
        else {
            // Registering with empty set would lead to retrieve all server content,
            // opposite of expected result. So we just register a dummy suite
            // to achive the our goal: for an empty suite filter no suites are retrieved.
            UiLog(serverName_).dbg() << " register empty suite list";

            // This will DROP existing handle(when using ch1_register(..)) and RESET defs, and return a new handle, and
            // defs to the client
            std::vector<std::string> fsl;
            fsl.push_back(SuiteFilter::dummySuite());
            ci_->ch1_register(autoAddNewSuites_, fsl);
        }
    }
    else {
        UiLog(serverName_).dbg() << " drop handle";
        ci_->ch1_drop();
        // if there was a handle to drop sync is done automatically
        if (!ci_->server_reply().full_sync()) {
            ci_->reset();
            UiLog(serverName_).dbg() << " sync begin";
            ci_->sync_local();
            UiLog(serverName_).dbg() << " sync end";
        }
    }

    // Attach the nodes to the observer
    attach(defsAccess.defs());

    UiLog(serverName_).dbg() << "<-- ComThread::reset";
}

// This is an observer notification method!!
void ServerComThread::update(const Node* node, const std::vector<ecf::Aspect::Type>& types) {
    // This function can only be called during a SYNC_LOCAl task!!!!
    assert(taskType_ == VTask::CommandTask || taskType_ == VTask::SyncTask || taskType_ == VTask::WhySyncTask ||
           taskType_ == VTask::JobStatusTask || taskType_ == VTask::ScriptSubmitTask ||
           taskType_ == VTask::ZombieCommandTask);

    std::vector<ecf::Aspect::Type> typesCopy = types;

    UiLog(serverName_).dbg() << "ComThread::update --> node: " << node->name();
    std::stringstream ss;
    aspectToStr(ss, types);
    UiLog(serverName_).dbg() << " aspects: " << ss.str();

    // If a node was already requested to be added/deleted in the thread we do not go further. At the end of the sync
    // we will regenerate everything (the tree as well in ServerHandle).
    if (rescanNeed_) {
        UiLog(serverName_).dbg() << " rescanNeed already set";
        return;
    }

    // This is a radical change
    if ((std::find(types.begin(), types.end(), ecf::Aspect::ADD_REMOVE_NODE) != types.end()) ||
        (std::find(types.begin(), types.end(), ecf::Aspect::ORDER) != types.end())) {
        UiLog(serverName_).dbg() << " emit rescanNeed()";
        rescanNeed_ = true;

        // We notify ServerHandler about the radical changes. When ServerHandler receives this signal
        // it will clear its tree, which stores shared pointers to the nodes (node_ptr). If these pointers are
        // reset update_delete() might be called, so it should not write any shared variables!
        Q_EMIT rescanNeed();

        return;
    }

    // This will notify SeverHandler
    UiLog(serverName_).dbg() << " emit nodeChanged()";
    Q_EMIT nodeChanged(node, types);
}

void ServerComThread::update(const Defs* /*dc*/, const std::vector<ecf::Aspect::Type>& types) {
    std::vector<ecf::Aspect::Type> typesCopy = types;

    UiLog(serverName_).dbg() << "ComThread::update --> defs";
    std::stringstream ss;
    aspectToStr(ss, types);
    UiLog(serverName_).dbg() << " aspects: " << ss.str();

    // If anything was requested to be deleted in the thread we do not go further
    // because it will trigger a full rescan in ServerHandler!
    if (rescanNeed_) {
        UiLog(serverName_).dbg() << " rescanNeed already set";
        return;
    }

    // This is a radical change
    if (std::find(types.begin(), types.end(), ecf::Aspect::ORDER) != types.end()) {
        UiLog(serverName_).dbg() << " emit rescanNeed()";
        rescanNeed_ = true;

        // We notify ServerHandler about the radical changes. When ServerHandler receives this signal
        // it will clear its tree, which stores shared pointers to the nodes (node_ptr). If these pointers are
        // reset update_delete() might be called, so it should not write any shared variables!
        Q_EMIT rescanNeed();

        return;
    }

    // This will notify SeverHandler
    UiLog(serverName_).dbg() << " emit defsChanged()";
    Q_EMIT defsChanged(typesCopy);
}

void ServerComThread::update_delete(const Node* nc) {
    auto* n = const_cast<Node*>(nc);
    n->detach(this);
}

// This only be called when ComThread is running or from the ComThread desctructor. So it is safe to set
// rescanNeed in it.
void ServerComThread::update_delete(const Defs* dc) {
    UiLog(serverName_).dbg() << "ServerComThread::update_delete -->";

    auto* d = const_cast<Defs*>(dc);
    d->detach(this);

    // If we are in  a SYNC_LOCAl task!!!!
    if (taskType_ == VTask::SyncTask) {
        // We notify ServerHandler about the radical changes. When ServerHandler receives this signal
        // it will clear its tree, which stores shared pointers to the nodes (node_ptr). If these pointers are
        // reset update_delete() might be called, so it should not write any shared variables!
        Q_EMIT rescanNeed();

        rescanNeed_ = true;
    }
}

// Attach each node and the defs to the observer
void ServerComThread::attach() {
    ServerDefsAccess defsAccess(server_); // will reliquish its resources on destruction
    defs_ptr d = defsAccess.defs();
    if (d == nullptr)
        return;

    attach(d);
}

// Attach  each node and the defs to the observer. The access to
// the defs is safe so we do not need to set a mutex on it.
void ServerComThread::attach(defs_ptr d) {
    if (d == nullptr)
        return;

    d->attach(this);

    const std::vector<suite_ptr>& suites = d->suiteVec();
    for (const auto& suite : suites) {
        attach(suite.get());
    }
}

// Attach a node to the observer
void ServerComThread::attach(Node* node) {
    std::vector<node_ptr> nodes;
    node->immediateChildren(nodes);

    node->attach(this);

    for (auto it = nodes.begin(); it != nodes.end(); ++it) {
        attach((*it).get());
    }
}

// Detach each node and the defs from the observer
void ServerComThread::detach() {
    ServerDefsAccess defsAccess(server_); // will reliquish its resources on destruction
    defs_ptr d = defsAccess.defs();
    if (d == nullptr)
        return;

    detach(d);
}

// Detach each node and the defs from the observer.  The access to
// the defs is safe so we do not need to set a mutex on it.
void ServerComThread::detach(defs_ptr d) {
    if (d == nullptr)
        return;

    d->detach(this);

    const std::vector<suite_ptr>& suites = d->suiteVec();
    for (const auto& suite : suites) {
        detach(suite.get());
    }
}

// Detach a node from the observer
void ServerComThread::detach(Node* node) {
    std::vector<node_ptr> nodes;
    node->immediateChildren(nodes);

    node->detach(this);

    for (auto it = nodes.begin(); it != nodes.end(); ++it) {
        detach((*it).get());
    }
}

void ServerComThread::aspectToStr(std::stringstream& ss, const std::vector<ecf::Aspect::Type>& t) const {
    for (auto it : t) {
        if (!ss.str().empty())
            ss << ",";
        ss << it;
    }
}

// performa a logout task without running a thread
void ServerComThread::blockingLogout() {
    Q_ASSERT(isRunning() == false);

    UiLog(serverName_).dbg() << " LOGOUT";
    detach();
    ci_->set_auto_sync(false);
    try {
        if (ci_->client_handle() > 0) {
            ci_->ch1_drop();
        }
    }
    catch (std::exception& e) {
        std::string errMsg = "Error during logout!";
        if (e.what()) {
            errMsg += " " + std::string(e.what());
        }
        UiLog(serverName_).err() << errMsg;
    }
}
