"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.Summarizer = exports.createSummarizingWarning = exports.SummarizingWarning = void 0;
const client_utils_1 = require("@fluid-internal/client-utils");
const internal_1 = require("@fluidframework/core-utils/internal");
const internal_2 = require("@fluidframework/telemetry-utils/internal");
const runningSummarizer_js_1 = require("./runningSummarizer.js");
const summarizerHeuristics_js_1 = require("./summarizerHeuristics.js");
const summaryGenerator_js_1 = require("./summaryGenerator.js");
const summarizingError = "summarizingError";
class SummarizingWarning extends internal_2.LoggingError {
    constructor(errorMessage, logged = false) {
        super(errorMessage);
        this.logged = logged;
        this.errorType = summarizingError;
        this.canRetry = true;
    }
    static wrap(error, logged = false, logger) {
        const newErrorFn = (errMsg) => new SummarizingWarning(errMsg, logged);
        return (0, internal_2.wrapErrorAndLog)(error, newErrorFn, logger);
    }
}
exports.SummarizingWarning = SummarizingWarning;
const createSummarizingWarning = (errorMessage, logged) => new SummarizingWarning(errorMessage, logged);
exports.createSummarizingWarning = createSummarizingWarning;
/**
 * Summarizer is responsible for coordinating when to generate and send summaries.
 * It is the main entry point for summary work.
 * It is created only by summarizing container (i.e. one with clientType === "summarizer")
 * @alpha
 */
class Summarizer extends client_utils_1.TypedEventEmitter {
    get ISummarizer() {
        return this;
    }
    constructor(
    /** Reference to runtime that created this object.
     * i.e. runtime with clientType === "summarizer"
     */
    runtime, configurationGetter, 
    /** Represents an object that can generate summary.
     * In practical terms, it's same runtime (this.runtime) with clientType === "summarizer".
     */
    internalsProvider, handleContext, summaryCollection, runCoordinatorCreateFn) {
        super();
        this.runtime = runtime;
        this.configurationGetter = configurationGetter;
        this.internalsProvider = internalsProvider;
        this.summaryCollection = summaryCollection;
        this.runCoordinatorCreateFn = runCoordinatorCreateFn;
        this._disposed = false;
        this.starting = false;
        this.stopDeferred = new internal_1.Deferred();
        this.handleSummarizeEvent = (eventProps) => {
            this.emit("summarize", eventProps);
        };
        this.logger = (0, internal_2.createChildLogger)({
            logger: this.runtime.baseLogger,
            namespace: "Summarizer",
        });
    }
    async run(onBehalfOf) {
        try {
            return await this.runCore(onBehalfOf);
        }
        catch (error) {
            this.stop("summarizerException");
            throw SummarizingWarning.wrap(error, false /* logged */, this.logger);
        }
        finally {
            this.close();
        }
    }
    /**
     * Stops the summarizer from running.  This will complete
     * the run promise, and also close the container.
     * @param reason - reason code for stopping
     */
    stop(reason) {
        this.stopDeferred.resolve(reason);
    }
    close() {
        // This will result in "summarizerClientDisconnected" stop reason recorded in telemetry,
        // unless stop() was called earlier
        this.dispose();
        this.runtime.disposeFn();
    }
    async runCore(onBehalfOf) {
        const runCoordinator = await this.runCoordinatorCreateFn(this.runtime);
        // Wait for either external signal to cancel, or loss of connectivity.
        const stopP = Promise.race([runCoordinator.waitCancelled, this.stopDeferred.promise]);
        void stopP.then((reason) => {
            this.logger.sendTelemetryEvent({
                eventName: "StoppingSummarizer",
                onBehalfOf,
                reason,
            });
        });
        if (runCoordinator.cancelled) {
            return runCoordinator.waitCancelled;
        }
        const runningSummarizer = await this.start(onBehalfOf, runCoordinator);
        // Wait for either external signal to cancel, or loss of connectivity.
        const stopReason = await stopP;
        // There are two possible approaches here:
        // 1. Propagate cancellation from this.stopDeferred to runCoordinator. This will ensure that we move to the exit
        //    faster, including breaking out of the RunningSummarizer.trySummarize() faster.
        //    We could create new coordinator and pass it to waitStop() -> trySummarizeOnce("lastSummary") flow.
        //    The con of this approach is that we might cancel active summary, and lastSummary will fail because it
        //    did not wait for ack/nack from previous summary. Plus we disregard any 429 kind of info from service
        //    that way (i.e. trySummarize() loop might have been waiting for 5 min because storage told us so).
        //    In general, it's more wasted resources.
        // 2. We can not do it and make waitStop() do last summary only if there was no active summary. This ensures
        //    that client behaves properly (from server POV) and we do not waste resources. But, it may mean we wait
        //    substantially longer for trySummarize() retries to play out and thus this summary loop may run into
        //    conflict with new summarizer client starting on different client.
        // As of now, #2 is implemented. It's more forward looking, as issue #7279 suggests changing design for new
        // summarizer client to not be created until current summarizer fully moves to exit, and that would reduce
        // cons of #2 substantially.
        // Cleanup after running
        await runningSummarizer.waitStop(!runCoordinator.cancelled && Summarizer.stopReasonCanRunLastSummary(stopReason));
        // Propagate reason and ensure that if someone is waiting for cancellation token, they are moving to exit
        runCoordinator.stop(stopReason);
        return stopReason;
    }
    /**
     * Should we try to run a last summary for the given stop reason?
     * Currently only allows "parentNotConnected"
     * @param stopReason - SummarizerStopReason
     * @returns `true` if the stop reason can run a last summary, otherwise `false`.
     */
    static stopReasonCanRunLastSummary(stopReason) {
        return stopReason === "parentNotConnected";
    }
    /**
     * Put the summarizer in a started state, including creating and initializing the RunningSummarizer.
     * The start request can come either from the SummaryManager (in the auto-summarize case) or from the user
     * (in the on-demand case).
     * @param onBehalfOf - ID of the client that requested that the summarizer start
     * @param runCoordinator - cancellation token
     * @param newConfig - Summary configuration to override the existing config when invoking the RunningSummarizer.
     * @returns A promise that is fulfilled when the RunningSummarizer is ready.
     */
    async start(onBehalfOf, runCoordinator) {
        if (this.runningSummarizer) {
            if (this.runningSummarizer.disposed) {
                throw new internal_2.UsageError("Starting a disposed summarizer");
            }
            return this.runningSummarizer;
        }
        if (this.starting) {
            throw new internal_2.UsageError("Attempting to start a summarizer that is already starting");
        }
        this.starting = true;
        // Initialize values and first ack (time is not exact)
        this.logger.sendTelemetryEvent({
            eventName: "RunningSummarizer",
            onBehalfOf,
            initSummarySeqNumber: this.runtime.deltaManager.initialSequenceNumber,
            config: JSON.stringify(this.configurationGetter()),
        });
        // Summarizing container ID (with clientType === "summarizer")
        const clientId = this.runtime.clientId;
        if (clientId === undefined) {
            throw new internal_2.UsageError("clientId should be defined if connected.");
        }
        this._heuristicData = new summarizerHeuristics_js_1.SummarizeHeuristicData(this.runtime.deltaManager.lastSequenceNumber, {
            /** summary attempt baseline for heuristics */
            refSequenceNumber: this.runtime.deltaManager.initialSequenceNumber,
            summaryTime: Date.now(),
        });
        const runningSummarizer = await runningSummarizer_js_1.RunningSummarizer.start(this.logger, this.summaryCollection.createWatcher(clientId), this.configurationGetter(), async (...args) => this.internalsProvider.submitSummary(...args), // submitSummaryCallback
        async (...args) => this.internalsProvider.refreshLatestSummaryAck(...args), // refreshLatestSummaryAckCallback
        this._heuristicData, this.summaryCollection, runCoordinator /* cancellationToken */, (reason) => runCoordinator.stop(reason) /* stopSummarizerCallback */, this.runtime);
        this.runningSummarizer = runningSummarizer;
        this.runningSummarizer.on("summarize", this.handleSummarizeEvent);
        this.starting = false;
        return runningSummarizer;
    }
    /**
     * Disposes of resources after running.  This cleanup will
     * clear any outstanding timers and reset some of the state
     * properties.
     * Called by ContainerRuntime when it is disposed, as well as at the end the run().
     */
    dispose() {
        // Given that the call can come from own ContainerRuntime, ensure that we stop all the processes.
        this.stop("summarizerClientDisconnected");
        this._disposed = true;
        if (this.runningSummarizer) {
            this.runningSummarizer.off("summarize", this.handleSummarizeEvent);
            this.runningSummarizer.dispose();
            this.runningSummarizer = undefined;
        }
    }
    summarizeOnDemand(options) {
        try {
            if (this._disposed || this.runningSummarizer?.disposed) {
                throw new internal_2.UsageError("Summarizer is already disposed.");
            }
            if (this.runtime.summarizerClientId !== undefined &&
                this.runtime.summarizerClientId !== this.runtime.clientId) {
                // If there is an elected summarizer, and it's not this one, don't allow on-demand summary.
                // This is to prevent the on-demand summary and heuristic-based summary from stepping on
                // each other.
                throw new internal_2.UsageError("On-demand summary attempted while an elected summarizer is present");
            }
            const builder = new summaryGenerator_js_1.SummarizeResultBuilder();
            if (this.runningSummarizer) {
                // Summarizer is already running. Go ahead and start.
                return this.runningSummarizer.summarizeOnDemand(options, builder);
            }
            // Summarizer isn't running, so we need to start it, which is an async operation.
            // Manage the promise related to creating the cancellation token here.
            // The promises related to starting, summarizing,
            // and submitting are communicated to the caller through the results builder.
            const coordinatorCreateP = this.runCoordinatorCreateFn(this.runtime);
            coordinatorCreateP
                .then((runCoordinator) => {
                // Successfully created the cancellation token. Start the summarizer.
                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                const startP = this.start(this.runtime.clientId, runCoordinator);
                startP
                    .then(async (runningSummarizer) => {
                    // Successfully started the summarizer. Run it.
                    runningSummarizer.summarizeOnDemand(options, builder);
                    // Wait for a command to stop or loss of connectivity before tearing down the summarizer and client.
                    const stopReason = await Promise.race([
                        this.stopDeferred.promise,
                        runCoordinator.waitCancelled,
                    ]);
                    await runningSummarizer.waitStop(false);
                    runCoordinator.stop(stopReason);
                    this.close();
                })
                    .catch((reason) => {
                    builder.fail("Failed to start summarizer", reason);
                });
            })
                .catch((reason) => {
                builder.fail("Failed to create cancellation token", reason);
            });
            return builder.build();
        }
        catch (error) {
            throw SummarizingWarning.wrap(error, false /* logged */, this.logger);
        }
    }
    enqueueSummarize(options) {
        if (this._disposed ||
            this.runningSummarizer === undefined ||
            this.runningSummarizer.disposed) {
            throw new internal_2.UsageError("Summarizer is not running or already disposed.");
        }
        return this.runningSummarizer.enqueueSummarize(options);
    }
    recordSummaryAttempt(summaryRefSeqNum) {
        this._heuristicData?.recordAttempt(summaryRefSeqNum);
    }
}
exports.Summarizer = Summarizer;
//# sourceMappingURL=summarizer.js.map