#!/usr/bin/env node
import { resolve, extname } from 'path'
import { pathToFileURL } from 'url'
import {
  createReadStream,
  createWriteStream
} from 'fs'
import { readFile } from 'fs/promises'
import addStream from 'add-stream'
import tempfile from 'tempfile'
import meow from 'meow'
import conventionalChangelog from 'conventional-changelog'

function relativeResolve (filePath) {
  return pathToFileURL(resolve(process.cwd(), filePath))
}

async function loadDataFile (filePath) {
  const resolvedFilePath = relativeResolve(filePath)
  const ext = extname(resolvedFilePath.toString())

  if (ext === '.json') {
    return JSON.parse(await readFile(resolvedFilePath, 'utf8'))
  }

  return (await import(resolvedFilePath)).default
}

const cli = meow(`
    Usage
      conventional-changelog

    Example
      conventional-changelog -i CHANGELOG.md --same-file

    Options
      -i, --infile              Read the CHANGELOG from this file

      -o, --outfile             Write the CHANGELOG to this file
                                If unspecified, it prints to stdout

      -s, --same-file           Outputting to the infile so you don't need to specify the same file as outfile

      -p, --preset              Name of the preset you want to use. Must be one of the following:
                                angular, atom, codemirror, conventionalcommits, ember, eslint, express, jquery or jshint

      -k, --pkg                 A filepath of where your package.json is located
                                Default is the closest package.json from cwd

      -a, --append              Should the newer release be appended to the older release
                                Default: false

      -r, --release-count       How many releases to be generated from the latest
                                If 0, the whole changelog will be regenerated and the outfile will be overwritten
                                Default: 1

      --skip-unstable           If given, unstable tags will be skipped, e.g., x.x.x-alpha.1, x.x.x-rc.2

      -u, --output-unreleased   Output unreleased changelog

      -v, --verbose             Verbose output. Use this for debugging
                                Default: false

      -n, --config              A filepath of your config script
                                Example of a config script: https://github.com/conventional-changelog/conventional-changelog/blob/master/packages/conventional-changelog-cli/test/fixtures/config.cjs

      -c, --context             A filepath of a json that is used to define template variables
      -l, --lerna-package       Generate a changelog for a specific lerna package (:pkg-name@1.0.0)
      -t, --tag-prefix          Tag prefix to consider when reading the tags
      --commit-path             Generate a changelog scoped to a specific directory
`, {
  importMeta: import.meta,
  booleanDefault: undefined,
  flags: {
    infile: {
      shortFlag: 'i',
      type: 'string'
    },
    outfile: {
      shortFlag: 'o',
      type: 'string'
    },
    sameFile: {
      shortFlag: 's',
      type: 'boolean'
    },
    preset: {
      shortFlag: 'p',
      type: 'string'
    },
    pkg: {
      shortFlag: 'k',
      type: 'string'
    },
    append: {
      shortFlag: 'a',
      type: 'boolean'
    },
    releaseCount: {
      shortFlag: 'r',
      type: 'number'
    },
    skipUnstable: {
      type: 'boolean'
    },
    outputUnreleased: {
      shortFlag: 'u',
      type: 'boolean'
    },
    verbose: {
      shortFlag: 'v',
      type: 'boolean'
    },
    config: {
      shortFlag: 'n',
      type: 'string'
    },
    context: {
      shortFlag: 'c',
      type: 'string'
    },
    lernaPackage: {
      shortFlag: 'l',
      type: 'string'
    },
    tagPrefix: {
      shortFlag: 't',
      type: 'string'
    }
  }
})

let config
const flags = cli.flags
const infile = flags.infile
let outfile = flags.outfile
let sameFile = flags.sameFile
const append = flags.append
const releaseCount = flags.releaseCount
const skipUnstable = flags.skipUnstable

if (infile && infile === outfile) {
  sameFile = true
} else if (sameFile) {
  if (infile) {
    outfile = infile
  } else {
    console.error('infile must be provided if same-file flag presents.')
    process.exit(1)
  }
}

let options = {
  preset: flags.preset,
  pkg: {
    path: flags.pkg
  },
  append,
  releaseCount,
  skipUnstable,
  outputUnreleased: flags.outputUnreleased,
  lernaPackage: flags.lernaPackage,
  tagPrefix: flags.tagPrefix
}

if (flags.verbose) {
  options.debug = console.info.bind(console)
  options.warn = console.warn.bind(console)
}

let templateContext

let outStream

try {
  if (flags.context) {
    templateContext = await loadDataFile(flags.context)
  }

  if (flags.config) {
    config = await loadDataFile(flags.config)
    options.config = config

    if (config.options) {
      options = {
        ...options,
        ...config.options,
        pkg: {
          ...options.pkg,
          ...config.options.pkg
        }
      }
    }
  } else {
    config = {}
  }
} catch (err) {
  console.error('Failed to get file. ' + err)
  process.exit(1)
}

const gitRawCommitsOpts = {
  ...config.gitRawCommitsOpts
}
if (flags.commitPath) gitRawCommitsOpts.path = flags.commitPath

const changelogStream = conventionalChangelog(options, templateContext, gitRawCommitsOpts, config.parserOpts, config.writerOpts)
  .on('error', (err) => {
    if (flags.verbose) {
      console.error(err.stack)
    } else {
      console.error(err.toString())
    }
    process.exit(1)
  })

function noInputFile () {
  if (outfile) {
    outStream = createWriteStream(outfile)
  } else {
    outStream = process.stdout
  }

  changelogStream
    .pipe(outStream)
}

if (infile && releaseCount !== 0) {
  const readStream = createReadStream(infile)
    .on('error', () => {
      if (flags.verbose) {
        console.warn('infile does not exist.')
      }

      if (sameFile) {
        noInputFile()
      }
    })

  if (sameFile) {
    if (options.append) {
      changelogStream
        .pipe(createWriteStream(outfile, {
          flags: 'a'
        }))
    } else {
      const tmp = tempfile()

      changelogStream
        .pipe(addStream(readStream))
        .pipe(createWriteStream(tmp))
        .on('finish', () => {
          createReadStream(tmp)
            .pipe(createWriteStream(outfile))
        })
    }
  } else {
    if (outfile) {
      outStream = createWriteStream(outfile)
    } else {
      outStream = process.stdout
    }

    let stream

    if (options.append) {
      stream = readStream
        .pipe(addStream(changelogStream))
    } else {
      stream = changelogStream
        .pipe(addStream(readStream))
    }

    stream
      .pipe(outStream)
  }
} else {
  noInputFile()
}
