/*  Copyright (C) 2012-2024 by László Nagy
    This file is part of Bear.

    Bear is a tool to generate compilation database for clang tooling.

    Bear is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Bear is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "EventsDatabaseWriter.h"
#include "libsys/Errors.h"

#include <google/protobuf/util/json_util.h>
#include <google/protobuf/stubs/common.h>

#include <fmt/format.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <cerrno>

using google::protobuf::util::JsonPrintOptions;

namespace {

    JsonPrintOptions create_print_options() {
        JsonPrintOptions print_options;
        print_options.add_whitespace = false;
#if GOOGLE_PROTOBUF_VERSION < 5026000
            print_options.always_print_primitive_fields = true;
#endif
        print_options.preserve_proto_field_names = true;
        print_options.always_print_enums_as_ints = false;
        return print_options;
    }

    const JsonPrintOptions print_options = create_print_options();
}

namespace ic::collect::db {

    rust::Result<EventsDatabaseWriter::Ptr> EventsDatabaseWriter::create(const fs::path &file) {
        int fd = open(file.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 00644);
        if (fd == -1) {
            auto message = fmt::format("Events db open failed (file {}): {}", file.string(), sys::error_string(errno));
            return rust::Err(std::runtime_error(message));
        }
        std::shared_ptr<EventsDatabaseWriter> result =
                std::make_shared<EventsDatabaseWriter>(file, fd);
        return rust::Ok(result);
    }

    EventsDatabaseWriter::EventsDatabaseWriter(fs::path path, int file) noexcept
            : path_(std::move(path))
            , file_(file)
    { }

    EventsDatabaseWriter::~EventsDatabaseWriter() noexcept {
        close(file_);
    }

    rust::Result<int> EventsDatabaseWriter::insert_event(const rpc::Event &event) {
        return to_json(event)
                .and_then<int>([this](const auto &json) {
                    return write_to_file(json);
                })
                .and_then<int>([this](const auto &) {
                    return write_to_file("\n");
                });
    }

    rust::Result<std::string> EventsDatabaseWriter::to_json(const rpc::Event &event) noexcept {
        std::string json;
        if (const auto status = google::protobuf::util::MessageToJsonString(event, &json, print_options); !status.ok()) {
            auto message = fmt::format(
                    "Events db write failed (to file {}): JSON formatting error",
                    path_.string()
            );
            return rust::Err(std::runtime_error(message));
        }
        return rust::Ok(std::move(json));
    }

    rust::Result<int> EventsDatabaseWriter::write_to_file(const std::string& content) noexcept {
        const char* content_ptr = content.c_str();
        size_t content_length = content.size();
        while (content_length) {
            const int written_length = write(file_, content_ptr, content_length);
            if (written_length == -1) {
                auto message = fmt::format(
                        "Events db write failed (to file {}): {}",
                        path_.string(),
                        sys::error_string(errno)
                );
                errno = 0;
                return rust::Err(std::runtime_error(message));
            }
            content_length -= written_length;
            content_ptr += written_length;
        }
        return rust::Ok(1);
    }
}
