"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AsanaDbError = exports.Document = exports.ID_FIELD = exports.KEY_FIELD = exports.CLASS_FIELD = void 0;
exports.CLASS_FIELD = '_class';
exports.KEY_FIELD = '_key';
exports.ID_FIELD = '_id';
/**
 * A Compository document — a map-like object that also knows its label ({@link clazz}),
 * key field ({@link _key}), and vertex id ({@link _id}).
 *
 * @example
 * const doc = new Document('Article', '_id', 42n)
 *   .add('title', 'Hello')
 *   .add('body',  'World')
 *   .addChild(new Document('Tag').add('name', 'news'));
 *
 * @example
 * // From a plain object (class name inferred from constructor name)
 * const doc = Document.from(article, 'Article');
 *
 * Documents returned by {@link AsanaDbClient} are pre-bound so you can call
 * {@link save}, {@link fetch}, {@link update}, {@link delete} directly.
 */
class Document {
    clazz;
    _key;
    _id;
    /** @internal */
    _client;
    _data = new Map();
    constructor(className = 'Document', keyField = '_id', id) {
        this.clazz = className;
        this._key = keyField;
        this._id = id !== undefined ? BigInt(id) : undefined;
        this.syncInternalFields();
    }
    get id() { return this._id; }
    // ── Factory ────────────────────────────────────────────────────────────────
    /**
     * Build a Document from a plain object. Class name is taken from
     * `obj.constructor.name` unless overridden.
     */
    static from(obj, className, keyField, id) {
        const inferred = obj.constructor?.name ?? 'Document';
        const doc = new Document(className ?? inferred, keyField, id);
        for (const [key, value] of Object.entries(obj)) {
            doc.set(key, value);
        }
        // Pick up _id from the object itself if not passed explicitly
        doc.extractInternalFields();
        doc.syncInternalFields();
        return doc;
    }
    /** Parse a JSON string into a Document. Nested objects become child Documents. */
    static fromJSON(json) {
        const parsed = JSON.parse(json);
        return Document.fromObject(parsed);
    }
    /** @internal */
    static fromObject(obj) {
        const doc = new Document();
        for (const [key, value] of Object.entries(obj)) {
            doc._data.set(key, Document.convertValue(value));
        }
        doc.extractInternalFields();
        return doc;
    }
    // ── Map-like interface ─────────────────────────────────────────────────────
    get(key) { return this._data.get(key); }
    set(key, value) { this._data.set(key, value); return this; }
    has(key) { return this._data.has(key); }
    remove(key) { return this._data.delete(key); }
    get size() { return this._data.size; }
    [Symbol.iterator]() {
        return this._data[Symbol.iterator]();
    }
    keys() { return this._data.keys(); }
    values() { return this._data.values(); }
    entries() { return this._data.entries(); }
    // ── Builder ────────────────────────────────────────────────────────────────
    /** Flow-style setter. */
    add(key, value) {
        this._data.set(key, value);
        return this;
    }
    /**
     * Attach a child Document under its {@link clazz} key.
     * First child stored directly; second of same class starts an array.
     */
    addChild(child) {
        const key = child.clazz;
        const existing = this._data.get(key);
        if (existing === undefined) {
            this._data.set(key, child);
        }
        else if (Array.isArray(existing)) {
            existing.push(child);
        }
        else {
            this._data.set(key, [existing, child]);
        }
        return this;
    }
    removeChild(child) {
        const key = child.clazz;
        const existing = this._data.get(key);
        if (Array.isArray(existing)) {
            const filtered = existing.filter((c) => c !== child);
            this._data.set(key, filtered.length === 1 ? filtered[0] : filtered);
        }
        else {
            this._data.delete(key);
        }
        return this;
    }
    // ── Typed getters ──────────────────────────────────────────────────────────
    getString(key) {
        const v = this._data.get(key);
        return v !== undefined ? String(v) : undefined;
    }
    getNumber(key) {
        const v = this._data.get(key);
        return typeof v === 'number' ? v : typeof v === 'bigint' ? Number(v) : undefined;
    }
    getBigInt(key) {
        const v = this._data.get(key);
        return typeof v === 'bigint' ? v : typeof v === 'number' ? BigInt(v) : undefined;
    }
    getBoolean(key) {
        const v = this._data.get(key);
        return typeof v === 'boolean' ? v : undefined;
    }
    getDocument(key) {
        const v = this._data.get(key);
        return v instanceof Document ? v : undefined;
    }
    getDocuments(key) {
        const v = this._data.get(key);
        return Array.isArray(v) && v.every((x) => x instanceof Document)
            ? v
            : undefined;
    }
    // ── CRUD ──────────────────────────────────────────────────────────────────
    /** Persist this document. Uses {@link clazz} as the vertex label. */
    async save() {
        if (!this._client)
            throw new Error('Document is not bound to a client.');
        await this._client.insertOne(this);
    }
    /**
     * Reload this document from the server.
     * Fetches by {@link id} when set, otherwise by the {@link _key} field value.
     */
    async fetch() {
        if (!this._client)
            throw new Error('Document is not bound to a client.');
        if (this._id !== undefined)
            return this._client.findById(this._id);
        const keyValue = this._data.get(this._key);
        if (keyValue === undefined)
            throw new Error(`Document has neither _id nor a '${this._key}' value — cannot fetch.`);
        return this._client.findByKey(this._key, keyValue);
    }
    /** Replace this document on the server by {@link id}. */
    async update() {
        if (!this._client)
            throw new Error('Document is not bound to a client.');
        if (this._id === undefined)
            throw new Error('Document has no _id — cannot update.');
        await this._client.replaceOne(this._id, this);
    }
    /** Delete this document from the server by {@link id}. */
    async delete() {
        if (!this._client)
            throw new Error('Document is not bound to a client.');
        if (this._id === undefined)
            throw new Error('Document has no _id — cannot delete.');
        await this._client.deleteOne(this._id);
    }
    // ── Serialization ──────────────────────────────────────────────────────────
    /** Return a plain object suitable for JSON serialization (including internal fields). */
    toJSON() {
        const obj = {};
        for (const [k, v] of this._data) {
            obj[k] = Document.serializeValue(v);
        }
        obj[exports.CLASS_FIELD] = this.clazz;
        obj[exports.KEY_FIELD] = this._key;
        if (this._id !== undefined)
            obj[exports.ID_FIELD] = Number(this._id);
        return obj;
    }
    toString() { return JSON.stringify(this.toJSON()); }
    // ── Internal helpers ───────────────────────────────────────────────────────
    syncInternalFields() {
        this._data.set(exports.CLASS_FIELD, this.clazz);
        this._data.set(exports.KEY_FIELD, this._key);
        if (this._id !== undefined)
            this._data.set(exports.ID_FIELD, this._id);
    }
    extractInternalFields() {
        const cls = this._data.get(exports.CLASS_FIELD);
        if (typeof cls === 'string')
            this.clazz = cls;
        const key = this._data.get(exports.KEY_FIELD);
        if (typeof key === 'string')
            this._key = key;
        const id = this._data.get(exports.ID_FIELD);
        if (typeof id === 'number')
            this._id = BigInt(id);
        else if (typeof id === 'bigint')
            this._id = id;
    }
    static convertValue(value) {
        if (value === null || value === undefined)
            return value;
        if (typeof value === 'object' && !Array.isArray(value)) {
            return Document.fromObject(value);
        }
        if (Array.isArray(value)) {
            const all = value.every((v) => typeof v === 'object' && v !== null && !Array.isArray(v));
            if (all && value.length > 0) {
                return value.map((v) => Document.fromObject(v));
            }
            return value.map((v) => Document.convertValue(v));
        }
        return value;
    }
    static serializeValue(value) {
        if (value instanceof Document)
            return value.toJSON();
        if (Array.isArray(value))
            return value.map((v) => Document.serializeValue(v));
        if (typeof value === 'bigint')
            return Number(value);
        return value;
    }
}
exports.Document = Document;
/** Thrown when the server responds with status !== "ok". */
class AsanaDbError extends Error {
    constructor(message) {
        super(message);
        this.name = 'AsanaDbError';
    }
}
exports.AsanaDbError = AsanaDbError;
//# sourceMappingURL=document.js.map