#include "tool.h"

#include <maps/wikimap/mapspro/services/mrc/libs/db/include/track_point_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/ugc/assignment_object_gateway.h>
#include <maps/libs/common/include/base64.h>
#include <maps/libs/common/include/exception.h>
#include <yandex/maps/geolib3/sproto.h>
#include <maps/libs/log8/include/log8.h>
#include <yandex/maps/mrc/signal_queue/result_queue.h>
#include <yandex/maps/mrc/signal_queue/results.h>
#include <yandex/maps/proto/common2/i18n.sproto.h>
#include <yandex/maps/proto/common2/geo_object.sproto.h>
#include <yandex/maps/proto/common2/geometry.sproto.h>
#include <yandex/maps/proto/common2/metadata.sproto.h>
#include <yandex/maps/proto/common2/response.sproto.h>
#include <yandex/maps/proto/offline-mrc/results.sproto.h>

#include <cstddef>
#include <cstdint>
#include <fstream>
#include <sstream>
#include <string_view>
#include <variant>
#include <vector>

#include <boost/filesystem.hpp>
#include <boost/lexical_cast.hpp>
#include <sqlite3.h>

namespace fs = boost::filesystem;
namespace presults = yandex::maps::sproto::offline::mrc::results;

namespace maps {
namespace mrc {
namespace {

constexpr size_t BATCH_SIZE = 100;

class SQLiteDb {
public:
    using Null = std::monostate;
    using Blob = std::vector<std::byte>;
    using Variant = std::variant<Null, int64_t, double, std::string, Blob>;

    explicit SQLiteDb(const std::string& path)
    {
        sqlite3* db = nullptr;
        auto result = sqlite3_open(path.c_str(), &db);
        db_.reset(db);
        check(result);
    }

    void exec(const std::string& sql)
    {
        sqlite3_stmt* stmt = nullptr;
        auto result = sqlite3_prepare_v2(db_.get(), sql.data(),
                                         (int)sql.size(), &stmt, 0);
        stmt_.reset(stmt);
        check(result);
        step();
    }

    bool fetch(std::vector<Variant>& row)
    {
        if (!stmt_) {
            return false;
        }
        int cols = sqlite3_column_count(stmt_.get());
        row.resize(cols);
        for (int i = 0; i < cols; ++i) {
            switch (sqlite3_column_type(stmt_.get(), i)) {
            case SQLITE_INTEGER:
                row[i] = (int64_t)sqlite3_column_int64(stmt_.get(), i);
                break;
            case SQLITE_FLOAT:
                row[i] = sqlite3_column_double(stmt_.get(), i);
                break;
            case SQLITE_TEXT: {
                auto ptr = (const char*)sqlite3_column_text(stmt_.get(), i);
                auto size = (size_t)sqlite3_column_bytes(stmt_.get(), i);
                row[i] = std::string{ptr, size};
            } break;
            case SQLITE_BLOB: {
                auto first = (Blob::const_pointer)sqlite3_column_blob(
                    stmt_.get(), i);
                auto last = first + sqlite3_column_bytes(stmt_.get(), i);
                row[i] = Blob{first, last};
            } break;
            default:
                row[i] = Null{};
            }
        }
        step();
        return true;
    }

private:
    struct DbDeleter {
        void operator()(sqlite3* db) const { sqlite3_close_v2(db); }
    };

    struct StmtDeleter {
        void operator()(sqlite3_stmt* stmt) const { sqlite3_finalize(stmt); }
    };

    std::unique_ptr<sqlite3, DbDeleter> db_;
    std::unique_ptr<sqlite3_stmt, StmtDeleter> stmt_;

    std::string errmsg() const
    {
        std::string result;
        if (db_) {
            result = sqlite3_errmsg(db_.get());
        }
        if (result.empty()) {
            result = "SQLite unknown error";
        }
        return result;
    }

    void check(int result) const
    {
        if (result != SQLITE_OK) {
            throw Exception{} << errmsg();
        }
    }

    void step()
    {
        switch (sqlite3_step(stmt_.get())) {
        case SQLITE_DONE:
            stmt_.reset(nullptr);
            break;
        case SQLITE_ROW:
            break;
        default:
            throw Exception{} << errmsg();
        }
    }
};

std::string concat(const std::string& prefix, int suffix)
{
    std::ostringstream os;
    os << prefix << std::setw(3) << std::setfill('0') << suffix;
    return os.str();
}

std::string base64Encode(db::TId id)
{
    auto str = std::to_string(id);
    return maps::base64Encode(TArrayRef(str.begin(), str.end()));
}

std::string makePath(const std::string& dir,
                     const std::string& infix,
                     db::TId assignmentId)
{
    std::ostringstream os;
    os << dir << "/mrc_" << infix << "_" << base64Encode(assignmentId)
       << ".sqlite";
    return os.str();
}

void restoreFile(const std::string& path)
{
    for (int i = 1; fs::exists(concat(path, i).c_str()); ++i) {
        std::ofstream os(path, std::ios_base::binary | std::ios_base::app);
        os.seekp(0, std::ios_base::end);
        std::ifstream is(concat(path, i), std::ios_base::binary);
        os << is.rdbuf();
        boost::filesystem::remove(concat(path, i));
    }
}

template <class Function>
void sqliteForEach(const std::string& path, Function&& f)
{
    if (!fs::exists(path.c_str())) {
        WARN() << path << " doesn't exist";
        return;
    }
    restoreFile(path);
    SQLiteDb db{path};
    db.exec("SELECT value FROM items ORDER BY key");
    size_t count = 0;
    for (std::vector<SQLiteDb::Variant> row; db.fetch(row);) {
        try {
            if (auto ptr = std::get_if<SQLiteDb::Blob>(&row[0])) {
                f(std::string_view{(const char*)ptr->data(), ptr->size()});
                ++count;
            }
            else {
                throw Exception{} << "invalid data";
            }
            if (!(count % BATCH_SIZE)) {
                INFO() << path << ": " << count;
            }
        }
        catch (const std::exception& e) {
            WARN() << path << ": " << e.what();
        }
    }
    INFO() << path << ": " << count << " (done)";
}

template <class Gateway, class Batch>
void flush(wiki::common::PoolHolder& dest, Batch&& src)
{
    auto txn = dest.pool().masterWriteableTransaction();
    Gateway{*txn}.insert(src);
    txn->commit();
    src.clear();
}

void uploadTracks(wiki::common::PoolHolder& db,
                  const std::string& dir,
                  db::TId assignmentId,
                  const std::string& deviceId)
{
    db::TrackPoints batch;
    sqliteForEach(makePath(dir, "tracks", assignmentId), [&](auto item) {
        auto trackPoint = boost::lexical_cast<presults::TrackPoint>(item);
        const auto& location = trackPoint.location();

        db::TrackPoint result{};
        result.setSourceId(deviceId)
            .setGeodeticPos(geolib3::sproto::decode(location.point()))
            .setTimestamp(chrono::TimePoint(
                std::chrono::milliseconds(trackPoint.time())))
            .setAssignmentId(assignmentId);

        if (location.accuracy()) {
            result.setAccuracyMeters(location.accuracy().get());
        }
        if (location.heading()) {
            result.setHeading(geolib3::Heading(location.heading().get()));
        }
        if (location.speed()) {
            result.setSpeedMetersPerSec(location.speed().get());
        }

        batch.push_back(std::move(result));
        if (!(batch.size() % BATCH_SIZE)) {
            flush<db::TrackPointGateway>(db, batch);
        }
    });
    flush<db::TrackPointGateway>(db, batch);
}

db::ugc::AssignmentObjectType
deserializeAssignmentObjectType(presults::ObjectType objectType)
{
    switch (objectType) {
    case presults::ObjectType::BARRIER:
        return db::ugc::AssignmentObjectType::Barrier;
    case presults::ObjectType::DEADEND:
        return db::ugc::AssignmentObjectType::Deadend;
    case presults::ObjectType::BAD_CONDITIONS:
        return db::ugc::AssignmentObjectType::BadConditions;
    case presults::ObjectType::NO_ENTRY:
        return db::ugc::AssignmentObjectType::NoEntry;
    default:
        break;
    }
    throw LogicError() << "Unexpected assignment object type " << objectType;
}

void uploadPinObjects(wiki::common::PoolHolder& db,
                      const std::string& dir,
                      db::TId assignmentId)
{
    db::ugc::AssignmentObjects batch;
    sqliteForEach(makePath(dir, "pin_objects", assignmentId), [&](auto item) {
        auto object = boost::lexical_cast<presults::Object>(item);

        db::ugc::AssignmentObject result(
            assignmentId,
            chrono::TimePoint(std::chrono::milliseconds(object.created())),
            geolib3::sproto::decode(object.point().get()),
            deserializeAssignmentObjectType(object.type().get()));

        if (object.comment()) {
            result.setComment(object.comment().get());
        }

        batch.push_back(result);
        if (!(batch.size() % BATCH_SIZE)) {
            flush<db::ugc::AssignmentObjectGateway>(db, batch);
        }
    });
    flush<db::ugc::AssignmentObjectGateway>(db, batch);
}

void uploadImages(const common::Config& cfg,
                  const std::string& dir,
                  db::TId assignmentId,
                  const std::string& deviceId)
{
    auto queue = maps::mrc::signal_queue::ResultsQueue(
        cfg.signalsUploader().queuePath());
    sqliteForEach(makePath(dir, "images", assignmentId), [&](auto item) {
        signal_queue::AssignmentImage result;
        result.data() = boost::lexical_cast<presults::Image>(item);
        result.assignmentId() = assignmentId;
        result.sourceId() = deviceId;
        queue.push(result);
    });
}

} // anonymous namespace

void uploadAssignment(const common::Config& cfg,
                      const std::string& inputDir,
                      db::TId assignmentId,
                      const std::string& deviceId)
{
    auto db = cfg.makePoolHolder();
    uploadImages(cfg, inputDir, assignmentId, deviceId);
    uploadTracks(db, inputDir, assignmentId, deviceId);
    uploadPinObjects(db, inputDir, assignmentId);
}

} // mrc
} // maps
