import diff from '@api-client/json/patch/diff.js';
import apply from '@api-client/json/patch/apply.js';
import { rebase } from '@api-client/json/patch/rebase.js';
import { nanoid } from 'nanoid';
import { Model } from '../core/Model.js';
var ModelState;
(function (ModelState) {
    /**
     * Not doing anything.
     */
    ModelState[ModelState["idle"] = 0] = "idle";
    /**
     * Currently querying for file data.
     */
    ModelState[ModelState["reading"] = 1] = "reading";
    /**
     * Data received and the result yielded no results or less than the limit.
     */
    ModelState[ModelState["ended"] = 2] = "ended";
})(ModelState || (ModelState = {}));
/**
 * A base class for models that represent patchable files.
 * It provides methods to read, apply server patches, and handle changes.
 * The model is designed to work with a file identified by an organization ID (oid)
 * and a file ID (fid).
 *
 * @template T The type of the file content.
 */
export class PatchableFileModel extends Model {
    /**
     * This is essentially a snapshot of the document that is
     * always consistent with the server (to our knowledge).
     * It is used to:
     * - compare with the current `value` to generate diff patches
     * - apply server patches once they arrive and then rebased client patches.
     * After that new revision has server and client patches.
     */
    revision;
    /**
     * The current state of the model.
     */
    state = ModelState.idle;
    bufferTimeoutId = null;
    bufferDelay;
    oid;
    fid;
    sdk;
    initCallback;
    /**
     * The current file media version.
     */
    version = 0;
    /**
     * The dependencies of the media object.
     * This is an array of objects that are referenced by the media object.
     * If the media object has no dependencies, this will be `undefined`.
     *
     * This should be manually populated then the application adds a dependency
     * to the media object. There's no way for us to know when a dependency is added
     * or removed from the media object, so we leave it up to the application to
     * manage this array.
     *
     * Note that dependencies are only fetched when the file is read for the first time.
     */
    dependencies;
    constructor(options) {
        super();
        this.bufferDelay = options.bufferDelay || 500;
        this.oid = options.oid;
        this.fid = options.fid;
        this.sdk = options.sdk;
        this.initCallback = options.initialize;
    }
    async read() {
        if (this.state !== ModelState.idle) {
            return;
        }
        this.state = ModelState.reading;
        try {
            const result = await this.sdk.file.readMedia(this.oid, this.fid);
            this.version = result.version;
            const media = result.media;
            this.dependencies = await this.readDependencies(media);
            this.setupFile(this.initCallback(result.media, this.dependencies));
        }
        catch (e) {
            const err = e;
            this.notifyError(new Error(`Unable to load the file. ${err.message}`));
            this.state = ModelState.idle;
            throw err;
        }
        this.dispatchEvent(new Event('querycomplete'));
    }
    /**
     * Tests whether the media object has dependencies.
     * If the media object has a `dependencyList` property that is an array
     * and contains at least one item, it returns a promise that resolves to
     * an array of dependencies. If the `dependencyList` is empty or not an array,
     * it returns a promise that resolves to `undefined`.
     * @param media The media object to read dependencies from.
     */
    async readDependencies(media) {
        const typed = media;
        if (!Array.isArray(typed.dependencyList) || !typed.dependencyList.length) {
            return undefined;
        }
        const result = await this.sdk.dataCatalog.listDependencies(typed.dependencyList);
        return result.items.length ? result.items : undefined;
    }
    setupPreviousData(content) {
        const typed = content;
        if (typeof typed.toJSON === 'function') {
            this.revision = typed.toJSON();
        }
        else {
            this.revision = structuredClone(content);
        }
    }
    /**
     * Applies incoming server patches to the document and rebases client patches.
     *
     * This method integrates server-side changes into the document and reconciles
     * them with any local changes made by the client. It ensures that client
     * changes are applied on top of the latest server state.
     *
     * **Process:**
     *
     * 1.  Applies the `server` patches to the `revision`.
     * 2.  Calculates the client's local changes (diff between `revision` and `value`).
     * 3.  If there are local changes, rebases them against the server patches.
     * 4.  Applies the rebased client patches to the `revision`.
     * 5.  Updates the `value` to match the `revision`.
     * 6. Dispatches `change` event.
     *
     * @param server - The server patches to be applied to the document.
     * @returns The rebased **client** patches that were applied to the document.
     *   These patches are safe to send to the server. Returns an empty array if there were no local changes.
     * @throws {Error} If the local patches cannot be applied to the document after
     *   server changes. This indicates a conflict that the client application
     *   should handle (e.g., by notifying the user and refreshing the document).
     *
     * @example
     * ```typescript
     * const doc = new JsonDocument({ a: 1, b: [2, 3] });
     *
     * // Simulate local changes (UI edits)
     * doc.value.a = 4;
     * doc.value.b.push(5);
     *
     * // Simulate a server patch
     * const serverPatch: Operation[] = [{ op: 'add', path: '/b/0', value: 0 }];
     *
     * // Apply the server patch and rebase local changes
     * const rebasedClientPatches = doc.applyServer(serverPatch);
     *
     * console.log(doc.value); // Output: { a: 4, b: [0, 2, 3, 5] }
     * console.log(rebasedClientPatches);
     * // Output: [{ op: 'replace', path: '/a', value: 4 }, { op: 'add', path: '/b/3', value: 5 }]
     * ```
     */
    applyServerPatch(info) {
        const { value } = this;
        if (typeof info.newVersion !== 'number') {
            throw new Error('The server patch must contain a new version number.');
        }
        this.version = info.newVersion;
        if (!value) {
            return [];
        }
        const client = diff(this.revision, this.readValue());
        const ar = apply(this.revision, info.patch);
        let result = [];
        if (client.length) {
            result = rebase(info.patch, client);
            apply(ar.doc, result);
        }
        this.revision = ar.doc;
        const content = this.initCallback(ar.doc, this.dependencies);
        this.setValue(content);
        return result;
    }
    /**
     * Called when a change is made to the data object.
     */
    async handleChange() {
        this.scheduleSend();
    }
    /**
     * Schedules sending the buffered patches to the server after a delay.
     */
    scheduleSend() {
        if (this.bufferTimeoutId !== null) {
            clearTimeout(this.bufferTimeoutId);
        }
        this.bufferTimeoutId = setTimeout(() => {
            this.send();
            this.bufferTimeoutId = null;
        }, this.bufferDelay);
    }
    /**
     * Sends the buffered patches to the server.
     */
    async send() {
        const patch = diff(this.revision, this.readValue());
        if (patch.length === 0) {
            return;
        }
        try {
            await this.sendPatchToServer(patch);
        }
        catch (e) {
            const err = e;
            this.notifyError(err);
            this.state = ModelState.idle;
            throw err;
            // Retry logic or other error handling can be added here
        }
    }
    async sendPatchToServer(patch) {
        const info = {
            timestamp: Date.now(),
            patch,
            // app: 'quick-api',
            // appVersion: 'v1',
            id: nanoid(),
            version: this.version,
        };
        const result = await this.sdk.file.patchMedia(this.oid, this.fid, info);
        this.version = result.newVersion;
        // update revision so the current patches become a base.
        this.revision = this.createRevision();
    }
    /**
     * Checks whether this contents is identified by the passed `oid` and `fid`
     * @param oid The organization id
     * @param fid The file id
     * @returns Whether the current file is the same as the passed arguments.
     */
    isFile(oid, fid) {
        return this.oid === oid && this.fid === fid;
    }
}
//# sourceMappingURL=PatchableFileModel.js.map