/*
 * Copyright 2019 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_fileutils.h>
#include <buildboxcommon_identity.h>
#include <buildboxcommon_logging.h>

#include <absl/log/globals.h>
#include <absl/log/initialize.h>
#include <absl/log/log_sink_registry.h>
#include <algorithm>
#include <cerrno>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <ctime>
#include <exception>
#include <grpcpp/support/method_handler.h>
#include <iomanip>
#include <iterator>
#include <sstream>
#include <unistd.h>

template <> struct std::default_delete<FILE> {
    void operator()(FILE *file) const noexcept
    {
        // NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
        (void)fclose(file);
    }
};

namespace buildboxcommon {

namespace logging {

const std::map<std::string, LogLevel> &stringToLogLevelMap()
{
    static const std::map<std::string, LogLevel> map = {
        {"trace", LogLevel::TRACE},
        {"debug", LogLevel::DEBUG},
        {"info", LogLevel::INFO},
        {"warning", LogLevel::WARNING},
        {"error", LogLevel::ERROR}};

    return map;
}

const std::map<LogLevel, std::string> &logLevelToStringMap()
{
    static const std::map<LogLevel, std::string> map = {
        {LogLevel::TRACE, "trace"},
        {LogLevel::DEBUG, "debug"},
        {LogLevel::INFO, "info"},
        {LogLevel::WARNING, "warning"},
        {LogLevel::ERROR, "error"}};

    return map;
}

class DirectoryLogSink : public absl::LogSink {
  public:
    explicit DirectoryLogSink(const std::string &directory,
                              const std::string &programName)
        : d_directory(directory), d_programName(programName)
    {
        const auto identity = getApplicationIdentity();
        d_hostname = identity.d_hostname;
        const char *user = getenv("USER");
        d_user =
            user != nullptr ? user : std::to_string(identity.d_process_uid);
    }

    void Send(const absl::LogEntry &entry) override
    {
        if (d_file == nullptr) {
            if (d_failed) {
                // Log file creation failed before and that failure has been
                // printed. Do not retry log file creation for every log line.
                return;
            }

            // Create log file on first log message to avoid creating empty
            // log files.
            std::ostringstream logFilenameStream;
            logFilenameStream << d_directory << '/' << d_programName << '.'
                              << d_hostname << '.' << d_user << ".log";

            struct tm localtime{};
            time_t t = absl::ToTimeT(entry.timestamp());
            if (localtime_r(&t, &localtime) != nullptr) {
                logFilenameStream
                    << '.' << std::put_time(&localtime, "%Y%m%d-%H%M%S");
            }

            logFilenameStream << '.' << getpid();

            d_file = std::unique_ptr<FILE>(
                fopen(logFilenameStream.str().c_str(), "a"));
            if (d_file == nullptr) {
                d_failed = true;
                std::cerr << "ERROR: Failed to create log file '"
                          << logFilenameStream.str()
                          << "': " << std::strerror(errno) << '\n';
                return;
            }
        }

        const auto msg = entry.text_message_with_prefix_and_newline();
        errno = 0;
        if (fwrite(msg.data(), 1, msg.size(), d_file.get()) < msg.size()) {
            if (!d_failed) {
                d_failed = true;
                std::cerr << "ERROR: Failed to write to log file";
                if (errno) {
                    std::cerr << ": " << std::strerror(errno);
                }
                std::cerr << '\n';
            }
        }
    }

  private:
    std::string d_directory;
    std::string d_programName;
    std::string d_hostname;
    std::string d_user;
    // Use FILE instead of std::ofstream as the latter isn't thread-safe.
    std::unique_ptr<FILE> d_file;
    bool d_failed = false;
};

Logger::Logger() {}

Logger &Logger::getLoggerInstance()
{
    static Logger logger;
    return logger;
}

void Logger::setOutputDirectory(const char *outputDirectory)
{
    if (d_abslLogInitialized) {
        BUILDBOXCOMMON_THROW_EXCEPTION(
            std::runtime_error, "Output directories must be specified "
                                "before Logger instance is initialized.");
    }

    d_logOutputDirectory = outputDirectory ? outputDirectory : "";
}

std::string Logger::getOutputDirectory() { return d_logOutputDirectory; }

void Logger::enableLoggingBothStderrAndFiles()
{
    // Error if logger is already initialized
    if (d_abslLogInitialized) {
        BUILDBOXCOMMON_THROW_EXCEPTION(
            std::runtime_error,
            "Attempted to enable logging to both stderr and files after "
            "Logger instance was initialized.");
    }
    // Error if output directory is not set
    if (d_logOutputDirectory.empty()) {
        BUILDBOXCOMMON_THROW_EXCEPTION(
            std::runtime_error,
            "Output directory must be set before enabling logging to both "
            "stderr and files.");
    }
    d_logBothStderrAndFiles = true;
}

void Logger::initialize(const char *programName)
{
    if (programName == nullptr || strlen(programName) == 0) {
        BUILDBOXCOMMON_THROW_EXCEPTION(
            std::runtime_error,
            "Initialize() must be called with a non-empty program name");
    }

    if (d_abslLogInitialized) {
        BUILDBOXCOMMON_THROW_EXCEPTION(
            std::runtime_error,
            "Attempted to initialize Logger instance more than once.");
    }

    if (!d_logOutputDirectory.empty()) {
        d_directoryLogSink = std::make_unique<DirectoryLogSink>(
            d_logOutputDirectory,
            buildboxcommon::FileUtils::pathBasename(programName));
        absl::AddLogSink(d_directoryLogSink.get());

        if (d_logBothStderrAndFiles) {
            absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo);
        }
    }
    else {
        absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo);
    }

    // Since we will add our own, we also disable abseil's own prefix using
    // the following global variable.
    absl::EnableLogPrefix(false);

    absl::InitializeLog();
    d_abslLogInitialized = true;
}

void Logger::setLogLevel(LogLevel logLevel)
{
    /* By default show all messages */
    auto abslMinSeverity = absl::LogSeverityAtLeast::kInfo;
    if (logLevel == buildboxcommon::LogLevel::WARNING) {
        abslMinSeverity = absl::LogSeverityAtLeast::kWarning;
    }
    else if (logLevel == buildboxcommon::LogLevel::ERROR) {
        abslMinSeverity = absl::LogSeverityAtLeast::kError;
    }

    absl::SetMinLogLevel(abslMinSeverity);

    /* For the verbose levels, we also set the VLOG max. level */
    if (logLevel == LogLevel::DEBUG) {
        absl::SetGlobalVLogLevel(buildboxcommon::VLOG_LEVEL_DEBUG);
    }
    else if (logLevel == LogLevel::TRACE) {
        absl::SetGlobalVLogLevel(buildboxcommon::VLOG_LEVEL_TRACE);
    }

    d_logLevel = logLevel;
}

LogLevel Logger::getLogLevel() { return d_logLevel; }

void Logger::disableStderr()
{
    // Setting the minimum severity to FATAL, which we never use, to
    // effectively hide all messages from stderr because otherwise abseil still
    // prints ERRORs.
    absl::SetStderrThreshold(absl::LogSeverityAtLeast::kFatal);
}

std::string stringifyLogLevels()
{
    std::string logLevels;
    for (const auto &stringLevelPair :
         buildboxcommon::logging::logLevelToStringMap()) {
        logLevels += stringLevelPair.second + "/";
    }
    logLevels.pop_back();
    return logLevels;
}

std::string printableCommandLine(const std::vector<std::string> &commandLine)
{
    if (commandLine.empty()) {
        return "";
    }
    std::ostringstream commandLineStream;
    // -1, to avoid putting space at end of string
    copy(commandLine.begin(), commandLine.end() - 1,
         std::ostream_iterator<std::string>(commandLineStream, " "));
    commandLineStream << commandLine.back();
    return commandLineStream.str();
}

std::ostream &operator<<(std::ostream &os, const LogPrefix &logPrefix)
{
    const std::chrono::system_clock::time_point now =
        std::chrono::system_clock::now();
    const time_t nowAsTimeT = std::chrono::system_clock::to_time_t(now);
    const std::chrono::milliseconds nowMs =
        std::chrono::duration_cast<std::chrono::milliseconds>(
            now.time_since_epoch()) %
        1000;
    const size_t slashIndex = logPrefix.d_file.find_last_of('/');
    const auto basenameStart =
        slashIndex != std::string_view::npos ? slashIndex + 1 : 0;

    struct tm localtime{};
    try {
        auto _ = localtime_r(&nowAsTimeT, &localtime);
        (void)_;
        os << std::put_time(&localtime, "%FT%T") << '.' << std::setfill('0')
           << std::setw(3) << nowMs.count() << std::put_time(&localtime, "%z");
    }
    catch (...) {
        os << "Time is broken...";
    }

    os << " [" << getpid() << ":" << pthread_self() << "] ["
       << logPrefix.d_file.substr(basenameStart) << ":"
       << logPrefix.d_lineNumber << "] ";
    os << "[" << logPrefix.d_severity << "] ";

    return os;
}

} // namespace logging

} // namespace buildboxcommon
