#!/usr/bin/env ruby
# SugarJar

require 'optparse'
require 'mixlib/shellout'
require_relative '../lib/sugarjar/commands'
require_relative '../lib/sugarjar/config'
require_relative '../lib/sugarjar/log'
require_relative '../lib/sugarjar/util'
require_relative '../lib/sugarjar/version'

SugarJar::Log.level = Logger::INFO

# Don't put defaults here, put them in SugarJar::Config - otherwise
# these defaults overwrite whatever is in config files.
options = { 'color' => true }
# If ENV['SUGARJAR_DEBUG'] is set, it overrides the config file,
# but not the command line options, so set that one here. Also
# start the logger at that level, in case we are debugging option loading
# itself
if ENV['SUGARJAR_LOGLEVEL']
  options['log_level'] = SugarJar::Log.level = ENV['SUGARJAR_LOGLEVEL'].to_sym
end
parser = OptionParser.new do |opts|
  opts.banner = 'Usage: sj <command> [<args>] [<options>]'

  opts.separator ''
  opts.separator 'Command, args, and options, can appear in any order.'
  opts.separator ''
  opts.separator 'OPTIONS:'

  opts.on('--[no-]fallthru', 'Fall-thru to git. [default: true]') do |fallthru|
    options['fallthru'] = fallthru
  end

  opts.on('--feature-prefix', 'Prefix to use for feature branches') do |prefix|
    options['feature_prefix'] = prefix
  end

  opts.on(
    '--github-cli CLI',
    %w{gh cli},
    'Github CLI to use ("gh" or "hub" or "auto"). Auto (the default) will ' +
    'prefer "gh" if it is available but will fall back to "hub." ' +
    '[default: "auto"]',
  ) do |cli|
    options['github_cli'] = cli
  end

  opts.on(
    '--github-host HOST',
    'The host for "hub". Note that we will set this in the local repo ' +
    'config so there is no need to have multiple config files for multiple ' +
    'github servers. Put your default one in your config file, and simply ' +
    'specify this option the first time you clone or touch a repo and it ' +
    'will be part of that repo until changed.',
  ) do |host|
    options['github_host'] = host
  end

  opts.on('--github-user USER', 'Github username') do |user|
    options['github_user'] = user
  end

  opts.on('-h', '--help', 'Print this help message') do
    puts opts
    exit
  end

  opts.on(
    '--ignore-dirty',
    'Tell command that check for a dirty repo to carry on anyway. ' +
    '[default: false]',
  ) do
    options['ignore_dirty'] = true
  end

  opts.on(
    '--ignore-prerun-failure',
    'Ignore preprun failure on *push commands. [default: false]',
  ) do
    options['ignore_prerun_failure'] = true
  end

  opts.on(
    '--log-level LEVEL',
    'Set logging level (fatal, error, warning, info, debug, trace). This can ' +
    'also be set via the SUGARJAR_LOGLEVEL environment variable. [default: ' +
    'info]',
  ) do |level|
    options['log_level'] = level
  end

  opts.on(
    '--[no-]pr-autofill',
    'When creating a PR, auto fill the title & description from the top ' +
    'commit if we are using "gh". [default: true]',
  ) do |autofill|
    options['pr_autofill'] = autofill
  end

  opts.on(
    '--[no-]pr-autostack',
    'When creating a PR, if this is a subfeature, should we make it a ' +
    'PR on the PR for the parent feature. If not specified, we prompt ' +
    'when this happens, when true always do this, when false never do ' +
    'this. Only applicable when usiing "gh" and on branch-based PRs.',
  ) do |autostack|
    options['pr_autostack'] = autostack
  end

  opts.on('--[no-]use-color', 'Enable color. [default: true]') do |color|
    options['color'] = color
  end

  opts.on('--version') do
    puts SugarJar::VERSION
    exit
  end

  # rubocop:disable Layout/HeredocIndentation
  opts.separator <<COMMANDS

COMMANDS:
  amend
              Amend the current commit. Alias for "git commit --amend".
              Accepts other arguments such as "-a" or files.

  amendq, qamend
              Same as "amend" but without changing the message. Alias for
              "git commit --amend --no-edit".

  bclean [<branch>]
              If safe, delete the current branch (or the specified branch).
              Unlike "git branch -d", bclean can handle squash-merged branches.
              Think of it as a smarter "git branch -d".

  bcleanall
              Walk all branches, and try to delete them if it's safe. See
              "bclean" for details.

  binfo
              Verbose information about the current branch.

  br
              Verbose branch list. An alias for "git branch -v".

  feature, f <branch_name>
              Create a "feature" branch. It's morally equivalent to
              "git checkout -b" except it defaults to creating it based on
              some form of 'master' instead of your current branch. In order
              of preference it will be upstream/master, origin/master, master,
              depending upon what remotes are available.

              Note that you can specify "--feature-prefix" (or add
              "feature_prefix" to your config) to have all features created
              with a prefix. This is useful for branch-based workflows where
              developers are expected to create branches names that, for
              example, start with their username.

  forcepush, fpush
              The same as "smartpush", but uses "--force-with-lease". This is
              a "safer" way of doing force-pushes and is the recommended way
              to push after rebasing or amending. Never do this to shared
              branches. Very convenient for keeping the branch behind a pull-
              request clean.

  lint
              Run any linters configured in .sugarjar.yaml.

  pullsuggestions, ps
              Pull any suggestions *that have been committed* in the GitHub UI.
              This will show the diff and prompt for confirmation before
              merging. Note that a fast-forward merge will be used.

  smartclone, sclone
              A smart wrapper to "git clone" that handles forking and managing
              remotes for you.
              It will clone a git repository using hub-style short name
              ("$org/$repo"). If the org of the repository is not the same
              as your github-user then it will fork the repo for you to
              your account (if not already done) and then setup your remotes
              so that "origin" is your fork and "upstream" is the upstream.

  smartlog, sl
              Inspired by Facebook's "sl" extension to Mercurial, this command
              will show you a tree of all your local branches relative to your
              upstream.

  smartpullrequest, smartpr, spr
              A smart wrapper to "hub pull-request" that checks if your repo
              is dirty before creating the pull request.

  smartpush, spush
              A smart wrapper to "git push" that runs whatever is defined in
              "on_push" in .sugarjar.yml, and only pushes if they succeed.

  subfeature, sf <feature>
              An alias for 'sj feature <feature> <current_branch>'

  unit
              Run any unitests configured in .sugarjar.yaml.

  up [<branch>]
              Rebase the current branch (or specified branch) intelligently.
              In most causes this will check for a main (or master) branch on
              upstream, then origin. If a branch explicitly tracks something
              else, then that will be used, instead.

  upall
              Same as "up", but for all branches.

  version
              Print the version of sugarjar, and then run 'hub version'
              to show the hub and git versions.

Be sure to checkout Sapling (https://sapling-scm.com/)! SugarJar was written as
a stop-gap to get Sapling features before it was open-sourced, and as such
Sapling may serve your needs even better.
COMMANDS

  # rubocop:enable Layout/HeredocIndentation
end

# we make a copy of these because we will assign back to the ARGV
# we parse later. We also need a pristine copy in case we want to
# run git as we were called.
argv_copy = ARGV.dup

# We don't have options yet, but we need an instance of SJ in order
# to list public methods. We will recreate it
sj = SugarJar::Commands.new(options.merge({ 'no_change' => true }))
extra_opts = []

# as with above, this can't go into 'options', until after we parse
# the command line args
config = SugarJar::Config.config

valid_commands = sj.public_methods - Object.public_methods
possible_valid_command = ARGV.any? do |arg|
  valid_commands.include?(arg.to_s.to_sym)
end

# if we're configured to fall thru and the subcommand isn't one
# we recognize, don't parse the options as they may be different
# than git's. For example `git config -l` - we error because we
# require an arguement to `-l`.
if config['fallthru'] && !possible_valid_command
  SugarJar::Log.debug(
    'Skipping option parsing: fall-thru is set and we do not recognize ' +
    'any subcommands',
  )
else
  SugarJar::Log.debug(
    'We MIGHT have a valid command... parse-command line options',
  )
  # We want to allow people to pass in extra args to be passed to
  # git commands, but OptionParser doesn't easily allow this. So we
  # loop over it, catching exceptions.
  begin
    # HOWEVER, anytime it throws an exception, for some reason, it clears
    # out all of ARGV, or whatever you passed to as ARGV.
    #
    # This not only prevents further parsing, but also means we lose
    # any non-option arguements (like the subcommand!)
    #
    # So we save a copy, and if we throw an exception, save the option that
    # caused it, remove that option from our copy, and then re-populate argv
    # with what's left.
    #
    # By doing this we not only get to parse all the options properly and
    # save unknown ones, but non-option arguements, which OptionParser
    # normally leaves in ARGV stay in ARGV.
    saved_argv = argv_copy.dup
    parser.parse!(argv_copy)
  rescue OptionParser::InvalidOption => e
    SugarJar::Log.debug("Saving unknown argument #{e.args}")
    extra_opts += e.args

    # e.args is an array, but it's only ever one arguement per exception
    saved_argv.delete(e.args.first)
    argv_copy = saved_argv.dup
    SugarJar::Log.debug(
      "Continuing option parsing with remaining ARGV: #{argv_copy}",
    )
    retry
  end
end

subcommand = argv_copy.reject { |x| x.start_with?('-') }.first

if ARGV.empty? || !subcommand
  puts parser
  exit
end

options = config.merge(options)

# Recreate SJ with all of our options
SugarJar::Log.level = options['log_level'].to_sym if options['log_level']
sj = SugarJar::Commands.new(options)

is_valid_command = valid_commands.include?(subcommand.to_sym)
argv_copy.delete(subcommand)
SugarJar::Log.debug("subcommand is #{subcommand}")

# Extra options we got, plus any left over arguements are what we
# pass to Commands so they can be passed to git as necessary
extra_opts += argv_copy
SugarJar::Log.debug("extra unknown options: #{extra_opts}")

if subcommand == 'help'
  puts parser
  exit
end

if is_valid_command
  SugarJar::Log.debug(
    "running #{subcommand}; extra opts: #{extra_opts.join(', ')}",
  )
  sj.send(subcommand.to_sym, *extra_opts)
elsif options['fallthru']
  SugarJar::Log.debug("Falling thru to: hub #{ARGV.join(' ')}")
  if options['github_cli'] == 'hub'
    exec('hub', *ARGV)
  else
    # If we're using 'gh', it doesn't have 'git fall thru' support, so
    # we pass thru directly to 'git'
    exec('git', *ARGV)
  end
else
  SugarJar::Log.error("No such subcommand: #{subcommand}")
end
