const archy = require('archy')
const Arborist = require('@npmcli/arborist')
const chalk = require('chalk')
const pacote = require('pacote')
const semver = require('semver')
const npa = require('npm-package-arg')
const { depth } = require('treeverse')
const {
  readTree: getFundingInfo,
  normalizeFunding,
  isValidFunding,
} = require('libnpmfund')

const npm = require('./npm.js')
const completion = require('./utils/completion/installed-deep.js')
const output = require('./utils/output.js')
const openUrl = require('./utils/open-url.js')
const usageUtil = require('./utils/usage.js')

const usage = usageUtil(
  'fund',
  'npm fund',
  'npm fund [--json] [--browser] [--unicode] [[<@scope>/]<pkg> [--which=<fundingSourceNumber>]'
)

const cmd = (args, cb) => fund(args).then(() => cb()).catch(cb)

function printJSON (fundingInfo) {
  return JSON.stringify(fundingInfo, null, 2)
}

const getPrintableName = ({ name, version }) => {
  const printableVersion = version ? `@${version}` : ''
  return `${name}${printableVersion}`
}

function printHuman (fundingInfo, { color, unicode }) {
  const seenUrls = new Map()

  const tree = obj =>
    archy(obj, '', { unicode })

  const result = depth({
    tree: fundingInfo,

    // composes human readable package name
    // and creates a new archy item for readable output
    visit: ({ name, version, funding }) => {
      const [fundingSource] = []
        .concat(normalizeFunding(funding))
        .filter(isValidFunding)
      const { url } = fundingSource || {}
      const pkgRef = getPrintableName({ name, version })
      let item = {
        label: pkgRef,
      }

      if (url) {
        item.label = tree({
          label: color ? chalk.bgBlack.white(url) : url,
          nodes: [pkgRef],
        }).trim()

        // stacks all packages together under the same item
        if (seenUrls.has(url)) {
          item = seenUrls.get(url)
          item.label += `, ${pkgRef}`
          return null
        } else
          seenUrls.set(url, item)
      }

      return item
    },

    // puts child nodes back into returned archy
    // output while also filtering out missing items
    leave: (item, children) => {
      if (item)
        item.nodes = children.filter(Boolean)

      return item
    },

    // turns tree-like object return by libnpmfund
    // into children to be properly read by treeverse
    getChildren: (node) =>
      Object.keys(node.dependencies || {})
        .map(key => ({
          name: key,
          ...node.dependencies[key],
        })),
  })

  const res = tree(result)
  return color ? chalk.reset(res) : res
}

async function openFundingUrl ({ path, tree, spec, fundingSourceNumber }) {
  const arg = npa(spec, path)
  const retrievePackageMetadata = () => {
    if (arg.type === 'directory') {
      if (tree.path === arg.fetchSpec) {
        // matches cwd, e.g: npm fund .
        return tree.package
      } else {
        // matches any file path within current arborist inventory
        for (const item of tree.inventory.values()) {
          if (item.path === arg.fetchSpec)
            return item.package
        }
      }
    } else {
      // tries to retrieve a package from arborist inventory
      // by matching resulted package name from the provided spec
      const [item] = [...tree.inventory.query('name', arg.name)]
        .filter(i => semver.valid(i.package.version))
        .sort((a, b) => semver.rcompare(a.package.version, b.package.version))

      if (item)
        return item.package
    }
  }

  const { funding } = retrievePackageMetadata() ||
    await pacote.manifest(arg, npm.flatOptions).catch(() => ({}))

  const validSources = []
    .concat(normalizeFunding(funding))
    .filter(isValidFunding)

  const matchesValidSource =
    validSources.length === 1 ||
    (fundingSourceNumber > 0 && fundingSourceNumber <= validSources.length)

  if (matchesValidSource) {
    const index = fundingSourceNumber ? fundingSourceNumber - 1 : 0
    const { type, url } = validSources[index]
    const typePrefix = type ? `${type} funding` : 'Funding'
    const msg = `${typePrefix} available at the following URL`
    return new Promise((resolve, reject) =>
      openUrl(url, msg, err => err
        ? reject(err)
        : resolve()
      ))
  } else if (validSources.length && !(fundingSourceNumber >= 1)) {
    validSources.forEach(({ type, url }, i) => {
      const typePrefix = type ? `${type} funding` : 'Funding'
      const msg = `${typePrefix} available at the following URL`
      output(`${i + 1}: ${msg}: ${url}`)
    })
    output('Run `npm fund [<@scope>/]<pkg> --which=1`, for example, to open the first funding URL listed in that package')
  } else {
    const noFundingError = new Error(`No valid funding method available for: ${spec}`)
    noFundingError.code = 'ENOFUND'

    throw noFundingError
  }
}

const fund = async (args) => {
  const opts = npm.flatOptions
  const spec = args[0]
  const numberArg = opts.which

  const fundingSourceNumber = numberArg && parseInt(numberArg, 10)

  const badFundingSourceNumber =
    numberArg !== undefined &&
      (String(fundingSourceNumber) !== numberArg || fundingSourceNumber < 1)

  if (badFundingSourceNumber) {
    const err = new Error('`npm fund [<@scope>/]<pkg> [--which=fundingSourceNumber]` must be given a positive integer')
    err.code = 'EFUNDNUMBER'
    throw err
  }

  if (opts.global) {
    const err = new Error('`npm fund` does not support global packages')
    err.code = 'EFUNDGLOBAL'
    throw err
  }

  const where = npm.prefix
  const arb = new Arborist({ ...opts, path: where })
  const tree = await arb.loadActual()

  if (spec) {
    await openFundingUrl({
      path: where,
      tree,
      spec,
      fundingSourceNumber,
    })
    return
  }

  const print = opts.json
    ? printJSON
    : printHuman

  output(
    print(
      getFundingInfo(tree),
      opts
    )
  )
}

module.exports = Object.assign(cmd, { usage, completion })
