Source: lib/core/runtime/AvenxLogger.js

/**
 * @file AvenxLogger.js
 * @description Centralized logging module for the Avenx-JS framework.
 * Supports trace, debug, info, warn, error, fatal log levels, alias log -> info,
 * global silent/off option, custom formatters, and custom transports.
 */

export const LogLevels = {
  trace: 0,
  debug: 1,
  info: 2,
  warn: 3,
  error: 4,
  fatal: 5,
  off: 6,
  silent: 6,
};

/**
 * Formats component context metadata (componentName, fileName) into a diagnostic tag.
 * @param {object} context - Context object or component instance.
 * @returns {string} Formatted context tag string or empty string.
 */
export function formatContextTag(context) {
  if (!context || typeof context !== 'object' || context instanceof Error) {
    return '';
  }

  let compName = context.componentName;
  if (!compName && context.component) {
    const comp = context.component;
    compName = comp.componentName || comp.name || (comp.constructor && comp.constructor.name !== 'Object' && comp.constructor.name !== 'Function' ? comp.constructor.name : null);
  }
  if (!compName && context.name && context.name !== 'Error' && !context.name.endsWith('Error')) {
    compName = context.name;
  }
  if (!compName && context.constructor && context.constructor.name !== 'Object' && context.constructor.name !== 'Function' && context.constructor.name !== 'Error' && !context.constructor.name.endsWith('Error')) {
    compName = context.constructor.name;
  }

  let file = context.fileName || context.__filename || context.file;
  if (!file && context.component) {
    const comp = context.component;
    file = comp.fileName || comp.__filename || comp.file;
  }

  if (compName && file) {
    return `[${compName} <${file}>]`;
  }
  if (compName) {
    return `[${compName}]`;
  }
  if (file) {
    return `[<${file}>]`;
  }
  return '';
}

/**
 * Default formatter for browser runtime.
 * Prefixes messages with [Avenx level] and formats component context metadata if present.
 * Preserves interactive object logs by prepending to string or prepending as separate arg.
 * @param {string} level - Log level name.
 * @param {any[]} args - Array of raw arguments.
 * @returns {any[]} Array of formatted arguments.
 */
export function defaultFormatter(level, args) {
  const prefix = `[Avenx ${level}]`;
  if (!args || args.length === 0) {
    return [prefix];
  }

  let contextTag = '';
  const cleanArgs = [...args];

  const lastArg = cleanArgs[cleanArgs.length - 1];
  if (
    lastArg &&
    typeof lastArg === 'object' &&
    !(lastArg instanceof Error) &&
    !Array.isArray(lastArg)
  ) {
    const isExplicitContext = Boolean(
      lastArg.componentName ||
        lastArg.fileName ||
        lastArg.__filename ||
        lastArg.component ||
        lastArg.$logContext ||
        lastArg.__isAvenxComponent
    );
    if (isExplicitContext) {
      const tag = formatContextTag(lastArg);
      if (tag) {
        contextTag = tag;
        cleanArgs.pop();
      }
    }
  }

  if (cleanArgs.length > 0) {
    if (typeof cleanArgs[0] === 'string') {
      const firstStr = contextTag ? `${prefix} ${contextTag} ${cleanArgs[0]}` : `${prefix} ${cleanArgs[0]}`;
      return [firstStr, ...cleanArgs.slice(1)];
    }
    if (cleanArgs[0] instanceof Error) {
      return contextTag ? [`${prefix} ${contextTag}`, ...cleanArgs] : cleanArgs;
    }
  }

  return contextTag ? [`${prefix} ${contextTag}`, ...cleanArgs] : [prefix, ...cleanArgs];
}

/**
 * Default console transport.
 * Dispatches messages to console methods dynamically.
 */
export const consoleTransport = {
  log(level, formattedArgs) {
    const method = level === 'fatal' ? 'error' : level === 'trace' ? 'debug' : console[level] ? level : 'log';
    if (typeof console !== 'undefined' && console[method]) {
      console[method](...formattedArgs);
    }
  },
};

/**
 * @typedef {Object} LoggingConfig
 * @property {('trace'|'debug'|'info'|'warn'|'error'|'fatal')} [level='info'] - The minimum severity level to output.
 * @property {boolean} [silent=false] - Global silence setting. If true, suppresses all logging.
 * @property {Function} [formatter=defaultFormatter] - Custom formatting callback function. Receives (level, args).
 * @property {Array<Object|Function>} [transports=[consoleTransport]] - Collection of transport targets.
 */

/**
 * Central logger class for Avenx-JS framework.
 * @example
 * // 1. Basic initialization during application setup:
 * const app = new AvenxApp({
 *   logging: {
 *     level: 'debug',
 *     silent: false
 *   }
 * });
 * @example
 * // 2. Usage inside component methods:
 * // Import the shared logger from the runtime entry point — component
 * // instances do not expose `this.logger`.
 * import { logger } from 'avenx-core/runtime';
 *
 * export default {
 *   name: 'TargetSyncComponent',
 *   methods: {
 *     async syncDatabase(targets) {
 *       logger.info('Starting celestial synchronization...', { count: targets.length });
 *       try {
 *         if (!targets || targets.length === 0) {
 *           logger.warn('Sync skipped: list is empty.');
 *           return;
 *         }
 *         logger.debug('Processing batch.', { sampleId: targets[0].id });
 *       } catch (error) {
 *         logger.error('Database sync failed.', { error: error.message });
 *       }
 *     }
 *   }
 * };
 * @example
 * // 3. Registering a Custom Formatter:
 * const customFormatter = (level, args) => {
 *   return [`[MY-APP] [${level.toUpperCase()}]:`, ...args];
 * };
 * const loggerWithFormatter = new AvenxLogger({ formatter: customFormatter });
 * @example
 * // 4. Registering a Custom Transport:
 * const fileTransport = {
 *   log(level, formattedArgs, rawArgs) {
 *     // Append custom streaming/file logic here
 *     fs.appendFileSync('./app.log', formattedArgs.join(' ') + '\n');
 *   }
 * };
 * const loggerWithTransport = new AvenxLogger({ transports: [fileTransport] });
 */
export class AvenxLogger {
  /**
   * Creates an instance of AvenxLogger.
   * @param {object} [config] - Application logger configuration options.
   */
  constructor(config = {}) {
    this.config = {
      level: 'info',
      silent: false,
      formatter: defaultFormatter,
      transports: [consoleTransport],
    };
    this.bindings = {};
    this.configure(config);
  }

  /**
   * Configures the logger instance options.
   * @param {object} config - Configuration options.
   */
  configure(config) {
    if (!config) return;
    this.config = {
      ...this.config,
      ...config,
    };
    // Ensure lowercase for level
    if (typeof this.config.level === 'string') {
      this.config.level = this.config.level.toLowerCase();
    }
    // Validate level against known LogLevels; fall back to 'info' on mismatch
    if (LogLevels[this.config.level] === undefined) {
      this.write('warn', `Invalid log level "${this.config.level}" — falling back to "info"`);
      this.config.level = 'info';
    }
  }

  /**
   * Sets the minimum log severity level programmatically.
   * @param {string} level - Log level name ('trace', 'debug', 'info', 'warn', 'error', 'fatal', 'off', 'silent').
   */
  setLevel(level) {
    this.configure({ level });
  }

  /**
   * Helper to check if a specific level should be logged.
   * @param {string} level - Log level to test.
   * @returns {boolean} True if logger should log the given level.
   */
  shouldLog(level) {
    if (this.config.silent || this.config.level === 'silent' || this.config.level === 'off') {
      return false;
    }
    const currentPriority = LogLevels[this.config.level] !== undefined ? LogLevels[this.config.level] : LogLevels.info;
    const targetPriority = LogLevels[level] !== undefined ? LogLevels[level] : LogLevels.info;
    return targetPriority >= currentPriority;
  }

  /**
   * Writes the log statement through configured formatter and transports.
   * @param {string} level - Log level name.
   * @param {...any} args - Arguments to log.
   */
  write(level, ...args) {
    if (!this.shouldLog(level)) {
      return;
    }
    const formatted = this.config.formatter ? this.config.formatter(level, args) : args;

    const transports = Array.isArray(this.config.transports) ? this.config.transports : [consoleTransport];
    for (const transport of transports) {
      if (typeof transport === 'function') {
        transport(level, formatted, args);
      } else if (transport && typeof transport.log === 'function') {
        transport.log(level, formatted, args);
      }
    }
  }

  /**
   * Creates a child logger instance that inherits the parent's log level, transports, and formatter.
   * Supports a string shorthand for prefix-only binding, or an object with prefix and/or componentName.
   * Parent bindings are merged with the new bindings (child overrides parent on conflict).
   * @param {string|object} [bindings] - Bindings configuration for the child logger.
   * @param {string} [bindings.prefix] - A prefix string prepended to every formatted log message.
   * @param {string} [bindings.componentName] - Component name injected as context metadata, formatted as [ComponentName] by defaultFormatter.
   * @returns {AvenxLogger} A new logger instance with inherited configuration and merged bindings.
   * @example
   * // String shorthand
   * const authLogger = logger.child('[AuthBridge]');
   * authLogger.info('User logged in');
   * // Output: [AuthBridge] [Avenx info] User logged in
   * @example
   * // Object bindings with context
   * const dbLogger = logger.child({ prefix: '[DB]', componentName: 'DatabaseBridge' });
   * dbLogger.warn('Connection slow');
   * // Output: [DB] [Avenx warn] [DatabaseBridge] Connection slow
   * @example
   * // Nested child loggers
   * const child = logger.child('[Parent]').child('[Child]');
   */
  child(bindings = {}) {
    const normBindings = typeof bindings === 'string' ? { prefix: bindings } : bindings;
    const childLogger = new AvenxLogger(this.config);
    childLogger.bindings = { ...this.bindings, ...normBindings };

    const parentFormatter = this.config.formatter || defaultFormatter;
    childLogger.config.formatter = (level, args) => {
      const injectArgs = [...args];

      if (childLogger.bindings.componentName) {
        const lastArg = injectArgs[injectArgs.length - 1];
        const hasContext =
          lastArg &&
          typeof lastArg === 'object' &&
          !Array.isArray(lastArg) &&
          !(lastArg instanceof Error) &&
          (lastArg.componentName ||
            lastArg.fileName ||
            lastArg.__filename ||
            lastArg.component ||
            lastArg.$logContext ||
            lastArg.__isAvenxComponent);
        if (!hasContext) {
          injectArgs.push({ componentName: childLogger.bindings.componentName });
        }
      }

      const formatted = parentFormatter(level, injectArgs);

      const prefix = childLogger.bindings.prefix || '';
      if (prefix && formatted.length > 0) {
        if (typeof formatted[0] === 'string') {
          formatted[0] = `${prefix} ${formatted[0]}`;
        } else {
          formatted.unshift(prefix);
        }
      }

      return formatted;
    };

    return childLogger;
  }

  /**
   * Logs a message with trace level.
   * @param {...any} args - Arguments to log.
   */
  trace(...args) {
    this.write('trace', ...args);
  }

  /**
   * Logs a message with debug level.
   * @param {...any} args - Arguments to log.
   */
  debug(...args) {
    this.write('debug', ...args);
  }

  /**
   * Logs a message with info level.
   * @param {...any} args - Arguments to log.
   */
  info(...args) {
    this.write('info', ...args);
  }

  /**
   * Alias for info level logging.
   * @param {...any} args - Arguments to log.
   */
  log(...args) {
    this.write('info', ...args);
  }

  /**
   * Logs a message with warn level.
   * @param {...any} args - Arguments to log.
   */
  warn(...args) {
    this.write('warn', ...args);
  }

  /**
   * Logs a message with error level.
   * @param {...any} args - Arguments to log.
   */
  error(...args) {
    this.write('error', ...args);
  }

  /**
   * Logs a message with fatal level.
   * @param {...any} args - Arguments to log.
   */
  fatal(...args) {
    this.write('fatal', ...args);
  }
}

export const logger = new AvenxLogger();