var isValidSemver = require('semver/functions/valid')
var cleanSemver = require('semver/functions/clean')
var validateLicense = require('validate-npm-package-license')
var hostedGitInfo = require('hosted-git-info')
var moduleBuiltin = require('node:module')
var depTypes = ['dependencies', 'devDependencies', 'optionalDependencies']
var extractDescription = require('./extract_description')
var url = require('url')
var typos = require('./typos.json')

var isEmail = str => str.includes('@') && (str.indexOf('@') < str.lastIndexOf('.'))

module.exports = {
  // default warning function
  warn: function () {},

  fixRepositoryField: function (data) {
    if (data.repositories) {
      this.warn('repositories')
      data.repository = data.repositories[0]
    }
    if (!data.repository) {
      return this.warn('missingRepository')
    }
    if (typeof data.repository === 'string') {
      data.repository = {
        type: 'git',
        url: data.repository,
      }
    }
    var r = data.repository.url || ''
    if (r) {
      var hosted = hostedGitInfo.fromUrl(r)
      if (hosted) {
        r = data.repository.url
          = hosted.getDefaultRepresentation() === 'shortcut' ? hosted.https() : hosted.toString()
      }
    }

    if (r.match(/github.com\/[^/]+\/[^/]+\.git\.git$/)) {
      this.warn('brokenGitUrl', r)
    }
  },

  fixTypos: function (data) {
    Object.keys(typos.topLevel).forEach(function (d) {
      if (Object.prototype.hasOwnProperty.call(data, d)) {
        this.warn('typo', d, typos.topLevel[d])
      }
    }, this)
  },

  fixScriptsField: function (data) {
    if (!data.scripts) {
      return
    }
    if (typeof data.scripts !== 'object') {
      this.warn('nonObjectScripts')
      delete data.scripts
      return
    }
    Object.keys(data.scripts).forEach(function (k) {
      if (typeof data.scripts[k] !== 'string') {
        this.warn('nonStringScript')
        delete data.scripts[k]
      } else if (typos.script[k] && !data.scripts[typos.script[k]]) {
        this.warn('typo', k, typos.script[k], 'scripts')
      }
    }, this)
  },

  fixFilesField: function (data) {
    var files = data.files
    if (files && !Array.isArray(files)) {
      this.warn('nonArrayFiles')
      delete data.files
    } else if (data.files) {
      data.files = data.files.filter(function (file) {
        if (!file || typeof file !== 'string') {
          this.warn('invalidFilename', file)
          return false
        } else {
          return true
        }
      }, this)
    }
  },

  fixBinField: function (data) {
    if (!data.bin) {
      return
    }
    if (typeof data.bin === 'string') {
      var b = {}
      var match
      if (match = data.name.match(/^@[^/]+[/](.*)$/)) {
        b[match[1]] = data.bin
      } else {
        b[data.name] = data.bin
      }
      data.bin = b
    }
  },

  fixManField: function (data) {
    if (!data.man) {
      return
    }
    if (typeof data.man === 'string') {
      data.man = [data.man]
    }
  },
  fixBundleDependenciesField: function (data) {
    var bdd = 'bundledDependencies'
    var bd = 'bundleDependencies'
    if (data[bdd] && !data[bd]) {
      data[bd] = data[bdd]
      delete data[bdd]
    }
    if (data[bd] && !Array.isArray(data[bd])) {
      this.warn('nonArrayBundleDependencies')
      delete data[bd]
    } else if (data[bd]) {
      data[bd] = data[bd].filter(function (filtered) {
        if (!filtered || typeof filtered !== 'string') {
          this.warn('nonStringBundleDependency', filtered)
          return false
        } else {
          if (!data.dependencies) {
            data.dependencies = {}
          }
          if (!Object.prototype.hasOwnProperty.call(data.dependencies, filtered)) {
            this.warn('nonDependencyBundleDependency', filtered)
            data.dependencies[filtered] = '*'
          }
          return true
        }
      }, this)
    }
  },

  fixDependencies: function (data) {
    objectifyDeps(data, this.warn)
    addOptionalDepsToDeps(data, this.warn)
    this.fixBundleDependenciesField(data)

    ;['dependencies', 'devDependencies'].forEach(function (deps) {
      if (!(deps in data)) {
        return
      }
      if (!data[deps] || typeof data[deps] !== 'object') {
        this.warn('nonObjectDependencies', deps)
        delete data[deps]
        return
      }
      Object.keys(data[deps]).forEach(function (d) {
        var r = data[deps][d]
        if (typeof r !== 'string') {
          this.warn('nonStringDependency', d, JSON.stringify(r))
          delete data[deps][d]
        }
        var hosted = hostedGitInfo.fromUrl(data[deps][d])
        if (hosted) {
          data[deps][d] = hosted.toString()
        }
      }, this)
    }, this)
  },

  fixModulesField: function (data) {
    if (data.modules) {
      this.warn('deprecatedModules')
      delete data.modules
    }
  },

  fixKeywordsField: function (data) {
    if (typeof data.keywords === 'string') {
      data.keywords = data.keywords.split(/,\s+/)
    }
    if (data.keywords && !Array.isArray(data.keywords)) {
      delete data.keywords
      this.warn('nonArrayKeywords')
    } else if (data.keywords) {
      data.keywords = data.keywords.filter(function (kw) {
        if (typeof kw !== 'string' || !kw) {
          this.warn('nonStringKeyword')
          return false
        } else {
          return true
        }
      }, this)
    }
  },

  fixVersionField: function (data, strict) {
    // allow "loose" semver 1.0 versions in non-strict mode
    // enforce strict semver 2.0 compliance in strict mode
    var loose = !strict
    if (!data.version) {
      data.version = ''
      return true
    }
    if (!isValidSemver(data.version, loose)) {
      throw new Error('Invalid version: "' + data.version + '"')
    }
    data.version = cleanSemver(data.version, loose)
    return true
  },

  fixPeople: function (data) {
    modifyPeople(data, unParsePerson)
    modifyPeople(data, parsePerson)
  },

  fixNameField: function (data, options) {
    if (typeof options === 'boolean') {
      options = { strict: options }
    } else if (typeof options === 'undefined') {
      options = {}
    }
    var strict = options.strict
    if (!data.name && !strict) {
      data.name = ''
      return
    }
    if (typeof data.name !== 'string') {
      throw new Error('name field must be a string.')
    }
    if (!strict) {
      data.name = data.name.trim()
    }
    ensureValidName(data.name, strict, options.allowLegacyCase)
    if (moduleBuiltin.builtinModules.includes(data.name)) {
      this.warn('conflictingName', data.name)
    }
  },

  fixDescriptionField: function (data) {
    if (data.description && typeof data.description !== 'string') {
      this.warn('nonStringDescription')
      delete data.description
    }
    if (data.readme && !data.description) {
      data.description = extractDescription(data.readme)
    }
    if (data.description === undefined) {
      delete data.description
    }
    if (!data.description) {
      this.warn('missingDescription')
    }
  },

  fixReadmeField: function (data) {
    if (!data.readme) {
      this.warn('missingReadme')
      data.readme = 'ERROR: No README data found!'
    }
  },

  fixBugsField: function (data) {
    if (!data.bugs && data.repository && data.repository.url) {
      var hosted = hostedGitInfo.fromUrl(data.repository.url)
      if (hosted && hosted.bugs()) {
        data.bugs = { url: hosted.bugs() }
      }
    } else if (data.bugs) {
      if (typeof data.bugs === 'string') {
        if (isEmail(data.bugs)) {
          data.bugs = { email: data.bugs }
        /* eslint-disable-next-line node/no-deprecated-api */
        } else if (url.parse(data.bugs).protocol) {
          data.bugs = { url: data.bugs }
        } else {
          this.warn('nonEmailUrlBugsString')
        }
      } else {
        bugsTypos(data.bugs, this.warn)
        var oldBugs = data.bugs
        data.bugs = {}
        if (oldBugs.url) {
          /* eslint-disable-next-line node/no-deprecated-api */
          if (typeof (oldBugs.url) === 'string' && url.parse(oldBugs.url).protocol) {
            data.bugs.url = oldBugs.url
          } else {
            this.warn('nonUrlBugsUrlField')
          }
        }
        if (oldBugs.email) {
          if (typeof (oldBugs.email) === 'string' && isEmail(oldBugs.email)) {
            data.bugs.email = oldBugs.email
          } else {
            this.warn('nonEmailBugsEmailField')
          }
        }
      }
      if (!data.bugs.email && !data.bugs.url) {
        delete data.bugs
        this.warn('emptyNormalizedBugs')
      }
    }
  },

  fixHomepageField: function (data) {
    if (!data.homepage && data.repository && data.repository.url) {
      var hosted = hostedGitInfo.fromUrl(data.repository.url)
      if (hosted && hosted.docs()) {
        data.homepage = hosted.docs()
      }
    }
    if (!data.homepage) {
      return
    }

    if (typeof data.homepage !== 'string') {
      this.warn('nonUrlHomepage')
      return delete data.homepage
    }
    /* eslint-disable-next-line node/no-deprecated-api */
    if (!url.parse(data.homepage).protocol) {
      data.homepage = 'http://' + data.homepage
    }
  },

  fixLicenseField: function (data) {
    const license = data.license || data.licence
    if (!license) {
      return this.warn('missingLicense')
    }
    if (
      typeof (license) !== 'string' ||
      license.length < 1 ||
      license.trim() === ''
    ) {
      return this.warn('invalidLicense')
    }
    if (!validateLicense(license).validForNewPackages) {
      return this.warn('invalidLicense')
    }
  },
}

function isValidScopedPackageName (spec) {
  if (spec.charAt(0) !== '@') {
    return false
  }

  var rest = spec.slice(1).split('/')
  if (rest.length !== 2) {
    return false
  }

  return rest[0] && rest[1] &&
    rest[0] === encodeURIComponent(rest[0]) &&
    rest[1] === encodeURIComponent(rest[1])
}

function isCorrectlyEncodedName (spec) {
  return !spec.match(/[/@\s+%:]/) &&
    spec === encodeURIComponent(spec)
}

function ensureValidName (name, strict, allowLegacyCase) {
  if (name.charAt(0) === '.' ||
      !(isValidScopedPackageName(name) || isCorrectlyEncodedName(name)) ||
      (strict && (!allowLegacyCase) && name !== name.toLowerCase()) ||
      name.toLowerCase() === 'node_modules' ||
      name.toLowerCase() === 'favicon.ico') {
    throw new Error('Invalid name: ' + JSON.stringify(name))
  }
}

function modifyPeople (data, fn) {
  if (data.author) {
    data.author = fn(data.author)
  }['maintainers', 'contributors'].forEach(function (set) {
    if (!Array.isArray(data[set])) {
      return
    }
    data[set] = data[set].map(fn)
  })
  return data
}

function unParsePerson (person) {
  if (typeof person === 'string') {
    return person
  }
  var name = person.name || ''
  var u = person.url || person.web
  var wrappedUrl = u ? (' (' + u + ')') : ''
  var e = person.email || person.mail
  var wrappedEmail = e ? (' <' + e + '>') : ''
  return name + wrappedEmail + wrappedUrl
}

function parsePerson (person) {
  if (typeof person !== 'string') {
    return person
  }
  var matchedName = person.match(/^([^(<]+)/)
  var matchedUrl = person.match(/\(([^()]+)\)/)
  var matchedEmail = person.match(/<([^<>]+)>/)
  var obj = {}
  if (matchedName && matchedName[0].trim()) {
    obj.name = matchedName[0].trim()
  }
  if (matchedEmail) {
    obj.email = matchedEmail[1]
  }
  if (matchedUrl) {
    obj.url = matchedUrl[1]
  }
  return obj
}

function addOptionalDepsToDeps (data) {
  var o = data.optionalDependencies
  if (!o) {
    return
  }
  var d = data.dependencies || {}
  Object.keys(o).forEach(function (k) {
    d[k] = o[k]
  })
  data.dependencies = d
}

function depObjectify (deps, type, warn) {
  if (!deps) {
    return {}
  }
  if (typeof deps === 'string') {
    deps = deps.trim().split(/[\n\r\s\t ,]+/)
  }
  if (!Array.isArray(deps)) {
    return deps
  }
  warn('deprecatedArrayDependencies', type)
  var o = {}
  deps.filter(function (d) {
    return typeof d === 'string'
  }).forEach(function (d) {
    d = d.trim().split(/(:?[@\s><=])/)
    var dn = d.shift()
    var dv = d.join('')
    dv = dv.trim()
    dv = dv.replace(/^@/, '')
    o[dn] = dv
  })
  return o
}

function objectifyDeps (data, warn) {
  depTypes.forEach(function (type) {
    if (!data[type]) {
      return
    }
    data[type] = depObjectify(data[type], type, warn)
  })
}

function bugsTypos (bugs, warn) {
  if (!bugs) {
    return
  }
  Object.keys(bugs).forEach(function (k) {
    if (typos.bugs[k]) {
      warn('typo', k, typos.bugs[k], 'bugs')
      bugs[typos.bugs[k]] = bugs[k]
      delete bugs[k]
    }
  })
}
