// mediadb C++ client — implementation (libhttppp + json-c)

#include "mediadb/client.h"

#include <algorithm>
#include <chrono>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <list>
#include <mutex>
#include <random>
#include <thread>
#include <unordered_map>

#include <httppp/http.h>
#include <httppp/httpdefinitions.h>

#include <json-c/json.h>

namespace fs = std::filesystem;

namespace mediadb {

// ── helpers ──

static std::string json_string(json_object* obj, const char* key) {
    json_object* v = json_object_object_get(obj, key);
    if (json_object_is_type(v, json_type_string) && json_object_get_string(v)) return json_object_get_string(v);
    return {};
}

static int json_int(json_object* obj, const char* key) {
    json_object* v = json_object_object_get(obj, key);
    if ((json_object_is_type(v, json_type_double) || json_object_is_type(v, json_type_int))) return static_cast<int>(json_object_get_double(v));
    return 0;
}

static std::uint64_t json_u64(json_object* obj, const char* key) {
    json_object* v = json_object_object_get(obj, key);
    if ((json_object_is_type(v, json_type_double) || json_object_is_type(v, json_type_int))) return static_cast<std::uint64_t>(json_object_get_double(v));
    return 0;
}

static StoreInfo parse_store(json_object* j) {
    StoreInfo s;
    s.id = json_string(j, "id");
    s.name = json_string(j, "name");
    s.created_at = json_string(j, "created_at");
    s.album_count = json_int(j, "album_count");
    return s;
}

static AlbumInfo parse_album(json_object* j) {
    AlbumInfo a;
    a.id = json_string(j, "id");
    a.name = json_string(j, "name");
    a.store_id = json_string(j, "store_id");
    a.created_at = json_string(j, "created_at");
    a.is_public = json_object_get_boolean(json_object_object_get(j, "is_public"));
    a.media_count = json_int(j, "media_count");
    return a;
}

static MediaInfo parse_media(json_object* j) {
    MediaInfo m;
    m.id = json_string(j, "id");
    m.filename = json_string(j, "filename");
    m.kind = json_string(j, "kind");
    m.content_type = json_string(j, "content_type");
    m.album_id = json_string(j, "album_id");
    m.store_id = json_string(j, "store_id");
    m.created_at = json_string(j, "created_at");
    m.size_bytes = json_u64(j, "size_bytes");
    return m;
}

static std::string mime_for_preview_fmt(const std::string& fmt) {
    if (fmt == "webp")  return "image/webp";
    if (fmt == "png")   return "image/png";
    if (fmt == "jpeg" || fmt == "jpg") return "image/jpeg";
    if (fmt == "avif")  return "image/avif";
    if (fmt == "mp4" || fmt == "av1" || fmt == "h265") return "video/mp4";
    return "application/octet-stream";
}

static std::string generate_boundary() {
    static const char charset[] = "0123456789abcdef";
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<> dist(0, 15);
    std::string boundary = "----mediadb";
    for (int i = 0; i < 16; ++i)
        boundary += charset[dist(gen)];
    return boundary;
}

// ── Impl ──

struct Client::Impl {
    std::string base_url;
    std::string token;
    size_t maxTries = 0;
    libhttppp::HttpUrl persistent_url;
    std::unique_ptr<libhttppp::HttpClient> persistent_client;

    // ── preview cache (LRU, max 128 MB) ──
    struct PreviewCacheKey {
        std::string media_id;
        int width;
        int height;
        std::string fmt;
        int t_seconds;
        int duration;
        bool operator==(const PreviewCacheKey& o) const {
            return media_id == o.media_id && width == o.width && height == o.height
                && fmt == o.fmt && t_seconds == o.t_seconds && duration == o.duration;
        }
    };
    struct PreviewCacheKeyHash {
        std::size_t operator()(const PreviewCacheKey& k) const {
            std::size_t h = std::hash<std::string>{}(k.media_id);
            h ^= std::hash<int>{}(k.width)  + 0x9e3779b9 + (h << 6) + (h >> 2);
            h ^= std::hash<int>{}(k.height) + 0x9e3779b9 + (h << 6) + (h >> 2);
            h ^= std::hash<std::string>{}(k.fmt) + 0x9e3779b9 + (h << 6) + (h >> 2);
            h ^= std::hash<int>{}(k.t_seconds) + 0x9e3779b9 + (h << 6) + (h >> 2);
            h ^= std::hash<int>{}(k.duration)  + 0x9e3779b9 + (h << 6) + (h >> 2);
            return h;
        }
    };
    struct PreviewCacheEntry {
        PreviewCacheKey key;
        std::vector<std::uint8_t> data;
        std::string content_type;
    };

    static constexpr std::size_t preview_cache_max_bytes_ = 128ULL * 1024 * 1024;
    std::mutex preview_cache_mutex_;
    std::list<PreviewCacheEntry> preview_cache_lru_;
    std::unordered_map<PreviewCacheKey, std::list<PreviewCacheEntry>::iterator, PreviewCacheKeyHash> preview_cache_map_;
    std::size_t preview_cache_bytes_ = 0;

    const PreviewCacheEntry* preview_cache_get(const PreviewCacheKey& key) {
        std::lock_guard<std::mutex> lk(preview_cache_mutex_);
        auto it = preview_cache_map_.find(key);
        if (it == preview_cache_map_.end()) return nullptr;
        preview_cache_lru_.splice(preview_cache_lru_.begin(), preview_cache_lru_, it->second);
        return &*it->second;
    }

    void preview_cache_put(const PreviewCacheKey& key, const std::vector<std::uint8_t>& data,
                           const std::string& content_type) {
        std::lock_guard<std::mutex> lk(preview_cache_mutex_);
        auto it = preview_cache_map_.find(key);
        if (it != preview_cache_map_.end()) {
            preview_cache_bytes_ -= it->second->data.size();
            preview_cache_lru_.erase(it->second);
            preview_cache_map_.erase(it);
        }
        while (preview_cache_bytes_ + data.size() > preview_cache_max_bytes_ && !preview_cache_lru_.empty()) {
            auto& back = preview_cache_lru_.back();
            preview_cache_bytes_ -= back.data.size();
            preview_cache_map_.erase(back.key);
            preview_cache_lru_.pop_back();
        }
        preview_cache_lru_.push_front(PreviewCacheEntry{key, data, content_type});
        preview_cache_map_[key] = preview_cache_lru_.begin();
        preview_cache_bytes_ += data.size();
    }

    Impl(const std::string& url, bool http3) : base_url(url), persistent_url(url, http3), use_http3(http3) {
        persistent_client = std::make_unique<libhttppp::HttpClient>(persistent_url, http3 ? 3 : 0);
    }

    bool use_http3 = false;

    libhttppp::HttpClient& getClient() {
        if (!persistent_client) {
            persistent_url = libhttppp::HttpUrl(base_url, use_http3);
            persistent_client = std::make_unique<libhttppp::HttpClient>(persistent_url, use_http3 ? 3 : 0);
        }
        return *persistent_client;
    }

    void prepare_get_request(libhttppp::HttpRequest& req, const std::string& path) {
        req.setRequestURL(path);
        if (!token.empty())
            req.setHeaderData("Authorization")->push_back("Bearer " + token);
    }

    std::vector<char> do_get(const std::string& path) {
        auto& client = getClient();
        libhttppp::HttpRequest req;
        prepare_get_request(req, path);
        std::vector<char> body = client.Get(req, maxTries);
        std::cerr << "[mdb-client] GET " << path
                  << " (" << body.size() << " bytes)" << std::endl;
        return body;
    }

    /// Stream GET response body through a callback.  Returns {status_code, content_type, total_bytes}.
    struct StreamResult {
        int status_code = 0;
        std::string content_type;
        std::uint64_t total_bytes = 0;
    };

    StreamResult do_get_stream(const std::string& path, const StreamCallback& cb) {
        auto& client = getClient();
        libhttppp::HttpRequest req;
        prepare_get_request(req, path);
        std::vector<char> body = client.Get(req, maxTries);

        StreamResult result;
        result.total_bytes = body.size();

        // Deliver body through callback in chunks
        constexpr size_t CHUNK = 8192;
        for (size_t off = 0; off < body.size(); off += CHUNK) {
            size_t n = std::min(CHUNK, body.size() - off);
            if (!cb(body.data() + off, n))
                break;
        }
        return result;
    }

    std::vector<char> do_post(const std::string& path,
                              const std::string& content_type,
                              const std::vector<char>& body) {
        auto& client = getClient();
        libhttppp::HttpRequest req;
        req.setRequestURL(path);

        if (!token.empty())
            req.setHeaderData("Authorization")->push_back("Bearer " + token);

        req.setHeaderData("Content-Type")->push_back(content_type);
        req.setHeaderData("Content-Length")->push_back(body.size());

        return client.Post(req, body, maxTries);
    }

    std::vector<char> do_put(const std::string& path,
                             const std::string& content_type,
                             const std::vector<char>& body) {
        auto& client = getClient();
        libhttppp::HttpRequest req;
        req.setRequestURL(path);

        if (!token.empty())
            req.setHeaderData("Authorization")->push_back("Bearer " + token);

        req.setHeaderData("Content-Type")->push_back(content_type);
        req.setHeaderData("Content-Length")->push_back(body.size());

        return client.Put(req, body, maxTries);
    }

    std::vector<char> do_delete(const std::string& path) {
        auto& client = getClient();
        libhttppp::HttpRequest req;
        req.setRequestURL(path);

        if (!token.empty())
            req.setHeaderData("Authorization")->push_back("Bearer " + token);

        return client.Delete(req, maxTries);
    }

    // JSON request: GET/POST/DELETE → parse JSON response
    json_object* request_json(const std::string& method, const std::string& path,
                        const std::string& json_body = {}) {
        std::vector<char> resp;

        if (method == "GET") {
            resp = do_get(path);
        } else if (method == "POST") {
            std::vector<char> body(json_body.begin(), json_body.end());
            resp = do_post(path, "application/json", body);
        } else if (method == "PUT") {
            std::vector<char> body(json_body.begin(), json_body.end());
            resp = do_put(path, "application/json", body);
        } else if (method == "DELETE") {
            resp = do_delete(path);
        } else {
            throw ClientError("unsupported HTTP method: " + method);
        }

        resp.push_back('\0');
        json_object* root = json_tokener_parse(resp.data());

        if (!root) {
            std::string detail = method + " " + path
                + " (" + std::to_string(resp.size() - 1) + " bytes)";
            if (resp.size() <= 512)
                detail += ": " + std::string(resp.data(), resp.size() - 1);
            throw ClientError("server returned invalid JSON for " + detail);
        }

        json_object* err = json_object_object_get(root, "error");
        if (json_object_is_type(err, json_type_string) && json_object_get_string(err)) {
            std::string msg = json_object_get_string(err);
            json_object_put(root);
            throw ClientError(msg);
        }

        return root;
    }

    // raw GET → returns body bytes
    std::vector<std::uint8_t> request_raw(const std::string& url_path,
                                          std::string* out_content_type = nullptr) {
        auto& client = getClient();
        libhttppp::HttpRequest req;
        prepare_get_request(req, url_path);
        std::vector<char> raw = client.Get(req, maxTries);
        (void)out_content_type; // content-type not available from Get()
        return std::vector<std::uint8_t>(
            reinterpret_cast<const std::uint8_t*>(raw.data()),
            reinterpret_cast<const std::uint8_t*>(raw.data()) + raw.size());
    }

    // raw PUT upload: metadata in headers, raw file bytes in body
    json_object* upload_raw(const std::string& album_id, const std::string& filename,
                      const std::uint8_t* data, size_t size) {
        auto& client = getClient();
        libhttppp::HttpRequest req;
        req.setRequestURL("/api/upload");

        if (!token.empty())
            req.setHeaderData("Authorization")->push_back("Bearer " + token);

        req.setHeaderData("Content-Type")->push_back("application/octet-stream");
        req.setHeaderData("X-Album-Id")->push_back(album_id);
        req.setHeaderData("X-Filename")->push_back(filename);

        std::vector<char> body(reinterpret_cast<const char*>(data),
                               reinterpret_cast<const char*>(data) + size);

        std::vector<char> resp = client.Put(req, body, maxTries);

        resp.push_back('\0');
        json_object* root = json_tokener_parse(resp.data());

        if (root) {
            json_object* err = json_object_object_get(root, "error");
            if (json_object_is_type(err, json_type_string) && json_object_get_string(err)) {
                std::string msg = json_object_get_string(err);
                json_object_put(root);
                throw ClientError(msg);
            }
        }

        return root;
    }
};

// ── Client ──

Client::Client(const std::string& base_url, bool /*verify_tls*/, bool http3) {
    std::string url = base_url;
    // strip trailing slash
    while (!url.empty() && url.back() == '/')
        url.pop_back();
    impl_ = std::make_unique<Impl>(url, http3);
}

Client::~Client() = default;
Client::Client(Client&&) noexcept = default;
Client& Client::operator=(Client&&) noexcept = default;

// ── auth ──

void Client::login(const std::string& username, const std::string& password) {
    json_object* body = json_object_new_object();
    json_object_object_add(body, "username", json_object_new_string(username.c_str()));
    json_object_object_add(body, "password", json_object_new_string(password.c_str()));
    const char* json = json_object_to_json_string(body);
    std::string json_str(json);
    json_object_put(body);

    json_object* resp = impl_->request_json("POST", "/api/login", json_str);
    impl_->token = json_string(resp, "token");
    json_object_put(resp);
    if (impl_->token.empty()) throw ClientError("login succeeded but no token returned");
}

void Client::set_token(const std::string& token) { impl_->token = token; }
const std::string& Client::token() const { return impl_->token; }
void Client::setMaxTries(size_t maxTries) { impl_->maxTries = maxTries; }

// ── stores ──

StoreInfo Client::create_store(const std::string& name) {
    json_object* body = json_object_new_object();
    json_object_object_add(body, "name", json_object_new_string(name.c_str()));
    const char* json = json_object_to_json_string(body);
    std::string json_str(json);
    json_object_put(body);

    json_object* resp = impl_->request_json("POST", "/stores", json_str);
    auto s = parse_store(resp);
    json_object_put(resp);
    return s;
}

std::vector<StoreInfo> Client::list_stores() {
    json_object* resp = impl_->request_json("GET", "/stores");
    std::vector<StoreInfo> out;
    json_object* arr = json_object_object_get(resp, "stores");
    if (json_object_is_type(arr, json_type_array)) {
        json_object* item = nullptr;
        for (size_t _i = 0; _i < json_object_array_length(arr); _i++) { item = json_object_array_get_idx(arr, _i);  out.push_back(parse_store(item)); }
    }
    json_object_put(resp);
    return out;
}

StoreInfo Client::get_store(const std::string& id) {
    json_object* resp = impl_->request_json("GET", "/stores/" + id);
    auto s = parse_store(resp);
    json_object_put(resp);
    return s;
}

void Client::delete_store(const std::string& id) {
    json_object* resp = impl_->request_json("DELETE", "/stores/" + id);
    if (resp) json_object_put(resp);
}

StoreInfo Client::rename_store(const std::string& id, const std::string& new_name) {
    json_object* body = json_object_new_object();
    json_object_object_add(body, "name", json_object_new_string(new_name.c_str()));
    const char* json = json_object_to_json_string(body);
    std::string json_str(json);
    json_object_put(body);

    json_object* resp = impl_->request_json("PUT", "/stores/" + id, json_str);
    auto s = parse_store(resp);
    json_object_put(resp);
    return s;
}

// ── albums ──

AlbumInfo Client::create_album(const std::string& store_id, const std::string& name) {
    json_object* body = json_object_new_object();
    json_object_object_add(body, "name", json_object_new_string(name.c_str()));
    const char* json = json_object_to_json_string(body);
    std::string json_str(json);
    json_object_put(body);

    json_object* resp = impl_->request_json("POST", "/stores/" + store_id + "/albums", json_str);
    auto a = parse_album(resp);
    json_object_put(resp);
    return a;
}

std::vector<AlbumInfo> Client::list_albums(const std::string& store_id) {
    json_object* resp = impl_->request_json("GET", "/stores/" + store_id + "/albums");
    std::vector<AlbumInfo> out;
    json_object* arr = json_object_object_get(resp, "albums");
    if (json_object_is_type(arr, json_type_array)) {
        json_object* item = nullptr;
        for (size_t _i = 0; _i < json_object_array_length(arr); _i++) { item = json_object_array_get_idx(arr, _i);  out.push_back(parse_album(item)); }
    }
    json_object_put(resp);
    return out;
}

AlbumInfo Client::get_album(const std::string& id) {
    json_object* resp = impl_->request_json("GET", "/albums/" + id);
    auto a = parse_album(resp);
    json_object_put(resp);
    return a;
}

void Client::delete_album(const std::string& id) {
    json_object* resp = impl_->request_json("DELETE", "/albums/" + id);
    if (resp) json_object_put(resp);
}

AlbumInfo Client::set_album_public(const std::string& id, bool is_public) {
    json_object* body = json_object_new_object();
    json_object_object_add(body, "public", json_object_new_boolean(is_public));
    const char* json = json_object_to_json_string(body);
    std::string json_str(json);
    json_object_put(body);

    json_object* resp = impl_->request_json("PUT", "/albums/" + id + "/public", json_str);
    auto a = parse_album(resp);
    json_object_put(resp);
    return a;
}

// ── media ──

MediaInfo Client::add_media(const std::string& album_id, const std::string& server_path) {
    json_object* body = json_object_new_object();
    json_object_object_add(body, "path", json_object_new_string(server_path.c_str()));
    const char* json = json_object_to_json_string(body);
    std::string json_str(json);
    json_object_put(body);

    json_object* resp = impl_->request_json("POST", "/albums/" + album_id + "/media", json_str);
    auto m = parse_media(resp);
    json_object_put(resp);
    return m;
}

std::vector<MediaInfo> Client::list_media(const std::string& album_id) {
    json_object* resp = impl_->request_json("GET", "/albums/" + album_id + "/media");
    std::vector<MediaInfo> out;
    json_object* arr = json_object_object_get(resp, "media");
    if (json_object_is_type(arr, json_type_array)) {
        json_object* item = nullptr;
        for (size_t _i = 0; _i < json_object_array_length(arr); _i++) { item = json_object_array_get_idx(arr, _i);  out.push_back(parse_media(item)); }
    }
    json_object_put(resp);
    return out;
}

MediaInfo Client::get_media(const std::string& id) {
    json_object* resp = impl_->request_json("GET", "/media/" + id);
    auto m = parse_media(resp);
    json_object_put(resp);
    return m;
}

std::vector<std::uint8_t> Client::get_media_data(const std::string& id) {
    return impl_->request_raw("/media/" + id + "/raw");
}

std::uint64_t Client::get_media_data_stream(const std::string& id,
                                             const StreamCallback& cb) {
    auto result = impl_->do_get_stream("/media/" + id + "/raw", cb);
    return result.total_bytes;
}

std::vector<std::uint8_t> Client::get_media_data_range(const std::string& id,
                                                        std::uint64_t offset,
                                                        std::uint64_t length) {
    auto& client = impl_->getClient();
    libhttppp::HttpRequest req;
    impl_->prepare_get_request(req, "/media/" + id + "/raw");

    std::string range_val = "bytes=" + std::to_string(offset) + "-"
                          + std::to_string(offset + length - 1);
    req.setHeaderData("Range")->push_back(range_val);

    std::vector<char> raw = client.Get(req, impl_->maxTries);
    return std::vector<std::uint8_t>(
        reinterpret_cast<const std::uint8_t*>(raw.data()),
        reinterpret_cast<const std::uint8_t*>(raw.data()) + raw.size());
}

std::uint64_t Client::get_media_size(const std::string& id) {
    auto info = get_media(id);
    return info.size_bytes;
}

MediaWithPreview Client::get_media_with_preview(const std::string& id,
                                                const PreviewOptions& opts) {
    MediaWithPreview out;
    out.info = get_media(id);
    out.preview = get_preview(id, opts, &out.preview_content_type);
    return out;
}

void Client::delete_media(const std::string& id) {
    json_object* resp = impl_->request_json("DELETE", "/media/" + id);
    if (resp) json_object_put(resp);
}

// ── upload ──

MediaInfo Client::upload(const std::string& album_id,
                         const std::string& filename,
                         const std::vector<std::uint8_t>& data) {
    json_object* resp = impl_->upload_raw(album_id, filename, data.data(), data.size());
    auto m = parse_media(resp);
    json_object_put(resp);
    return m;
}

MediaInfo Client::upload_file(const std::string& album_id,
                              const std::string& local_path) {
    if (!fs::exists(local_path))
        throw ClientError("file not found: " + local_path);

    const auto fsize = fs::file_size(local_path);
    std::vector<std::uint8_t> data(fsize);
    {
        std::ifstream in(local_path, std::ios::binary);
        if (!in.is_open()) throw ClientError("cannot open: " + local_path);
        in.read(reinterpret_cast<char*>(data.data()), static_cast<std::streamsize>(fsize));
    }

    const std::string filename = fs::path(local_path).filename().string();
    return upload(album_id, filename, data);
}

// ── preview ──

std::vector<std::uint8_t> Client::get_preview(const std::string& media_id,
                                               const PreviewOptions& opts,
                                               std::string* out_content_type) {
    Impl::PreviewCacheKey key{media_id, opts.width, opts.height,
                              opts.fmt.empty() ? "webp" : opts.fmt,
                              opts.t_seconds, opts.duration};
    if (auto* hit = impl_->preview_cache_get(key)) {
        if (out_content_type) *out_content_type = hit->content_type;
        return hit->data;
    }

    std::string url = "/media/" + media_id + "/preview?";
    if (opts.width > 0)    url += "w=" + std::to_string(opts.width) + "&";
    if (opts.height > 0)   url += "h=" + std::to_string(opts.height) + "&";
    if (!opts.fmt.empty()) url += "fmt=" + opts.fmt + "&";
    if (opts.t_seconds > 0) url += "t=" + std::to_string(opts.t_seconds) + "&";
    if (opts.duration > 0) url += "dur=" + std::to_string(opts.duration) + "&";

    const std::string ct = mime_for_preview_fmt(key.fmt);
    auto data = impl_->request_raw(url);
    impl_->preview_cache_put(key, data, ct);
    if (out_content_type) *out_content_type = ct;
    return data;
}

std::vector<std::uint8_t> Client::get_preview_range(const std::string& media_id,
                                                     const PreviewOptions& opts,
                                                     std::uint64_t offset,
                                                     std::uint64_t length,
                                                     std::string* out_content_type) {
    Impl::PreviewCacheKey key{media_id, opts.width, opts.height,
                              opts.fmt.empty() ? "webp" : opts.fmt,
                              opts.t_seconds, opts.duration};

    // ensure full preview is cached
    if (!impl_->preview_cache_get(key)) {
        get_preview(media_id, opts, nullptr);
    }

    auto* hit = impl_->preview_cache_get(key);
    if (!hit) throw ClientError("preview not available");

    if (out_content_type) *out_content_type = hit->content_type;

    if (offset >= hit->data.size()) return {};
    std::uint64_t actual = std::min(length, hit->data.size() - offset);
    return {hit->data.begin() + static_cast<std::ptrdiff_t>(offset),
            hit->data.begin() + static_cast<std::ptrdiff_t>(offset + actual)};
}

// ── export / import ──

std::vector<std::uint8_t> Client::export_db() {
    return impl_->request_raw("/api/export");
}

std::uint64_t Client::export_db_stream(const StreamCallback& cb) {
    auto result = impl_->do_get_stream("/api/export", cb);
    return result.total_bytes;
}

void Client::export_db_to_file(const std::string& local_path) {
    std::ofstream out(local_path, std::ios::binary | std::ios::trunc);
    if (!out.is_open()) throw ClientError("cannot open file for writing: " + local_path);

    export_db_stream([&out](const char* data, size_t len) -> bool {
        out.write(data, static_cast<std::streamsize>(len));
        return out.good();
    });
}

void Client::import_db(const std::vector<std::uint8_t>& data) {
    // Alias uint8_t* as char* — avoid copying the entire archive
    const char* raw = reinterpret_cast<const char*>(data.data());
    std::vector<char> body(raw, raw + data.size());
    std::vector<char> resp = impl_->do_put("/api/import", "application/octet-stream", body);

    resp.push_back('\0');
    json_object* root = json_tokener_parse(resp.data());
    if (root) {
        json_object* err = json_object_object_get(root, "error");
        if (json_object_is_type(err, json_type_string) && json_object_get_string(err)) {
            std::string msg = json_object_get_string(err);
            json_object_put(root);
            throw ClientError(msg);
        }
        json_object_put(root);
    }
}

void Client::import_db_from_file(const std::string& local_path) {
    if (!fs::exists(local_path))
        throw ClientError("file not found: " + local_path);

    const auto fsize = fs::file_size(local_path);
    std::vector<std::uint8_t> data(fsize);
    {
        std::ifstream in(local_path, std::ios::binary);
        if (!in.is_open()) throw ClientError("cannot open: " + local_path);
        in.read(reinterpret_cast<char*>(data.data()), static_cast<std::streamsize>(fsize));
    }
    import_db(data);
}

} // namespace mediadb
