'use strict';

var readable = require('../../../readable-stream/readable.js');
var forEach = require('../../../async/forEach.js');
require('../../../triple-beam/index.js');
var index$1 = require('../../../is-stream/index.js');
var exceptionHandler = require('./exception-handler.js');
var rejectionHandler = require('./rejection-handler.js');
var legacy = require('../../../winston-transport/legacy.js');
var profiler = require('./profiler.js');
require('./common.js');
require('./config/index.js');
var json = require('../../../logform/json.js');
var index = require('../../../../_virtual/index12.js');
var common = require('../../../../_virtual/common2.js');
var index$2 = require('../../../../_virtual/index8.js');

/**
 * logger.js: TODO: add file header description.
 *
 * (C) 2010 Charlie Robbins
 * MIT LICENCE
 */

var logger;
var hasRequiredLogger;

function requireLogger () {
	if (hasRequiredLogger) return logger;
	hasRequiredLogger = 1;

	const { Stream, Transform } = readable.readableExports;
	const asyncForEach = forEach.forEachExports;
	const { LEVEL, SPLAT } = index.__exports;
	const isStream = index$1.isStream_1;
	const ExceptionHandler = exceptionHandler.exceptionHandler;
	const RejectionHandler = rejectionHandler.rejectionHandler;
	const LegacyTransportStream = legacy.legacyExports;
	const Profiler = profiler.__require();
	const { warn } = common.__exports;
	const config = index$2.__exports;

	/**
	 * Captures the number of format (i.e. %s strings) in a given string.
	 * Based on `util.format`, see Node.js source:
	 * https://github.com/nodejs/node/blob/b1c8f15c5f169e021f7c46eb7b219de95fe97603/lib/util.js#L201-L230
	 * @type {RegExp}
	 */
	const formatRegExp = /%[scdjifoO%]/g;

	/**
	 * TODO: add class description.
	 * @type {Logger}
	 * @extends {Transform}
	 */
	class Logger extends Transform {
	  /**
	   * Constructor function for the Logger object responsible for persisting log
	   * messages and metadata to one or more transports.
	   * @param {!Object} options - foo
	   */
	  constructor(options) {
	    super({ objectMode: true });
	    this.configure(options);
	  }

	  child(defaultRequestMetadata) {
	    const logger = this;
	    return Object.create(logger, {
	      write: {
	        value: function (info) {
	          const infoClone = Object.assign(
	            {},
	            defaultRequestMetadata,
	            info
	          );

	          // Object.assign doesn't copy inherited Error
	          // properties so we have to do that explicitly
	          //
	          // Remark (indexzero): we should remove this
	          // since the errors format will handle this case.
	          //
	          if (info instanceof Error) {
	            infoClone.stack = info.stack;
	            infoClone.message = info.message;
	          }

	          logger.write(infoClone);
	        }
	      }
	    });
	  }

	  /**
	   * This will wholesale reconfigure this instance by:
	   * 1. Resetting all transports. Older transports will be removed implicitly.
	   * 2. Set all other options including levels, colors, rewriters, filters,
	   *    exceptionHandlers, etc.
	   * @param {!Object} options - TODO: add param description.
	   * @returns {undefined}
	   */
	  configure({
	    silent,
	    format,
	    defaultMeta,
	    levels,
	    level = 'info',
	    exitOnError = true,
	    transports,
	    colors,
	    emitErrs,
	    formatters,
	    padLevels,
	    rewriters,
	    stripColors,
	    exceptionHandlers,
	    rejectionHandlers
	  } = {}) {
	    // Reset transports if we already have them
	    if (this.transports.length) {
	      this.clear();
	    }

	    this.silent = silent;
	    this.format = format || this.format || json.__require()();

	    this.defaultMeta = defaultMeta || null;
	    // Hoist other options onto this instance.
	    this.levels = levels || this.levels || config.npm.levels;
	    this.level = level;
	    if (this.exceptions) {
	      this.exceptions.unhandle();
	    }
	    if (this.rejections) {
	      this.rejections.unhandle();
	    }
	    this.exceptions = new ExceptionHandler(this);
	    this.rejections = new RejectionHandler(this);
	    this.profilers = {};
	    this.exitOnError = exitOnError;

	    // Add all transports we have been provided.
	    if (transports) {
	      transports = Array.isArray(transports) ? transports : [transports];
	      transports.forEach(transport => this.add(transport));
	    }

	    if (
	      colors ||
	      emitErrs ||
	      formatters ||
	      padLevels ||
	      rewriters ||
	      stripColors
	    ) {
	      throw new Error(
	        [
	          '{ colors, emitErrs, formatters, padLevels, rewriters, stripColors } were removed in winston@3.0.0.',
	          'Use a custom winston.format(function) instead.',
	          'See: https://github.com/winstonjs/winston/tree/master/UPGRADE-3.0.md'
	        ].join('\n')
	      );
	    }

	    if (exceptionHandlers) {
	      this.exceptions.handle(exceptionHandlers);
	    }
	    if (rejectionHandlers) {
	      this.rejections.handle(rejectionHandlers);
	    }
	  }

	  isLevelEnabled(level) {
	    const givenLevelValue = getLevelValue(this.levels, level);
	    if (givenLevelValue === null) {
	      return false;
	    }

	    const configuredLevelValue = getLevelValue(this.levels, this.level);
	    if (configuredLevelValue === null) {
	      return false;
	    }

	    if (!this.transports || this.transports.length === 0) {
	      return configuredLevelValue >= givenLevelValue;
	    }

	    const index = this.transports.findIndex(transport => {
	      let transportLevelValue = getLevelValue(this.levels, transport.level);
	      if (transportLevelValue === null) {
	        transportLevelValue = configuredLevelValue;
	      }
	      return transportLevelValue >= givenLevelValue;
	    });
	    return index !== -1;
	  }

	  /* eslint-disable valid-jsdoc */
	  /**
	   * Ensure backwards compatibility with a `log` method
	   * @param {mixed} level - Level the log message is written at.
	   * @param {mixed} msg - TODO: add param description.
	   * @param {mixed} meta - TODO: add param description.
	   * @returns {Logger} - TODO: add return description.
	   *
	   * @example
	   *    // Supports the existing API:
	   *    logger.log('info', 'Hello world', { custom: true });
	   *    logger.log('info', new Error('Yo, it\'s on fire'));
	   *
	   *    // Requires winston.format.splat()
	   *    logger.log('info', '%s %d%%', 'A string', 50, { thisIsMeta: true });
	   *
	   *    // And the new API with a single JSON literal:
	   *    logger.log({ level: 'info', message: 'Hello world', custom: true });
	   *    logger.log({ level: 'info', message: new Error('Yo, it\'s on fire') });
	   *
	   *    // Also requires winston.format.splat()
	   *    logger.log({
	   *      level: 'info',
	   *      message: '%s %d%%',
	   *      [SPLAT]: ['A string', 50],
	   *      meta: { thisIsMeta: true }
	   *    });
	   *
	   */
	  /* eslint-enable valid-jsdoc */
	  log(level, msg, ...splat) {
	    // eslint-disable-line max-params
	    // Optimize for the hotpath of logging JSON literals
	    if (arguments.length === 1) {
	      // Yo dawg, I heard you like levels ... seriously ...
	      // In this context the LHS `level` here is actually the `info` so read
	      // this as: info[LEVEL] = info.level;
	      level[LEVEL] = level.level;
	      this._addDefaultMeta(level);
	      this.write(level);
	      return this;
	    }

	    // Slightly less hotpath, but worth optimizing for.
	    if (arguments.length === 2) {
	      if (msg && typeof msg === 'object') {
	        msg[LEVEL] = msg.level = level;
	        this._addDefaultMeta(msg);
	        this.write(msg);
	        return this;
	      }

	      msg = { [LEVEL]: level, level, message: msg };
	      this._addDefaultMeta(msg);
	      this.write(msg);
	      return this;
	    }

	    const [meta] = splat;
	    if (typeof meta === 'object' && meta !== null) {
	      // Extract tokens, if none available default to empty array to
	      // ensure consistancy in expected results
	      const tokens = msg && msg.match && msg.match(formatRegExp);

	      if (!tokens) {
	        const info = Object.assign({}, this.defaultMeta, meta, {
	          [LEVEL]: level,
	          [SPLAT]: splat,
	          level,
	          message: msg
	        });

	        if (meta.message) info.message = `${info.message} ${meta.message}`;
	        if (meta.stack) info.stack = meta.stack;

	        this.write(info);
	        return this;
	      }
	    }

	    this.write(Object.assign({}, this.defaultMeta, {
	      [LEVEL]: level,
	      [SPLAT]: splat,
	      level,
	      message: msg
	    }));

	    return this;
	  }

	  /**
	   * Pushes data so that it can be picked up by all of our pipe targets.
	   * @param {mixed} info - TODO: add param description.
	   * @param {mixed} enc - TODO: add param description.
	   * @param {mixed} callback - Continues stream processing.
	   * @returns {undefined}
	   * @private
	   */
	  _transform(info, enc, callback) {
	    if (this.silent) {
	      return callback();
	    }

	    // [LEVEL] is only soft guaranteed to be set here since we are a proper
	    // stream. It is likely that `info` came in through `.log(info)` or
	    // `.info(info)`. If it is not defined, however, define it.
	    // This LEVEL symbol is provided by `triple-beam` and also used in:
	    // - logform
	    // - winston-transport
	    // - abstract-winston-transport
	    if (!info[LEVEL]) {
	      info[LEVEL] = info.level;
	    }

	    // Remark: really not sure what to do here, but this has been reported as
	    // very confusing by pre winston@2.0.0 users as quite confusing when using
	    // custom levels.
	    if (!this.levels[info[LEVEL]] && this.levels[info[LEVEL]] !== 0) {
	      // eslint-disable-next-line no-console
	      console.error('[winston] Unknown logger level: %s', info[LEVEL]);
	    }

	    // Remark: not sure if we should simply error here.
	    if (!this._readableState.pipes) {
	      // eslint-disable-next-line no-console
	      console.error(
	        '[winston] Attempt to write logs with no transports, which can increase memory usage: %j',
	        info
	      );
	    }

	    // Here we write to the `format` pipe-chain, which on `readable` above will
	    // push the formatted `info` Object onto the buffer for this instance. We trap
	    // (and re-throw) any errors generated by the user-provided format, but also
	    // guarantee that the streams callback is invoked so that we can continue flowing.
	    try {
	      this.push(this.format.transform(info, this.format.options));
	    } finally {
	      this._writableState.sync = false;
	      // eslint-disable-next-line callback-return
	      callback();
	    }
	  }

	  /**
	   * Delays the 'finish' event until all transport pipe targets have
	   * also emitted 'finish' or are already finished.
	   * @param {mixed} callback - Continues stream processing.
	   */
	  _final(callback) {
	    const transports = this.transports.slice();
	    asyncForEach(
	      transports,
	      (transport, next) => {
	        if (!transport || transport.finished) return setImmediate(next);
	        transport.once('finish', next);
	        transport.end();
	      },
	      callback
	    );
	  }

	  /**
	   * Adds the transport to this logger instance by piping to it.
	   * @param {mixed} transport - TODO: add param description.
	   * @returns {Logger} - TODO: add return description.
	   */
	  add(transport) {
	    // Support backwards compatibility with all existing `winston < 3.x.x`
	    // transports which meet one of two criteria:
	    // 1. They inherit from winston.Transport in  < 3.x.x which is NOT a stream.
	    // 2. They expose a log method which has a length greater than 2 (i.e. more then
	    //    just `log(info, callback)`.
	    const target =
	      !isStream(transport) || transport.log.length > 2
	        ? new LegacyTransportStream({ transport })
	        : transport;

	    if (!target._writableState || !target._writableState.objectMode) {
	      throw new Error(
	        'Transports must WritableStreams in objectMode. Set { objectMode: true }.'
	      );
	    }

	    // Listen for the `error` event and the `warn` event on the new Transport.
	    this._onEvent('error', target);
	    this._onEvent('warn', target);
	    this.pipe(target);

	    if (transport.handleExceptions) {
	      this.exceptions.handle();
	    }

	    if (transport.handleRejections) {
	      this.rejections.handle();
	    }

	    return this;
	  }

	  /**
	   * Removes the transport from this logger instance by unpiping from it.
	   * @param {mixed} transport - TODO: add param description.
	   * @returns {Logger} - TODO: add return description.
	   */
	  remove(transport) {
	    if (!transport) return this;
	    let target = transport;
	    if (!isStream(transport) || transport.log.length > 2) {
	      target = this.transports.filter(
	        match => match.transport === transport
	      )[0];
	    }

	    if (target) {
	      this.unpipe(target);
	    }
	    return this;
	  }

	  /**
	   * Removes all transports from this logger instance.
	   * @returns {Logger} - TODO: add return description.
	   */
	  clear() {
	    this.unpipe();
	    return this;
	  }

	  /**
	   * Cleans up resources (streams, event listeners) for all transports
	   * associated with this instance (if necessary).
	   * @returns {Logger} - TODO: add return description.
	   */
	  close() {
	    this.exceptions.unhandle();
	    this.rejections.unhandle();
	    this.clear();
	    this.emit('close');
	    return this;
	  }

	  /**
	   * Sets the `target` levels specified on this instance.
	   * @param {Object} Target levels to use on this instance.
	   */
	  setLevels() {
	    warn.deprecated('setLevels');
	  }

	  /**
	   * Queries the all transports for this instance with the specified `options`.
	   * This will aggregate each transport's results into one object containing
	   * a property per transport.
	   * @param {Object} options - Query options for this instance.
	   * @param {function} callback - Continuation to respond to when complete.
	   */
	  query(options, callback) {
	    if (typeof options === 'function') {
	      callback = options;
	      options = {};
	    }

	    options = options || {};
	    const results = {};
	    const queryObject = Object.assign({}, options.query || {});

	    // Helper function to query a single transport
	    function queryTransport(transport, next) {
	      if (options.query && typeof transport.formatQuery === 'function') {
	        options.query = transport.formatQuery(queryObject);
	      }

	      transport.query(options, (err, res) => {
	        if (err) {
	          return next(err);
	        }

	        if (typeof transport.formatResults === 'function') {
	          res = transport.formatResults(res, options.format);
	        }

	        next(null, res);
	      });
	    }

	    // Helper function to accumulate the results from `queryTransport` into
	    // the `results`.
	    function addResults(transport, next) {
	      queryTransport(transport, (err, result) => {
	        // queryTransport could potentially invoke the callback multiple times
	        // since Transport code can be unpredictable.
	        if (next) {
	          result = err || result;
	          if (result) {
	            results[transport.name] = result;
	          }

	          // eslint-disable-next-line callback-return
	          next();
	        }

	        next = null;
	      });
	    }

	    // Iterate over the transports in parallel setting the appropriate key in
	    // the `results`.
	    asyncForEach(
	      this.transports.filter(transport => !!transport.query),
	      addResults,
	      () => callback(null, results)
	    );
	  }

	  /**
	   * Returns a log stream for all transports. Options object is optional.
	   * @param{Object} options={} - Stream options for this instance.
	   * @returns {Stream} - TODO: add return description.
	   */
	  stream(options = {}) {
	    const out = new Stream();
	    const streams = [];

	    out._streams = streams;
	    out.destroy = () => {
	      let i = streams.length;
	      while (i--) {
	        streams[i].destroy();
	      }
	    };

	    // Create a list of all transports for this instance.
	    this.transports
	      .filter(transport => !!transport.stream)
	      .forEach(transport => {
	        const str = transport.stream(options);
	        if (!str) {
	          return;
	        }

	        streams.push(str);

	        str.on('log', log => {
	          log.transport = log.transport || [];
	          log.transport.push(transport.name);
	          out.emit('log', log);
	        });

	        str.on('error', err => {
	          err.transport = err.transport || [];
	          err.transport.push(transport.name);
	          out.emit('error', err);
	        });
	      });

	    return out;
	  }

	  /**
	   * Returns an object corresponding to a specific timing. When done is called
	   * the timer will finish and log the duration. e.g.:
	   * @returns {Profile} - TODO: add return description.
	   * @example
	   *    const timer = winston.startTimer()
	   *    setTimeout(() => {
	   *      timer.done({
	   *        message: 'Logging message'
	   *      });
	   *    }, 1000);
	   */
	  startTimer() {
	    return new Profiler(this);
	  }

	  /**
	   * Tracks the time inbetween subsequent calls to this method with the same
	   * `id` parameter. The second call to this method will log the difference in
	   * milliseconds along with the message.
	   * @param {string} id Unique id of the profiler
	   * @returns {Logger} - TODO: add return description.
	   */
	  profile(id, ...args) {
	    const time = Date.now();
	    if (this.profilers[id]) {
	      const timeEnd = this.profilers[id];
	      delete this.profilers[id];

	      // Attempt to be kind to users if they are still using older APIs.
	      if (typeof args[args.length - 2] === 'function') {
	        // eslint-disable-next-line no-console
	        console.warn(
	          'Callback function no longer supported as of winston@3.0.0'
	        );
	        args.pop();
	      }

	      // Set the duration property of the metadata
	      const info = typeof args[args.length - 1] === 'object' ? args.pop() : {};
	      info.level = info.level || 'info';
	      info.durationMs = time - timeEnd;
	      info.message = info.message || id;
	      return this.write(info);
	    }

	    this.profilers[id] = time;
	    return this;
	  }

	  /**
	   * Backwards compatibility to `exceptions.handle` in winston < 3.0.0.
	   * @returns {undefined}
	   * @deprecated
	   */
	  handleExceptions(...args) {
	    // eslint-disable-next-line no-console
	    console.warn(
	      'Deprecated: .handleExceptions() will be removed in winston@4. Use .exceptions.handle()'
	    );
	    this.exceptions.handle(...args);
	  }

	  /**
	   * Backwards compatibility to `exceptions.handle` in winston < 3.0.0.
	   * @returns {undefined}
	   * @deprecated
	   */
	  unhandleExceptions(...args) {
	    // eslint-disable-next-line no-console
	    console.warn(
	      'Deprecated: .unhandleExceptions() will be removed in winston@4. Use .exceptions.unhandle()'
	    );
	    this.exceptions.unhandle(...args);
	  }

	  /**
	   * Throw a more meaningful deprecation notice
	   * @throws {Error} - TODO: add throws description.
	   */
	  cli() {
	    throw new Error(
	      [
	        'Logger.cli() was removed in winston@3.0.0',
	        'Use a custom winston.formats.cli() instead.',
	        'See: https://github.com/winstonjs/winston/tree/master/UPGRADE-3.0.md'
	      ].join('\n')
	    );
	  }

	  /**
	   * Bubbles the `event` that occured on the specified `transport` up
	   * from this instance.
	   * @param {string} event - The event that occured
	   * @param {Object} transport - Transport on which the event occured
	   * @private
	   */
	  _onEvent(event, transport) {
	    function transportEvent(err) {
	      // https://github.com/winstonjs/winston/issues/1364
	      if (event === 'error' && !this.transports.includes(transport)) {
	        this.add(transport);
	      }
	      this.emit(event, err, transport);
	    }

	    if (!transport['__winston' + event]) {
	      transport['__winston' + event] = transportEvent.bind(this);
	      transport.on(event, transport['__winston' + event]);
	    }
	  }

	  _addDefaultMeta(msg) {
	    if (this.defaultMeta) {
	      Object.assign(msg, this.defaultMeta);
	    }
	  }
	}

	function getLevelValue(levels, level) {
	  const value = levels[level];
	  if (!value && value !== 0) {
	    return null;
	  }
	  return value;
	}

	/**
	 * Represents the current readableState pipe targets for this Logger instance.
	 * @type {Array|Object}
	 */
	Object.defineProperty(Logger.prototype, 'transports', {
	  configurable: false,
	  enumerable: true,
	  get() {
	    const { pipes } = this._readableState;
	    return !Array.isArray(pipes) ? [pipes].filter(Boolean) : pipes;
	  }
	});

	logger = Logger;
	return logger;
}

exports.__require = requireLogger;
