/** @babel */

import _ from 'underscore-plus'
import os from 'os'
import stackTrace from 'stack-trace'
import fs from 'fs-plus'
import path from 'path'

const API_KEY = '7ddca14cb60cbd1cd12d1b252473b076'
const LIB_VERSION = require('../package.json')['version']
const StackTraceCache = new WeakMap()

export default class Reporter {
  constructor (params = {}) {
    this.request = params.request || window.fetch
    this.alwaysReport = params.hasOwnProperty('alwaysReport') ? params.alwaysReport : false
    this.reportPreviousErrors = params.hasOwnProperty('reportPreviousErrors') ? params.reportPreviousErrors : true
    this.resourcePath = this.normalizePath(params.resourcePath || process.resourcesPath)
    this.reportedErrors = []
    this.reportedAssertionFailures = []
  }

  buildNotificationJSON (error, params) {
    return {
      apiKey: API_KEY,
      notifier: {
        name: 'Atom',
        version: LIB_VERSION,
        url: 'https://www.atom.io'
      },
      events: [{
        payloadVersion: "2",
        exceptions: [this.buildExceptionJSON(error, params.projectRoot)],
        severity: params.severity,
        user: {
          id: params.userId
        },
        app: {
          version: params.appVersion,
          releaseStage: params.releaseStage
        },
        device: {
          osVersion: params.osVersion
        },
        metaData: error.metadata
      }]
    }
  }

  buildExceptionJSON (error, projectRoot) {
    return {
      errorClass: error.constructor.name,
      message: error.message,
      stacktrace: this.buildStackTraceJSON(error, projectRoot)
    }
  }

  buildStackTraceJSON (error, projectRoot) {
    return this.parseStackTrace(error).map(callSite => {
      return {
        file: this.scrubPath(callSite.getFileName()),
        method: callSite.getMethodName() || callSite.getFunctionName() || "none",
        lineNumber: callSite.getLineNumber(),
        columnNumber: callSite.getColumnNumber(),
        inProject: !/node_modules/.test(callSite.getFileName())
      }
    })
  }

  normalizePath (pathToNormalize) {
    return pathToNormalize
      .replace('file:///', '')  // Sometimes it's a uri
      .replace(/\\/g, '/')      // Unify path separators across Win/macOS/Linux
  }

  scrubPath (pathToScrub) {
    const absolutePath = this.normalizePath(pathToScrub)

    if (this.isBundledFile(absolutePath)) {
      return this.normalizePath(path.relative(this.resourcePath, absolutePath))
    } else {
      return absolutePath
        .replace(this.normalizePath(fs.getHomeDirectory()), '~') // Remove users home dir
        .replace(/.*(\/packages\/.*)/, '$1') // Remove everything before app.asar or packages
    }
  }

  getDefaultNotificationParams () {
    return {
      userId: atom.config.get('exception-reporting.userId'),
      appVersion: atom.getVersion(),
      releaseStage: this.getReleaseChannel(atom.getVersion()),
      projectRoot: atom.getLoadSettings().resourcePath,
      osVersion: `${os.platform()}-${os.arch()}-${os.release()}`
    }
  }

  getReleaseChannel (version) {
    return (version.indexOf('beta') > -1)
      ? 'beta'
      : (version.indexOf('dev') > -1)
        ? 'dev'
        : 'stable'
  }

  performRequest (json) {
    this.request.call(null, 'https://notify.bugsnag.com', {
      method: 'POST',
      headers: new Headers({'Content-Type': 'application/json'}),
      body: JSON.stringify(json)
    })
  }

  shouldReport (error) {
    if (this.alwaysReport) return true // Used in specs
    if (atom.config.get('core.telemetryConsent') !== 'limited') return false
    if (atom.inDevMode()) return false

    const topFrame = this.parseStackTrace(error)[0]
    const fileName = topFrame ? topFrame.getFileName() : null
    return fileName && (this.isBundledFile(fileName) || this.isTeletypeFile(fileName))
  }

  parseStackTrace (error) {
    let callSites = StackTraceCache.get(error)
    if (callSites) {
      return callSites
    } else {
      callSites = stackTrace.parse(error)
      StackTraceCache.set(error, callSites)
      return callSites
    }
  }

  requestPrivateMetadataConsent (error, message, reportFn) {
    let notification, dismissSubscription

    function reportWithoutPrivateMetadata () {
      if (dismissSubscription) {
        dismissSubscription.dispose()
      }
      delete error.privateMetadata
      delete error.privateMetadataDescription
      reportFn(error)
      if (notification) {
        notification.dismiss()
      }
    }

    function reportWithPrivateMetadata () {
      if (error.metadata == null) {
        error.metadata = {}
      }
      for (let key in error.privateMetadata) {
        let value = error.privateMetadata[key]
        error.metadata[key] = value
      }
      reportWithoutPrivateMetadata()
    }

    const name = error.privateMetadataRequestName
    if (name != null) {
      if (localStorage.getItem(`private-metadata-request:${name}`)) {
        return reportWithoutPrivateMetadata(error)
      } else {
        localStorage.setItem(`private-metadata-request:${name}`, true)
      }
    }

    notification = atom.notifications.addInfo(message, {
      detail: error.privateMetadataDescription,
      description: "Are you willing to submit this information to a private server for debugging purposes?",
      dismissable: true,
      buttons: [
        {
          text: "No",
          onDidClick: reportWithoutPrivateMetadata
        },
        {
          text: "Yes, Submit for Debugging",
          onDidClick: reportWithPrivateMetadata
        }
      ]
    })

    dismissSubscription = notification.onDidDismiss(reportWithoutPrivateMetadata)
  }

  addPackageMetadata (error) {
    let activePackages = atom.packages.getActivePackages()
    const availablePackagePaths = atom.packages.getPackageDirPaths()
    if (activePackages.length > 0) {
      let userPackages = {}
      let bundledPackages = {}
      for (let pack of atom.packages.getActivePackages()) {
        if (availablePackagePaths.includes(path.dirname(pack.path))) {
          userPackages[pack.name] = pack.metadata.version
        } else {
          bundledPackages[pack.name] = pack.metadata.version
        }
      }

      if (error.metadata == null) { error.metadata = {} }
      error.metadata.bundledPackages = bundledPackages
      error.metadata.userPackages = userPackages
    }
  }

  addPreviousErrorsMetadata (error) {
    if (!this.reportPreviousErrors) return
    if (!error.metadata) error.metadata = {}
    error.metadata.previousErrors = this.reportedErrors.map(error => error.message)
    error.metadata.previousAssertionFailures = this.reportedAssertionFailures.map(error => error.message)
  }

  reportUncaughtException (error) {
    if (!this.shouldReport(error)) return

    this.addPackageMetadata(error)
    this.addPreviousErrorsMetadata(error)

    if ((error.privateMetadata != null) && (error.privateMetadataDescription != null)) {
      this.requestPrivateMetadataConsent(error, "The Atom team would like to collect the following information to resolve this error:", error => this.reportUncaughtException(error))
      return
    }

    let params = this.getDefaultNotificationParams()
    params.severity = "error"
    this.performRequest(this.buildNotificationJSON(error, params))
    this.reportedErrors.push(error)
  }

  reportFailedAssertion (error) {
    if (!this.shouldReport(error)) return

    this.addPackageMetadata(error)
    this.addPreviousErrorsMetadata(error)

    if ((error.privateMetadata != null) && (error.privateMetadataDescription != null)) {
      this.requestPrivateMetadataConsent(error, "The Atom team would like to collect some information to resolve an unexpected condition:", error => this.reportFailedAssertion(error))
      return
    }

    let params = this.getDefaultNotificationParams()
    params.severity = "warning"
    this.performRequest(this.buildNotificationJSON(error, params))
    this.reportedAssertionFailures.push(error)
  }

  // Used in specs
  setRequestFunction (requestFunction) {
    this.request = requestFunction
  }

  isBundledFile (fileName) {
    return this.normalizePath(fileName).indexOf(this.resourcePath) === 0
  }

  isTeletypeFile (fileName) {
    const teletypePath = atom.packages.resolvePackagePath('teletype')
    return teletypePath && this.normalizePath(fileName).indexOf(teletypePath) === 0
  }
}


Reporter.API_KEY = API_KEY
Reporter.LIB_VERSION = LIB_VERSION
