const fs = require('fs')
const promisify = require('util').promisify
const readFile = promisify(fs.readFile)
const writeFile = promisify(fs.writeFile)
const { resolve } = require('path')
const updateDeps = require('./update-dependencies.js')
const updateScripts = require('./update-scripts.js')
const updateWorkspaces = require('./update-workspaces.js')

const parseJSON = require('json-parse-even-better-errors')

const _filename = Symbol('filename')
const _manifest = Symbol('manifest')
const _readFileContent = Symbol('readFileContent')

// a list of handy specialized helper functions that take
// care of special cases that are handled by the npm cli
const knownSteps = new Set([
  updateDeps,
  updateScripts,
  updateWorkspaces,
])

// list of all keys that are handled by "knownSteps" helpers
const knownKeys = new Set([
  ...updateDeps.knownKeys,
  'scripts',
  'workspaces',
])

class PackageJson {
  static async load (path) {
    return await new PackageJson(path).load()
  }

  constructor (path) {
    this[_filename] = resolve(path, 'package.json')
    this[_manifest] = {}
    this[_readFileContent] = ''
  }

  async load () {
    try {
      this[_readFileContent] =
        await readFile(this[_filename], 'utf8')
    } catch (err) {
      throw new Error('package.json not found')
    }

    try {
      this[_manifest] =
        parseJSON(this[_readFileContent])
    } catch (err) {
      throw new Error(`Invalid package.json: ${err}`)
    }

    return this
  }

  get content () {
    return this[_manifest]
  }

  update (content) {
    // validates both current manifest and content param
    const invalidContent =
      typeof this[_manifest] !== 'object'
        || typeof content !== 'object'
    if (invalidContent) {
      throw Object.assign(
        new Error(`Can't update invalid package.json data`),
        { code: 'EPACKAGEJSONUPDATE' }
      )
    }

    for (const step of knownSteps)
      this[_manifest] = step({ content, originalContent: this[_manifest] })

    // unknown properties will just be overwitten
    for (const [key, value] of Object.entries(content)) {
      if (!knownKeys.has(key))
        this[_manifest][key] = value
    }

    return this
  }

  async save () {
    const {
      [Symbol.for('indent')]: indent,
      [Symbol.for('newline')]: newline,
    } = this[_manifest]

    const format = indent === undefined ? '  ' : indent
    const eol = newline === undefined ? '\n' : newline
    const fileContent = `${
      JSON.stringify(this[_manifest], null, format)
    }\n`
      .replace(/\n/g, eol)

    if (fileContent.trim() !== this[_readFileContent].trim())
      return await writeFile(this[_filename], fileContent)
  }
}

module.exports = PackageJson
