#include <nlohmann/json.hpp>
#include <open62541/plugin/log_stdout.h>
#include <RFID/ReadDataMsg.h>
#include <recepture/Recepture.h>
#include <client/UaClient.h>
#include <ProcessCellClient.h>
#include "core/UaObjects.h"
#include "mqtt/client.h"
#include "mqttcomm/RfidMqttClient.h"
#include "configuration/Configuration.h"
#include <chrono>
#include <helpers/Constants.h>

ProcessCellClient ProcessCellInstance;
ProcessCellClient TransportCellInstance;

static void StateNotProcessedAction(Recepture& recipe);
static void StateTransportingAction(Recepture& recipe);

static void FindAndOrderTrasnport(Recepture& recipe, uint64_t cellIdDestination);
static std::string TransformOperationIdToString(UA_Byte operationId);

static void ClientCycle();
static void Release();

bool clientRunning = false;
bool running = true;
static void stopHandler(int sign) {
    UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "Received ctrl-c");
    running = false;
    clientRunning = false;
}

static std::string TransformOperationIdToString(UA_Byte operationId) {
    std::string operation = "";
    switch (operationId) {
        case 1:
            operation = "storage";
            return operation;
        case 2:
            operation = "action";
            return operation;
        case 3:
            operation = "transport";
            return operation;
    }
}




static void StateNotProcessedAction(Recepture& recipe) {
    ProcedureStep step = recipe.GetActualProcedureStep();
    std::string operationName = TransformOperationIdToString(step.operationId);
    if(!ProcessCellInstance.FindCell(operationName)) {
        UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "Could Not Find Process Cell --> ENDING!");
        return;
    }


    auto actualReservationChanged = [&recipe](UaSubscription& subscription, UA_DataValue* dataValue) {
        uint64_t newValue = GetVariantValue<uint64_t>(dataValue->value);
        std::cout << "Actual reservation Id changed Subscription Id[" << subscription.GetId() << "] Value: " << newValue << std::endl;
        // if actual reservation == myreservation
        if(newValue != 0 && ProcessCellInstance.cellReservationId == newValue) {
            // Order transport
            ProcessCellInstance.iterateForSub = false;
            ProcessCellInstance.cellSubscription.RemoveMonitoredItem(ProcessCellInstance.reservationMonitoredItemId);
            UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "It's my turn, going to find transport!");
            FindAndOrderTrasnport(recipe, recipe.productionState.reservedActionCellId);
        }
    };
    // Make subscription on actual reservation
    ProcessCellInstance.reservationMonitoredItemId = ProcessCellInstance.cellSubscription.AddMonitoredItem(ProcessCellInstance.CellActualReservationId, actualReservationChanged);

    if(ProcessCellInstance.MakeResevation(recipe.header.serialNumber, step.operationId, step.order, step.parameterA, step.parameterB, ProcessCellInstance.cellReservationId)) {
        recipe.productionState.SetState(EProcedureStepState::ReservedAction);
        recipe.productionState.SetReservedActionCellId((uint64_t) ProcessCellInstance.connectedCellId);
        recipe.productionState.SetActionReservationId((uint64_t) ProcessCellInstance.cellReservationId);
        UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "Action successfully reserved!");

        ProcessCellInstance.iterateForSub = true;
    } else {
        UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "Action reservation failed!");
    }
}






static void FindAndOrderTrasnport(Recepture& recipe, uint64_t cellIdDestination) {
    ProcedureStep step = ProcedureStep::GetTransportStep();
    std::string operationName = TransformOperationIdToString(step.operationId);

    if(!TransportCellInstance.FindCell(operationName)) {
        UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "Could Not Find Transport Cell --> ENDING!");
        return;
    }

    auto actualReservationChanged = [&recipe, cellIdDestination, step](UaSubscription& subscription, UA_DataValue* dataValue) {
        uint64_t newValue = GetVariantValue<uint64_t>(dataValue->value);
        std::cout << "Actual reservation Id changed for transport Subscription Id[" << subscription.GetId() << "] Value: " << newValue << std::endl;
        if(TransportCellInstance.cellReservationId == newValue) {
            // Run transport
            std::cout << "Going to run transport." << std::endl;
            recipe.productionState.SetState(EProcedureStepState::Transporting);
            TransportCellInstance.RunAction(step.operationId, TransportCellInstance.connectedCellId, cellIdDestination);

            // Cancel subscriptions
            TransportCellInstance.iterateForSub = false;
            TransportCellInstance.cellSubscription.RemoveMonitoredItem(TransportCellInstance.reservationMonitoredItemId);

            //END old connections
            Release();
        }
    };
    // Make subscription on actual reservation
    TransportCellInstance.reservationMonitoredItemId = TransportCellInstance.cellSubscription.AddMonitoredItem(TransportCellInstance.CellActualReservationId, actualReservationChanged);

    if(TransportCellInstance.MakeResevation(recipe.header.serialNumber, step.operationId, step.order, step.parameterA, step.parameterB, TransportCellInstance.cellReservationId)) {
        recipe.productionState.SetState(EProcedureStepState::ReservedTransport);
        recipe.productionState.SetReservedTransportCellId((uint64_t) TransportCellInstance.connectedCellId);
        recipe.productionState.SetTransportReservationId((uint64_t) TransportCellInstance.cellReservationId);

        UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "Transport successfully reserved!");
        TransportCellInstance.iterateForSub = true;
    } else {
        UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "Transport reservation failed!");
    }
}






/**
 * This state action is done when product lands on new position in factory and the last saved state is Transporting.
 * It means that on last position, he ordered transport.
 * @param recipe
 */
static void StateTransportingAction(Recepture& recipe) {
    // Pripojit sa na transportnu bunku a skontrolovat proces prenosu
    ProcedureStep transport_step = ProcedureStep::GetTransportStep();
    std::string operationName = TransformOperationIdToString(transport_step.operationId);

    if(!TransportCellInstance.FindCell(operationName)) {
        UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "Could Not Find Transport Cell --> ENDING!");
        return;
    }
    // Vycitanie statusu o presune
    if(TransportCellInstance.connectedCellId == recipe.productionState.reservedTransportCellId) {
        uint32_t manuState = TransportCellInstance.GetManufacturingState();
        if(manuState == 20) {
            uint32_t manuStatus = TransportCellInstance.GetManufacturingStatus();
            TransportCellInstance.WriteManufacturingDoneCmd();
        }
        TransportCellInstance.DeleteResevation(recipe.productionState.transportReservationId);
    } else {
        UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "The wrong transport cell founded, can't read status!");
    }


    // Pripojit sa na localhost a porovnat localhost cell id s rezervovanou
    ProcedureStep step = recipe.GetActualProcedureStep();
    if(ProcessCellInstance.ConnectToLocal())
        UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "Connected to local Process Cell!");

    if(recipe.productionState.reservedActionCellId == ProcessCellInstance.connectedCellId)
        UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "Transported on good position.");
    else {
        UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "Transported on bad position.");
        return;
    }
    std::cout << "Going to run action" << std::endl;
    recipe.productionState.SetReservedTransportCellId(0);
    recipe.productionState.SetTransportReservationId(0);
    recipe.productionState.SetState(EProcedureStepState::InProgress);

    ProcessCellInstance.RunAction(step.operationId, step.parameterA, step.parameterB);

    Release();
}

// Po dokonceni akcie sa vrati vyrobok na citacku
static void StateInProgressAction(Recepture& recipe) {
    if(ProcessCellInstance.ConnectToLocal())
        UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "Connected to local Process Cell!");

    uint32_t manuState = ProcessCellInstance.GetManufacturingState();
    if(manuState == 20) { // ak je DONE
        UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "Action is DONE.");


        recipe.productionState.SetState(EProcedureStepState::Done);
        recipe.productionState.SetReservedActionCellId(0);
        recipe.productionState.SetActionReservationId(0);

        uint32_t manuStatus = ProcessCellInstance.GetManufacturingStatus();

        ProcessCellInstance.WriteManufacturingDoneCmd();

        // AK to bol posledny krok
        if(recipe.procedureStepList.size() == recipe.header.GetProcedureLength() && manuStatus == 1) {
            // Everything is DONE
            uint64_t timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(
                    std::chrono::system_clock::now().time_since_epoch()).count();
            recipe.header.SetDispenseDateTime(timestamp);
            recipe.header.SetStatus(1);// 1=OK a kedy bude 5=NOK
            // TODO transport na vydaj
            FindAndOrderTrasnport(recipe, 1);

            ProcessCellInstance.DeleteResevation(recipe.productionState.actionReservationId);
        }
        else if(manuStatus == 5 ) {
            // Everything is DONE
            uint64_t timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(
                    std::chrono::system_clock::now().time_since_epoch()).count();
            recipe.header.SetDispenseDateTime(timestamp);
            recipe.header.SetStatus(5);// 1=OK a kedy bude 5=NOK
            // TODO transport na NOK
            FindAndOrderTrasnport(recipe, 5);

            ProcessCellInstance.DeleteResevation(recipe.productionState.actionReservationId);
        }
        else { // Ak to nebol posledny krok a akcia dopadla dobre
            // Change state
            recipe.productionState.SetCurrentStep(recipe.productionState.step + 1);
            recipe.productionState.SetState(EProcedureStepState::NotProcessed);

            //END old connections
            Release();
            sleep(2);

            // Run new state action
            StateNotProcessedAction(recipe);
            // TODO kedy zrusit rezervaciu na vyrobnej bunke?
        }
    }
    else {
        UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "Action is not DONE yet.");
        return;
    }
}

int main() {
    Configuration cfg;
    cfg.Load(Constants::CFG_FILE_NAME);

    std::thread clientThread(ClientCycle);

    RfidMqttClient mqttClient(cfg.mqttCfg);
    mqttClient.Connect();

    try {
        std::cout << "Subscribing to topics." << std::endl;
        mqttClient.GetClient().subscribe(cfg.mqttCfg.rfidReadDataTopic, cfg.mqttCfg.qos);

        while (running) {
            auto msg =  mqttClient.GetClient().consume_message();

            if (!msg) {
                if (! mqttClient.GetClient().is_connected()) {
                    std::cout << "Lost connection. Attempting reconnect" << std::endl;
                    if (mqttClient.TryReconnect()) {
                        mqttClient.GetClient().subscribe(cfg.mqttCfg.rfidReadDataTopic, cfg.mqttCfg.qos);
                        std::cout << "Reconnected" << std::endl;
                        continue;
                    } else std::cout << "Reconnect failed." << std::endl;
                } else
                    std::cout << "An error occurred retrieving messages." << std::endl;

                break;
            }

            std::cout << std::endl << std::endl;

            nlohmann::json msg_in_json = nlohmann::json::parse(msg->to_string());
            //std::cout << msg->get_topic() << ": " << msg->to_string() << std::endl;
            if(msg->get_topic() == cfg.mqttCfg.rfidReadDataTopic && msg_in_json.size() > 2) {
                ReadDataMsg readerRawData = msg_in_json;
                if (readerRawData.readState == "OK" && !clientRunning) {
                    std::vector<uint8_t> rawUserData = readerRawData.GetUserRawData();
                    Recepture recepture(rawUserData, mqttClient);
                    std::cout << recepture << std::endl;

                    switch (recepture.productionState.state) {
                        case EProcedureStepState::NotProcessed:
                            StateNotProcessedAction(recepture);
                            break;
                        case EProcedureStepState::Transporting:
                            StateTransportingAction(recepture);
                            break;
                        case EProcedureStepState::InProgress:
                            StateInProgressAction(recepture);
                            break;
                    }
                }
            }
        }
    } catch (const mqtt::exception& exc) {
        std::cerr << exc.what() << std::endl;
    }

    mqttClient.Disconnect();

    return 0;
}

static void ClientCycle() {
    while (running) {

        if(ProcessCellInstance.CanIterateSub())
            UA_Client_run_iterate(ProcessCellInstance.cellUaClient.GetClient(), 100);

        if(TransportCellInstance.CanIterateSub())
            UA_Client_run_iterate(TransportCellInstance.cellUaClient.GetClient(), 100);

        clientRunning = ProcessCellInstance.isConnected || TransportCellInstance.isConnected;
    }
}

static void Release() {
    // Clean everything
    ProcessCellInstance.Release();
    TransportCellInstance.Release();
}