// mds_convert — convert MDS1/MDS2 store files to MDS3 format
//
// Usage: mds_convert <data_dir>
//   Converts all .mdb store files in <data_dir> from MDS1/MDS2 to MDS3.
//   The index file (index.mdb) is left unchanged.
//   Original files are backed up as <store_id>.mdb.bak before conversion.

#include <algorithm>
#include <cstdint>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>

namespace fs = std::filesystem;

// ---- binary I/O helpers (same as types.h) ----

static void bin_write_u32(std::ostream& out, std::uint32_t v) {
    out.write(reinterpret_cast<const char*>(&v), 4);
}
static void bin_write_u64(std::ostream& out, std::uint64_t v) {
    out.write(reinterpret_cast<const char*>(&v), 8);
}
static void bin_write_str(std::ostream& out, const std::string& s) {
    bin_write_u32(out, static_cast<std::uint32_t>(s.size()));
    if (!s.empty()) out.write(s.data(), static_cast<std::streamsize>(s.size()));
}
static std::uint32_t bin_read_u32(std::istream& in) {
    std::uint32_t v = 0; in.read(reinterpret_cast<char*>(&v), 4); return v;
}
static std::uint64_t bin_read_u64(std::istream& in) {
    std::uint64_t v = 0; in.read(reinterpret_cast<char*>(&v), 8); return v;
}
static std::string bin_read_str(std::istream& in) {
    auto len = bin_read_u32(in);
    if (len == 0) return {};
    std::string s(len, '\0'); in.read(s.data(), len); return s;
}

// ---- format constants ----

static constexpr char STORE_MAGIC_V1[4] = {'M','D','S','1'};
static constexpr char STORE_MAGIC_V2[4] = {'M','D','S','2'};
static constexpr char STORE_MAGIC_V3[4] = {'M','D','S','3'};
static constexpr char INDEX_MAGIC_V2[4] = {'M','D','B','2'};
static constexpr char INDEX_MAGIC_V3[4] = {'M','D','B','3'};
static constexpr char INDEX_MAGIC_V4[4] = {'M','D','B','4'};
static constexpr char EXPORT_MAGIC_V1[4] = {'M','D','E','1'};
static constexpr char EXPORT_MAGIC_V2[4] = {'M','D','E','2'};

static constexpr std::uint8_t REC_ALBUM = 1;
static constexpr std::uint8_t REC_MEDIA = 2;

// ---- conversion ----

static bool is_index_file(const fs::path& p) {
    return p.filename() == "index.mdb";
}

static bool is_non_store_file(const fs::path& p) {
    // Check magic bytes to skip index and export files
    std::ifstream in(p, std::ios::binary);
    if (!in.is_open()) return true;
    char magic[4]{};
    in.read(magic, 4);
    if (std::memcmp(magic, INDEX_MAGIC_V2, 4) == 0) return true;
    if (std::memcmp(magic, INDEX_MAGIC_V3, 4) == 0) return true;
    if (std::memcmp(magic, INDEX_MAGIC_V4, 4) == 0) return true;
    if (std::memcmp(magic, EXPORT_MAGIC_V1, 4) == 0) return true;
    if (std::memcmp(magic, EXPORT_MAGIC_V2, 4) == 0) return true;
    return false;
}

static bool convert_store(const fs::path& path) {
    std::ifstream in(path, std::ios::binary);
    if (!in.is_open()) {
        std::cerr << "  ERROR: cannot open " << path << "\n";
        return false;
    }

    char magic[4]{};
    in.read(magic, 4);
    if (std::memcmp(magic, STORE_MAGIC_V3, 4) == 0) {
        std::cerr << "  already MDS3, skipping\n";
        return true;
    }
    const bool is_v2 = (std::memcmp(magic, STORE_MAGIC_V2, 4) == 0);
    if (!is_v2 && std::memcmp(magic, STORE_MAGIC_V1, 4) != 0) {
        std::cerr << "  ERROR: unknown magic, skipping\n";
        return false;
    }

    const std::string version = is_v2 ? "MDS2" : "MDS1";
    std::cerr << "  converting from " << version << " to MDS3\n";

    // Write to temp file
    const std::string tmp = path.string() + ".mds3.tmp";
    {
        std::ofstream out(tmp, std::ios::binary | std::ios::trunc);
        if (!out.is_open()) {
            std::cerr << "  ERROR: cannot create temp file\n";
            return false;
        }

        // MDS3 header: magic(4) + reserved(4)
        out.write(STORE_MAGIC_V3, 4);
        std::uint32_t reserved = 0;
        out.write(reinterpret_cast<const char*>(&reserved), 4);

        auto num_albums = bin_read_u32(in);
        std::size_t total_albums = 0, total_media = 0;

        for (std::uint32_t ai = 0; ai < num_albums && in.good(); ++ai) {
            std::string album_id = bin_read_str(in);
            std::string album_name = bin_read_str(in);
            std::string album_created = bin_read_str(in);

            bool album_public = false;
            if (is_v2) {
                std::uint8_t pub = 0;
                in.read(reinterpret_cast<char*>(&pub), 1);
                album_public = (pub != 0);
            }

            // Build album body
            std::ostringstream abody(std::ios::binary);
            bin_write_str(abody, album_id);
            bin_write_str(abody, album_name);
            bin_write_str(abody, album_created);
            { std::uint8_t pub = album_public ? 1 : 0;
              abody.write(reinterpret_cast<const char*>(&pub), 1); }
            std::string abody_str = abody.str();

            // Write album record
            out.write(reinterpret_cast<const char*>(&REC_ALBUM), 1);
            auto absz = static_cast<std::uint64_t>(abody_str.size());
            out.write(reinterpret_cast<const char*>(&absz), 8);
            out.write(abody_str.data(), static_cast<std::streamsize>(abody_str.size()));
            ++total_albums;

            auto num_media = bin_read_u32(in);
            for (std::uint32_t mi = 0; mi < num_media && in.good(); ++mi) {
                std::string media_id = bin_read_str(in);
                std::string orig_filename = bin_read_str(in);
                std::string media_kind = bin_read_str(in);
                std::string content_type = bin_read_str(in);
                std::string created_at = bin_read_str(in);
                std::uint64_t data_size = bin_read_u64(in);

                // Build media metadata body
                std::ostringstream mbody(std::ios::binary);
                bin_write_str(mbody, media_id);
                bin_write_str(mbody, album_id);
                bin_write_str(mbody, orig_filename);
                bin_write_str(mbody, media_kind);
                bin_write_str(mbody, content_type);
                bin_write_str(mbody, created_at);
                bin_write_u64(mbody, data_size);
                std::string mbody_str = mbody.str();

                auto total_body = static_cast<std::uint64_t>(mbody_str.size()) + data_size;

                // Write media record header
                out.write(reinterpret_cast<const char*>(&REC_MEDIA), 1);
                out.write(reinterpret_cast<const char*>(&total_body), 8);

                // Write media metadata
                out.write(mbody_str.data(), static_cast<std::streamsize>(mbody_str.size()));

                // Copy raw data in chunks
                constexpr std::size_t BUF = 1024 * 1024;
                std::vector<char> buf(BUF);
                std::uint64_t remaining = data_size;
                while (remaining > 0 && in.good()) {
                    auto chunk = static_cast<std::streamsize>(
                        std::min<std::uint64_t>(remaining, BUF));
                    in.read(buf.data(), chunk);
                    auto got = in.gcount();
                    if (got > 0) out.write(buf.data(), got);
                    remaining -= static_cast<std::uint64_t>(got);
                    if (got < chunk) break;
                }
                ++total_media;
            }
        }

        std::cerr << "  converted " << total_albums << " albums, "
                  << total_media << " media\n";
    }
    in.close();

    // Backup original, rename temp to final
    const std::string bak = path.string() + ".bak";
    fs::rename(path, bak);
    fs::rename(tmp, path);
    std::cerr << "  backup saved as " << bak << "\n";
    return true;
}

int main(int argc, char* argv[]) {
    if (argc < 2) {
        std::cerr << "Usage: mds_convert <data_dir>\n"
                  << "  Converts all MDS1/MDS2 store files to MDS3 format.\n"
                  << "  Originals are backed up as *.mdb.bak\n";
        return 1;
    }

    const fs::path data_dir(argv[1]);
    if (!fs::is_directory(data_dir)) {
        std::cerr << "ERROR: " << data_dir << " is not a directory\n";
        return 1;
    }

    int converted = 0, skipped = 0, errors = 0;

    for (const auto& entry : fs::directory_iterator(data_dir)) {
        if (!entry.is_regular_file()) continue;
        if (entry.path().extension() != ".mdb") continue;
        if (is_index_file(entry.path())) continue;
        if (is_non_store_file(entry.path())) {
            std::cerr << entry.path().filename().string() << ": not a store file, skipping\n";
            ++skipped;
            continue;
        }

        std::cerr << entry.path().filename().string() << ":\n";
        if (convert_store(entry.path())) {
            ++converted;
        } else {
            // Check if it was skipped (already v3) vs actual error
            std::ifstream check(entry.path(), std::ios::binary);
            char m[4]{};
            check.read(m, 4);
            if (std::memcmp(m, STORE_MAGIC_V3, 4) == 0) ++skipped;
            else ++errors;
        }
    }

    std::cerr << "\nDone: " << converted << " converted, "
              << skipped << " already MDS3, "
              << errors << " errors\n";
    return errors > 0 ? 1 : 0;
}
