// Copyright (c) 2009-2026 SAP SE, All Rights Reserved

/**
 * @file
 *
 * As a general design principle, the amount of code used to set-up and perform logging is regarded as potentially expensive,
 * and it's a bad idea to run this code when not needed (e.g. when debugging is not enabled).
 *
 * To avoid unnecessary computation, StagedLogger supports callback functions for the detail parameter.
 * These callbacks are only executed when the log level is DEBUG or higher, ensuring that expensive operations
 * (like formatting, mapping, or data transformations) are skipped entirely when debug logging is disabled.
 *
 * Usage:
 * const oLogger = new StagedLogger({
 *     title: "Grow a tree",
 *     moduleName: "sap.ushell.services.ClientSideTargetResolution",
 *     stages: [
 *         "STAGE1: Buy seeds",
 *         "STAGE2: Plant seeds",
 *         "STAGE3: Water",
 *         "STAGE4: Wait"
 *     ]
 * });
 *
 * const oStage1 = oLogger.getStage(1);
 *
 * // Simple string logging (no details)
 * oStage1.log("Going to the shop");
 * oStage1.log("Buying seeds", "-");
 *
 * // Logging with detail lines as callback function
 * oStage1.logLines(
 *     "Seeds purchased successfully",
 *     () => inventory.getSeeds().map(seed => `${seed.name}: ${seed.quantity}`),
 *     "✓"
 * );
 *
 * // End the logging session and output the log
 * oLogger.end();
 *
 * @version 1.148.1
 */
sap.ui.define([
    "sap/base/Log",
    "sap/ushell/utils/StagedLogger/Stage"
], (
    Log,
    Stage
) => {
    "use strict";

    /**
     * @alias sap.ushell.utils.StagedLogger
     * @class
     * @classdesc A logger for multi-staged operations.
     *
     * @since 1.145.0
     * @private
     */
    class StagedLogger {
        static #iNextReportCount = 1;
        // For testing purposes - track active instances
        static #activeInstances = new Set();

        #vTitle = null;
        #sModuleName = null;
        #aStages = [];

        #bRunning;

        #iReportId = null;

        /**
         * @param {object} oConfig Configuration options
         * @param {string|function} oConfig.title A title for the log operation or a callback function that returns it
         * @param {string} oConfig.moduleName The component/module name to be used when logging to the console
         * @param {string[]} oConfig.stages An array of stage names
         *
         * @since 1.145.0
         * @private
         */
        constructor (oConfig) {
            this.#bRunning = true;

            this.#iReportId = StagedLogger.#iNextReportCount++;
            this.#vTitle = typeof oConfig.title === "function" ? oConfig.title : () => oConfig.title;
            this.#sModuleName = oConfig.moduleName;
            this.#aStages = oConfig.stages.map((sStageName) => {
                return [
                    sStageName,
                    "-".repeat(sStageName.length)
                ];
            });
            if (Log.getLevel() >= Log.Level.DEBUG) {
                StagedLogger.#activeInstances.add(this);
            }
        }

        /**
         * Get a stage object for logging
         * @param {int} iStageIndex The stage index (1-based)
         * @returns {sap.ushell.utils.StagedLogger.Stage} Stage object with log methods
         *
         * @since 1.145.0
         * @private
         */
        getStage (iStageIndex) {
            if (!this.#aStages[iStageIndex - 1]) {
                throw new Error(`Stage ${iStageIndex} does not exist`);
            }

            return new Stage(
                this._logToStage.bind(this, iStageIndex),
                this.end.bind(this)
            );
        }

        /**
         * Stores one or more lines of log in memory (no console output).
         *
         * @param {int} iStage The stage number (1-based index).
         * @param {string} [sMessage] A single line message to log.
         * @param {string[]|function():string[]} [vDetails] Multiple detail lines to log or a callback function that returns them.
         * @param {string} [sPrefix] Optional prefix for the lines.
         * @param {boolean} [bNumberLines] If true, numbers the lines (1-, 2-, etc.).
         *
         * @throws {Error} If called on an ended logger
         *
         * @since 1.145.0
         * @private
         */
        _logToStage (iStage, sMessage, vDetails, sPrefix, bNumberLines) {
            if (Log.getLevel() < Log.Level.DEBUG) {
                return;
            }
            if (!this.#bRunning) {
                throw new Error("Logger .log was called on an ended logger");
            }

            const aDetails = typeof vDetails === "function" ? vDetails() : vDetails;

            // prefix must be added to the single line only if there are no multiple lines
            if (!aDetails && sMessage && sPrefix) {
                sMessage = `${sPrefix} ${sMessage}`;
            }

            const aLogStage = this.#aStages[iStage - 1];

            if (sMessage) {
                aLogStage.push(sMessage);
            }

            if (aDetails) {
                if (sPrefix) {
                    // indent lines if a single line (the heading) was given
                    sPrefix = (sMessage ? " " : "") + sPrefix;
                }

                const aLinesWithPrefix = aDetails.map((sLine, iIdx) => {
                    return (bNumberLines ? (iIdx + 1) : "") + (sPrefix ? `${sPrefix} ` : "") + sLine;
                });
                aLogStage.push.apply(aLogStage, aLinesWithPrefix);
            }
        }

        /**
         * Ends the staged logging session and outputs all accumulated log stages to the debug log.
         * Only logs if the current log level is DEBUG or higher.
         * Can be called multiple times safely (subsequent calls are no-ops).
         *
         * @since 1.145.0
         * @private
         */
        end () {
            if (!this.#bRunning) {
                throw new Error("Logger .end was called on an already ended logger");
            }

            if (Log.getLevel() >= Log.Level.DEBUG) {
                const sLogString = this.#aStages.map((aStage) => {
                    return aStage.join("\n");
                }).join("\n\n");

                const sTitle = typeof this.#vTitle === "function" ? this.#vTitle() : this.#vTitle;

                Log.debug(
                    `[REPORT #${this.#iReportId}] ${sTitle}`, // avoid timestamp on the same line
                    `\n${sLogString // avoid overlap between title and first log line
                    }\n`, // avoid overlap between last line and module name
                    this.#sModuleName
                );
            }

            this.#bRunning = false;
            StagedLogger.#activeInstances.delete(this);
        }

        /**
         * Returns the set of active StagedLogger instances.
         * For testing purposes only.
         *
         * @returns {Set<sap.ushell.utils.StagedLogger>} The active instances.
         *
         * @since 1.145.0
         * @private
         */
        static getActiveInstances () {
            return StagedLogger.#activeInstances;
        }

        /**
         * Resets the report counter.
         * For testing purposes only.
         *
         * @since 1.145.0
         * @private
         */
        static resetReportCount () {
            StagedLogger.#iNextReportCount = 1;
        }
    }

    return StagedLogger;
});
