/*
 * Copyright 2020 Bloomberg Finance LP
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <buildboxcommon_logstreamwriter.h>

#include <buildboxcommon_commandline.h>
#include <buildboxcommon_connectionoptions_commandline.h>
#include <buildboxcommon_exception.h>
#include <buildboxcommon_logging.h>
#include <buildboxcommon_logging_commandline.h>

#include <csignal>
#include <fcntl.h>
#include <sys/types.h>
#include <unistd.h>

#include <iostream>
#include <span>
#include <string>
#include <vector>

volatile sig_atomic_t signalReceived = false;

void signalHandler(int signum)
{
    BUILDBOX_LOG_INFO("Received signal " << signum << ", finishing.");
    signalReceived = true;
}

void streamFromFd(const int fd,
                  const buildboxcommon::ConnectionOptions &connectionOptions,
                  const std::string &resourceName, const bool followFd)
{
    buildboxcommon::LogStreamWriter writer(resourceName, connectionOptions);
    BUILDBOX_LOG_INFO("Writer ready, starting to stream");

    const auto pageSize = sysconf(_SC_PAGESIZE);
    const auto bufferSize = (pageSize > 0) ? pageSize : 4096;
    std::unique_ptr<char[]> buffer(new char[bufferSize]);

    bool keepReading = true;
    while (keepReading) {
        const ssize_t bytesRead = read(fd, buffer.get(), bufferSize);
        if (bytesRead > 0) {
            writer.write(std::string(buffer.get(), bytesRead));
        }

        keepReading = followFd ? !signalReceived : (bytesRead > 0);
    };

    BUILDBOX_LOG_INFO("Sending last request with finish_write == true...")
    const bool streamSucceeded = writer.commit();
    if (streamSucceeded) {
        BUILDBOX_LOG_INFO("Stream committed successfully.")
    }
    else {
        BUILDBOX_LOG_ERROR("Stream failed, data not committed.")
    }
}

buildboxcommon::LogStream
createLogStream(const std::string &parent,
                const buildboxcommon::ConnectionOptions &connectionOptions)
{
    return buildboxcommon::LogStreamWriter::createLogStream(parent,
                                                            connectionOptions);
}

int openFile(const std::string &path)
{
    const int fd = open(path.c_str(), O_RDONLY | O_CLOEXEC);
    if (fd == -1) {
        const auto error_reason = strerror(errno);
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "Could not open \"" << path << "\""
                                << "ERROR: " << error_reason);
    }
    return fd;
}

void registerTerminationSignalsHandler()
{
    BUILDBOX_LOG_INFO("Following the file. To stop terminate this process.");

    struct sigaction sa{};
    sa.sa_handler = signalHandler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;

    if (sigaction(SIGINT, &sa, nullptr) == -1) {
        BUILDBOX_LOG_ERROR("Unable to register signal handler for SIGINT");
    }
    if (sigaction(SIGTERM, &sa, nullptr) == -1) {
        BUILDBOX_LOG_ERROR("Unable to register signal handler for SIGTERM");
    }
}

int main(int argc, char *argv[])
{
    try {
        using buildboxcommon::CommandLineTypes;
        std::vector<CommandLineTypes::ArgumentSpec> d_spec = {
            {"resource-name",
             "LogStream write resource name. If not provided, creates one "
             "calling "
             "CreateLogStream()",
             CommandLineTypes::TypeInfo(
                 CommandLineTypes::DataType::COMMANDLINE_DT_STRING),
             CommandLineTypes::ArgumentSpec::O_OPTIONAL,
             CommandLineTypes::ArgumentSpec::C_WITH_ARG},
            {"resource-parent",
             "The parent resource name given to CreateLogStream()",
             CommandLineTypes::TypeInfo(
                 CommandLineTypes::DataType::COMMANDLINE_DT_STRING),
             CommandLineTypes::ArgumentSpec::O_OPTIONAL,
             CommandLineTypes::ArgumentSpec::C_WITH_ARG,
             CommandLineTypes::DefaultValue("")},
            {"file", "File to stream (if not specified, stdin)",
             CommandLineTypes::TypeInfo(
                 CommandLineTypes::DataType::COMMANDLINE_DT_STRING),
             CommandLineTypes::ArgumentSpec::O_OPTIONAL,
             CommandLineTypes::ArgumentSpec::C_WITH_ARG},
            {"follow",
             "Read file and stream changes until signaled to "
             "exit. (Only with --file.)",
             CommandLineTypes::TypeInfo(
                 CommandLineTypes::DataType::COMMANDLINE_DT_BOOL),
             CommandLineTypes::ArgumentSpec::O_OPTIONAL,
             CommandLineTypes::ArgumentSpec::C_WITHOUT_ARG}};

        auto loggingSpec = buildboxcommon::loggingCommandLineSpec();
        d_spec.insert(d_spec.end(), loggingSpec.cbegin(), loggingSpec.cend());

        const auto connectionOptionsSpec =
            buildboxcommon::ConnectionOptionsCommandLine("", "");
        d_spec.insert(d_spec.end(), connectionOptionsSpec.spec().cbegin(),
                      connectionOptionsSpec.spec().cend());

        d_spec.emplace_back(
            "logstream-server", "[Deprecated] Use --remote instead",
            CommandLineTypes::TypeInfo(
                CommandLineTypes::DataType::COMMANDLINE_DT_STRING),
            CommandLineTypes::ArgumentSpec::O_OPTIONAL,
            CommandLineTypes::ArgumentSpec::C_WITH_ARG);

        buildboxcommon::CommandLine commandLine(d_spec);
        auto args = std::span(argv, argc);
        if (!commandLine.parse(argc, argv)) {
            commandLine.usage();
            return 1;
        }

        if (commandLine.exists("help") || commandLine.exists("version")) {
            return 0;
        }

        buildboxcommon::logging::Logger::getLoggerInstance().initialize(
            args[0]);
        buildboxcommon::LogLevel logLevel = buildboxcommon::LogLevel::ERROR;
        if (!buildboxcommon::parseLoggingOptions(commandLine, logLevel)) {
            return 1;
        }
        BUILDBOX_LOG_SET_LEVEL(logLevel);

        // Server details:
        buildboxcommon::ConnectionOptions connectionOptions;
        buildboxcommon::ConnectionOptionsCommandLine::configureChannel(
            commandLine, "", &connectionOptions);

        if (commandLine.exists("logstream-server")) {
            if (commandLine.exists("remote")) {
                std::cerr << "WARNING: --logstream-server and --remote are "
                             "specified together. Deprecated "
                             "--logstream-server option is ignored\n";
            }
            else {
                connectionOptions.d_url =
                    commandLine.getString("logstream-server");
            }
        }

        // Input source:
        const bool readFromFile = commandLine.exists("file");
        const bool followFile = readFromFile && commandLine.exists("follow");

        try {
            int fd = 0;
            std::string source;
            if (readFromFile) {
                source = commandLine.getString("file");
                fd = openFile(source);

                if (followFile) {
                    registerTerminationSignalsHandler();
                }
            }
            else {
                fd = STDIN_FILENO;
                source = "stdin";
            }

            auto resourceName = commandLine.getString("resource-name", "");
            if (resourceName.empty()) {
                const auto parent =
                    commandLine.getString("resource-parent", "");
                BUILDBOX_LOG_INFO("No resource name provided, calling "
                                  "CreateLogStream() with parent=\""
                                  << parent << "\"");

                const auto logstream =
                    createLogStream(parent, connectionOptions);
                BUILDBOX_LOG_INFO("CreateLogStream() returned [name="
                                  << logstream.name()
                                  << ", write_resource_name="
                                  << logstream.write_resource_name() << "]");

                resourceName = logstream.write_resource_name();
            }

            BUILDBOX_LOG_INFO("Will stream \"" << source << "\" to \""
                                               << connectionOptions.d_url
                                               << "/" << resourceName << "\"");
            streamFromFd(fd, connectionOptions, resourceName, followFile);
        }
        catch (const std::exception &e) {
            BUILDBOX_LOG_ERROR("Caught exception ["
                               << e.what() << "] while running " << args[0]);
            return -1;
        }
    }
    catch (const std::exception &exception) {
        // This catch should not be relied on and is here as a backstop.
        BUILDBOX_LOG_ERROR("Caught exception in main " << exception.what());
        return 1;
    }
}
