#include "machine.h"

#include <library/cpp/http/io/stream.h>

#include <util/datetime/base.h>
#include <util/system/guard.h>
#include <util/generic/ptr.h>
#include <util/network/socket.h>
#include <util/string/cast.h>
#include <util/system/shellcommand.h>

#define MAIN_THREADS_COUNT 10

namespace NRtyInfraTests {

////  TMachine  ///////////////////////////////////////////////////////////////

    void TMachine::Run(const TScriptData& script) {
        Cout << "Running..." << Endl;
        MainQueue.Start(MAIN_THREADS_COUNT);
        const TScriptData::TMain& mainSeq = script.GetMainSequence();
        for (TScriptData::TMain::const_iterator i = mainSeq.begin(), e = mainSeq.end(); i != e; ++i) {
            const TCommand& command = *i;
            DoLogMessage(command, "in progress");
            const TString& name = command.first
                       ,& id = command.second;
            if (name != WAIT) {
                TMainWorker *worker = new TMainWorker(script, command);
                if (!!id) {
                    TWaitMap::iterator eventItr = WaitMap.find(id);
                    VERIFY_AND_THROW(WaitMap.end() == eventItr, MakeMessage(command, "id is already in use"));
                    TSimpleSharedPtr<TSystemEvent> eventPtr = WaitMap[id] = new TSystemEvent;
                    worker->SetEvent(eventPtr.Get());
                    VERIFY_AND_THROW(MainQueue.Add(worker), MakeMessage(command, "failed to enqueue"));
                    DoLogMessage(command, "enqueued");
                } else {
                    worker->Process(nullptr);
                    DoLogMessage(command, "done");
                }
            } else {
                TWaitMap::iterator eventItr = WaitMap.find(id);
                VERIFY_AND_THROW(eventItr != WaitMap.end(), MakeMessage(command, "id is not found"));
                TSimpleSharedPtr<TSystemEvent> eventPtr = eventItr->second;
                eventPtr->Wait();
                DoLogMessage(command, "done");
            }
        }
        MainQueue.Stop();
    }

    TString TMachine::MakeMessage(const TCommand& command, const TString& message) {
        return "Objective " + command.first
             + ", id: " + command.second
             + ", state: " + message;
    }

    void TMachine::DoLogMessage(const TCommand& command, const TString& message) {
        Cout << MakeMessage(command, message) << Endl;
    }

    void TMachine::WriteOut(const TString& message) {
        TGuard<TMutex> g(OutputMutex);
        Cout << message << Endl;
    }

    const TString TMachine::WAIT("WAIT");

////  TBaseWorker  ////////////////////////////////////////////////////////////

    TBasicWorker::TBasicWorker(const TScriptData &script, const TCommand &command)
        : Script(script)
        , Command(command)
        , Objective(script.GetObject(command.first))
    {
        //
    }

    TBasicWorker::~TBasicWorker() {
        //
    }


////  TMainWorker  ////////////////////////////////////////////////////////////

    TMainWorker::TMainWorker(const TScriptData &script, const TCommand &command, TSystemEvent *event)
        : TBasicWorker(script, command)
        , IsDone(event)
        , Name(command.first)
        , Id(command.second)
    {
        //
    }

    TMainWorker::~TMainWorker() {
        if (!!IsDone) {
            IsDone->Signal();
        }
    }

    const TMainWorker& TMainWorker::SetEvent(TSystemEvent* event) {
        IsDone = event;
        return *this;
    }

    void TMainWorker::Process(void* /*ThreadSpecificResource*/) {
        THolder<TMainWorker> suicide(this);
        THolder<IObjectInQueue> worker;

        const TString& type = Script.GetValue(Objective, TYPE);
        worker.Reset(CreateSpecificWorker(type));

        size_t nThreads = GetValue<size_t>(Objective, "nThreads", 1);
        ui64 nTimes = GetValue<ui64>(Objective, "nTimes", 1);

        ObjectQueue.Start(nThreads);
        for (ui64 i = 0; i != nTimes; ++i) {
            VERIFY_AND_THROW(ObjectQueue.Add(worker.Get()), "command failed");
        }
        ObjectQueue.Stop();
    }


    IObjectInQueue* TMainWorker::CreateSpecificWorker(const TString& type) {
        IObjectInQueue* result(nullptr);
        if (type == SYSTEM) {
            result = new TSyscomWorker(Script, Command);
        } else if (type == GET) {
            result = new TGetWorker(Script, Command);
        }else {
            ythrow yexception() << "wrong object in MAIN, type=" << type;
        }
        return result;
    }

    const TString TMainWorker::TYPE("Type");
    const TString TMainWorker::SYSTEM("SYSTEM");
    const TString TMainWorker::GET("GET");


////  TWorkerData  ////////////////////////////////////////////////////////////

    TGenericWorkerData::TGenericWorkerData(const TScriptData &script, const TCommand &command)
        : TBasicWorker(script, command)
    {
        const TString& type = Script.GetValue(Objective, TMainWorker::TYPE);
        LogLabel = type
                 + " objective "
                 + Command.first
                 + (!!Command.second ? " (id=" + (Command.second + ")") : "");
        FailsTest = GetValue<bool>(Objective, "FailsTest", true);
        AttemptsNumber = GetValue<int>(Objective, "AttemptsNumber", 1);
        AttemptsInterval = GetValue<int>(Objective, "AttemptsInterval", 100);

        ExpectedCode = GetValue<int>(Objective, "ExpCode", 200);
        CompileRegex(GetValue<TString>(Objective, "AnswerRegex", ".*"));
    }

    TGenericWorkerData::~TGenericWorkerData() {
        //
    }

    void TGenericWorkerData::Process(void*) {
        TTestResult result(true, "");
        for (int i = 0; i < AttemptsNumber; ++i) {
            Cout << "executing " << LogLabel << ", try: " << i + 1 << Endl;
            result = ProcessImpl();
            if (result.first)
                break;
            Sleep(TDuration::MicroSeconds(AttemptsInterval));
        }
        VERIFY_AND_THROW(result.first, LogLabel << " failed");
    }

    TGenericWorkerData::TTestResult TGenericWorkerData::ProcessImpl() {
        return TTestResult(true, "");
    }

    void TGenericWorkerData::CompileRegex(TString value) {
        TUtf16String TmpBufww = UTF8ToWide(value);
        TWtringBuf TmpBufw(TmpBufww);
        AnswerRegex = Pire::Fsm(Pire::Lexer(TmpBufw.begin(), TmpBufw.end())
            .AddFeature(Pire::Features::CaseInsensitive())
            .SetEncoding(Pire::Encodings::Utf8())
            .Parse()
            .Surround()
            ).Compile<Pire::SimpleScanner>();
    }

    bool TGenericWorkerData::IsMatchRegex(TString value) {
        TStringBuf TmpBufs(value);
        return Pire::Runner(AnswerRegex)
                    .Begin()
                    .Run(TmpBufs.begin(), TmpBufs.end())
                    .End();
    }

/////
    THttpWorkerData::THttpWorkerData(ui16 port, const TString& url, const TString& host)
        : Port(port)
        , Url(url)
        , Host(host)
    {
        //
    }

    THttpWorkerData::~THttpWorkerData() {
        //
    }

    TString THttpWorkerData::SendGet(TString& answer) {

        TSocket socket(TNetworkAddress(Host, Port));
        TSocketOutput so(socket);
        THttpOutput hOut(&so);
        hOut << "GET " + Url + " HTTP/1.1\r\n\r\n";
        TSocketInput si(socket);
        THttpInput hIn(&si);
        TString code = hIn.FirstLine();
        answer = hIn.ReadAll();
        Cout << "reply code: " << code << "\nreport: " << answer << "\n";

        return code;
    }


////  TGetWorker  //////////////////////////////////////////////////////////

    TGetWorker::TGetWorker(const TScriptData &script, const TCommand &command)
        : TGenericWorkerData(script, command)
        , THttpWorkerData()
    {
        bool useComp = !!GetValue(Objective, "Comp", "");
        if (useComp) {
            const TAttrs& comp = Script.GetObjectByAttr(Objective, "Comp");

            Host = GetValue<TString>(comp, "Host", "localhost");
            Port = GetValue<ui16>(comp, "Cport", 0);
            if (Port == 0){
                Port = GetValue<ui16>(comp, "Port", 80);
            }
        }else{
            Host = GetValue<TString>(Objective, "Host", "");
            Port = GetValue<ui16>(Objective, "Port", 80);
        }
        Url = GetValue<TString>(Objective, "Url");
    }

    TGetWorker::~TGetWorker() {
        //
    }

    TGenericWorkerData::TTestResult TGetWorker::ProcessImpl() {
        TTestResult result(true, "");

        TString code, answer;
        code = SendGet(answer);

        result.first = !(FailsTest && (code.find(ToString(ExpectedCode)) == TString::npos || !IsMatchRegex(answer)));
        return result;
    }

//////////////////////////////////////////////////////////////////////////

////  TSyscomWorker  //////////////////////////////////////////////////////////

    TSyscomWorker::TSyscomWorker(const TScriptData &script, const TCommand &command)
        : TGenericWorkerData(script, command)
        , SystemCommand()
    {
        ExpectedCode = GetValue<int>(Objective, "ExpCode", 0);
        SystemCommand = Script.GetValue(Objective, "Path")
                      + " "
                      + GetValue<TString>(Objective, "Opts", "");
    }

    TSyscomWorker::~TSyscomWorker() {
        //
    }

    TGenericWorkerData::TTestResult TSyscomWorker::ProcessImpl() {
        TTestResult result(true, "");
        TShellCommandOptions execOptions;
        execOptions.SetClearSignalMask(true)
                   .SetCloseAllFdsOnExec(true)
                   .SetAsync(true)
                   .SetLatency(TShellCommandOptions::DefaultSyncPollDelayMs)
                   .SetDetachSession(true)
                   .SetOutputStream(&Cout)
                   .SetErrorStream(&Cerr);
        TShellCommand exec(SystemCommand.data(), execOptions);
        exec.Run();
        exec.Wait();
        int code = exec.GetStatus();
        int gotError = 1;
        if (code == TShellCommand::SHELL_FINISHED)
            gotError = 0;
        Cout << LogLabel << " finished with: " << code << ", isError: " << gotError << Endl;
        result.first = !(FailsTest && ExpectedCode != gotError);
        return result;
    }
}
