/**
 * @module ObservableBase_V1 witness class
 *
 */
import { extractErrorMsg, } from '@ibgib/helper-gib/dist/helpers/utils-helper.mjs';
import { ROOT } from '@ibgib/ts-gib/dist/V1/constants.mjs';
import { getIbGibAddr } from '@ibgib/ts-gib/dist/helper.mjs';
import { GLOBAL_LOG_A_LOT } from '../../../core-constants.mjs';
import { argy_, isCommand, isWitness, resulty_ } from '../../../witness/witness-helper.mjs';
import { ObservableCmd, OBSERVABLE_CMD_VALUES, } from './observable-types.mjs';
import { LightWitnessBase_V1 } from '../../../witness/light-witness-base-v1.mjs';
import { isObserver } from '../subject/subject-helper.mjs';
import { isSubscription, newupSubscription } from '../subscription/subscription-helper.mjs';
import { getObservableEventIbGib } from './observable-event/observable-event-helper.mjs';
/**
 * for logging. import this constant from your project.
 */
const logalot = GLOBAL_LOG_A_LOT; // change this when you want to turn off verbose logging
/**
 * sketching...
 * under construction...
 */
export class ObservableBase_V1 extends LightWitnessBase_V1 {
    /**
     * Log context for convenience with logging. (Ignore if you don't want to use this.)
     */
    lc = `[${ObservableBase_V1.name}]`;
    /**
     * Reference to the local ibgibs service, which is one way at getting at the
     * local user space.
     */
    metaspace;
    _subscribers = {};
    _isComplete = false;
    /**
     * this is used for {@link SubjectData_V1.replay}
     */
    _pastPayloads = [];
    /**
     * if observable errors out, this should be the stored error.
     */
    _pastError;
    // #region public api properties
    get isCompleteOrErrored() {
        return this._isComplete || !!this._pastError;
    }
    get isComplete() { return this._isComplete; }
    get isErrored() { return !!this._pastError; }
    // #endregion public api properties
    constructor(initialData, initialRel8ns) {
        super(initialData, initialRel8ns);
    }
    /**
     * The observable's primary function is to accept subscribers
     * who wish to observe the stream of ibgibs this observable creates.
     *
     * So there are two ways to subscribe:
     *
     * ## 1. Use an ObservableCmd (via this.argy(...) for convenience)
     *
     * @see {@link subscribe}
     *
     * ## 2. Use any ol' Witness IbGib

     * @see {@link witness_otherWitness}
     *
     * @param arg
     * @returns
     *
     * @see {@link argy}
     */
    async witness(arg) {
        const lc = `${this.lc}[${this.witness.name}]`;
        try {
            if (!this._isInitialized) {
                await this.initialized;
            }
            const { ib } = arg;
            if (!ib) {
                throw new Error(`arg.ib required (E: 87659d093b82436ce2f7a87e402a5423)`);
            }
            if (isCommand({ ibGib: arg })) {
                return this.witness_cmd({ arg: arg });
            }
            else if (isWitness({ ibGib: arg })) {
                return this.witness_otherWitness({ arg: arg });
            }
            else {
                throw new Error(`unknown arg. expect observable cmd or ibgib witness (E: dc2667cffac5b8d7b1bf2ea737c81a23)`);
            }
        }
        catch (error) {
            console.error(`${lc} ${extractErrorMsg(error) ?? 'unknown error (E: 3e22bea4c7fb4668bf13d7146b927869)'}`);
            throw error;
        }
    }
    // #region witness sub-functions
    /**
     * default implementation here subscribes the witness.
     *
     * override this for custom handling cmds.
     *
     * @returns idk, an ibgib
     */
    async witness_cmd({ arg, }) {
        const lc = `${this.lc}[${this.witness_cmd.name}]`;
        try {
            if (logalot) {
                console.log(`${lc} starting... (I: 69257fac8328b9de9755fdd237b92723)`);
            }
            if (arg.data.cmd === ObservableCmd.subscribe) {
                return this.witness_subscribe_cmd({ ibGib: arg });
            }
            else {
                throw new Error(`unknown command. must be of type ObservableCmd (${OBSERVABLE_CMD_VALUES}) (E: 5910e5cbafead25ea2917a65a3fc4123)`);
            }
        }
        catch (error) {
            console.error(`${lc} ${error.message}`);
            throw error;
        }
        finally {
            if (logalot) {
                console.log(`${lc} complete.`);
            }
        }
    }
    async witness_otherWitness({ arg, }) {
        const lc = `${this.lc}[${this.witness_otherWitness.name}]`;
        try {
            if (logalot) {
                console.log(`${lc} starting... (I: 871d0b7a2bef4596bad430f1cbacde02)`);
            }
            if (isSubscription({ ibGib: arg })) {
                return this.witness_subscriptionIbGib({ arg: arg });
            }
            else {
                return this.witness_subscribe_nonCmdWitness({ arg });
            }
        }
        catch (error) {
            console.error(`${lc} ${error.message}`);
            throw error;
        }
        finally {
            if (logalot) {
                console.log(`${lc} complete.`);
            }
        }
    }
    async witness_subscriptionIbGib({ arg, }) {
        const lc = `${this.lc}[${this.witness_subscriptionIbGib.name}]`;
        try {
            if (logalot) {
                console.log(`${lc} starting... (I: 4ac764776ef7e3f9074eb95a999ba423)`);
            }
            if (!arg.data) {
                throw new Error(`arg.data required (E: 764ae772727ceba95a18140fdd938323)`);
            }
            const key = this.getSubscriberKey({ subscription: arg });
            const [subscription, _observer] = this._subscribers[key];
            // will the stored subscription always be the same instance as the arg?
            if (arg === subscription) {
                delete this._subscribers[key];
            }
            else {
                throw new Error(`(UNEXPECTED) arg and subscription are two different instances? (E: aab5ef9bdc92abb9e816e2c443ab5723)`);
            }
            return ROOT;
        }
        catch (error) {
            console.error(`${lc} ${error.message}`);
            throw error;
        }
        finally {
            if (logalot) {
                console.log(`${lc} complete.`);
            }
        }
    }
    async replayPastPayloads({ observer, subscription, }) {
        const lc = `${this.lc}[${this.replayPastPayloads.name}]`;
        try {
            if (logalot) {
                console.log(`${lc} starting... (I: b3c8f9c76a4af030cf52fdcf01c3fd23)`);
            }
            // we want to be able to abort replay if the subscription is
            // unsubscribed during this execution. (imagine a long list to
            // replay and the subscriber cancels subscription after they found
            // out what they needed).
            const subscriberKey = this.getSubscriberKey({ subscription });
            const fnStillSubscribed = () => {
                return !!this._subscribers[subscriberKey];
            };
            // we cannot do a for loop because the pastPayloads may change
            // during execution. so we will do manual "jit" checking for the
            // index.
            let i = -1;
            let keepGoing = true;
            do {
                i++;
                if (i < this._pastPayloads.length) {
                    // stillSubscribed = await subscription.unsubscribed();
                    if (!fnStillSubscribed()) {
                        if (logalot) {
                            console.log(`${lc} subscription canceled before past payloads dispatch completed. (I: f1778f868446a518030d1cf7800c8a23)`);
                        }
                        return; /* <<<< returns early */
                    }
                    try {
                        const payload = this._pastPayloads[i];
                        await observer.next(payload);
                    }
                    catch (error) {
                        console.error(`${lc} observer errored on payload (i: ${i}). aborting replay (E: 1d7efc7c48e1491da4317ef333031077)`);
                        return; /* <<<< returns early */
                    }
                }
                else {
                    keepGoing = false;
                }
            } while (keepGoing);
            if (this.isErrored && observer.error && fnStillSubscribed()) {
                try {
                    await observer.error(this._pastError);
                }
                catch (error) {
                    console.error(`${lc} observer.error(this._pastError) errored itself (E: 18310e5e032345e7bdc3f2479807866c)`);
                }
            }
            else if (this.isComplete && observer.complete && fnStillSubscribed()) {
                try {
                    await observer.complete();
                }
                catch (error) {
                    console.error(`${lc} observer.complete errored (E: d9d027f5520f4d7eadfc669d66b5b550)`);
                }
            }
        }
        catch (error) {
            console.error(`${lc} ${error.message}`);
            throw error;
        }
        finally {
            if (logalot) {
                console.log(`${lc} complete.`);
            }
        }
    }
    async witness_subscribe_cmd({ ibGib, }) {
        const lc = `${this.lc}[${this.witness_subscribe_cmd.name}]`;
        try {
            if (logalot) {
                console.log(`${lc} starting... (I: 0c4c75c3fae297af6449c1048ce55423)`);
            }
            // validate arg and extract observer from it
            if ((ibGib.ibGibs ?? []).length !== 1) {
                throw new Error(`ibGib.ibGibs required. should contain one ObserverWitness (E: 3ed99b32e22284573c497df788a9f923)`);
            }
            const observer = ibGib.ibGibs[0];
            // create the subscription
            const subscription = await newupSubscription();
            if (logalot) {
                console.log(`${lc} subscription created (I: 89ccd97803c66292f9d7334e1e2de523)`);
            }
            await subscription.witness(this);
            await subscription.initialized;
            if (logalot) {
                console.log(`${lc} subscription initialized (after witnessing this observable as its src). (I: 3021483402b21c4df64a9173bd223a23)`);
            }
            // store subscription with observer for unsubscribe purposes BEFORE spinning off
            // replayPastPayloads, because that relies on cancellation detection of looking
            // at this key
            this._subscribers[this.getSubscriberKey({ subscription })] = [subscription, observer];
            if (logalot) {
                console.log(`${lc} stored subscriber (subscription, observer) (I: fb09660d885e33b80188dd0386ffc723)`);
            }
            // spin off dispatch of previous payloads if applicable AFTER adding
            // subscriber to this._subscribers.
            if (this.data.replay && this._pastPayloads.length > 0) {
                // SPINS OFF!!
                this.replayPastPayloads({ observer, subscription });
            }
            // return it!
            return subscription;
        }
        catch (error) {
            console.error(`${lc} ${error.message}`);
            throw error;
        }
        finally {
            if (logalot) {
                console.log(`${lc} complete.`);
            }
        }
    }
    /**
     * we have been given a witness ibGib that is not a command.  by default, an
     * observable will subscribe a witness, and when a value comes down the pipe
     * the witness will have its `witness` fn called a la a visitor pattern. If
     * an error occurs, it will be propagated. I'm not sure how to do the
     * `observer.complete` analog.
     * @param arg ibgib object with `witness` function
     */
    async witness_subscribe_nonCmdWitness({ arg }) {
        const lc = `${this.lc}[${this.witness_subscribe_nonCmdWitness.name}]`;
        try {
            if (logalot) {
                console.log(`${lc} starting... (I: d9571dfcab4247d594c838233971886a)`);
            }
            throw new Error(`not impl (E: 76219cf238ba4f849480bd7c82eefab9)`);
        }
        catch (error) {
            console.error(`${lc} ${error.message}`);
            throw error;
        }
        finally {
            if (logalot) {
                console.log(`${lc} complete.`);
            }
        }
    }
    // #endregion witness sub-functions
    // #region public api
    /**
     * subscribes the given `observer` to this observable's stream.
     *
     * ## notes
     *
     * * The `observer` can be any ibgib witness (not specifically expose the
     *   {@link ObserverWitness} contract), but that witness will have to be
     *   able to deal with this observable's outputs via that witness ibGib's
     *   `witness` function (visitor pattern).
     * * this is a wrapper that internally produces an ObservableCmd
     *
     * @param observer
     *
     * @returns subscription used for unsubscribing
     */
    async subscribe(observer) {
        const lc = `${this.lc}[${this.subscribe.name}]`;
        try {
            if (logalot) {
                console.log(`${lc} starting... (I: 0dec9a06c562711e19482d45a7075e23)`);
            }
            let resSubscription;
            if (isObserver({ ibGib: observer })) {
                // witness with Observer shape
                const arg = await this.argy({
                    argData: {
                        cmd: ObservableCmd.subscribe,
                        ibGibAddrs: [getIbGibAddr({ ibGib: observer })],
                    }, // I'm struggling to think of a descending TCmdData 'subscribe' that has other properties
                    ibGibs: [observer]
                });
                resSubscription = await this.witness(arg);
            }
            else {
                // plain ol' witness (without a `next` fn atow 11/2023)
                resSubscription = await this.witness(observer);
            }
            if (!isSubscription({ ibGib: resSubscription })) {
                throw new Error(`(UNEXPECTED) resSubscription is not a SubscriptionWitness? (E: c29bec4e3a48d6f1299143399ac55a23)`);
            }
            return resSubscription;
        }
        catch (error) {
            console.error(`${lc} ${error.message}`);
            throw error;
        }
        finally {
            if (logalot) {
                console.log(`${lc} complete.`);
            }
        }
    }
    // #endregion public api
    // #region helpers
    getSubscriberKey({ subscription, }) {
        if (subscription.data?.uuid) {
            return subscription.data.uuid;
        }
        else {
            throw new Error(`(UNEXPECTED) subscription.data.uuid falsy? (E: 8d078e6f8155f7567fd9378557e91823)`);
        }
    }
    async dispatchToEachSubscriber({ payload, fnForObserver, observableEventType, }) {
        const lc = `${this.lc}[${this.dispatchToEachSubscriber.name}]`;
        try {
            if (logalot) {
                console.log(`${lc} starting... (I: 0b990ce892f689d14458565717e66a23)`);
            }
            // dispatch to subscribers (serially)
            const subscriberTuples = Object.values(this._subscribers);
            for (let i = 0; i < subscriberTuples.length; i++) {
                const [subscription, observerOrWitness] = subscriberTuples[i];
                // depends on if subscriber is observer or plain witness...
                if (isObserver({ ibGib: observerOrWitness })) {
                    // subscriber uses observer interface
                    const observer = observerOrWitness;
                    await fnForObserver(observer, payload);
                    if (observer.complete) {
                        // we remove subscribers on unsubscribe, but just in case we
                        // will double-check here if we are still subscribed
                        const unsubscribed = await subscription.unsubscribed();
                        if (!unsubscribed) {
                            // dispatch to observer's via its `next` fn
                            await observer.complete();
                        }
                        else {
                            console.warn(`${lc} (UNEXPECTED) subscriber reference still held but is unsubscribed? skipping for now (i.e. won't remove subscriber) in case that code is in yielded execution and just hasn't gotten around to it with event loop (but that would be weird). (W: 1d98f35d2fdd4345954c947265d9fb85)`);
                        }
                    }
                    else {
                        // no error handler for this observer
                        if (logalot) {
                            console.log(`${lc} no error handler for observer. skipping. (I: 6a8a52c2012c464790eb406d2ae0383f)`);
                        }
                    }
                }
                else {
                    // subscriber is an ibGib witness (not an observer interface)
                    // so wrap the payload into an event and pass to
                    // `subscriber.witness` fn
                    const observableEventWrapper = await getObservableEventIbGib({
                        eventType: observableEventType,
                        srcObservable: this,
                        payload,
                    });
                    await observerOrWitness.witness(observableEventWrapper);
                }
            }
        }
        catch (error) {
            console.error(`${lc} ${error.message}`);
            throw error;
        }
        finally {
            if (logalot) {
                console.log(`${lc} complete.`);
            }
        }
    }
    /**
     * validates against common witness qualities.
     *
     * Override this with a call to `super.validateThis` for custom validation
     * for descending witness classes.
     *
     * @returns validation errors common to all observable witnesses, if any errors exist.
     */
    async validateThis() {
        const lc = `${this.lc}[${this.validateThis.name}]`;
        try {
            if (logalot) {
                console.log(`${lc} starting...`);
            }
            if (!this.data) {
            }
            const errors = [
            // ...await super.validateThis(),
            // ...validateCommonObservableData({ data: this.data }),
            ];
            return errors;
        }
        catch (error) {
            console.error(`${lc} ${extractErrorMsg(error)}`);
            throw error;
        }
        finally {
            if (logalot) {
                console.log(`${lc} complete.`);
            }
        }
    }
    /**
     * builds an arg ibGib.
     *
     * wrapper convenience to avoid long generic calls.
     */
    async argy({ argData, ibMetadata, noTimestamp, ibGibs, }) {
        const arg = await argy_({
            argData,
            ibMetadata,
            noTimestamp
        });
        if (ibGibs) {
            arg.ibGibs = ibGibs;
        }
        return arg;
    }
    /**
     * builds a result ibGib, if indeed a result ibgib is required.
     *
     * This is only useful in witnesses that have more structured
     * inputs/outputs. For those that simply accept any ibgib incoming and
     * return a primitive like ib^gib or whatever, then this is unnecessary.
     *
     * wrapper convenience to avoid long generic calls.
     */
    async resulty({ resultData, ibGibs, }) {
        const result = await resulty_({
            // ibMetadata: getObservableResultMetadata({space: this}),
            resultData,
        });
        if (ibGibs) {
            result.ibGibs = ibGibs;
        }
        return result;
    }
}
//# sourceMappingURL=observable-base-v1.mjs.map