# encoding: utf-8
require "pluginmanager/command"
require "jar-dependencies"
require "jar_install_post_install_hook"
require "file-dependencies/gem"

class LogStash::PluginManager::Update < LogStash::PluginManager::Command
  REJECTED_OPTIONS = [:path, :git, :github]
  # These are local gems used by LS and needs to be filtered out of other plugin gems
  NON_PLUGIN_LOCAL_GEMS = ["logstash-core", "logstash-core-event-java", "logstash-core-plugin-api"]

  parameter "[PLUGIN] ...", "Plugin name(s) to upgrade to latest version", :attribute_name => :plugins_arg
  option "--[no-]verify", :flag, "verify plugin validity before installation", :default => true
  option "--local", :flag, "force local-only plugin update. see bin/logstash-plugin package|unpack", :default => false

  def execute
    # Turn off any jar dependencies lookup when running with `--local`
    ENV["JARS_SKIP"] = "true" if local?

    # remove "system" local gems used by LS
    local_gems = gemfile.locally_installed_gems.map(&:name) - NON_PLUGIN_LOCAL_GEMS

    if local_gems.size > 0
      if update_all?
        plugins_with_path = local_gems
      else
        plugins_with_path = plugins_arg & local_gems
      end

      warn_local_gems(plugins_with_path) if plugins_with_path.size > 0
    end
    update_gems!
  end

  private
  def update_all?
    plugins_arg.size == 0
  end

  def warn_local_gems(plugins_with_path)
    puts("Update is not supported for manually defined plugins or local .gem plugin installations, skipping: #{plugins_with_path.join(", ")}")
  end

  def update_gems!
    # If any error is raise inside the block the Gemfile will restore a backup of the Gemfile
    previous_gem_specs_map = find_latest_gem_specs

    # remove any version constrain from the Gemfile so the plugin(s) can be updated to latest version
    # calling update without requiremend will remove any previous requirements
    plugins = plugins_to_update(previous_gem_specs_map)
    # Skipping the major version validation when using a local cache as we can have situations
    # without internet connection.
    filtered_plugins = plugins.map { |plugin| gemfile.find(plugin) }
      .compact
      .reject { |plugin| REJECTED_OPTIONS.any? { |key| plugin.options.has_key?(key) } }
      .each   { |plugin| gemfile.update(plugin.name) }

    # force a disk sync before running bundler
    gemfile.save

    puts("Updating #{filtered_plugins.collect(&:name).join(", ")}") unless filtered_plugins.empty?

    # any errors will be logged to $stderr by invoke!
    # Bundler cannot update and clean gems in one operation so we have to call the CLI twice.
    options = {:update => plugins, :rubygems_source => gemfile.gemset.sources}
    options[:local] = true if local?
    output = LogStash::Bundler.invoke!(options)
    # We currently dont removed unused gems from the logstash installation
    # see: https://github.com/elastic/logstash/issues/6339
    # output = LogStash::Bundler.invoke!(:clean => true)
    display_updated_plugins(previous_gem_specs_map)
  rescue => exception
    gemfile.restore!
    report_exception("Updated Aborted", exception)
  ensure
    display_bundler_output(output)
  end

  # create list of plugins to update
  def plugins_to_update(previous_gem_specs_map)
    if update_all?
      previous_gem_specs_map.values.map{|spec| spec.name}
    else
      # If the plugins isn't available in the gemspec or in 
      # the gemfile defined with a local path, we assume the plugins is not
      # installed.
      not_installed = plugins_arg.select{|plugin| !previous_gem_specs_map.has_key?(plugin.downcase) && !gemfile.find(plugin) }
      signal_error("Plugin #{not_installed.join(', ')} is not installed so it cannot be updated, aborting") unless not_installed.empty?
      plugins_arg
    end
  end

  # We compare the before the update and after the update
  def display_updated_plugins(previous_gem_specs_map)
    update_count = 0
    find_latest_gem_specs.values.each do |spec|
      name = spec.name.downcase
      if previous_gem_specs_map.has_key?(name)
        if spec.version != previous_gem_specs_map[name].version
          puts("Updated #{spec.name} #{previous_gem_specs_map[name].version.to_s} to #{spec.version.to_s}")
          update_count += 1
        end
      else
        puts("Installed #{spec.name} #{spec.version.to_s}")
        update_count += 1
      end
    end

    puts("No plugin updated") if update_count.zero?
  end

  # retrieve only the latest spec for all locally installed plugins
  # @return [Hash] result hash {plugin_name.downcase => plugin_spec}
  def find_latest_gem_specs
    LogStash::PluginManager.all_installed_plugins_gem_specs(gemfile).inject({}) do |result, spec|
      previous = result[spec.name.downcase]
      result[spec.name.downcase] = previous ? [previous, spec].max_by{|s| s.version} : spec
      result
    end
  end
end
