/*
 * Copyright (c) 2023-2025 Datalayer, Inc.
 * Distributed under the terms of the Modified BSD License.
 */
/**
 * Base Client class providing core configuration and token management.
 * @module client/base
 */
import { DEFAULT_SERVICE_URLS } from '../api/constants.js';
import { AuthenticationManager } from './auth/index.js';
/** Base Client class providing core configuration and token management. */
export class DatalayerClientBase {
    /** URL for IAM service */
    iamRunUrl;
    /** URL for Runtimes service */
    runtimesRunUrl;
    /** URL for Spacer service */
    spacerRunUrl;
    /** Authentication token */
    token;
    /** Environments */
    environments = [];
    /** Method lifecycle handlers */
    handlers;
    /** Authentication manager */
    auth;
    /**
     * Create a DatalayerClient base instance.
     * @param config - Client configuration options
     */
    constructor(config) {
        this.iamRunUrl = config.iamRunUrl || DEFAULT_SERVICE_URLS.IAM;
        this.runtimesRunUrl =
            config.runtimesRunUrl || DEFAULT_SERVICE_URLS.RUNTIMES;
        this.spacerRunUrl = config.spacerRunUrl || DEFAULT_SERVICE_URLS.SPACER;
        this.token = config.token;
        this.handlers = config.handlers;
        // Initialize authentication manager with custom storage if provided
        this.auth = new AuthenticationManager(this.iamRunUrl, config.storage);
        // If a token was provided, store it in the auth manager
        if (this.token) {
            this.auth.storeToken(this.token);
        }
    }
    /**
     * Get the current configuration including service URLs and token.
     * @returns Current configuration
     */
    getConfig() {
        return {
            iamRunUrl: this.iamRunUrl,
            runtimesRunUrl: this.runtimesRunUrl,
            spacerRunUrl: this.spacerRunUrl,
            token: this.token,
        };
    }
    /** Get the IAM service URL. */
    getIamRunUrl() {
        return this.iamRunUrl;
    }
    /** Get the Runtimes service URL. */
    getRuntimesRunUrl() {
        return this.runtimesRunUrl;
    }
    /** Get the Spacer service URL. */
    getSpacerRunUrl() {
        return this.spacerRunUrl;
    }
    /** Get the current authentication token. */
    getToken() {
        return this.token;
    }
    /**
     * set the authentication token for all API requests.
     * @param token - New authentication token
     */
    async setToken(token) {
        this.token = token;
    }
    /**
     * Wrap all Client methods with handlers for cross-cutting concerns.
     * Called automatically by the DatalayerClient constructor.
     *
     * @internal
     */
    wrapAllMethods() {
        if (!this.handlers) {
            return; // No handlers configured, nothing to wrap
        }
        // Get all method names from the prototype chain
        const methodNames = this.getAllMethodNames();
        // Wrap each method with handlers
        methodNames.forEach(methodName => {
            const original = this[methodName];
            if (typeof original !== 'function') {
                return;
            }
            // Detect if the original method is async by checking if it's an AsyncFunction
            const isAsync = original.constructor.name === 'AsyncFunction';
            if (isAsync) {
                // Create async wrapped version for originally async methods
                this[methodName] = async (...args) => {
                    // Call beforeCall handler if defined
                    if (this.handlers?.beforeCall) {
                        await Promise.resolve(this.handlers.beforeCall(methodName, args));
                    }
                    try {
                        // Call the original async method
                        const result = await original.apply(this, args);
                        // Call afterCall handler if defined
                        if (this.handlers?.afterCall) {
                            await Promise.resolve(this.handlers.afterCall(methodName, result));
                        }
                        return result;
                    }
                    catch (error) {
                        // Call onError handler if defined
                        if (this.handlers?.onError) {
                            await Promise.resolve(this.handlers.onError(methodName, error));
                        }
                        throw error;
                    }
                };
            }
            else {
                // Create sync wrapped version for originally sync methods
                this[methodName] = (...args) => {
                    // Call beforeCall handler if defined (sync version)
                    if (this.handlers?.beforeCall) {
                        const beforeResult = this.handlers.beforeCall(methodName, args);
                        // If beforeCall returns a Promise, we can't await it in sync context
                        if (beforeResult instanceof Promise) {
                            // Promise ignored in sync context
                        }
                    }
                    try {
                        // Call the original sync method
                        const result = original.apply(this, args);
                        // Call afterCall handler if defined (sync version)
                        if (this.handlers?.afterCall) {
                            const afterResult = this.handlers.afterCall(methodName, result);
                            if (afterResult instanceof Promise) {
                                // Promise ignored in sync context
                            }
                        }
                        return result;
                    }
                    catch (error) {
                        // Call onError handler if defined (sync version)
                        if (this.handlers?.onError) {
                            const errorResult = this.handlers.onError(methodName, error);
                            if (errorResult instanceof Promise) {
                                // Promise ignored in sync context
                            }
                        }
                        throw error;
                    }
                };
            }
        });
    }
    /**
     * Get all method names from mixins only.
     * @returns Array of mixin method names to wrap
     * @internal
     */
    getAllMethodNames() {
        const methodNames = new Set();
        // First, collect all base class methods to exclude
        const baseClassMethods = new Set();
        const basePrototype = DatalayerClientBase.prototype;
        Object.getOwnPropertyNames(basePrototype).forEach(name => {
            baseClassMethods.add(name);
        });
        // Also exclude methods from the concrete Client class itself
        const sdkPrototype = Object.getPrototypeOf(this).constructor.prototype;
        Object.getOwnPropertyNames(sdkPrototype).forEach(name => {
            baseClassMethods.add(name);
        });
        // Now walk the prototype chain and only include mixin methods
        let obj = Object.getPrototypeOf(this);
        while (obj && obj !== Object.prototype) {
            const names = Object.getOwnPropertyNames(obj);
            names.forEach(name => {
                // Only include if:
                // 1. Not a constructor
                // 2. Not a private method (starts with _)
                // 3. Not a base class method
                // 4. Is actually a function
                if (name !== 'constructor' &&
                    !name.startsWith('_') &&
                    !baseClassMethods.has(name)) {
                    const descriptor = Object.getOwnPropertyDescriptor(obj, name);
                    if (descriptor && typeof descriptor.value === 'function') {
                        methodNames.add(name);
                    }
                }
            });
            obj = Object.getPrototypeOf(obj);
        }
        return Array.from(methodNames);
    }
    // Utility Methods
    calculateCreditsFromMinutes(minutes, burningRate) {
        const burningRatePerMinute = burningRate * 60;
        return Math.ceil(minutes * burningRatePerMinute);
    }
}
