#include "yt_command.h"

#include <saas/library/yt/error/helpers.h>

#include <library/cpp/logger/global/global.h>

#include <util/folder/path.h>
#include <util/string/builder.h>
#include <util/generic/set.h>

TYTCommand::TYTCommand(TInputs inputs, TOutputs outputs, TString name, bool verbose, NSaas::TYTLaunchReport& report, const NYT::TNode& acl)
    : Inputs(std::move(inputs))
    , Outputs(std::move(outputs))
    , Report(report)
    , Name(std::move(name))
    , Verbose(verbose)
    , Acl(acl)
{
}

void TYTCommand::PrepareSpec() {
    try {
        PrintVerbose("Preparing spec ...");
        DoPrepareSpec();
        PrintVerbose("Preparing spec ... done");
    } catch(...) {
        auto error = CurrentExceptionMessage();
        PrintVerbose("Preparing spec ... aborted by exception " + error, true);
        ythrow TStandaloneIndexerError(Name) << error;
    }
}

void TYTCommand::Run(NYT::IClientBase* client, bool& resume) {
    auto tx = client->StartTransaction();
    try {
        if (resume && !Outputs.empty()) {
            PrintVerbose("Checking if output already exists ...");
            if (OutputExists(tx.Get())) {
                PrintVerbose("Checking if output already exists ... yes, skipping operation");
                tx->Commit();
                return ;
            }
            PrintVerbose("Checking if output already exists ... no");
            resume = false;
        }
        FillInputAttrs(tx.Get());
        DeleteOutput(tx.Get());
        CreateOutputDirectories(tx.Get());
        PrintVerbose("Running ...");
        DoRun(tx.Get());
        PrintVerbose("Running ... done");

    } catch (const NYT::TErrorResponse& e) {
        tx->Abort();
        auto error = e.GetError().FullDescription();
        PrintVerbose("aborted by YT error: " + error, true);
        ythrow TStandaloneIndexerError(Name) << error;

    } catch (const NYT::TOperationFailedError& e) {
        auto operation = tx->AttachOperation(e.GetOperationId());
        auto rootClient = tx->GetParentClient();
        auto operationAttrs = rootClient->GetOperation(e.GetOperationId());
        TString name = "";
        if (operationAttrs.Spec.Defined()) {
            name = operationAttrs.Spec->ChildAsString("title");
        }

        auto operationError = e.GetError().FullDescription();
        PrintVerbose("Operation " + name + " failed with error: " + operationError, true);
        PrintVerbose("Operation link: " + operation->GetWebInterfaceUrl(), true);
        if (!e.GetFailedJobInfo().empty()) {
            auto error = NErrorHelpers::CutYtJobOutput(e.GetFailedJobInfo().front().Stderr, 1500);
            PrintVerbose("Failed job stderr: " + error, true);
        }
        tx->Abort();
        ythrow TStandaloneIndexerError(Name) << operationError;

    } catch(...) {
        auto error = CurrentExceptionMessage();
        tx->Abort();
        PrintVerbose("aborted by exception " + error, true);
        ythrow TStandaloneIndexerError(Name) << error;
    }

    tx->Commit();
}

bool TYTCommand::OutputExists(NYT::IClientBase* client) {
    for (const auto& it : Outputs) {
        if (!client->Exists(it.Path_))
            return false;
    }
    return true;
}

void TYTCommand::DeleteOutput(NYT::IClientBase* client) {
    for (const auto& it : Outputs) {
        if (client->Exists(it.Path_)) {
            PrintVerbose("Deleting " + it.Path_ + " ...");
            client->Remove(it.Path_);
            PrintVerbose("Deleting " + it.Path_ + " ... done");
        }
    }
}

void TYTCommand::FillInputAttrs(NYT::IClientBase* client) {
    if (Inputs.empty()) {
        return ;
    }
    PrintVerbose("Collecting attributes of input tables ...");
    InputAttrs.reserve(Inputs.size());
    for (auto& it : Inputs) {
        if (!client->Exists(it.Path_)) {
            ythrow yexception() << "Input table " << it.Path_ << " doesn't exist";
        }
        InputAttrs.push_back(client->Get(it.Path_ + "/@"));
        Y_ENSURE(InputAttrs.back().GetType() == NYT::TNode::Map);
    }
    PrintVerbose("Collecting attributes of input tables ... done");
}

void TYTCommand::CreateOutputDirectories(NYT::IClientBase* client) {
    TSet<NYT::TYPath> dirs;
    for (const auto& it : Outputs) {
        TFsPath p(it.Path_);
        dirs.insert("/" + p.Dirname()); // TFsPath strips the leading '/' from cypress path, see YT-7833
    }
    for (const auto& d : dirs) {
        // need to use Exists() instead of Force(true) because Force overwrites existing nodes with empty ones
        if (!client->Exists(d)) {
            client->Create(d, NYT::NT_MAP, NYT::TCreateOptions().Recursive(true));
            PrintVerbose("Created: " + d.Quote());
        }
    }
}

void TYTCommand::PrintVerbose(const TString& msg, bool isError) {
    if (Verbose) {
        if (isError) {
            ERROR_LOG << Name << ": " << msg << Endl;
            Report.AddError(TStringBuilder() << Name << ": " << msg << "\n");
        } else {
            INFO_LOG << Name << ": " << msg << Endl;
        }
    }
}

NYT::TOperationOptions TYTCommand::AddAclIfNeed(const NYT::TOperationOptions& src) {
    if (Acl.IsUndefined()) {
        return src;
    }

    NYT::TOperationOptions ret = src;
    if (!ret.Spec_) {
        ret.Spec_.ConstructInPlace();
    }
    (*ret.Spec_)("acl", Acl);
    return ret;
}
