#include "cluster_task.h"
#include "script_controller.h"
#include <saas/deploy_manager/debug/messages.h>
#include <saas/deploy_manager/meta/cluster.h>
#include <saas/deploy_manager/scripts/common/deploy/deploy_builder.h>
#include <saas/util/logging/tskv_log.h>
#include <library/cpp/json/json_reader.h>

#include <util/system/hostname.h>

namespace NRTYDeploy {

    TString RSToString(TClusterTask::TResultStatus status) {
        switch (status) {
        case TClusterTask::rsCantEnqueue:
            return "CANT_ENQUEUE";
        case TClusterTask::rsDuplication:
            return "DUPLICATION";
        case TClusterTask::rsEnqueued:
            return "ENQUEUED";
        case TClusterTask::rsSuspended:
            return "SUSPENDED";
        case TClusterTask::rsFailedOnConstruction:
            return "FAILED_ON_CONSTRUCTION";
        case TClusterTask::rsFailedOnProcess:
            return "FAILED_ON_PROCESS";
        case TClusterTask::rsInProgress:
            return "IN_PROGRESS";
        case TClusterTask::rsLoadingFailed:
            return "LOADING_FAILED";
        case TClusterTask::rsSaved:
            return "SAVED";
        case TClusterTask::rsSavingFailed:
            return "SAVING_FAILED";
        case TClusterTask::rsSuccess:
            return "FINISHED";
        default:
            FAIL_LOG("Incorrect behaviour");
        }
    }

    TClusterTask::TResultStatus RSFromString(const TString& status) {
        if (status == "CANT_ENQUEUE")
            return TClusterTask::rsCantEnqueue;
        if (status == "DUPLICATION")
            return TClusterTask::rsDuplication;
        if (status == "ENQUEUED")
            return TClusterTask::rsEnqueued;
        if (status == "SUSPENDED")
            return TClusterTask::rsSuspended;
        if (status == "FAILED_ON_CONSTRUCTION")
            return TClusterTask::rsFailedOnConstruction;
        if (status == "FAILED_ON_PROCESS")
            return TClusterTask::rsFailedOnProcess;
        if (status == "IN_PROGRESS")
            return TClusterTask::rsInProgress;
        if (status == "LOADING_FAILED")
            return TClusterTask::rsLoadingFailed;
        if (status == "SAVED")
            return TClusterTask::rsSaved;
        if (status == "SAVING_FAILED")
            return TClusterTask::rsSavingFailed;
        if (status == "FINISHED")
            return TClusterTask::rsSuccess;
        DEBUG_LOG << "Incorrect value for convert " << status << Endl;
        return TClusterTask::rsUndefined;
    }

    bool TClusterTaskId::ExtractTimestampFromId(const TString& id, TInstant& result) {
        try {
            TMap<TString, TString> vals;
            NUtil::TTSKVRecordParser::Parse<';', '='>(id, vals);

            const ui64 tsDiscr = vals.find("tsdiscr") == vals.end() ? 10 : ::FromString<ui64>(vals["tsdiscr"]);
            VERIFY_WITH_LOG(vals.find("time") != vals.end(), "Incorrec ClusterTaskId format");
            const ui64 ts = ::FromString<ui64>(vals["time"]);

            result = TInstant::Seconds(ts * tsDiscr);
            return true;
        } catch (...) {
            ERROR_LOG << "Can't parse id: " << id << Endl;
        }
        return false;
    }

    TString TClusterTaskId::BuildId(const TString& hash) const {
        CHECK_WITH_LOG(TaskType != "UNDEFINED");
        TString nonParsedDataInfo = BuildDescription(hash);
        TString resultInfo = "type=" + TaskType + ";srv=" + Service + ";time=" + ::ToString(GetTimestamp()) + ";hash=" + ToString(FnvHash<ui64>(nonParsedDataInfo.data(), nonParsedDataInfo.size())) + ";tsdiscr=" + ::ToString(GetTSDiscr());
        INFO_LOG << nonParsedDataInfo << " -> " << resultInfo << Endl;
        return resultInfo;
    }

    TString TClusterTaskId::BuildDescription(const TString& hash) const {
        CHECK_WITH_LOG(TaskType != "UNDEFINED");
        return "ct=" + CType + ";hash=" + hash;
    }

    void TClusterTask::DoBuildTask() {
        VERIFY_WITH_LOG(false, "Not builder mode");
    };

    namespace {
        class TClusterTaskSaver: public IObjectInQueue {
        private:
            const TClusterTask* Task;
        public:

            TClusterTaskSaver(const TClusterTask* task)
                : Task(task)
            {
            }

            void Process(void* /*ThreadSpecificResource*/) override {
                THolder<TClusterTaskSaver> this_(this);
                Task->Save(false);
                SendGlobalDebugMessage<TDebugMessageAfterSaveClusterTask>();
            }
        };
    }

    void TClusterTask::OnAfterActionStep(const NDaemonController::IControllerAgentCallback::TActionContext& context, NDaemonController::TAction& action) {
        SaveQueue.SafeAdd(new NRTYDeploy::TClusterTaskSaver(this));
        action.DropForceStore();
        if (action.IsFinished()) {
            if (action.GetLockType() != NDaemonController::TAction::ltNoLock) {
                TGuard<TMutex> g(MutexLocks);
                TString lockName = context.ToString();
                auto i = ActionLocks.find(lockName);
                if (i != ActionLocks.end() && i->second.size()) {
                    i->second.erase(i->second.begin());
                }
            }
        }
    }

    void TClusterTask::OnPing(const NDaemonController::IControllerAgentCallback::TActionContext& /*context*/, NDaemonController::TAction& action) {
        if (action.GetNeedsForceStore()) {
            SaveQueue.SafeAdd(new NRTYDeploy::TClusterTaskSaver(this));
            action.DropForceStore();
        }
    }

    void TClusterTask::OnBeforeActionStep(const NDaemonController::IControllerAgentCallback::TActionContext& context, NDaemonController::TAction& action) {
        if (!action.IsStarted()) {
            TString lockName = context.ToString();
            if (action.GetLockType() != NDaemonController::TAction::ltNoLock) {
                TAbstractLock::TPtr lock;
                if (action.GetLockType() == NDaemonController::TAction::ltReadLock) {
                    lock = TClusterTask::CommonData->GetStorage().ReadLockNode(lockName);
                }
                if (action.GetLockType() == NDaemonController::TAction::ltWriteLock) {
                    lock = TClusterTask::CommonData->GetStorage().WriteLockNode(lockName);
                }
                TGuard<TMutex> g(MutexLocks);
                ActionLocks[lockName].push_back(lock);
            }
        }
        SaveQueue.SafeAdd(new NRTYDeploy::TClusterTaskSaver(this));
        action.DropForceStore();
    }

    bool TClusterTask::IsExecutable() const {
        TScriptController controller(CommonData->GetStorage(), Id);
        return controller.IsExecutable();
    }

    void TClusterTask::Process(void* /*ThreadSpecificResource*/) {
        DEBUG_LOG << "Task " << Id << " execution start..." << Endl;
        CHECK_WITH_LOG(CurrentStatus == ctsLoaded);
        CHECK_WITH_LOG(ResultStatus == rsEnqueued || ResultStatus == rsInProgress);
        SetStatus(ctsLoaded, rsInProgress);
        try {
            SaveQueue.Start(1);
            Script->Execute(ExecuteScriptThreads, dynamic_cast<NDaemonController::IControllerAgentCallback*>(this));
            SaveQueue.Stop();
            if (Script->IsFinished()) {
                if (Script->IsSuccess()) {
                    SetStatus(ctsNormally, rsSuccess);
                } else {
                    SetStatus(ctsFailed, rsFailedOnProcess, Script->GetStatusInfo());
                }
            } else {
                TScriptController controller(CommonData->GetStorage(), Id);
                if (controller.IsExecutable()) {
                    SetStatus(ctsFailed, rsFailedOnProcess, Script->GetStatusInfo());
                } else {
                    SetStatus(ctsNormally, rsSuspended);
                }
            }
        } catch (...) {
            SetStatus(ctsFailed, rsFailedOnProcess, CurrentExceptionMessage());
        }
        Save(false);
        DEBUG_LOG << "Task " << Id << " finished" << Endl;

    }

    bool TClusterTask::IsFinished() const {
        return ResultStatus != rsInProgress &&
            ResultStatus != rsEnqueued &&
            ResultStatus != rsSaved
            ;
    }

    bool TClusterTask::IsFailed() const {
        return IsFinished() && ResultStatus != rsSuccess;
    }

    NJson::TJsonValue TClusterTask::Serialize(bool /*forLogging*/) const {
        NJson::TJsonValue result;
        result.InsertValue("script", Script->Serialize());
        result.InsertValue("service", Service);
        result.InsertValue("service_type", ServiceType);
        result.InsertValue("is_finished", IsFinished());
        result.InsertValue("ctype", CType);
        result.InsertValue("result_info", ResultStatusInfo);
        result.InsertValue("result_code", RSToString(ResultStatus));
        result.InsertValue("type", TaskType);
        result.InsertValue("executor", ExecutorHost);
        return result;
    }

    void TClusterTask::Save(bool atFirst) const {
        TInstant start = Now();
        VERIFY_WITH_LOG(TaskType != "UNDEFINED", "Incorrect task construction");
        try {
            if (!atFirst) {
                CHECK_WITH_LOG(!!Id);
            } else {
                CHECK_WITH_LOG(!Id);
                Id = BuildId(Script->BuildHash());
            }

            DEBUG_LOG << "Save " << Id << " ..." << Endl;

            if (atFirst && CommonData->GetStorage().ExistsNode("/cluster_tasks/" + Id)) {
                TString taskText;
                CommonData->GetStorage().GetValue("/cluster_tasks/" + Id, taskText);
                NJson::TJsonValue jsonValue;
                if (NJson::ReadJsonFastTree(taskText, &jsonValue) && (jsonValue.IsArray() || jsonValue.IsMap())) {
                    SetStatus(ctsFailed, rsDuplication, "Duplicated task Id " + Id);
                    return;
                } else {
                    ERROR_LOG << "DUPLICATION failed: " << taskText << Endl;
                    CommonData->GetStorage().RemoveNode("/cluster_tasks/" + Id);
                }
            }

            NJson::TJsonValue result = Serialize();
            TString currentProgress = WriteJson(&result, true, true);
            if (CurrentProgress != currentProgress) {
                if (IsFinished())
                    DEBUG_LOG << "Task saved: " << currentProgress << Endl;

                if (IsFailed()) {
                    CommonData->GetStorage().SetValue("/failed_tasks/" + Id, currentProgress, false);
                }

                if (!CommonData->GetStorage().SetValue("/cluster_tasks/" + Id, currentProgress, false)) {
                    SetStatus(ctsFailed, rsSavingFailed, "Can't store task Id " + Id);
                } else {
                    if (atFirst)
                        SetStatus(ctsSaved, rsSaved);
                    CurrentProgress = currentProgress;
                }
                DEBUG_LOG << "Save " << Id << " ...OK(" << (Now() - start) << ")" << Endl;
            } else
                DEBUG_LOG << "Save " << Id << " ...OK(" << (Now() - start) << ", fake)" << Endl;
        }
        catch (...) {
            SetStatus(ctsFailed, rsSavingFailed, "Failed on save task " + Id + ":" + CurrentExceptionMessage());
        }
    }

    const TString& TClusterTask::GetId() const {
        CHECK_WITH_LOG(!!Id);
        return Id;
    }

    void TClusterTask::BuildTask() {
        try {
            CHECK_WITH_LOG(!Id);
            TDebugMessageDoBuildTaskProcessing dbgMsg(GetTaskType());
            SendGlobalMessage(dbgMsg);
            DoBuildTask();
            if (ResultStatus == TClusterTask::rsFailedOnConstruction) {
                return;
            }

            Save(true);
            CHECK_WITH_LOG(!!Id);

            TVector<TString> tasks;
            bool isDuplication = ResultStatus == TClusterTask::rsDuplication;
            if (isDuplication) {
                CommonData->GetQueue().GetTasks("deploy", Service + "-" + CType + "-" + Id, tasks);
                if (!tasks.size()) {
                    SetStatus(ctsSaved, rsSaved);
                } else {
                    SetStatus(ctsEnqueued, rsEnqueued);
                }
            }

            if (CurrentStatus == ctsSaved) {
                if (CreateSuspended) {
                    SetStatus(ctsSuspended, rsSuspended);
                } else {
                    EnqueueTask(false);
                }
                if (!isDuplication)
                    Save(false);
            }
        } catch (...) {
            SetStatus(ctsFailed, rsFailedOnConstruction, "cannot build task " + Id + ": " + CurrentExceptionMessage());
        }
    }

    void TClusterTask::EnqueueTask(bool forceSave) {
        if (!CommonData->GetQueue().Enqueue("deploy", Service + "-" + CType + "-" + Id, Id)) {
            SetStatus(ctsFailed, rsCantEnqueue, "cannot enqueue task " + Id);
        } else {
            SetStatus(ctsEnqueued, rsEnqueued);
        }
        if (forceSave)
            Save(false);
    }

    TClusterTask::TClusterTask(const TString& id, NRTYDeploy::ICommonData* commonData, ui32 executeScriptThreads) {
        CurrentStatus = ctsEnqueued;
        ResultStatus = rsEnqueued;
        CommonData = commonData;
        TaskType = "UNDEFINED";
        ExecuteScriptThreads = executeScriptThreads;
        Load(id);
        if (CurrentStatus != ctsFailed)
            CHECK_WITH_LOG(TaskType != "UNDEFINED");
    }

    void TClusterTask::SetStatus(TCurrentStatus cStatus, TResultStatus rStatus, const TString& message) const {
        DEBUG_LOG << "ClusterTask " << Id << " status: " << (ui32)cStatus << "/" << RSToString(rStatus) << "/" << message << Endl;
        ResultStatus = rStatus;
        CurrentStatus = cStatus;
        ResultStatusInfo = message;
    }

    void TClusterTask::InitExecutorHost() {
        ExecutorHost = GetHostName();
    }

    void TClusterTask::Load(const TString& id) {
        try {
            TInstant start = Now();
            Id = id;
            DEBUG_LOG << "Load " << Id << " ..." << Endl;
            TString progress;
            if (!CommonData->GetStorage().GetValue("/cluster_tasks/" + Id, progress)) {
                SetStatus(ctsFailed, rsLoadingFailed, "No exists /cluster_tasks/" + Id);
                return;
            }

            CurrentProgress = progress;

            NJson::TJsonValue value;
            TStringStream ss;
            ss << progress;
            if (!NJson::ReadJsonTree(&ss, &value)) {
                SetStatus(ctsFailed, rsLoadingFailed, "Can't parse json: " + progress);
                return;
            }

            if (!value.IsMap()) {
                SetStatus(ctsFailed, rsLoadingFailed, "Incorrect task format");
                return;
            }

            const NJson::TJsonValue::TMapType& map = value.GetMap();
            if (map.find("script") == map.end() || map.find("service") == map.end() || map.find("ctype") == map.end()) {
                SetStatus(ctsFailed, rsLoadingFailed, "Incorrect format " + value.GetStringRobust());
                return;
            }
            Script.Reset(new NRTYScript::TRTYClusterScript(new TScriptController(CommonData->GetStorage(), Id)));
            if (!Script->Deserialize(map.at("script"))) {
                SetStatus(ctsFailed, rsLoadingFailed, "Can't parse script " + value.GetStringRobust());
                return;
            }

            ExecutorHost = map.at("executor").GetStringRobust();
            Service = map.at("service").GetStringRobust();
            ServiceType = map.at("service_type").GetStringRobust();
            CType = map.at("ctype").GetStringRobust();
            ResultStatusInfo = map.at("result_info").GetStringRobust();
            ResultStatus = RSFromString(map.at("result_code").GetStringRobust());
            if (ResultStatus == rsUndefined) {
                SetStatus(ctsFailed, rsLoadingFailed, "Can't parse result_code " + value.GetStringRobust());
                return;
            }

            TaskType = map.at("type").GetStringRobust();
            DEBUG_LOG << "Load " << Id << " ...OK(" << (Now() - start) << ")" << Endl;
        }
        catch (...) {
            SetStatus(ctsFailed, rsLoadingFailed, "Can't load task " + CurrentExceptionMessage());
            return;
        }
        SetStatus(ctsLoaded, ResultStatus);
    }

    TClusterTask::TCgiContext TClusterTask::TCgiContext::Parse(const TCgiParameters& cgi, const TString& defaultServiceType) {
        TClusterTask::TCgiContext result;
        result.CType = cgi.Get("ctype");
        result.Service = cgi.Get("service");
        result.Parent = cgi.Get("parent_task");
        result.ServiceType = cgi.Get("service_type");
        result.CreateSuspended = cgi.Has("suspended");

        if (!NRTYDeployInfo::IDeployComponentInfo::TFactory::Has(result.ServiceType)) {
            if (NRTYDeployInfo::IDeployComponentInfo::TFactory::Has(result.Service))
                result.ServiceType = result.Service;
            else
                result.ServiceType = defaultServiceType;
        }

        CHECK_WITH_LOG(NRTYDeployInfo::IDeployComponentInfo::TFactory::Has(result.ServiceType));
        return result;
    }

    void TClusterTask::RemoveExpiredTasks(NRTYDeploy::ICommonData* commonData, const TDuration& lifetime, TManualEvent& active) {
        TVector<TString> childNodes;
        if (!commonData->GetStorage().GetNodes("/cluster_tasks/", childNodes)) {
            INFO_LOG << "There is no task to check for remove" << Endl;
        }
        for (size_t i = 0; i < childNodes.size() && !active.WaitT(TDuration::Zero()); ++i) {
            const TString& id = childNodes[i];
            TInstant creationTime;
            if (!ExtractTimestampFromId(id, creationTime))
                continue;
            DEBUG_LOG << id << " checking" << Endl;
            if (Now() - creationTime > lifetime) {
                TString pathLock = TFsPath("/cluster_tasks/" + id).Fix().GetPath();
                auto lock = commonData->GetStorage().WriteLockNode(pathLock, TDuration::Seconds(2));
                if (!lock) {
                    DEBUG_LOG << id << " locked already" << Endl;
                    continue;
                }
                DEBUG_LOG << id << " locked" << Endl;
                INFO_LOG << creationTime << Endl;
                if (commonData->GetStorage().RemoveNode("/cluster_tasks/" + id, false)) {
                    INFO_LOG << "Remove expired task: taskid=" << id << Endl;
                } else {
                    WARNING_LOG << "Can't remove task: id=" << id << Endl;
                }
                if (commonData->GetStorage().RemoveNode("/cluster_tasks/" + id + ".state", false)) {
                    INFO_LOG << "Remove expired task's state file: id=" << id << Endl;
                }
            }
        }
    }

    TWaitAsyncAction::TFactory::TRegistrator<TWaitAsyncAction> TWaitAsyncAction::Registrator(WAIT_ACTION_NAME);
};
