require 'grit'
require 'pathname'

module Socialcast
  module Gitx
    module Git
      private
      def assert_not_protected_branch!(branch, action)
        raise "Cannot #{action} reserved branch" if reserved_branch?(branch)
      end

      def assert_in_last_known_good_staging(branch)
        branches_in_last_known_staging = branches(:remote => true, :merged => "origin/#{last_known_good_staging_branch}")
        unless branches_in_last_known_staging.include? branch
          raise "Cannot release #{branch} unless it has already been promoted separately to #{staging_branch} and the build has passed."
        end
      end

      # lookup the current branch of the PWD
      def current_branch
        repo = Grit::Repo.new(Dir.pwd)
        Grit::Head.current(repo).name
      end

      # lookup the current repository of the PWD
      # ex: git@github.com:socialcast/socialcast-git-extensions.git OR https://github.com/socialcast/socialcast-git-extensions.git
      def current_repo
        repo = `git config -z --get remote.origin.url`.strip
        repo.gsub(/\.git$/,'').split(/[:\/]/).last(2).join('/')
      end

      # @returns [String] github username (ex: 'wireframe') of the current github.user
      # @returns empty [String] when no github.user is set on the system
      def current_user
        `git config -z --global --get github.user`.strip
      end

      def backport_to(branch, shas)
        run_cmd "git checkout #{base_branch}"
        run_cmd "git checkout -b #{branch}"
        begin
          run_cmd "git cherry-pick #{shas.join(' ')}"
        rescue
          while true
            proceed = $terminal.ask "Error during cherry-pick.  You can proceed by resolving the conflicts and using 'git cherry-pick --continue' to finish the cherry-pick in another terminal. Would you like to proceed (y/n)?"
            if proceed.to_s.downcase == 'n'
              run_cmd "git cherry-pick --abort"
              exit 1
            elsif proceed.to_s.downcase == 'y'
              break
            else
              say "Invalid response"
            end
          end
        end
        run_cmd "git push origin HEAD"
      end

      # retrieve a list of branches
      def branches(options = {})
        branches = []
        args = []
        args << '-r' if options[:remote]
        args << "--merged #{options[:merged].is_a?(String) ? options[:merged] : ''}" if options[:merged]
        output = `git branch #{args.join(' ')}`.split("\n")
        output.each do |branch|
          branch = branch.gsub(/\*/, '').strip.split(' ').first
          branch = branch.split('/').last if options[:remote]
          branches << branch unless reserved_branch?(branch)
        end
        branches.uniq
      end

      # reset the specified branch to the same set of commits as the destination branch
      # reverts commits on aggregate branches back to a known good state
      # returns list of branches that were removed
      def nuke_branch(branch, head_branch)
        return [] if branch == head_branch
        raise "Only aggregate branches are allowed to be reset: #{aggregate_branches}" unless aggregate_branch?(branch)
        say "Resetting "
        say "#{branch} ", :green
        say "branch to "
        say head_branch, :green

        run_cmd "git checkout #{base_branch}"
        refresh_branch_from_remote head_branch
        removed_branches = branches(:remote => true, :merged => "origin/#{branch}") - branches(:remote => true, :merged => "origin/#{head_branch}")
        run_cmd "git branch -D #{branch}" rescue nil
        run_cmd "git push origin --delete #{branch}" rescue nil
        run_cmd "git checkout -b #{branch}"
        share_branch branch
        run_cmd "git checkout #{base_branch}"

        removed_branches
      end

      # share the local branch in the remote repo
      def share_branch(branch)
        run_cmd "git push origin #{branch}"
        track_branch branch
      end

      def track_branch(branch)
        run_cmd "git branch --set-upstream #{branch} origin/#{branch}"
      end

      # integrate a branch into a destination aggregate branch
      # blow away the local aggregate branch to ensure pulling into most recent "clean" branch
      def integrate_branch(branch, destination_branch)
        assert_not_protected_branch!(branch, 'integrate') unless aggregate_branch?(destination_branch)
        unless aggregate_branch?(destination_branch) || [base_branch, Socialcast::Gitx::DEFAULT_BASE_BRANCH].include?(destination_branch)
          raise "Only aggregate branches are allowed for integration: #{aggregate_branches}"
        end
        say "Integrating "
        say "#{branch} ", :green
        say "into "
        say destination_branch, :green

        refresh_branch_from_remote destination_branch
        run_cmd "git pull . #{branch}"
        run_cmd "git push origin HEAD"
        run_cmd "git checkout #{branch}"
      end

      # nuke local branch and pull fresh version from remote repo
      def refresh_branch_from_remote(destination_branch)
        run_cmd "git branch -D #{destination_branch}" rescue nil
        run_cmd "git fetch origin"
        run_cmd "git checkout #{destination_branch}"
      end

      def aggregate_branch?(branch)
        aggregate_branches.include?(branch) || branch.starts_with?('last_known_good')
      end

      def reserved_branch?(branch)
        aggregate_branch?(branch) || reserved_branches.include?(branch)
      end

      # build a summary of changes
      def changelog_summary(branch)
        changes = `git diff --stat origin/#{base_branch}...#{branch}`.split("\n")
        stats = changes.pop
        if changes.length > 5
          dirs = changes.map do |file_change|
            filename = "#{file_change.split.first}"
            dir = filename.gsub(/\/[^\/]+$/, '')
            dir
          end
          dir_counts = Hash.new(0)
          dirs.each {|dir| dir_counts[dir] += 1 }
          changes = dir_counts.to_a.sort_by {|k,v| v}.reverse.first(5).map {|k,v| "#{k} (#{v} file#{'s' if v > 1})"}
        end
        (changes + [stats]).join("\n")
      end

      # launch configured editor to retreive message/string
      def editor_input(initial_text = '')
        require 'tempfile'
        Tempfile.open('reviewrequest.md') do |f|
          f << initial_text
          f.flush

          editor = ENV['EDITOR'] || 'vi'
          flags = case editor
          when 'mate', 'emacs'
            '-w'
          when 'mvim'
            '-f'
          else
            ''
          end
          pid = fork { exec "#{editor} #{flags} #{f.path}" }
          Process.waitpid(pid)
          description = File.read(f.path)
          description.gsub(/^\#.*/, '').chomp.strip
        end
      end

      # load SC Git Extensions Config YAML
      # @returns [Hash] of configuration options from YAML file (if it exists)
      def config
        @config ||= begin
          if config_file.exist?
            YAML.load_file(config_file)
          else
            {}
          end
        end
      end

      # @returns a [Pathname] for the scgitx.yml Config File
      # from either ENV['SCGITX_CONFIG_PATH'] or default $PWD/config/scgitx.yml
      def config_file
        Pathname((ENV['SCGITX_CONFIG_PATH'] || ([Dir.pwd, '/config/scgitx.yml']).join))
      end

      # load Review buddies from the SCGITX Configuration YML
      # @returns [Hash] of review buddy mapping from Config YML (ex: {'wireframe' => {'socialcast_username' => 'RyanSonnek', 'buddy' => 'vanm'}})
      def review_buddies
        config['review_buddies'] || {}
      end

      def specialty_reviewers
        config['specialty_reviewers'] || {}
      end

      def base_branch
        ENV['BASE_BRANCH'] || config['base_branch'] || Socialcast::Gitx::DEFAULT_BASE_BRANCH
      end

      def staging_branch
        config['staging_branch'] || Socialcast::Gitx::DEFAULT_STAGING_BRANCH
      end

      def last_known_good_staging_branch
        config['last_known_good_staging_branch'] || Socialcast::Gitx::DEFAULT_LAST_KNOWN_GOOD_STAGING_BRANCH
      end

      def prototype_branch
        config['prototype_branch'] || Socialcast::Gitx::DEFAULT_PROTOTYPE_BRANCH
      end

      def aggregate_branches
        @aggregate_branches ||= [staging_branch, prototype_branch]
      end

      def reserved_branches
        @reserved_branches ||= %w{ HEAD master next_release } + [base_branch, config['reserved_branches'], config['base_branch']].flatten.compact.uniq + aggregate_branches
      end
    end
  end
end