import Logger from "../../../logger.js";
import { of as calculateIpfsHash } from "typestub-ipfs-only-hash";
import { calculateStringSizeSameAsIpfsAddCidV0, hideClassPrivateProps, isStringDomain, retryKuboIpfsAddAndProvide } from "../../../util.js";
import { stringify as deterministicStringify } from "safe-stable-stringify";
import { PKCError } from "../../../pkc-error.js";
import { storePublication } from "./local-community/publication-store.js";
import { verifyCommunity } from "../../../signer/signatures.js";
import { deriveCommentIpfsFromCommentTableRow } from "../util.js";
import { RpcLocalCommunity } from "../../../community/rpc-local-community.js";
import * as remeda from "remeda";
import { CommunityIpfsSchema } from "../../../community/schema.js";
import { MAX_FILE_SIZE_BYTES_FOR_COMMUNITY_IPFS } from "../../../community/community-client-manager.js";
import { sha256 } from "js-sha256";
import { generateDefaultChallenges } from "./local-community/defaults.js";
import { createNewLocalCommunityDb, getDbInternalState, initDbHandlerIfNeeded, initInternalCommunityAfterFirstUpdateNoMerge, initInternalCommunityBeforeFirstUpdateNoMerge, initNewLocalCommunityPropsNoMerge, updateDbInternalState, updateInstancePropsWithStartedCommunityOrDb } from "./local-community/db-state.js";
export { createNewLocalCommunityDb, updateInstancePropsWithStartedCommunityOrDb };
import { listenToIncomingRequests } from "./local-community/pubsub.js";
import { repinCommentsIPFSIfNeeded } from "./local-community/cleanup.js";
import { handleChallengeAnswer as handleChallengeAnswerFreeFunction, handleChallengeExchange as handleChallengeExchangeFreeFunction, handleChallengeRequest as handleChallengeRequestFreeFunction, publishChallengeVerification, publishIdempotentDuplicateVerification } from "./local-community/challenges.js";
import { checkPublicationValidity } from "./local-community/publication-validation.js";
import { calculateLocalMfsPathForCommentUpdate, updateCommentsThatNeedToBeUpdated } from "./local-community/comment-updates.js";
import { purgeDisapprovedCommentsOlderThan } from "./local-community/cleanup.js";
import { addOldPageCidsToCidsToUnpin, calculateLatestUpdateTrigger, calculateNewPostUpdates, resolveIpnsAndLogIfPotentialProblematicSequence, shouldResolveDomainForVerification, updateCommunityIpnsIfNeeded } from "./local-community/ipns-publishing.js";
import { deleteCommunity, start as lifecycleStart, stop as lifecycleStop, update as lifecycleUpdate } from "./local-community/lifecycle.js";
import { edit as editCommunity } from "./local-community/editing.js";
import { cancelExportEmbedded, cloneExportRecord, deleteExportRecord, exportCommunityEmbedded, loadAndPruneExportsFromKeyv } from "./local-community/export.js";
// This is a sub we have locally in our pkc datapath, in a NodeJS environment
export class LocalCommunity extends RpcLocalCommunity {
    constructor(pkc) {
        super(pkc);
        this.raw = {};
        this._postUpdatesBuckets = [86400, 604800, 2592000, 3153600000]; // 1 day, 1 week, 1 month, 100 years. Expecting to be sorted from smallest to largest
        this._defaultCommunityChallenges = generateDefaultChallenges();
        this._challengeExchangesFromLocalPublishers = {}; // key is stringified challengeRequestId and value is true if the challenge exchange is ongoing
        this._cidsToUnPin = new Set();
        this._mfsPathsToRemove = new Set();
        this._communityUpdateTrigger = false;
        this._combinedHashOfPendingCommentsCids = sha256("");
        this._publishLoopPromise = undefined;
        this._updateLoopPromise = undefined;
        this._firstUpdateAfterStart = true;
        this._internalStateUpdateId = "";
        this._lastPubsubTopicRoutingProvideAt = undefined;
        this._mirroredStartedOrUpdatingCommunity = undefined; // The pkc._startedCommunities we're subscribed to
        this._pendingEditProps = [];
        this._blocksToRm = [];
        this._postsAllPageCids = undefined;
        // Community export (issue #79). _exports is the public list surfaced through
        // `community.exports`; _activeExports tracks in-flight backups so pkc.destroy() can cancel
        // them; _exportQueue serializes back-to-back exports on the same community per spec.
        this._exports = [];
        this._activeExports = new Map();
        this._exportQueue = Promise.resolve();
        this.handleChallengeExchange = this.handleChallengeExchange.bind(this);
        this._setState("stopped");
        this.started = false;
        this._stopHasBeenCalled = false;
        // need to make sure these props are undefined on the constructor level, so they wouldn't show while logging
        //@ts-expect-error
        this._pageGenerator = undefined;
        //@ts-expect-error
        this._challengeAnswerPromises = undefined;
        //@ts-expect-error
        this._challengeAnswerResolveReject = undefined;
        //@ts-expect-error
        this._ongoingChallengeExchanges = undefined;
        //@ts-expect-error
        this._duplicatePublicationAttempts = undefined;
        //@ts-expect-error
        this._internalStateUpdateId = undefined;
        //@ts-expect-error
        this._dbHandler = undefined;
        hideClassPrivateProps(this);
    }
    // This will be stored in DB and also shared between instances via _updateInstancePropsWithStartedCommunityOrDb.
    // Must NOT convert posts to CID-ref format here — that's only for DB storage (done in _updateDbInternalState).
    // CID-ref conversion strips preloaded page data which breaks reply CommentUpdate resolution
    // when other instances read the shared state.
    toJSONInternalAfterFirstUpdate() {
        const rpcJson = this.toJSONInternalRpcAfterFirstUpdate();
        return {
            ...rpcJson.community,
            ...remeda.omit(rpcJson.localCommunity, ["started", "startedState"]),
            updateCid: rpcJson.runtimeFields.updateCid,
            signer: remeda.pick(this.signer, ["privateKey", "type", "address", "shortAddress", "publicKey"]),
            _internalStateUpdateId: this._internalStateUpdateId,
            _cidsToUnPin: [...this._cidsToUnPin],
            _mfsPathsToRemove: [...this._mfsPathsToRemove],
            _pendingEditProps: this._pendingEditProps
        };
    }
    toJSONInternalBeforeFirstUpdate() {
        const rpcJson = this.toJSONInternalRpcBeforeFirstUpdate();
        return {
            ...remeda.omit(rpcJson.localCommunity, ["started", "startedState"]),
            signer: remeda.pick(this.signer, ["privateKey", "type", "address", "shortAddress", "publicKey"]),
            _internalStateUpdateId: this._internalStateUpdateId,
            _pendingEditProps: this._pendingEditProps
        };
    }
    toJSONInternalRpcAfterFirstUpdate() {
        const base = super.toJSONInternalRpcAfterFirstUpdate();
        return {
            ...base,
            localCommunity: {
                ...base.localCommunity,
                signer: remeda.pick(this.signer, ["publicKey", "address", "shortAddress", "type"])
            }
        };
    }
    toJSONInternalRpcBeforeFirstUpdate() {
        const base = super.toJSONInternalRpcBeforeFirstUpdate();
        return {
            localCommunity: {
                ...base.localCommunity,
                signer: remeda.pick(this.signer, ["publicKey", "address", "shortAddress", "type"])
            }
        };
    }
    async _updateStartedValue() {
        this.started = await this._dbHandler.isCommunityStartLocked(this.address);
    }
    async initNewLocalCommunityPropsNoMerge(newProps) {
        return initNewLocalCommunityPropsNoMerge(this, newProps);
    }
    async initInternalCommunityAfterFirstUpdateNoMerge(newProps) {
        return initInternalCommunityAfterFirstUpdateNoMerge(this, newProps);
    }
    async initInternalCommunityBeforeFirstUpdateNoMerge(newProps) {
        return initInternalCommunityBeforeFirstUpdateNoMerge(this, newProps);
    }
    async initDbHandlerIfNeeded() {
        return initDbHandlerIfNeeded(this);
    }
    async _validateCommunitySizeSchemaAndSignatureBeforePublishing(recordToPublishRaw) {
        const log = Logger("pkc-js:local-community:_validateCommunitySchemaAndSignatureBeforePublishing");
        const stringifiedNewCommunityRecord = deterministicStringify(recordToPublishRaw);
        const calculatedSizeOfNewCommunityRecord = await calculateStringSizeSameAsIpfsAddCidV0(stringifiedNewCommunityRecord);
        // Check if the community record size is less than 1MB
        if (calculatedSizeOfNewCommunityRecord > MAX_FILE_SIZE_BYTES_FOR_COMMUNITY_IPFS) {
            const error = new PKCError("ERR_LOCAL_COMMUNITY_RECORD_TOO_LARGE", {
                calculatedSizeOfNewCommunityRecord,
                maxSize: MAX_FILE_SIZE_BYTES_FOR_COMMUNITY_IPFS,
                recordToPublishRaw,
                address: this.address
            });
            log.error(`Local community (${this.address}) produced a record that is too large (${calculatedSizeOfNewCommunityRecord.toFixed(2)} bytes). Maximum size is ${MAX_FILE_SIZE_BYTES_FOR_COMMUNITY_IPFS} bytes.`, error);
            throw error;
        }
        const parseRes = CommunityIpfsSchema.safeParse(recordToPublishRaw);
        if (!parseRes.success) {
            const error = new PKCError("ERR_LOCAL_COMMUNITY_PRODUCED_INVALID_SCHEMA", {
                invalidRecord: recordToPublishRaw,
                err: parseRes.error
            });
            log.error(`Local community (${this.address}) produced an invalid CommunityIpfs schema`, error);
            throw error;
        }
        const verificationOpts = {
            community: recordToPublishRaw,
            communityIpnsName: this.signer.address,
            resolveAuthorNames: false,
            clientsManager: this._clientsManager,
            validatePages: true,
            cacheIfValid: false
        };
        try {
            const validation = await verifyCommunity(verificationOpts);
            if (!validation.valid) {
                throw new PKCError("ERR_LOCAL_COMMUNITY_PRODUCED_INVALID_SIGNATURE", {
                    validation,
                    verificationOpts
                });
            }
        }
        catch (e) {
            log.error(`Local community (${this.address}) produced an invalid signature`, e);
            throw e;
        }
        verificationOpts.community = JSON.parse(stringifiedNewCommunityRecord); // let's stringify and parse again to make sure we're not using any invalid data
        try {
            const validation = await verifyCommunity(verificationOpts);
            if (!validation.valid) {
                throw new PKCError("ERR_LOCAL_COMMUNITY_PRODUCED_INVALID_SIGNATURE", {
                    validation,
                    verificationOpts
                });
            }
        }
        catch (e) {
            log.error(`Local community (${this.address}) produced an invalid signature after stringifying and parsing again. This is a critical bug.`, e);
            throw e;
        }
        if (this.shouldResolveDomainForVerification()) {
            try {
                log(`Resolving domain ${this.address} to make sure it's the same as signer.address ${this.signer.address}`);
                await this._assertDomainResolvesCorrectly(this.address);
            }
            catch (e) {
                log.error(e);
                this.emit("error", e);
            }
        }
    }
    async handleChallengeRequest(request, isLocalPublisher) {
        return handleChallengeRequestFreeFunction(this, request, isLocalPublisher);
    }
    async handleChallengeAnswer(challengeAnswer) {
        return handleChallengeAnswerFreeFunction(this, challengeAnswer);
    }
    async handleChallengeExchange(pubsubMsg) {
        return handleChallengeExchangeFreeFunction(this, pubsubMsg);
    }
    async _addCommentRowToIPFS(unpinnedCommentRow, log) {
        const ipfsClient = this._clientsManager.getDefaultKuboRpcClient();
        const finalCommentIpfsJson = deriveCommentIpfsFromCommentTableRow(unpinnedCommentRow);
        const commentIpfsContent = deterministicStringify(finalCommentIpfsJson);
        const contentHash = await calculateIpfsHash(commentIpfsContent);
        if (contentHash !== unpinnedCommentRow.cid) {
            throw Error("Unable to recreate the CommentIpfs. This is a critical error");
        }
        const addRes = await retryKuboIpfsAddAndProvide({
            ipfsClient: ipfsClient._client,
            log,
            content: commentIpfsContent,
            addOptions: { pin: true },
            provideOptions: { recursive: true },
            provideInBackground: false
        });
        if (addRes.path !== unpinnedCommentRow.cid)
            throw Error("Unable to recreate the CommentIpfs. This is a critical error");
        log.trace("Pinned comment", unpinnedCommentRow.cid, "of community", this.address, "to IPFS node");
    }
    async _assertDomainResolvesCorrectly(newAddressAsDomain) {
        if (isStringDomain(newAddressAsDomain)) {
            const resolvedIpnsFromNewDomain = await this._clientsManager.resolveCommunityNameIfNeeded({
                communityName: newAddressAsDomain,
                // Admin domain edits don't need second-fresh data.
                cache: { maxAge: 600 }
            });
            if (resolvedIpnsFromNewDomain !== this.signer.address)
                throw new PKCError("ERR_DOMAIN_COMMUNITY_ADDRESS_TXT_RECORD_POINT_TO_DIFFERENT_ADDRESS", {
                    currentCommunityAddress: this.address,
                    newAddressAsDomain,
                    resolvedIpnsFromNewDomain,
                    signerAddress: this.signer.address,
                    started: this.started
                });
        }
    }
    async start() {
        return lifecycleStart(this);
    }
    async update() {
        return lifecycleUpdate(this);
    }
    async stop() {
        return lifecycleStop(this);
    }
    async delete() {
        return deleteCommunity(this);
    }
    async edit(newCommunityOptions) {
        return (await editCommunity(this, newCommunityOptions));
    }
    get exports() {
        return this._exports.map(cloneExportRecord);
    }
    async export(options) {
        return exportCommunityEmbedded(this, options);
    }
    async exportCommunityModLogs(opts) {
        await this.initDbHandlerIfNeeded(); // create the handler if missing (never-started community)
        await this._dbHandler.initDbIfNeeded(); // re-open the connection if a prior stop() closed it
        try {
            return { moderations: this._dbHandler.queryAllCommentModerations(opts) };
        }
        finally {
            // Don't leave a DB connection open on a community that isn't running.
            if (this.state === "stopped")
                this._dbHandler.destoryConnection();
        }
    }
    async _cancelExport(exportId) {
        return cancelExportEmbedded(this, exportId);
    }
    async _deleteExport(exportId) {
        return deleteExportRecord(this, exportId);
    }
    async _loadExportsFromKeyv() {
        return loadAndPruneExportsFromKeyv(this);
    }
    // The three helpers below stay as methods (in addition to being free functions in their
    // respective modules) because integration tests in test/node/community/ monkey-patch
    // community._xxx = async () => { throw ... } to inject failures into the start/publish
    // loops. Production callers in lifecycle.ts/db-state.ts/etc. go through these methods
    // (not the bare imports) so the patches still take effect.
    async _getDbInternalState(includeMutable = false) {
        return getDbInternalState(this, includeMutable);
    }
    async _listenToIncomingRequests() {
        return listenToIncomingRequests(this);
    }
    async _repinCommentsIPFSIfNeeded() {
        return repinCommentsIPFSIfNeeded(this);
    }
    // Kept as a method (in addition to the free function) because the pseudonymityMode
    // integration tests in test/node/community/features/ invoke it directly on the
    // community instance to set up parent comments. Not on master's public API surface,
    // but treated as one by those tests.
    async storePublication(request, pendingApproval) {
        return storePublication(this, request, pendingApproval);
    }
    // Method facades for helpers stubbed/called as methods by garbage.collection and
    // start.community integration tests. Internal production callers in lifecycle.ts
    // and the facade above go through these methods (not the bare imports) so test
    // stubs intercept correctly.
    async updateCommunityIpnsIfNeeded(args) {
        return updateCommunityIpnsIfNeeded(this, args.commentUpdateRowsToPublishToIpfs);
    }
    async _addOldPageCidsToCidsToUnpin(curPages, newPages, addToBlockRm) {
        return addOldPageCidsToCidsToUnpin(this, curPages, newPages, addToBlockRm);
    }
    shouldResolveDomainForVerification() {
        return shouldResolveDomainForVerification(this);
    }
    // Method facades for helpers stubbed by the garbage.collection.community test's
    // mock community (it relies on these being instance methods for vi.fn() overrides).
    // Production callers in ipns-publishing.ts/lifecycle.ts/editing.ts go through the
    // methods so the stubs intercept.
    async _calculateNewPostUpdates() {
        return calculateNewPostUpdates(this);
    }
    _calculateLatestUpdateTrigger() {
        return calculateLatestUpdateTrigger(this);
    }
    async _resolveIpnsAndLogIfPotentialProblematicSequence() {
        return resolveIpnsAndLogIfPotentialProblematicSequence(this);
    }
    async _updateDbInternalState(props) {
        return updateDbInternalState(this, props);
    }
    // Method facades for additional private helpers that integration tests
    // (unique.publishing, purge.expire.rejection, edgecases.page.generation)
    // invoke directly on the instance via `(ctx.community as LocalCommunity)._foo(...)`.
    async _updateCommentsThatNeedToBeUpdated() {
        return updateCommentsThatNeedToBeUpdated(this);
    }
    async _purgeDisapprovedCommentsOlderThan() {
        return purgeDisapprovedCommentsOlderThan(this);
    }
    async _publishChallengeVerification(challengeResult, request) {
        return publishChallengeVerification(this, challengeResult, request);
    }
    async _publishIdempotentDuplicateVerification(...args) {
        return publishIdempotentDuplicateVerification(this, ...args);
    }
    async _checkPublicationValidity(...args) {
        return checkPublicationValidity(this, ...args);
    }
    _calculateLocalMfsPathForCommentUpdate(...args) {
        return calculateLocalMfsPathForCommentUpdate(this, ...args);
    }
}
//# sourceMappingURL=local-community.js.map