# frozen_string_literal: true

require 'progressbar'
require 'delegate'

class Gemfury::Command::App < Thor
  include Gemfury::Command::Authorization
  UserAgent = "Gemfury CLI #{Gemfury::VERSION}"
  PackageExtensions = %w[gem egg tar.gz tgz nupkg].freeze

  # Impersonation
  class_option :as, desc: 'Access an account other than your own'
  class_option :api_token, desc: 'API token to use for commands'
  class_option :no_warnings, hide: true, type: :boolean

  # Make sure we retain the default exit behaviour of 0 even on argument errors
  def self.exit_on_failure?
    false
  end

  map '-v' => :version
  desc 'version', 'Show Gemfury version', hide: true
  def version
    shell.say Gemfury::VERSION
  end

  ### PACKAGE MANAGEMENT ###
  option :public, type: :boolean, desc: 'Create as public package'
  option :quiet, type: :boolean, aliases: '-q', desc: 'Do not show progress bar', default: false
  desc 'push FILE', 'Upload a new version of a package'
  def push(*gems)
    with_checks_and_rescues do
      push_files(:push, gems)
    end
  end

  desc 'list', 'List your packages'
  def list
    with_checks_and_rescues do
      gems = client.list
      shell.say "\n*** GEMFURY PACKAGES ***\n\n"

      va = [%w[name kind version privacy]]
      gems.each do |g|
        va << [g['name'], g['language'],
               g.dig('latest_version', 'version') || 'beta',
               g['private'] ? 'private' : 'public ']
      end

      shell.print_table(va)
    end
  end

  desc 'versions NAME', 'List all the package versions'
  def versions(gem_name)
    with_checks_and_rescues do
      versions = client.versions(gem_name)
      shell.say "\n*** #{gem_name.capitalize} Versions ***\n\n"

      va = []
      va = [%w[version uploaded_by uploaded]]
      versions.each do |v|
        uploaded = time_ago(Time.parse(v['created_at']).getlocal)
        va << [v['version'], v['created_by']['name'], uploaded]
      end

      shell.print_table(va)
    end
  end

  desc 'yank NAME', 'Delete a package version'
  method_options %w[version -v] => :required
  def yank(gem_name)
    with_checks_and_rescues do
      version = options[:version]
      client.yank_version(gem_name, version)
      shell.say "\n*** Yanked #{gem_name}-#{version} ***\n\n"
    end
  end

  ### AUTHENTICATION ###
  desc 'logout', 'Remove Gemfury credentials'
  def logout
    if !has_credentials?
      shell.say 'You are logged out'
    elsif shell.yes? 'Are you sure you want to log out? [yN]'
      with_checks_and_rescues { client.logout }
      wipe_credentials!
      shell.say 'You have been logged out'
    end
  end

  desc 'login', 'Save Gemfury credentials'
  def login
    with_checks_and_rescues do
      me = client.account_info['name']
      shell.say %(You are logged in as "#{me}"), :green
    end
  end

  desc 'whoami', 'Show current user'
  def whoami
    if has_credentials?
      login
    else

      shell.say 'You are not logged in', :green

    end
  end

  desc 'accounts', 'Show info about your Gemfury accounts'
  def accounts
    with_checks_and_rescues do
      accounts = client.accounts

      va = [%w[name kind permission]]
      accounts.each do |a|
        va << [a['name'], a['type'], a['viewer_permission'].downcase]
      end

      shell.print_table(va)
    end
  end

  ### COLLABORATION MANAGEMENT ###
  map 'sharing:add' => 'sharing_add'
  map 'sharing:remove' => 'sharing_remove'

  desc 'sharing', 'List collaborators'
  def sharing
    with_checks_and_rescues do
      account_info = client.account_info
      me = account_info['username']

      collaborators = client.list_collaborators
      if collaborators.empty?
        shell.say %(You (#{me}) are the only collaborator\n), :green
      else
        shell.say %(\n*** Collaborators for "#{me}" ***\n), :green

        va = [%w[username permission]]

        va << [me, 'owner'] if account_info['type'] == 'user'

        collaborators.each { |c| va << [c['username'], c['permission']] }

        shell.print_table(va)
      end
    end
  end

  desc 'sharing:add EMAIL', 'Add a collaborator'
  def sharing_add(username)
    with_checks_and_rescues do
      client.add_collaborator(username)
      shell.say "Invited #{username} as a collaborator"
    end
  end

  desc 'sharing:remove EMAIL', 'Remove a collaborator'
  def sharing_remove(username)
    with_checks_and_rescues do
      client.remove_collaborator(username)
      shell.say "Removed #{username} as a collaborator"
    end
  end

  ### MIGRATION (Pushing directories) ###
  desc 'migrate DIR', 'Upload all packages within a directory'
  def migrate(*paths)
    with_checks_and_rescues do
      gem_paths = Dir.glob(paths.map do |p|
        if File.directory?(p)
          PackageExtensions.map { |ext| "#{p}/**/*.#{ext}" }
        elsif File.file?(p)
          p
        end
      end.flatten.compact)

      if gem_paths.empty?
        die!('Problem: No valid packages found', nil, :migrate)
      else
        shell.say 'Found the following packages:'
        gem_paths.each { |p| shell.say "  #{File.basename(p)}" }
        push_files(:migrate, gem_paths) if shell.yes? 'Upload these files to Gemfury? [yN]', :green
      end
    end
  end

  ### GIT REPOSITORY MANAGEMENT ###
  map 'git:list'    => 'git_list'
  map 'git:reset'   => 'git_reset'
  map 'git:rename'  => 'git_rename'
  map 'git:rebuild' => 'git_rebuild'

  desc 'git:list', 'List Git repositories'
  def git_list
    with_checks_and_rescues do
      repos = client.git_repos['repos']
      shell.say "\n*** GEMFURY GIT REPOS ***\n\n"
      names = repos.map { |r| r['name'] }
      names.sort.each { |n| shell.say(n) }
    end
  end

  desc 'git:rename REPO_NAME NEW_NAME', 'Rename a Git repository'
  def git_rename(repo, new_name)
    with_checks_and_rescues do
      client.git_update(repo, repo: { name: new_name })
      shell.say "Renamed #{repo} repository to #{new_name}\n"
    end
  end

  desc 'git:reset REPO_NAME', 'Remove a Git repository'
  def git_reset(repo)
    with_checks_and_rescues do
      client.git_reset(repo)
      shell.say "\n*** Yanked #{repo} repository ***\n\n"
    end
  end

  desc 'git:rebuild REPO_NAME', 'Rebuild a Git repository'
  method_options %w[revision -r] => :string
  def git_rebuild(repo)
    with_checks_and_rescues do
      params = { revision: options[:revision] }
      shell.say "\n*** Rebuilding #{repo} repository ***\n\n"
      shell.say client.git_rebuild(repo, build: params)
    end
  end

  ### GIT REPOSITORY BUILD CONFIG ###
  map 'git:config'       => 'git_config'
  map 'git:config:set'   => 'git_config_set'
  map 'git:config:unset' => 'git_config_unset'

  desc 'git:config REPO_NAME', "List Git repository's build environment"
  def git_config(repo)
    with_checks_and_rescues do
      vars = client.git_config(repo)['config_vars']
      shell.say "*** #{repo} build config ***\n"
      shell.print_table(vars.map do |kv|
        ["#{kv[0]}:", kv[1]]
      end)
    end
  end

  desc 'git:config:set REPO_NAME KEY=VALUE ...', "Update Git repository's build environment"
  def git_config_set(repo, *vars)
    with_checks_and_rescues do
      updates = vars.to_h { |v| v.split('=', 2) }
      client.git_config_update(repo, updates)
      shell.say "Updated #{repo} build config"
    end
  end

  desc 'git:config:unset REPO_NAME KEY ...', "Remove variables from Git repository's build environment"
  def git_config_unset(repo, *vars)
    with_checks_and_rescues do
      updates = vars.to_h { |v| [v, nil] }
      client.git_config_update(repo, updates)
      shell.say "Updated #{repo} build config"
    end
  end

  private

  def client
    opts = {}
    opts[:user_api_key] = @user_api_key if @user_api_key
    opts[:account] = options[:as] if options[:as]
    client = Gemfury::Client.new(opts)
    client.user_agent = UserAgent
    client
  end

  def with_checks_and_rescues(&block)
    @user_api_key = options[:api_token] if options[:api_token]

    msg = '[DEPRECATED] This CLI is no longer supported. Please upgrade to the new CLI: https://gemfury.com/guide/cli'
    shell.say(msg, :yellow) if !options[:quiet] && !options[:no_warnings] && !current_command_chain.include?(:logout)

    with_authorization(&block)
  rescue Gemfury::InvalidGemVersion => e
    shell.say 'You have a deprecated Gemfury client', :red
    if shell.yes? 'Would you like to update it now? [yN]'
      exec('gem update gemfury')
    else
      shell.say 'No problem. You can also run "gem update gemfury"'
    end
  rescue Gemfury::Conflict => e
    die!('Oops! Locked for another user. Try again later.', e)
  rescue Gemfury::Forbidden => e
    die!("Oops! You're not allowed to access this", e)
  rescue Gemfury::NotFound => e
    die!("Oops! Doesn't look like this exists", e)
  rescue Gemfury::Error => e
    die!('Oops! %s' % e.message, e)
  rescue StandardError => e
    die!('Oops! Something went wrong. Please contact support.', e)
  end

  def push_files(command, gem_paths)
    files = gem_paths.map do |g|
      g.is_a?(String) ? File.new(g) : g
    rescue StandardError
      nil
    end.compact

    files = files.map { |g| ProgressIO.new(g) } if !options[:quiet] && !shell.mute? && $stdout.tty?

    die!('Problem: No valid packages found', nil, command) if files.empty?

    push_options = {}
    push_options[:public] = options[:public] unless options[:public].nil?

    error_ex = nil

    files.each do |file|
      show_bar = file.is_a?(ProgressIO) && file.show_bar?
      title = "Uploading #{File.basename(file.path)} "

      begin
        if show_bar
          begin
            client.push_gem(file, push_options)
          ensure
            shell.say "\e[A\e[0K", nil, false
            shell.say title
          end
        else
          shell.say title
          client.push_gem(file, push_options)
        end

        shell.say '- done'
      rescue Gemfury::CorruptGemFile => e
        shell.say '- problem processing this package', :red
        error_ex = e
      rescue Gemfury::DupeVersion => e
        shell.say '- this version already exists', :red
        error_ex = e
      rescue Gemfury::TimeoutError, Errno::EPIPE => e
        shell.say '- this file is too much to handle', :red
        shell.say '  Visit http://www.gemfury.com/large-package for more info'
        error_ex = e
      rescue Gemfury::Error => e
        shell.say "- #{e.message.downcase}", :red
        error_ex = e
      rescue StandardError => e
        shell.say '- oops', :red
        error_ex = e
      end
    end

    return if error_ex.nil?

    die!('There was a problem uploading at least 1 package', error_ex)
  end

  C50K = 50_000

  class ProgressIO < SimpleDelegator
    attr_reader :content_type, :original_filename, :local_path

    def initialize(filename_or_io, content_type = 'application/octet-stream', fname = nil)
      io = filename_or_io
      local_path = ''

      if io.respond_to? :read
        local_path = filename_or_io.respond_to?(:path) ? filename_or_io.path : 'local.path'
      else
        io = File.open(filename_or_io)
        local_path = filename_or_io
      end

      fname ||= local_path

      @content_type = content_type
      @original_filename = File.basename(fname)
      @local_path = local_path

      filesize = if io.respond_to? :size
                   io.size
                 else
                   io.stat.size
                 end

      if filesize > C50K
        title = 'Uploading %s ' % File.basename(fname)
        @bar = ProgressBar.create(title: title, total: filesize)
      else
        @bar = nil
      end

      super(io)
    end

    def show_bar?
      @bar != nil
    end

    def read(length)
      buf = __getobj__.read(length)
      @bar.progress += buf.bytesize unless @bar.nil? || buf.nil?

      buf
    end
  end

  def die!(msg, err = nil, command = nil)
    shell.say msg, :red
    help(command) if command
    shell.say %(#{err.class.name}: #{err}\n#{err.backtrace.join("\n")}) if err && ENV['DEBUG']
    exit(1)
  end

  def time_ago(tm)
    ago = tm.strftime('%F %R')

    in_secs = Time.now - tm
    if in_secs < 60
      ago += ' (~ %ds ago)' % in_secs
    elsif in_secs < 3600
      ago += ' (~ %sm ago)' % (in_secs / 60).floor
    elsif in_secs < (3600 * 24)
      ago += ' (~ %sh ago)' % (in_secs / 3600).floor
    end

    ago
  end
end
