import { _optionalChain } from '@sentry/utils';
import { readFile } from 'fs';
import { defineIntegration, convertIntegrationFnToClass } from '@sentry/core';
import { LRUMap, addContextToFrame } from '@sentry/utils';

const FILE_CONTENT_CACHE = new LRUMap(100);
const DEFAULT_LINES_OF_CONTEXT = 7;
const INTEGRATION_NAME = 'ContextLines';

// TODO: Replace with promisify when minimum supported node >= v8
function readTextFileAsync(path) {
  return new Promise((resolve, reject) => {
    readFile(path, 'utf8', (err, data) => {
      if (err) reject(err);
      else resolve(data);
    });
  });
}

const _contextLinesIntegration = ((options = {}) => {
  const contextLines = options.frameContextLines !== undefined ? options.frameContextLines : DEFAULT_LINES_OF_CONTEXT;

  return {
    name: INTEGRATION_NAME,
    // TODO v8: Remove this
    setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function
    processEvent(event) {
      return addSourceContext(event, contextLines);
    },
  };
}) ;

const contextLinesIntegration = defineIntegration(_contextLinesIntegration);

/**
 * Add node modules / packages to the event.
 * @deprecated Use `contextLinesIntegration()` instead.
 */
// eslint-disable-next-line deprecation/deprecation
const ContextLines = convertIntegrationFnToClass(INTEGRATION_NAME, contextLinesIntegration)

;

async function addSourceContext(event, contextLines) {
  // keep a lookup map of which files we've already enqueued to read,
  // so we don't enqueue the same file multiple times which would cause multiple i/o reads
  const enqueuedReadSourceFileTasks = {};
  const readSourceFileTasks = [];

  if (contextLines > 0 && _optionalChain([event, 'access', _2 => _2.exception, 'optionalAccess', _3 => _3.values])) {
    for (const exception of event.exception.values) {
      if (!_optionalChain([exception, 'access', _4 => _4.stacktrace, 'optionalAccess', _5 => _5.frames])) {
        continue;
      }

      // We want to iterate in reverse order as calling cache.get will bump the file in our LRU cache.
      // This ends up prioritizes source context for frames at the top of the stack instead of the bottom.
      for (let i = exception.stacktrace.frames.length - 1; i >= 0; i--) {
        const frame = exception.stacktrace.frames[i];
        // Call cache.get to bump the file to the top of the cache and ensure we have not already
        // enqueued a read operation for this filename
        if (frame.filename && !enqueuedReadSourceFileTasks[frame.filename] && !FILE_CONTENT_CACHE.get(frame.filename)) {
          readSourceFileTasks.push(_readSourceFile(frame.filename));
          enqueuedReadSourceFileTasks[frame.filename] = 1;
        }
      }
    }
  }

  // check if files to read > 0, if so, await all of them to be read before adding source contexts.
  // Normally, Promise.all here could be short circuited if one of the promises rejects, but we
  // are guarding from that by wrapping the i/o read operation in a try/catch.
  if (readSourceFileTasks.length > 0) {
    await Promise.all(readSourceFileTasks);
  }

  // Perform the same loop as above, but this time we can assume all files are in the cache
  // and attempt to add source context to frames.
  if (contextLines > 0 && _optionalChain([event, 'access', _6 => _6.exception, 'optionalAccess', _7 => _7.values])) {
    for (const exception of event.exception.values) {
      if (exception.stacktrace && exception.stacktrace.frames) {
        await addSourceContextToFrames(exception.stacktrace.frames, contextLines);
      }
    }
  }

  return event;
}

/** Adds context lines to frames */
function addSourceContextToFrames(frames, contextLines) {
  for (const frame of frames) {
    // Only add context if we have a filename and it hasn't already been added
    if (frame.filename && frame.context_line === undefined) {
      const sourceFileLines = FILE_CONTENT_CACHE.get(frame.filename);

      if (sourceFileLines) {
        try {
          addContextToFrame(sourceFileLines, frame, contextLines);
        } catch (e) {
          // anomaly, being defensive in case
          // unlikely to ever happen in practice but can definitely happen in theory
        }
      }
    }
  }
}

// eslint-disable-next-line deprecation/deprecation

/**
 * Reads file contents and caches them in a global LRU cache.
 * If reading fails, mark the file as null in the cache so we don't try again.
 *
 * @param filename filepath to read content from.
 */
async function _readSourceFile(filename) {
  const cachedFile = FILE_CONTENT_CACHE.get(filename);

  // We have already attempted to read this file and failed, do not try again
  if (cachedFile === null) {
    return null;
  }

  // We have a cache hit, return it
  if (cachedFile !== undefined) {
    return cachedFile;
  }

  // Guard from throwing if readFile fails, this enables us to use Promise.all and
  // not have it short circuiting if one of the promises rejects + since context lines are added
  // on a best effort basis, we want to throw here anyways.

  // If we made it to here, it means that our file is not cache nor marked as failed, so attempt to read it
  let content = null;
  try {
    const rawFileContents = await readTextFileAsync(filename);
    content = rawFileContents.split('\n');
  } catch (_) {
    // if we fail, we will mark the file as null in the cache and short circuit next time we try to read it
  }

  FILE_CONTENT_CACHE.set(filename, content);
  return content;
}

export { ContextLines, contextLinesIntegration };
//# sourceMappingURL=contextlines.js.map
