#!/usr/bin/ruby
# encoding: utf-8
#
# Copyright © 2013-2016, Lucas Nussbaum <lucas@debian.org>
# Copyright © 2015-2016, Tomasz Nitecki <tnnn@tnnn.pl>
# 
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

# Exit with a return code 0 (we don't want break apt/dpkg even if an error occurred).
Signal.trap('EXIT') { exit 0 }

# Gracefully handle SIGPIPE.
Signal.trap('SIGPIPE', 'SYSTEM_DEFAULT')

require 'pp'
require 'debian'
require 'net/http'
require 'zlib'
require 'stringio'
require 'json'
require 'optparse'
require 'fileutils'
require 'time'
require 'etc'

include Debian

ENV['LC_ALL'] = 'C.UTF-8'

$quiet = false
$all = false
$old = false
$kas = false
$apt = false
$show = []
$proxy_url = ENV['HTTP_PROXY'] || ENV['http_proxy']

optparse = OptionParser.new do |opts|
  opts.on('-h', '--help', 'show help') do
    puts opts
    puts "\nAllowed types for '--show' option (see manual for more information):"
    puts "\twnpp, newcomer, help, no-testing, testing-autorm, rfs, O, RFA, RFH, ITA, pseudo-package"
    puts "\nTo see opportunities suitable for newcomers you should run how-can-i-help as:"
    puts "\thow-can-i-help --old --show newcomer"
    puts "\nYou can check manual for a more detailed description:"
    puts "\tman how-can-i-help"
    exit(0)
  end

  opts.on('-v', '--version', 'show version') do
    puts "Please use, e.g., 'dpkg -l how-can-i-help'"
    exit(0)
  end

  opts.on('-a', '--all', 'show new opportunities for contribution to all available Debian packages') do
    $all = true
  end

  opts.on('-o', '--old', 'show opportunities that were already shown before (will also show the new ones)') do
    $old = true
  end

  opts.on('-q', '--quiet', 'do not display header and footer') do
    $quiet = true
  end

  opts.on('-j', '--json', 'display output in JSON format') do
    $kas = true
  end

  opts.on('-p', '--apt', 'always exit with code 0; by default run by apt hook to prevent apt failures') do
    $apt = true
  end

  opts.on('-s', '--show <type>...', Array,
          'show only specific types of opportunities (types have to be separated by commas)') do |show|
    $show = show
  end

end
optparse.parse!

# If we are not in apt mode, return to normal exit code handling.
unless $apt
  Signal.trap('EXIT') {}
end

HOME=Dir.home rescue Etc.getpwuid.dir

# Define application parameters.
unless HOME
  puts 'how-can-i-help: Unable to resolve your $HOME directory - cannot continue.'
  exit(1)
end

unless Dir.exists?(HOME)
  puts 'how-can-i-help: Your $HOME seems to point to a nonexistent location - cannot continue.'
  exit(1)
end

HELPITEMS_URL = 'https://udd.debian.org/how-can-i-help.json.gz'
CACHEDIR = "#{HOME}/.cache/how-can-i-help"
CONFIGDIR = "#{HOME}/.config/how-can-i-help"
SEEN_LOCAL = "#{CACHEDIR}/seen.json"
CACHE = "#{CACHEDIR}/how-can-i-help.json.gz"
PACKAGES = "#{CONFIGDIR}/packages"
IGNORED_TYPES = "#{CONFIGDIR}/ignored"

def system_r(s)
  system(s) or raise
end

FileUtils.mkdir_p CACHEDIR unless File.exists?(CACHEDIR)

if File::exists?(SEEN_LOCAL)
  seen = JSON::parse(IO::read(SEEN_LOCAL))
else
  seen = []
end

# Cache request data
uri = URI(HELPITEMS_URL)
request = Net::HTTP::Get.new(uri.request_uri)
if File::exists?(CACHE)
  stat = File.stat CACHE
  request['If-Modified-Since'] = stat.mtime.httpdate
end

# Dealing with proxy, authenticated or not
proxy_uri = $proxy_url.nil? ? OpenStruct.new : URI.parse($proxy_url)
proxy_user, proxy_pass = proxy_uri.userinfo.split(/:/) if proxy_uri.userinfo
http_object = Net::HTTP.new(uri.host, uri.port, proxy_uri.host, proxy_uri.port,
                     proxy_user, proxy_pass)
http_object.use_ssl = uri.scheme == 'https'
# proceeding get_response

begin
  response = http_object.request(request)
rescue
  puts "how-can-i-help: Error downloading data file: #{$!}"
  exit(1)
end
open CACHE, 'w' do |cc|
  cc.write response.body.to_s
end if response.is_a?(Net::HTTPSuccess)

gz = Zlib::GzipReader.open(CACHE)
helpitems = JSON::parse(gz.read)

# get installed packages
packages = []
str = `dpkg -l`
if str.respond_to?(:encode!)
  str.encode!('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '')
end
str.split(/\n/).each do |l|
  if l =~ /^(i|h).\s+([^ ]+)\s/
    packages << $2
  end
end

# add user defined packages
if File.file?(PACKAGES)
  additionals = File.read(PACKAGES).gsub(/\s+/m, ' ').strip.split(' ')
  packages += additionals
end

# add user defined ignored types
ignored_types = []
if File.file?(IGNORED_TYPES)
  selected_type = File.read(IGNORED_TYPES).gsub(/\s+/m, ' ').strip.split(' ')
  ignored_types += selected_type
end

# Show a 'gift' tag depreciation warning.
if ignored_types.include?('gift') || $show.include?('gift')
  puts "how-can-i-help: 'gift' tag is deprecated - please use 'newcomer' instead."
end

# Support for 'newcomer' tag as 'gift' UDD flag.
if ignored_types.include?('newcomer')
  ignored_types.push('gift')
end
if $show.include?('newcomer')
  $show.push('gift')
end

helpitems_filtered = []
helpitems.each do |hi|
  next if (not $old) and seen.include?(hi['hash'])
  next if !($show.empty?) && !($show.include?(hi['type']) || (hi['type'] == 'wnpp' && $show.include?(hi['wnpptype'])))
  next if $show.empty? && (ignored_types.include?(hi['type']) || (hi['type'] == 'wnpp' && ignored_types.include?(hi['wnpptype'])))
  if $all
    helpitems_filtered << hi
  elsif hi['type'] == 'wnpp'
    if packages & hi['packages'] != []
      helpitems_filtered << hi
    end
  elsif hi['type'] == 'gift'
    if packages.include?(hi['package']) || (hi['pseudo-package'] == true && !ignored_types.include?('pseudo-package'))
      helpitems_filtered << hi
    end
  elsif hi['type'] == 'help'
    if packages.include?(hi['package'])
      helpitems_filtered << hi
    end
  elsif hi['type'] == 'no-testing'
    if packages.include?(hi['package'])
      helpitems_filtered << hi
    end
  elsif hi['type'] == 'testing-autorm'
    if packages & hi['packages'] != []
      helpitems_filtered << hi
    end
  elsif hi['type'] == 'rfs'
    if packages & hi['packages'] != []
      helpitems_filtered << hi
    end
  end
end

# If we are to show JSON output and there are packages to be shown, then we generate and print it.
if ($kas) && helpitems_filtered.length > 0
    puts(JSON::pretty_generate(helpitems_filtered))
end

# Normal, human-readable output.
unless $kas

  unless $quiet
    puts '======  How can you help?  (doc: https://wiki.debian.org/how-can-i-help ) ======'
    puts
  end

  wnpp = helpitems_filtered.select { |e| e['type'] == 'wnpp' }
  gift = helpitems_filtered.select { |e| e['type'] == 'gift' && e['pseudo-package'] != true }
  infra = helpitems_filtered.select { |e| e['type'] == 'gift' && e['pseudo-package'] == true }
  help = helpitems_filtered.select { |e| e['type'] == 'help' }
  notesting = helpitems_filtered.select { |e| e['type'] == 'no-testing' }
  autoremoval = helpitems_filtered.select { |e| e['type'] == 'testing-autorm' }
  rfs = helpitems_filtered.select { |e| e['type'] == 'rfs' }

  def wnpptype(t)
    return 'O (Orphaned)' if t == 'O'
    return 'RFA (Maintainer looking for adopter)' if t == 'RFA'
    return 'RFH (Maintainer looking for help)' if t == 'RFH'
    return 'ITA (Someone working on adoption)' if t == 'ITA'
  end

  def wnppsortorder(t)
    return 0 if t == 'O'
    return 1 if t == 'RFA'
    return 2 if t == 'ITA'
    return 3 if t == 'RFH'
    return 4
  end

  def wnppcompare(a, b)
    c = wnppsortorder(a['wnpptype']) <=> wnppsortorder(b['wnpptype'])
    if c == 0
      return a['source'] <=> b['source']
    else
      return c
    end
  end

  if wnpp.length > 0
    puts $old ? 'Packages where help is needed, including orphaned ones (from WNPP):' : 'New packages where help is needed, including orphaned ones (from WNPP):'
    wnpp.sort { |a, b| wnppcompare(a,b) }.each do |r|
      puts " - #{r['source']} - https://bugs.debian.org/#{r['wnppbug']} - #{wnpptype(r['wnpptype'])}"
    end
    puts
  end

  if gift.length > 0
    puts $old ? 'Bugs affecting packages, suitable for new contributors (tagged \'newcomer\'):' : 'New bugs affecting packages, suitable for new contributors (tagged \'newcomer\'):'
    gift.sort_by { |r| [r['package'], r['bug']] }.each do |r|
      puts " - #{r['package']} - https://bugs.debian.org/#{r['bug']} - #{r['title']}"
    end
    puts
  end

  if infra.length > 0
    puts $old ? 'Bugs affecting Debian infrastructure (tagged \'newcomer\'):' : 'New bugs affecting Debian infrastructure (tagged \'newcomer\'):'
    infra.sort_by { |r| [r['package'], r['bug']] }.each do |r|
      puts " - #{r['package']} - https://bugs.debian.org/#{r['bug']} - #{r['title']}"
    end
    puts
  end

  if help.length > 0
    puts $old ? 'Bugs where assistance is requested (tagged \'help\'):' : 'New bugs where assistance is requested (tagged \'help\'):'
    help.sort_by { |r| [r['package'], r['bug']] }.each do |r|
      puts " - #{r['package']} - https://bugs.debian.org/#{r['bug']} - #{r['title']}"
    end
    puts
  end

  if notesting.length > 0
    puts $old ? 'Packages removed from Debian \'testing\' (the maintainer might need help):' : 'New packages removed from Debian \'testing\' (the maintainer might need help):'
    notesting.sort_by { |r| [r['source'], r['package']] }.each do |r|
      puts " - #{r['package']} - https://tracker.debian.org/pkg/#{r['source']}"
    end
    puts
  end

  if autoremoval.length > 0
    puts $old ? 'Packages going to be removed from Debian \'testing\' (the maintainer might need help):' : 'New packages going to be removed from Debian \'testing\' (the maintainer might need help):'
    autoremoval.sort_by { |r| [r['source'], r['package']] }.each do |r|
      bugs = r['bugs'].map { |b| "##{b}" }
      if bugs.count == 0
        bugs = ''
      elsif bugs.count > 1
        bugs = " (bugs: #{bugs.sort.join(', ')})"
      else
        bugs = " (bug: #{bugs[0]})"
      end
      puts " - #{r['source']} - https://tracker.debian.org/pkg/#{r['source']} - removal on #{Time.at(r['removal_time']).to_date.to_s}#{bugs}"
    end
    puts
  end

  if rfs.length > 0
    puts $old ? 'Packages waiting for sponsorship (reviews/tests are also useful):' : 'New packages waiting for sponsorship (reviews/tests are also useful):'
    rfs.sort_by { |r| [r['source'], r['id']] }.each do |r|
      puts " - #{r['source']} - https://bugs.debian.org/#{r['id']} - #{r['title']}"
    end
    puts
  end

  if not $old and not $quiet
    puts '-----  Show old opportunities as well as new ones: how-can-i-help --old  -----'
  end
end

# Mark seen packages.
unless $old
  seen = helpitems.map { |hi| hi['hash'] } & seen
  seen = seen + helpitems_filtered.map { |hi| hi['hash'] }
  File::open(SEEN_LOCAL, 'w') do |fd|
    JSON::dump(seen, fd)
  end
end
