// mde_check — validate MDE1/MDE2 export files
//
// Usage: mde_check <file.mdb>
//   Parses the MDE export file and reports structure, entry counts,
//   and any format errors (truncated strings, size mismatches, etc.).

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

namespace fs = std::filesystem;

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

static constexpr char EXPORT_MAGIC_V1[4] = {'M','D','E','1'};
static constexpr char EXPORT_MAGIC_V2[4] = {'M','D','E','2'};

// ---- binary read helpers ----

static bool read_bytes(std::istream& in, void* dest, std::size_t n) {
    in.read(reinterpret_cast<char*>(dest), static_cast<std::streamsize>(n));
    return static_cast<std::size_t>(in.gcount()) == n;
}

static bool read_u32(std::istream& in, std::uint32_t& out) {
    return read_bytes(in, &out, 4);
}

static bool read_u64(std::istream& in, std::uint64_t& out) {
    return read_bytes(in, &out, 8);
}

static bool read_str(std::istream& in, std::string& out, const char* field_name,
                     std::size_t file_size) {
    std::uint32_t len;
    if (!read_u32(in, len)) {
        std::cerr << "  ERROR: truncated string length for " << field_name
                  << " at offset " << (static_cast<std::size_t>(in.tellg()) - in.gcount()) << "\n";
        return false;
    }
    if (len > file_size) {
        std::cerr << "  ERROR: string length " << len << " for " << field_name
                  << " exceeds file size (" << file_size << ") at offset "
                  << (static_cast<std::size_t>(in.tellg())) << "\n";
        return false;
    }
    if (len == 0) { out.clear(); return true; }
    out.resize(len);
    if (!read_bytes(in, out.data(), len)) {
        std::cerr << "  ERROR: truncated string data for " << field_name
                  << " (expected " << len << " bytes) at offset "
                  << (static_cast<std::size_t>(in.tellg()) - in.gcount()) << "\n";
        return false;
    }
    return true;
}

// ---- check if string looks like valid UTF-8 text ----

static bool looks_printable(const std::string& s) {
    for (unsigned char c : s) {
        if (c < 0x20 && c != '\t' && c != '\n' && c != '\r') return false;
    }
    return true;
}

int main(int argc, char* argv[]) {
    if (argc < 2) {
        std::cerr << "Usage: mde_check <file.mdb>\n";
        return 1;
    }

    const char* path = argv[1];
    if (!fs::exists(path)) {
        std::cerr << "File not found: " << path << "\n";
        return 1;
    }

    auto file_size = fs::file_size(path);
    std::cout << "File: " << path << "\n";
    std::cout << "Size: " << file_size << " bytes\n";

    std::ifstream in(path, std::ios::binary);
    if (!in.is_open()) {
        std::cerr << "Cannot open file\n";
        return 1;
    }

    // ---- magic ----
    char magic[4]{};
    if (!read_bytes(in, magic, 4)) {
        std::cerr << "ERROR: file too small for magic bytes\n";
        return 1;
    }

    bool is_v2 = (std::memcmp(magic, EXPORT_MAGIC_V2, 4) == 0);
    bool is_v1 = (std::memcmp(magic, EXPORT_MAGIC_V1, 4) == 0);

    if (!is_v1 && !is_v2) {
        std::cerr << "ERROR: unknown magic: "
                  << magic[0] << magic[1] << magic[2] << magic[3]
                  << " (0x" << std::hex
                  << (unsigned)(unsigned char)magic[0]
                  << (unsigned)(unsigned char)magic[1]
                  << (unsigned)(unsigned char)magic[2]
                  << (unsigned)(unsigned char)magic[3]
                  << std::dec << ")\n";
        return 1;
    }

    std::cout << "Format: " << (is_v2 ? "MDE2" : "MDE1") << "\n";

    // ---- num stores ----
    std::uint32_t num_stores;
    if (!read_u32(in, num_stores)) {
        std::cerr << "ERROR: truncated store count\n";
        return 1;
    }
    std::cout << "Stores: " << num_stores << "\n\n";

    std::size_t total_albums = 0;
    std::size_t total_media = 0;
    std::size_t total_data_bytes = 0;
    std::size_t zero_size_media = 0;
    std::size_t errors = 0;

    for (std::uint32_t si = 0; si < num_stores; ++si) {
        std::string store_id, store_name, store_created;
        if (!read_str(in, store_id, "store_id", file_size) ||
            !read_str(in, store_name, "store_name", file_size) ||
            !read_str(in, store_created, "store_created", file_size)) {
            std::cerr << "ERROR: truncated store header at store #" << si << "\n";
            ++errors;
            break;
        }

        std::cout << "Store #" << si << ": id=" << store_id
                  << " name=\"" << store_name << "\""
                  << " created=" << store_created << "\n";

        if (!looks_printable(store_id) || !looks_printable(store_name)) {
            std::cerr << "  WARNING: store fields contain non-printable characters\n";
        }

        std::uint32_t num_albums;
        if (!read_u32(in, num_albums)) {
            std::cerr << "  ERROR: truncated album count\n";
            ++errors;
            break;
        }
        std::cout << "  Albums: " << num_albums << "\n";

        for (std::uint32_t ai = 0; ai < num_albums; ++ai) {
            std::string album_id, album_name, album_created;
            if (!read_str(in, album_id, "album_id", file_size) ||
                !read_str(in, album_name, "album_name", file_size) ||
                !read_str(in, album_created, "album_created", file_size)) {
                std::cerr << "  ERROR: truncated album header at album #" << ai
                          << " in store #" << si << "\n";
                ++errors;
                goto done;
            }

            bool album_public = false;
            if (is_v2) {
                std::uint8_t pub;
                if (!read_bytes(in, &pub, 1)) {
                    std::cerr << "  ERROR: truncated is_public byte\n";
                    ++errors;
                    goto done;
                }
                album_public = (pub != 0);
                if (pub > 1) {
                    std::cerr << "  WARNING: is_public=" << (int)pub
                              << " (expected 0 or 1)\n";
                }
            }

            std::cout << "  Album #" << ai << ": id=" << album_id
                      << " name=\"" << album_name << "\""
                      << " created=" << album_created;
            if (is_v2) std::cout << " public=" << (album_public ? "yes" : "no");
            std::cout << "\n";

            if (!looks_printable(album_id) || !looks_printable(album_name)) {
                std::cerr << "    WARNING: album fields contain non-printable characters\n";
            }

            std::uint32_t num_media;
            if (!read_u32(in, num_media)) {
                std::cerr << "    ERROR: truncated media count\n";
                ++errors;
                goto done;
            }
            std::cout << "    Media: " << num_media << "\n";

            for (std::uint32_t mi = 0; mi < num_media; ++mi) {
                auto entry_offset = static_cast<std::size_t>(in.tellg());

                std::string id, filename, kind, content_type, created;
                if (!read_str(in, id, "media_id", file_size) ||
                    !read_str(in, filename, "original_filename", file_size) ||
                    !read_str(in, kind, "media_kind", file_size) ||
                    !read_str(in, content_type, "content_type", file_size) ||
                    !read_str(in, created, "created_at", file_size)) {
                    std::cerr << "    ERROR: truncated media header at media #"
                              << mi << " offset=" << entry_offset << "\n";
                    ++errors;
                    goto done;
                }

                std::uint64_t size_bytes;
                if (!read_u64(in, size_bytes)) {
                    std::cerr << "    ERROR: truncated size_bytes at media #"
                              << mi << "\n";
                    ++errors;
                    goto done;
                }

                auto data_offset = static_cast<std::size_t>(in.tellg());

                // Validate size
                bool size_ok = true;
                if (size_bytes > file_size) {
                    std::cerr << "    ERROR: media #" << mi << " id=" << id
                              << " size_bytes=" << size_bytes
                              << " exceeds file size!\n";
                    ++errors;
                    size_ok = false;
                }
                if (data_offset + size_bytes > file_size) {
                    std::cerr << "    ERROR: media #" << mi << " id=" << id
                              << " data would extend past EOF"
                              << " (data_offset=" << data_offset
                              << " + size=" << size_bytes
                              << " > file_size=" << file_size << ")\n";
                    ++errors;
                    size_ok = false;
                }

                if (size_bytes == 0) {
                    ++zero_size_media;
                }

                // Print entry summary
                std::cout << "    [" << mi << "] id=" << id
                          << " file=\"" << filename << "\""
                          << " kind=" << kind
                          << " type=" << content_type
                          << " size=" << size_bytes
                          << " @offset=" << data_offset;

                if (!size_ok) {
                    std::cout << " CORRUPT";
                    goto done;
                }

                // Skip over media data
                if (size_bytes > 0) {
                    in.seekg(static_cast<std::streamoff>(size_bytes), std::ios::cur);
                    if (!in.good()) {
                        std::cout << " TRUNCATED\n";
                        std::cerr << "    ERROR: could not seek past media data\n";
                        ++errors;
                        goto done;
                    }
                }

                std::cout << " OK\n";

                total_data_bytes += size_bytes;
                ++total_media;
            }
            ++total_albums;
        }
    }

done:
    auto end_pos = static_cast<std::size_t>(in.tellg());
    std::size_t remaining = 0;
    if (end_pos < file_size && in.good()) {
        remaining = file_size - end_pos;
    }

    std::cout << "\n=== Summary ===\n";
    std::cout << "Format:         " << (is_v2 ? "MDE2" : "MDE1") << "\n";
    std::cout << "File size:      " << file_size << " bytes\n";
    std::cout << "Stores:         " << num_stores << "\n";
    std::cout << "Albums:         " << total_albums << "\n";
    std::cout << "Media entries:  " << total_media << "\n";
    std::cout << "Total data:     " << total_data_bytes << " bytes\n";
    if (zero_size_media > 0) {
        std::cout << "Zero-size:      " << zero_size_media << " entries (missing data)\n";
    }
    if (remaining > 0) {
        std::cout << "Trailing bytes: " << remaining << " (unexpected)\n";
        ++errors;
    }
    std::cout << "Errors:         " << errors << "\n";
    std::cout << "Result:         " << (errors == 0 ? "OK" : "ERRORS FOUND") << "\n";

    return errors == 0 ? 0 : 1;
}
