require 'semantic'
require 'semantic/core_ext'
require 'mixlib/shellout'
require 'highline/import'
require 'git'

module CookbookRelease
  class GitUtilities

    attr_accessor :no_prompt
    attr_reader :sub_dir

    def initialize(options={})
      @tag_prefix = options[:tag_prefix] || ''
      cwd = options[:cwd] || Dir.pwd
      @sub_dir = options[:sub_dir] # if nil takes the cwd
      @shellout_opts = {
        cwd: cwd
      }
      @g = Git.open(cwd)
    end

    def self.git?(dir)
      File.directory?(::File.join(dir, '.git'))
    end

    def self.find_root(dir = Dir.pwd)
      # Do not consider given dir as part of the git repo if not in git hierarchy or dir is not tracked
      return if ::Mixlib::ShellOut.new('git ls-files --error-unmatch .', cwd: dir).run_command.error?

      cmd = Mixlib::ShellOut.new("git rev-parse --show-toplevel", cwd: dir)
      cmd.run_command
      cmd.error? ? nil : cmd.stdout.chomp
    end

    def reset_command(new_version)
      remote = choose_remote
      "git tag -d #{new_version} ; git push #{remote} :#{new_version}"
    end

    def clean_index?
      clean_index = Mixlib::ShellOut.new("git diff --exit-code", @shellout_opts)
      clean_index.run_command
      clean_staged = Mixlib::ShellOut.new("git diff --exit-code --cached", @shellout_opts)
      clean_staged.run_command
      !clean_index.error? && !clean_staged.error?
    end

    def clean_index!
      raise "All changes must be committed!" unless clean_index?
    end

    def _compute_last_release
      tag = Mixlib::ShellOut.new([
        'git describe',
        "--abbrev=0",
        "--tags",
        "--match \"#{@tag_prefix}[0-9]*\.[0-9]*\.[0-9]*\""
      ].join(" "), @shellout_opts)
      tag.run_command
      rel = tag.stdout.sub(/^#{@tag_prefix}/, '').chomp
      rel.empty? ? nil : rel
    end

    def has_any_release?
      !!_compute_last_release
    end

    def compute_last_release
      last = _compute_last_release
      unless last
        $stderr.puts "No last release found, defaulting to 0.1.0"
        last = '0.1.0'
      end
      last.to_version
    end

    def compute_changelog(since, short_sha = true)
      ref = "#{@tag_prefix}#{since}"
      @g.log(500).object(@sub_dir).between(ref, 'HEAD').map do |commit|
        message = commit.message.lines.map(&:chomp).compact.delete_if(&:empty?)
        Commit.new(
          author: commit.author.name,
          email: commit.author.email,
          subject: message.delete_at(0),
          hash: short_sha ? commit.sha[0,7] : commit.sha,
          body: message.empty? ? nil : message.join("\n"),
          is_merge_commit: commit.parents.length > 1,
          nodes_only: @g.diff(commit.sha, "#{commit.sha}~1").name_status.reject do |path, change_type|
            path.include?('nodes/') && ['A', 'D'].include?(change_type) # basic node additions or deletions only
          end.count.zero?
        )
      end.reject { |commit| commit[:is_merge_commit] }
    end

    def tag(version)
      @g.add_tag("#{@tag_prefix}#{version}")
    end

    def choose_remote
      cmd = Mixlib::ShellOut.new("git remote", @shellout_opts)
      cmd.run_command
      cmd.error!
      remotes = cmd.stdout.split("\n")
      if remotes.size == 1 || @no_prompt
        puts "Choosing remote #{remotes.first}" if @no_prompt
        remotes.first
      else
        choose(*remotes)
      end
    end

    def push_tag(version)
      remote = choose_remote
      cmd = Mixlib::ShellOut.new("git push #{remote} #{@tag_prefix}#{version}", @shellout_opts)
      cmd.run_command
      cmd.error!
    end
  end
end
