import { createAbortError, doesDomainAddressHaveCapitalLetter, hideClassPrivateProps, ipnsNameToIpnsOverPubsubTopic, isIpns, isStringDomain, pubsubTopicToDhtKey, shortifyAddress, timestamp } from "../util.js";
import Logger from "../logger.js";
import { TypedEmitter } from "tiny-typed-emitter";
import { FailedToFetchCommunityFromGatewaysError, PKCError } from "../pkc-error.js";
import * as remeda from "remeda";
import { ModQueuePages, PostsPages } from "../pages/pages.js";
import { parseRawPages } from "../pages/util.js";
import { CommunityIpfsSchema } from "./schema.js";
import { CommunityClientsManager } from "./community-client-manager.js";
import { getPKCAddressFromPublicKeySync } from "../signer/util.js";
import { findUpdatingCommunity, refreshTrackedCommunityAliases, trackUpdatingCommunity, untrackUpdatingCommunity } from "../pkc/tracked-instance-registry-util.js";
export class RemoteCommunity extends TypedEmitter {
    constructor(pkc) {
        super();
        // to be overridden by local community classes
        this.startedState = "stopped";
        this.raw = {};
        this._updatingCommunityInstanceWithListeners = undefined; // The pkc._updatingCommunities we're subscribed to
        this._numOfListenersForUpdatingInstance = 0;
        this._pkc = pkc;
        this._setState("stopped");
        this._updatingState = "stopped";
        this._defineIpnsAccessorProps();
        this._defineEnumerableUpdatingState();
        // these functions might get separated from their `this` when used
        this.update = this.update.bind(this);
        this.stop = this.stop.bind(this);
        this.on("error", (...args) => this.listenerCount("error") === 1 && this._pkc.emit("error", ...args)); // only bubble up to pkc if no other listeners are attached
        this._clientsManager = new CommunityClientsManager(this);
        this.clients = this._clientsManager.clients;
        this.posts = new PostsPages({
            pageCids: {},
            pages: {},
            pkc: this._pkc,
            community: this
        });
        this.modQueue = new ModQueuePages({ pageCids: {}, pkc: this._pkc, community: this, pages: {} });
        hideClassPrivateProps(this);
    }
    _createStopAbortController() {
        if (!this._stopAbortController || this._stopAbortController.signal.aborted)
            this._stopAbortController = new AbortController();
        return this._stopAbortController;
    }
    _getStopAbortSignal() {
        return this._stopAbortController?.signal;
    }
    _isStopAbortRequested() {
        return Boolean(this._stopAbortController?.signal.aborted);
    }
    _abortStopOperations(reason) {
        if (!this._stopAbortController || this._stopAbortController.signal.aborted)
            return;
        this._stopAbortController.abort(createAbortError(reason));
    }
    _clearStopAbortController() {
        this._stopAbortController = undefined;
    }
    _defineEnumerableUpdatingState() {
        const proto = Object.getPrototypeOf(this);
        const updatingStateDescriptor = Object.getOwnPropertyDescriptor(proto, "updatingState");
        if (!updatingStateDescriptor)
            return;
        Object.defineProperty(this, "updatingState", {
            ...updatingStateDescriptor,
            enumerable: true
        });
    }
    _defineIpnsAccessorProps() {
        Object.defineProperties(this, {
            _ipnsName: { enumerable: false, configurable: true, writable: true, value: undefined },
            _ipnsHops: { enumerable: false, configurable: true, writable: true, value: undefined },
            _ipnsPubsubTopic: { enumerable: false, configurable: true, writable: true, value: undefined },
            _ipnsPubsubTopicRoutingCid: { enumerable: false, configurable: true, writable: true, value: undefined }
        });
        Object.defineProperties(this, {
            ipnsName: {
                enumerable: true,
                configurable: true,
                get: () => this._getIpnsName(),
                set: (value) => this._setIpnsName(value)
            },
            ipnsHops: {
                enumerable: true,
                configurable: true,
                get: () => this._getIpnsHops(),
                set: (value) => this._setIpnsHops(value)
            },
            ipnsPubsubTopic: {
                enumerable: true,
                configurable: true,
                get: () => this._getIpnsPubsubTopic(),
                set: (value) => this._setIpnsPubsubTopic(value)
            },
            ipnsPubsubTopicRoutingCid: {
                enumerable: true,
                configurable: true,
                get: () => this._getIpnsPubsubTopicRoutingCid(),
                set: (value) => this._setIpnsPubsubTopicRoutingCid(value)
            }
        });
    }
    _updateLocalPostsInstance(newPosts) {
        const log = Logger("pkc-js:remote-community:_updateLocalPostsInstanceIfNeeded");
        const postsPagesCreationTimestamp = this.updatedAt;
        this.posts._community = this;
        if (!newPosts)
            // The community has changed its address, need to reset the posts
            this.posts.resetPages();
        else if ((!("pages" in newPosts) || !newPosts.pages || Object.keys(newPosts.pages).length === 0) &&
            newPosts.pageCids &&
            Object.keys(newPosts.pageCids).length > 0) {
            // only pageCids is provided (or pages is empty)
            this.posts.updateProps({
                pageCids: newPosts.pageCids,
                community: this,
                pages: {}
            });
        }
        else if ((!newPosts.pageCids || Object.keys(newPosts.pageCids).length === 0) &&
            "pages" in newPosts &&
            newPosts.pages &&
            Object.keys(newPosts.pages).length > 0) {
            // was only provided with a single preloaded page, no page cids
            if (typeof postsPagesCreationTimestamp !== "number")
                throw Error("community.updatedAt should be defined when updating posts");
            const parsedPages = parseRawPages(newPosts);
            this.posts.updateProps({
                ...parsedPages,
                community: this,
                pageCids: {}
            });
        }
        else if ("pages" in newPosts &&
            newPosts.pages &&
            Object.keys(newPosts.pages).length > 0 &&
            "pageCids" in newPosts &&
            newPosts.pageCids &&
            Object.keys(newPosts.pageCids).length > 0) {
            // both pageCids and pages are provided
            log.trace(`Updating the props of community (${this.address}) posts`);
            if (typeof postsPagesCreationTimestamp !== "number")
                throw Error("community.updatedAt should be defined when updating posts");
            const parsedPages = parseRawPages(newPosts);
            this.posts.updateProps({
                ...parsedPages,
                community: this,
                pageCids: newPosts?.pageCids || {}
            });
        }
    }
    _updateLocalModQueueInstance(newModQueue) {
        this.modQueue._community = this;
        if (!newModQueue)
            // The community has changed its address, need to reset the posts
            this.modQueue.resetPages();
        else if (newModQueue.pageCids) {
            // only pageCids is provided
            this.modQueue.updateProps({
                pageCids: newModQueue.pageCids,
                community: this,
                pages: {}
            });
        }
    }
    initCommunityIpfsPropsNoMerge(newProps) {
        const log = Logger("pkc-js:remote-community:initCommunityIpfsPropsNoMerge");
        this.raw.communityIpfs = newProps;
        this.initRemoteCommunityPropsNoMerge(newProps);
        const unknownProps = remeda.difference(remeda.keys.strict(this.raw.communityIpfs), remeda.keys.strict(CommunityIpfsSchema.shape));
        if (unknownProps.length > 0) {
            log(`Found unknown props on community (${this.address}) ipfs record`, unknownProps);
            Object.assign(this, remeda.pick(this.raw.communityIpfs, unknownProps));
        }
    }
    _updateIpnsPubsubPropsIfNeeded(newProps) {
        // The IPNS name we resolve/subscribe to is the ANCHOR of the (possibly delegated) chain.
        // For a delegated community the content is signed by the terminal (minter) key, so deriving
        // the ipns name from signature.publicKey would point us at the minter instead of the anchor.
        // ipnsHops[0] is the anchor; prefer it when the resolution chain is known.
        // See docs/protocol/delegated-ipns.md.
        const anchorFromHops = this.ipnsHops?.[0];
        if ("ipnsName" in newProps && newProps.ipnsName) {
            this.ipnsName = newProps.ipnsName;
            this.ipnsPubsubTopic = ipnsNameToIpnsOverPubsubTopic(this.ipnsName);
            this.ipnsPubsubTopicRoutingCid = pubsubTopicToDhtKey(this.ipnsPubsubTopic);
        }
        else if (anchorFromHops) {
            this.ipnsName = anchorFromHops;
            this.ipnsPubsubTopic = ipnsNameToIpnsOverPubsubTopic(this.ipnsName);
            this.ipnsPubsubTopicRoutingCid = pubsubTopicToDhtKey(this.ipnsPubsubTopic);
        }
        else if (newProps.signature?.publicKey && this.signature?.publicKey !== newProps.signature?.publicKey) {
            // The signature public key has changed, we need to update the ipns name and pubsub topic
            this.ipnsName = getPKCAddressFromPublicKeySync(newProps.signature.publicKey);
            this.ipnsPubsubTopic = ipnsNameToIpnsOverPubsubTopic(this.ipnsName);
            this.ipnsPubsubTopicRoutingCid = pubsubTopicToDhtKey(this.ipnsPubsubTopic);
        }
        else if ("address" in newProps && typeof newProps.address === "string" && isIpns(newProps.address)) {
            // Address is already an IPNS name; initialize pubsub fields immediately.
            this.ipnsName = newProps.address;
            this.ipnsPubsubTopic = ipnsNameToIpnsOverPubsubTopic(this.ipnsName);
            this.ipnsPubsubTopicRoutingCid = pubsubTopicToDhtKey(this.ipnsPubsubTopic);
        }
        if (!this.pubsubTopicRoutingCid) {
            if ("pubsubTopicRoutingCid" in newProps)
                this.pubsubTopicRoutingCid = newProps.pubsubTopicRoutingCid;
            else if (this.raw.communityIpfs)
                this.pubsubTopicRoutingCid = pubsubTopicToDhtKey(newProps.pubsubTopic ||
                    this.pubsubTopic ||
                    ("address" in newProps ? newProps.address : undefined) ||
                    this.address);
        }
    }
    initRemoteCommunityPropsNoMerge(newProps) {
        // This function is not strict, and will assume all props can be undefined, except address
        // Carry over the resolved IPNS delegation chain when the source provides it (e.g.
        // createCommunity(loadedCommunity) clones an already-resolved instance). ipnsHops cannot be
        // re-derived from the address, so copy it before deriving identity below — this keeps the
        // clone anchored to ipnsHops[0] and lets runtime fields round-trip. A CommunityIpfs record
        // never carries ipnsHops, so this is a no-op for the normal resolution/RPC-update paths.
        // See docs/protocol/delegated-ipns.md.
        const incomingIpnsHops = newProps.ipnsHops;
        if (Array.isArray(incomingIpnsHops) && incomingIpnsHops.length > 0)
            this._ipnsHops = incomingIpnsHops;
        this.title = newProps.title;
        this.description = newProps.description;
        this.lastPostCid = newProps.lastPostCid;
        this.lastCommentCid = newProps.lastCommentCid;
        this.protocolVersion = newProps.protocolVersion;
        this.roles = newProps.roles;
        this.features = newProps.features;
        this.suggested = newProps.suggested;
        this.rules = newProps.rules;
        this.flairs = newProps.flairs;
        this.postUpdates = newProps.postUpdates;
        this.challenges = newProps.challenges;
        this.statsCid = newProps.statsCid;
        this.createdAt = newProps.createdAt;
        this.updatedAt = newProps.updatedAt;
        this.encryption = newProps.encryption;
        this._updateIpnsPubsubPropsIfNeeded(newProps);
        this.pubsubTopic = newProps.pubsubTopic;
        this.signature = newProps.signature;
        // Compute runtime fields: publicKey, name, address
        // community.publicKey is the ANCHOR IPNS name (the user-facing identity). For a delegated
        // community the content is signed by the terminal (minter) key, so we must NOT derive
        // publicKey from signature.publicKey — that would expose the minter as the identity. The
        // anchor is ipnsHops[0] when the chain has been resolved. See docs/protocol/delegated-ipns.md.
        const explicitPublicKey = "publicKey" in newProps ? newProps.publicKey : undefined;
        const anchorFromHops = this.ipnsHops?.[0];
        if (anchorFromHops) {
            this.publicKey = anchorFromHops;
        }
        else if (newProps.signature?.publicKey) {
            this.publicKey = getPKCAddressFromPublicKeySync(newProps.signature.publicKey);
        }
        else if (explicitPublicKey) {
            this.publicKey = explicitPublicKey;
        }
        else if (!this.publicKey &&
            "address" in newProps &&
            typeof newProps.address === "string" &&
            !isStringDomain(newProps.address)) {
            this.publicKey = newProps.address;
        }
        if (typeof newProps.name === "string")
            this.name = newProps.name;
        else if (!this.name &&
            "address" in newProps &&
            typeof newProps.address === "string" &&
            isStringDomain(newProps.address)) {
            this.name = newProps.address;
        }
        // Only set address during initial creation (no address yet).
        // Once set, address is immutable -- record updates must not override it.
        if (!this.address) {
            const explicitAddress = "address" in newProps ? newProps.address : undefined;
            const derivedAddress = this.name || this.publicKey || explicitPublicKey || explicitAddress;
            if (derivedAddress)
                this.setAddress(derivedAddress);
        }
        else {
            // Address already set -- refresh tracking aliases without changing address
            refreshTrackedCommunityAliases(this._pkc, this);
            this._assertHasIdentity();
        }
        this._updateLocalPostsInstance(newProps.posts);
        this._updateLocalModQueueInstance(newProps.modQueue);
        // Exclusive Instance props
        if ("updateCid" in newProps && newProps.updateCid)
            this.updateCid = newProps.updateCid;
    }
    _assertHasIdentity() {
        if (!this.name && !this.publicKey) {
            throw new Error(`Community identity invariant violated: both name and publicKey are undefined (address: ${this.address})`);
        }
    }
    setAddress(newAddress) {
        // check if domain or ipns
        // else, throw an error
        if (doesDomainAddressHaveCapitalLetter(newAddress))
            throw new PKCError("ERR_COMMUNITY_NAME_HAS_CAPITAL_LETTER", { communityAddress: newAddress });
        const isDomain = newAddress.includes(".");
        if (!isDomain && !isIpns(newAddress))
            throw new PKCError("ERR_INVALID_COMMUNITY_ADDRESS_SCHEMA", { communityAddress: newAddress, isDomain, isIpns: false });
        this.address = newAddress;
        this.shortAddress = shortifyAddress(this.address);
        // Sync wire-format name field: domains go into `name`, non-domains clear it
        this.name = isStringDomain(newAddress) ? newAddress : undefined;
        // For non-domain addresses, the address IS the publicKey (IPNS name from ed25519 key)
        if (!isStringDomain(newAddress) && !this.publicKey) {
            this.publicKey = newAddress;
        }
        this.posts._community = this;
        this.modQueue._community = this;
        refreshTrackedCommunityAliases(this._pkc, this);
        this._assertHasIdentity();
    }
    _clearDataForKeyMigration(newPublicKey) {
        this.raw.communityIpfs = undefined;
        this.updateCid = undefined;
        // Clear all display fields via initRemoteCommunityPropsNoMerge with empty props.
        // Address immutability in initRemoteCommunityPropsNoMerge ensures address won't change.
        this.initRemoteCommunityPropsNoMerge({});
        // Update to new key and IPNS routing props
        this.publicKey = newPublicKey;
        this.ipnsName = newPublicKey;
        this.ipnsPubsubTopic = ipnsNameToIpnsOverPubsubTopic(newPublicKey);
        this.ipnsPubsubTopicRoutingCid = pubsubTopicToDhtKey(this.ipnsPubsubTopic);
        this._assertHasIdentity();
    }
    _toJSONIpfsBaseNoPosts() {
        const communityIpfsKeys = remeda.keys.strict(remeda.omit(CommunityIpfsSchema.shape, ["posts", "modQueue"]));
        return remeda.pick(this, communityIpfsKeys);
    }
    toJSONRpcRemote() {
        if (!this.updateCid || !this.raw.communityIpfs) {
            // Post key-migration cleared state — tell client to reset its instance
            return {
                resetInstance: true,
                runtimeFields: {
                    newPublicKey: this.publicKey,
                    nameResolved: this.nameResolved,
                    updatingState: this.updatingState
                }
            };
        }
        return {
            community: this.raw.communityIpfs,
            runtimeFields: {
                updateCid: this.updateCid,
                updatingState: this.updatingState,
                nameResolved: this.nameResolved,
                ipnsHops: this.ipnsHops
            }
        };
    }
    get updatingState() {
        if (this._updatingCommunityInstanceWithListeners) {
            return this._updatingCommunityInstanceWithListeners.community.updatingState;
        }
        else
            return this._updatingState;
    }
    _getIpnsName() {
        return this._updatingCommunityInstanceWithListeners?.community.ipnsName ?? this._ipnsName;
    }
    _setIpnsName(value) {
        this._ipnsName = value;
    }
    _getIpnsHops() {
        return this._updatingCommunityInstanceWithListeners?.community.ipnsHops ?? this._ipnsHops;
    }
    _setIpnsHops(value) {
        this._ipnsHops = value;
    }
    _getIpnsPubsubTopic() {
        return this._updatingCommunityInstanceWithListeners?.community.ipnsPubsubTopic ?? this._ipnsPubsubTopic;
    }
    _setIpnsPubsubTopic(value) {
        this._ipnsPubsubTopic = value;
    }
    _getIpnsPubsubTopicRoutingCid() {
        return this._updatingCommunityInstanceWithListeners?.community.ipnsPubsubTopicRoutingCid ?? this._ipnsPubsubTopicRoutingCid;
    }
    _setIpnsPubsubTopicRoutingCid(value) {
        this._ipnsPubsubTopicRoutingCid = value;
    }
    _setState(newState) {
        if (newState === this.state)
            return;
        this.state = newState;
        this.emit("statechange", this.state);
    }
    _setStateNoEmission(newState) {
        if (newState === this.state)
            return;
        this.state = newState;
    }
    _changeStateEmitEventEmitStateChangeEvent(opts) {
        // this code block is only called on a community whose update loop is already started
        // never called in a community that's mirroring a community with an update loop
        const shouldEmitStateChange = opts.newState && opts.newState !== this.state;
        const shouldEmitUpdatingStateChange = opts.newUpdatingState && opts.newUpdatingState !== this.updatingState;
        const shouldEmitStartedStateChange = opts.newStartedState && opts.newStartedState !== this.startedState;
        if (opts.newState)
            this._setStateNoEmission(opts.newState);
        if (opts.newUpdatingState)
            this._setUpdatingStateNoEmission(opts.newUpdatingState);
        if (opts.newStartedState)
            this._setStartedStateNoEmission(opts.newStartedState);
        this.emit(opts.event.name, ...opts.event.args);
        if (shouldEmitStateChange)
            this.emit("statechange", this.state);
        if (shouldEmitUpdatingStateChange)
            this.emit("updatingstatechange", this.updatingState);
        if (shouldEmitStartedStateChange)
            this.emit("startedstatechange", this.startedState);
    }
    _setUpdatingStateNoEmission(newState) {
        if (newState === this.updatingState)
            return;
        this._updatingState = newState;
    }
    _setUpdatingStateWithEventEmissionIfNewState(newState) {
        if (newState === this._updatingState)
            return;
        this._updatingState = newState;
        this.emit("updatingstatechange", this._updatingState);
    }
    _setStartedStateNoEmission(newState) {
        if (newState === this.startedState)
            return;
        this.startedState = newState;
    }
    _setStartedStateWithEmission(newState) {
        if (newState === this.startedState)
            return;
        this.startedState = newState;
        this.emit("startedstatechange", this.startedState);
    }
    // Errors that retrying to load the ipns record will not help
    // Instead we should abort the retries, and emit an error event to notify the user to do something about it
    _isRetriableErrorWhenLoading(err) {
        if (!(err instanceof PKCError))
            return false; // If it's not a recognizable error, then we throw to notify the user
        if (err.code === "ERR_COMMUNITY_SIGNATURE_IS_INVALID" ||
            err.code === "ERR_INVALID_COMMUNITY_IPFS_SCHEMA" ||
            err.code === "ERR_THE_COMMUNITY_IPNS_RECORD_POINTS_TO_DIFFERENT_ADDRESS_THAN_WE_EXPECTED" ||
            err.code === "ERR_OVER_DOWNLOAD_LIMIT" ||
            err.code === "ERR_INVALID_JSON" ||
            err.code === "ERR_NO_RESOLVER_FOR_NAME" ||
            // Delegated-IPNS chain failures are definitive (forged/invalid chain or unsupported value),
            // not transient. See docs/protocol/delegated-ipns.md.
            err.code === "ERR_GATEWAY_IPNS_RECORD_CHAIN_INVALID" ||
            err.code === "ERR_IPNS_MAX_HOPS_EXCEEDED" ||
            err.code === "ERR_RESOLVED_IPNS_TO_UNSUPPORTED_VALUE")
            return false;
        if (err instanceof FailedToFetchCommunityFromGatewaysError) {
            // If all gateway errors are non retriable, then the error is non retriable
            for (const gatewayError of Object.values(err.details.gatewayToError)) {
                // Invalid signature from a gateway is retriable (transient gateway issue, e.g. stale IPNS cache)
                if (gatewayError instanceof PKCError && gatewayError.code === "ERR_COMMUNITY_SIGNATURE_IS_INVALID")
                    return true;
                if (this._isRetriableErrorWhenLoading(gatewayError))
                    return true;
            }
            return false; // if all gateways are non retriable, then we should not retry
        }
        return true;
    }
    _setCommunityIpfsPropsFromUpdatingCommunitiesIfPossible() {
        const log = Logger("pkc-js:comment:_setCommunityIpfsPropsFromUpdatingCommunitiesIfPossible");
        const updatingCommunity = findUpdatingCommunity(this._pkc, { publicKey: this.publicKey, name: this.name });
        if (updatingCommunity?.raw?.communityIpfs && (this.updatedAt || 0) < updatingCommunity.raw.communityIpfs.updatedAt) {
            this.initCommunityIpfsPropsNoMerge(updatingCommunity.raw.communityIpfs);
            this.updateCid = updatingCommunity.updateCid;
            log.trace(`New Remote Community instance`, this.address, `will use CommunityIpfs from pkc._updatingCommunities[${this.address}] with updatedAt`, this.updatedAt, "that's", timestamp() - this.updatedAt, "seconds old");
            this.emit("update", this);
        }
    }
    // nameResolved reflects whether THIS community's own name resolved to its publicKey. When we mirror an
    // updating instance that we share only by publicKey — e.g. a community loaded by a raw IPNS key sharing a
    // key with a resolved-domain sibling (migrating.bso), or two different domains that resolve to the same
    // key — the sibling's nameResolved does NOT describe us. Adopt it only when the names match; otherwise
    // restore the value we had before mirroring, undoing anything deepMergeRuntimeFields copied in. See #119.
    _adoptMirroredNameResolved(source, nameResolvedBeforeMirror) {
        if (this.name === source.name) {
            if (typeof source.nameResolved === "boolean")
                this.nameResolved = source.nameResolved;
        }
        else
            this.nameResolved = nameResolvedBeforeMirror;
    }
    async _initCommunityInstanceWithListeners() {
        const trackedUpdatingCommunity = findUpdatingCommunity(this._pkc, { publicKey: this.publicKey, name: this.name });
        if (!trackedUpdatingCommunity)
            throw Error("should be defined at this stage");
        const log = Logger("pkc-js:remote-community:update");
        const communityInstance = trackedUpdatingCommunity;
        return {
            community: communityInstance,
            update: () => {
                const nameResolvedBeforeMirror = this.nameResolved;
                if (!communityInstance.raw.communityIpfs || !communityInstance.updateCid) {
                    if (communityInstance.publicKey)
                        this._clearDataForKeyMigration(communityInstance.publicKey);
                }
                else {
                    this.initCommunityIpfsPropsNoMerge(communityInstance.raw.communityIpfs);
                    this.updateCid = communityInstance.updateCid;
                }
                this._adoptMirroredNameResolved(communityInstance, nameResolvedBeforeMirror);
                log(`Remote Community instance`, this.address, `received update event from pkc._updatingCommunities[${this.address}] with updatedAt`, this.updatedAt, "that's", timestamp() - this.updatedAt, "seconds old");
                this.emit("update", this);
            },
            error: (error) => {
                this.emit("error", error);
            },
            updatingstatechange: (newUpdatingState) => {
                this.emit("updatingstatechange", newUpdatingState);
            },
            statechange: async (newState) => {
                if (newState === "stopped" && this.state !== "stopped")
                    await this.stop();
            }
        };
    }
    async fetchLatestCommunityOrSubscribeToEvent() {
        const log = Logger("pkc-js:remote-community:update:updateOnce");
        if (!findUpdatingCommunity(this._pkc, { publicKey: this.publicKey, name: this.name })) {
            // Pass publicKey alongside name/address so the updating community can use publicKey fallback
            const createOpts = this.publicKey && isStringDomain(this.address)
                ? { name: this.address, publicKey: this.publicKey }
                : { address: this.address };
            const updatingCommunity = await this._pkc.createCommunity(createOpts);
            trackUpdatingCommunity(this._pkc, updatingCommunity);
            log("Creating a new entry for this._pkc._updatingCommunities", this.address);
        }
        const communityInstance = findUpdatingCommunity(this._pkc, { publicKey: this.publicKey, name: this.name });
        if (!communityInstance)
            throw Error("should be defined at this stage");
        if (communityInstance === this) {
            // Already tracking this instance; start the loop directly without mirroring to itself
            this._clientsManager.startUpdatingLoop().catch((err) => log.error("Failed to start update loop of community", err));
            return;
        }
        this._updatingCommunityInstanceWithListeners = await this._initCommunityInstanceWithListeners();
        this._updatingCommunityInstanceWithListeners.community.on("update", this._updatingCommunityInstanceWithListeners.update);
        this._updatingCommunityInstanceWithListeners.community.on("updatingstatechange", this._updatingCommunityInstanceWithListeners.updatingstatechange);
        this._updatingCommunityInstanceWithListeners.community.on("error", this._updatingCommunityInstanceWithListeners.error);
        this._updatingCommunityInstanceWithListeners.community.on("statechange", this._updatingCommunityInstanceWithListeners.statechange);
        const clientKeys = remeda.keys.strict(this.clients);
        for (const clientType of clientKeys)
            if (this.clients[clientType])
                for (const clientUrl of Object.keys(this.clients[clientType]))
                    this.clients[clientType][clientUrl].mirror(this._updatingCommunityInstanceWithListeners.community.clients[clientType][clientUrl]);
        this._updatingCommunityInstanceWithListeners.community._numOfListenersForUpdatingInstance++;
        if (this._updatingCommunityInstanceWithListeners.community.state === "stopped") {
            this._updatingCommunityInstanceWithListeners.community._setState("updating");
            this._updatingCommunityInstanceWithListeners.community._clientsManager
                .startUpdatingLoop()
                .catch((err) => log.error("Failed to start update loop of community", err));
        }
    }
    async update() {
        if (this.state !== "stopped")
            return; // No need to do anything if community is already updating
        const log = Logger("pkc-js:remote-community:update");
        this._setState("updating");
        await this.fetchLatestCommunityOrSubscribeToEvent();
        if (this.raw.communityIpfs)
            this.emit("update", this);
    }
    async _cleanUpUpdatingCommunityInstanceWithListeners() {
        if (!this._updatingCommunityInstanceWithListeners)
            throw Error("should be defined at this stage");
        const log = Logger("pkc-js:remote-community:stop:cleanUpUpdatingCommunityInstanceWithListeners");
        const updatingCommunity = this._updatingCommunityInstanceWithListeners.community;
        if (typeof updatingCommunity.ipnsName === "string")
            this._ipnsName = updatingCommunity.ipnsName;
        if (Array.isArray(updatingCommunity.ipnsHops))
            this._ipnsHops = updatingCommunity.ipnsHops;
        if (typeof updatingCommunity.ipnsPubsubTopic === "string")
            this._ipnsPubsubTopic = updatingCommunity.ipnsPubsubTopic;
        if (typeof updatingCommunity.ipnsPubsubTopicRoutingCid === "string")
            this._ipnsPubsubTopicRoutingCid = updatingCommunity.ipnsPubsubTopicRoutingCid;
        this._updatingState = this._updatingCommunityInstanceWithListeners.community.updatingState; // need to capture latest updating state before removing listeners
        // this instance is subscribed to pkc._updatingCommunity[address]
        // removing listeners should reset pkc._updatingCommunity by itself when there are no subscribers
        this._updatingCommunityInstanceWithListeners.community.removeListener("statechange", this._updatingCommunityInstanceWithListeners.statechange);
        this._updatingCommunityInstanceWithListeners.community.removeListener("update", this._updatingCommunityInstanceWithListeners.update);
        this._updatingCommunityInstanceWithListeners.community.removeListener("updatingstatechange", this._updatingCommunityInstanceWithListeners.updatingstatechange);
        this._updatingCommunityInstanceWithListeners.community.removeListener("error", this._updatingCommunityInstanceWithListeners.error);
        const clientKeys = remeda.keys.strict(this.clients);
        for (const clientType of clientKeys)
            if (this.clients[clientType])
                for (const clientUrl of Object.keys(this.clients[clientType]))
                    this.clients[clientType][clientUrl].unmirror();
        this._updatingCommunityInstanceWithListeners.community._numOfListenersForUpdatingInstance--;
        if (this._updatingCommunityInstanceWithListeners.community._numOfListenersForUpdatingInstance === 0 &&
            this._updatingCommunityInstanceWithListeners.community.state !== "stopped") {
            log("Cleaning up pkc._updatingCommunities", this.address, "There are no communities using it for updates");
            // Untrack before stop() to prevent findUpdatingCommunity from returning a dying entry
            // during the async stop window
            untrackUpdatingCommunity(this._pkc, this._updatingCommunityInstanceWithListeners.community);
            await this._updatingCommunityInstanceWithListeners.community.stop();
        }
        this._updatingCommunityInstanceWithListeners = undefined;
    }
    async stop() {
        if (this.state === "stopped")
            return; // no-op if already stopped, mirrors update()'s idempotency
        if (this.state !== "updating")
            throw new PKCError("ERR_CALLED_COMMUNITY_STOP_WITHOUT_UPDATE", { address: this.address });
        const log = Logger("pkc-js:remote-community:stop");
        this._abortStopOperations(`Aborting community operations for ${this.address} because community.stop() was called`);
        if (this._updatingCommunityInstanceWithListeners)
            await this._cleanUpUpdatingCommunityInstanceWithListeners();
        else {
            // this instance is pkc._updatingCommunity[address] itself
            await this._clientsManager.stopUpdatingLoop();
            untrackUpdatingCommunity(this._pkc, this);
        }
        this._setUpdatingStateWithEventEmissionIfNewState("stopped");
        this._setState("stopped");
        this.posts._stop();
        this.modQueue._stop();
    }
    // functions to be overridden in local community classes
    async edit(options) {
        throw Error("Can't edit a remote community");
    }
    async delete() {
        throw Error("Can't delete a remote community");
    }
    async start() {
        throw Error("Can't start a remote community");
    }
    // Community export (issue #79). Overridden by LocalCommunity and the RPC variants.
    // The base implementation rejects because a read-only RemoteCommunity has no DB to back up.
    get exports() {
        return [];
    }
    async export(options) {
        throw new PKCError("ERR_COMMUNITY_NOT_LOCAL", { address: this.address });
    }
    // Read the community moderation log (commentModeration records). Overridden by LocalCommunity
    // (direct DB read) and RpcLocalCommunity (RPC call). The base rejects because a read-only
    // RemoteCommunity has no community DB to read mod logs from.
    async exportCommunityModLogs(opts) {
        throw new PKCError("ERR_COMMUNITY_NOT_LOCAL", { address: this.address });
    }
}
//# sourceMappingURL=remote-community.js.map