import { nanoid } from "nanoid";
import { BehaviorSubject } from "rxjs";
import { getEventHash } from "nostr-tools";
/** Wraps a promise in an abort signal */
function wrapInSignal(promise, signal) {
    return new Promise((res, rej) => {
        signal.throwIfAborted();
        let done = false;
        // reject promise if abort signal is triggered
        signal.addEventListener("abort", () => {
            if (!done)
                rej(signal.reason || undefined);
            done = true;
        });
        return promise.then((v) => {
            if (!done)
                res(v);
            done = true;
        }, (err) => {
            if (!done)
                rej(err);
            done = true;
        });
    });
}
/** An error thrown when a signer is used with the wrong pubkey */
export class SignerMismatchError extends Error {
}
/** A base class for all accounts */
export class BaseAccount {
    pubkey;
    signer;
    id = nanoid(8);
    get type() {
        const cls = Reflect.getPrototypeOf(this).constructor;
        return cls.type;
    }
    /** Disable request queueing */
    disableQueue;
    metadata$ = new BehaviorSubject(undefined);
    get metadata() {
        return this.metadata$.value;
    }
    set metadata(metadata) {
        this.metadata$.next(metadata);
    }
    get nip04() {
        if (!this.signer.nip04)
            return undefined;
        return {
            encrypt: async (pubkey, plaintext) => this.operation(() => this.signer.nip04.encrypt(pubkey, plaintext)),
            decrypt: async (pubkey, plaintext) => this.operation(() => this.signer.nip04.decrypt(pubkey, plaintext)),
        };
    }
    get nip44() {
        if (!this.signer.nip44)
            return undefined;
        return {
            encrypt: async (pubkey, plaintext) => this.operation(() => this.signer.nip44.encrypt(pubkey, plaintext)),
            decrypt: async (pubkey, plaintext) => this.operation(() => this.signer.nip44.decrypt(pubkey, plaintext)),
        };
    }
    constructor(pubkey, signer) {
        this.pubkey = pubkey;
        this.signer = signer;
    }
    // This should be overwritten by a sub class
    toJSON() {
        throw new Error("Not implemented");
    }
    /** A method that wraps any signer interaction, this allows the account to wait for unlock or queue requests */
    operation(operation) {
        // If the queue is enabled, wait our turn in the queue
        return this.waitForQueue(operation);
    }
    /** Adds the common fields to the serialized output of a toJSON method */
    saveCommonFields(json) {
        return { ...json, id: this.id, pubkey: this.pubkey, metadata: this.metadata, type: this.type };
    }
    /** Sets an accounts id and metadata. NOTE: This should only be used in fromJSON methods */
    static loadCommonFields(account, json) {
        if (json.id)
            account.id = json.id;
        if (json.metadata)
            account.metadata = json.metadata;
        return account;
    }
    /** Gets the pubkey from the signer */
    getPublicKey() {
        return this.operation(async () => {
            const result = await this.signer.getPublicKey();
            if (this.pubkey !== result)
                throw new SignerMismatchError("Account signer mismatch");
            return result;
        });
    }
    /** sign the event and make sure its signed with the correct pubkey */
    signEvent(template) {
        // If the template does not have a pubkey, set it to the accounts pubkey
        if (!Reflect.has(template, "pubkey"))
            Reflect.set(template, "pubkey", this.pubkey);
        return this.operation(async () => {
            const id = getEventHash(template);
            const result = await this.signer.signEvent(template);
            if (result.pubkey !== this.pubkey)
                throw new SignerMismatchError("Signer signed with wrong pubkey");
            if (result.id !== id)
                throw new SignerMismatchError("Signer modified event");
            return result;
        });
    }
    /** Aborts all pending requests in the queue */
    abortQueue(reason) {
        if (this.abort)
            this.abort.abort(reason);
    }
    /** internal queue */
    queueLength = 0;
    lock = null;
    abort = null;
    reduceQueue() {
        // shorten the queue
        this.queueLength--;
        // if this was the last request, remove the lock
        if (this.queueLength === 0) {
            this.lock = null;
            this.abort = null;
        }
    }
    waitForQueue(operation) {
        if (this.disableQueue)
            return operation();
        // if there is already a pending request, wait for it
        if (this.lock && this.abort) {
            // create a new promise that runs after the lock
            const p = wrapInSignal(this.lock.then(() => {
                // if the abort signal is triggered, don't call the signer
                this.abort?.signal.throwIfAborted();
                return operation();
            }), this.abort.signal);
            // set the lock the new promise that ignores errors
            this.lock = p.catch(() => { }).finally(this.reduceQueue.bind(this));
            this.queueLength++;
            return p;
        }
        else {
            const result = operation();
            // if the result is async, set the new lock
            if (result instanceof Promise) {
                this.abort = new AbortController();
                const p = wrapInSignal(result, this.abort.signal);
                // set the lock the new promise that ignores errors
                this.lock = p.catch(() => { }).finally(this.reduceQueue.bind(this));
                this.queueLength = 1;
            }
            return result;
        }
    }
}
