"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.UserContextManager = exports.UserContextLoader = void 0;
const tslib_1 = require("tslib");
const path = require("path");
const fs = require("fs");
const _ = require("lodash");
const codiac_relay_contracts_1 = require("@codiac.io/codiac-relay-contracts");
const codiac_ops_common_1 = require("@codiac.io/codiac-ops-common");
const codiac_constants_1 = require("./codiac-constants");
const init_1 = require("./commands/shell/init");
/** Simply deserializes the currently cached user context from its config file.
 * This class exists so that the load methods can be called before an api client gets instantiated. */
class UserContextLoader {
    constructor(cliEnviro, logger) {
        this.cliEnviro = cliEnviro;
        this.logger = logger;
    }
    /** Returns the path to the session file for the current shell.
     * Uses CODIAC_SESSION_ID (set via `eval "$(cod shell init)"`) when available,
     * falling back to process.ppid for users who have not yet configured shell integration. */
    getSessionFilePath() {
        var _a;
        const sessionsDir = path.join(this.cliEnviro.localProgramFolder, codiac_constants_1.CliConstants.SESSIONS_FOLDER_NAME);
        const sessionKey = (_a = process.env[init_1.CODIAC_SESSION_ID_VAR]) !== null && _a !== void 0 ? _a : `ppid-${process.ppid}`;
        return path.join(sessionsDir, `${sessionKey}.json`);
    }
    /** (context is Machine-User scoped)
   *  Attempts to load the user and select from their tenant tokens and enterprise contexts persisted locally by the CLI.
   *  Returns undefined for not found and throws any encountered exceptions.
   *
   * The full user context will STILL be returned no matter what,
   * whether or not there was a matching tenant profile.
   * But if the context has no matching tenant profile,
   * the CURRENT tenantCode, token, and enterprise will be undefined (ie: a tenantless user, a principal user).
   *
   * This way, if the cache is stale, the current user can still authenticate to refresh it.
   * eg: if the user was just seconds ago added to a tenant,
   * this pattern can pick up the new profile from the remote auth service without requiring a manual login.
   *
   * @param tenantCode Enforces the given tenantCode. When set, current context will be ignored if it does not match it.
   * */
    loadUserContext(tenantCode) {
        var _a;
        let context;
        let source;
        // 1. Retrieve and deserialize (priority: session file → global file)
        const sessionCntx = this.loadUserContext_fromSessionFile();
        if (sessionCntx) {
            context = sessionCntx;
            const sessionKey = (_a = process.env[init_1.CODIAC_SESSION_ID_VAR]) !== null && _a !== void 0 ? _a : `ppid-${process.ppid}`;
            source = `session file (${sessionKey})`;
        }
        else {
            context = this.loadUserContext_fromFile(tenantCode);
            source = "global file";
        }
        // Non-blocking stale session cleanup — runs in background, never blocks the command.
        this.cleanStaleSessions().catch(() => { });
        if (context) {
            // 2. Switch to requested tenant code (if given)
            if (tenantCode != undefined && !_.isEmpty(tenantCode)) {
                if (context.currentTenantCode !== tenantCode) {
                    this.trySetCurrentProfileFor(tenantCode, context);
                }
            }
            this.logger.trace(`Current user context (from ${source} cache).  User: ${context.userName}, TenantCode: ${context.currentTenantCode}`);
        }
        return context;
    }
    /** (context is Machine-User scoped)
     *  Attempts to load the user and select from their tenant tokens and enterprise contexts persisted locally by the CLI.
     *  Returns undefined for not found and throws any encountered exceptions.
     *
     * The full user context will STILL be returned no matter what,
     * whether or not there was a matching tenant profile.
     * But if the context has no matching tenant profile,
     * the CURRENT tenantCode, token, and enterprise will be undefined (ie: a tenantless user, a principal user).
     *
     * This way, if the cache is stale, the current user can still authenticate to refresh it.
     * eg: if the user was just seconds ago added to a tenant,
     * this pattern can pick up the new profile from the remote auth service without requiring a manual login.
     *
     * @param tenantCode Enforces the given tenantCode. When set, current context will be ignored if it does not match it.
     * */
    loadUserContext_fromFile(tenantCode) {
        // let output: UserContext | undefined;
        // let homeDir = "~/"; // path.posix.resolve("~/");  // NOTE:  This tilde (~) may not be valid on a windows box.
        // let pathName = path.posix.join(homeDir, OpsConstants.CLI_CONFIG_FOLDER_NAME, CliConstants.USER_CONTEXT_FILENAME);
        let pathName = path.posix.join(this.cliEnviro.localProgramFolder, codiac_constants_1.CliConstants.USER_CONTEXT_FILENAME);
        this.logger.trace(`Reading ${pathName}...`);
        let machineUserContext = codiac_ops_common_1.JsonUtils.deserializeJsonFile(pathName);
        // if (machineUserContext) {
        //   if (tenantCode === undefined) {
        //     // output = machineUserContext;
        //   } else {  // ENFORCE the given tenantCode
        //     if (machineUserContext.currentTenantCode === tenantCode) {
        //       // output = machineUserContext;
        //     } else {
        //       this.trySetCurrentProfileFor(tenantCode, machineUserContext);
        //       // output = machineUserContext;
        //     }
        //   }
        //   this.logger.trace(`Current user context (from cache).  User: ${machineUserContext.userName}, TenantCode: ${machineUserContext.currentTenantCode}`)
        // }
        return machineUserContext; // output;
    }
    /** Deserializes the user context from the session file for the current shell.
     * Returns undefined if no session file exists for this shell instance. */
    loadUserContext_fromSessionFile() {
        const sessionPath = this.getSessionFilePath();
        this.logger.trace(`Reading session file ${sessionPath}...`);
        return codiac_ops_common_1.JsonUtils.deserializeJsonFile(sessionPath);
    }
    /** Removes ppid-fallback session files whose parent shell process is no longer alive.
     * UUID-keyed session files (from shell integration) are left in place — they have no
     * liveness signal and are typically small enough to be harmless.
     * Runs best-effort; never throws. */
    cleanStaleSessions() {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const sessionsDir = path.join(this.cliEnviro.localProgramFolder, codiac_constants_1.CliConstants.SESSIONS_FOLDER_NAME);
            if (!fs.existsSync(sessionsDir))
                return;
            const entries = fs.readdirSync(sessionsDir);
            for (const entry of entries) {
                const match = entry.match(/^ppid-(\d+)\.json$/);
                if (!match)
                    continue; // skip UUID-keyed files
                const pid = parseInt(match[1], 10);
                if (!isProcessAlive(pid)) {
                    const filePath = path.join(sessionsDir, entry);
                    fs.rm(filePath, { force: true }, () => {
                        this.logger.trace(`Removed stale ppid-fallback session file for PID ${pid}.`);
                    });
                }
            }
        });
    }
    /** Mutates the given object's current tenant properties
     * (currentToken, currentEnterprise, currentTenantCode, tokenExpiration)
     * to those of the current tenant code,
     * by either allowing the current state,
     * or by applying any relevant tenant profile that exists onboard the given context object.
     *
     * If no matching profile exists, "current" properties will be set to undefined (like a principal user),
     * but the context itself WILL STILL BE RETURNED.
    */
    trySetCurrentProfileFor(tenantCode, userContext) {
        userContext.currentEnterprise = userContext.enterpriseContexts[tenantCode];
        userContext.currentToken = userContext.tokens[tenantCode];
        // These expirations really need to be stored in the tokens collection
        // machineUserContext.tokenExpiration = (machineUserContext.currentToken) ? parseTokenExpiration(machineUserContext.currentToken) : undefined;
        userContext.tokenExpiration = undefined;
        // Only set the tenant code if they have a profile for it
        userContext.currentTenantCode = undefined;
        if ((userContext.currentToken !== undefined && userContext.currentToken !== "")
            || (userContext.currentEnterprise !== undefined)) {
            userContext.currentTenantCode = tenantCode;
        }
    }
    /** Obliterates the current user state by deleting the context file. */
    clearUserContext() {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            return new Promise((resolve, reject) => {
                try {
                    // ---------------------------------------
                    // 1. Session file (this console instance)
                    // ---------------------------------------
                    const sessionPath = path.normalize(this.getSessionFilePath());
                    if (fs.existsSync(sessionPath)) {
                        fs.rm(sessionPath, { force: true }, () => {
                            this.logger.debug(`Session context file deleted [${sessionPath}].`);
                        });
                    }
                    // ---------------------------------------
                    // 3. Global file
                    // ---------------------------------------
                    let globalPath = path.posix.join(this.cliEnviro.localProgramFolder, codiac_constants_1.CliConstants.USER_CONTEXT_FILENAME);
                    globalPath = path.normalize(globalPath);
                    if (fs.existsSync(globalPath)) {
                        fs.rm(globalPath, { force: true }, () => {
                            this.logger.debug(`User context file deleted [${globalPath}].`);
                            resolve();
                        });
                    }
                    else {
                        this.logger.debug(`Nothing to do: user context file does not exist at [${globalPath}]`);
                        resolve();
                    }
                }
                catch (err) {
                    reject(err);
                }
            });
        });
    }
}
exports.UserContextLoader = UserContextLoader;
/** Provides cruds for the user context, persisting to the local context config file. */
class UserContextManager {
    constructor(cliEnviro, authClient, logger) {
        this.cliEnviro = cliEnviro;
        this.authClient = authClient;
        this.logger = logger;
        this.loader = new UserContextLoader(this.cliEnviro, this.logger);
    }
    /** Extends the user token so we can get the user's tenant profiles.
     *  If the token is expired, or something has changed, it fails.
     *  Throws any exceptions.
     */
    refreshUserContext(currentContext) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            return new Promise((resolve, reject) => tslib_1.__awaiter(this, void 0, void 0, function* () {
                try {
                    let result = {};
                    if (!currentContext)
                        currentContext = new codiac_relay_contracts_1.UserContext();
                    if (currentContext.currentToken) {
                        let refreshResult = yield this.authClient.Auth.TokenRefresh({});
                        if (refreshResult.message)
                            this.logger.debug(`Token refresh: ${refreshResult.message}`);
                        if (refreshResult.output) {
                            result.user = refreshResult.output; // We're not getting the user.profiles here
                        }
                        if (refreshResult.success && refreshResult.output) {
                            currentContext.currentToken = this.authClient.authToken;
                            if (currentContext.currentToken)
                                currentContext.tokenExpiration = this.authClient.extractTokenExpiration(currentContext.currentToken);
                            this.writeUserContext(currentContext);
                            result.context = currentContext;
                        }
                    }
                    resolve(result);
                }
                catch (err) {
                    reject(err);
                }
            }));
        });
    }
    /** Writes the given UserContext to the Toyhauler user config folder on the local machine.
     * TODO:  Consider writing this to the cache folder instead of the config folder.
     * Throws any encountered exceptions. */
    writeUserContext(userContext) {
        // ---------------------------------------
        // 1. Session file (isolated per console instance, keyed by parent shell PID)
        // ---------------------------------------
        const sessionPath = this.loader.getSessionFilePath();
        const sessionsDir = path.dirname(sessionPath);
        if (!fs.existsSync(sessionsDir)) {
            fs.mkdirSync(sessionsDir, { recursive: true });
        }
        this.logger.debug(`Writing UserContext to session file ${sessionPath}`);
        codiac_ops_common_1.JsonUtils.serializeJsonFile(userContext, sessionPath);
        // ---------------------------------------
        // 3. Global file (backward-compat fallback for first-run / tooling that reads it directly)
        // ---------------------------------------
        let globalPath = path.join(this.cliEnviro.localProgramFolder, codiac_constants_1.CliConstants.USER_CONTEXT_FILENAME);
        this.logger.debug(`Writing UserContext to global file ${globalPath}`);
        codiac_ops_common_1.JsonUtils.serializeJsonFile(userContext, globalPath);
    }
    /** Persists the current user, its token, and its tenantCode to the user context
     *  and the userContext file stored on the local machine. */
    saveToUserContext(currentContext, userName, token, tenantCode, tokenExpiration, roles) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            return new Promise((resolve, reject) => tslib_1.__awaiter(this, void 0, void 0, function* () {
                try {
                    // if (userName) {
                    if (!currentContext) { // wanna load it here?  this._cntx = await this.loadUserContext(user.tenantCode);
                        currentContext = new codiac_relay_contracts_1.UserContext(userName, tenantCode, token, tokenExpiration, undefined, roles);
                    }
                    else {
                        currentContext.userName = userName;
                        currentContext.currentTenantCode = tenantCode;
                        currentContext.currentToken = token;
                        currentContext.tokenExpiration = tokenExpiration;
                        currentContext.roles = roles;
                        if (tenantCode)
                            currentContext.tokens[tenantCode] = token;
                    }
                    yield this.writeUserContext(currentContext);
                    // } else {
                    //   // null input - do nothing.
                    //   TODO:  Consider either throwing an error here OR changing signature to return an undefined.
                    // }
                    resolve(currentContext);
                }
                catch (err) {
                    reject(err);
                }
            }));
        });
    }
}
exports.UserContextManager = UserContextManager;
/** Returns true if the given PID corresponds to a running process, false otherwise.
 * Uses signal 0 (no-op) to probe existence without affecting the process. */
function isProcessAlive(pid) {
    try {
        process.kill(pid, 0);
        return true;
    }
    catch (_a) {
        return false;
    }
}
//# sourceMappingURL=user-context-loader.js.map