#!/usr/bin/ruby -w
##############################################################################
# tpkg package management and deployment tool
##############################################################################

# Ensure we can find tpkg.rb
#$:.unshift File.dirname(__FILE__)
$:.unshift File.join(File.dirname(__FILE__), "..", "lib")

require 'optparse'
require 'tpkg'

#
# Parse the command line options
#

@action = nil
@action_value = nil
@debug = false
@prompt = true
@quiet = false
@sudo = true
@lockforce = false
@force = false
@deploy = false
@deploy_params = ARGV   # hold parameters for how to invoke tpkg on the machines we're deploying to
@deploy_options = {}    # options for how to run the deployer
@servers = nil
@worker_count = 10
@rerun_with_sudo = false
@tpkg_options = {}	# options for instantiating Tpkg object
@init_options = {}      # options for how to run init scripts
@other_options = {}     
@compress = nil


def rerun_with_sudo_if_necessary
  if Process.euid != 0 && @sudo
    warn "Executing with sudo"
    if ENV['TPKG_HOME']
      exec('sudo', 'env', "TPKG_HOME=#{ENV['TPKG_HOME']}", $0, *ARGV)
    else
      exec('sudo', $0, *ARGV)
    end
  end
end

opts = OptionParser.new(nil, 24, '  ')
opts.banner = 'Usage: tpkg [options]'
opts.on('--servers', '-s', '=SERVERS', Array, 'Servers to apply the actions to') do |opt|
  @servers = opt
  @deploy = true
  @deploy_params = @deploy_params - ['--servers', '-s', @servers.join(","), "--servers=#{@servers.join(',')}"]
end
opts.on('--make', '-m', '=DIRECTORY', 'Make a package out of the contents of the directory') do |opt|
  @action = :make
  @action_value = opt
end
opts.on('--extract', '-x', '=DIRECTORY', 'Extract the metadata for a directory of packages') do |opt|
  @action = :extract
  @action_value = opt
end
opts.on('--install', '-i', '=PACKAGES', 'Install one or more packages', Array) do |opt|
  @rerun_with_sudo = true
  @action = :install
  @action_value = opt
end
opts.on('--upgrade', '-u', '=PACKAGES', 'Upgrade one or more packages', Array) do |opt|
  @rerun_with_sudo = true
  @action = :upgrade
  @action_value = opt
end
opts.on('--downgrade', '=PACKAGES', 'Downgrade one or more packages', Array) do |opt|
  @other_options[:downgrade] = true
  @rerun_with_sudo = true
  @action = :upgrade
  @action_value = opt
end
opts.on('--ua', 'Upgrade all packages') do |opt|
  @rerun_with_sudo = true
  @action = :upgrade
end
opts.on('--remove', '-r', '=PACKAGES', 'Remove one or more packages', Array) do |opt|
  @rerun_with_sudo = true
  @action = :remove
  @action_value = opt
end
opts.on('--rd', '=PACKAGES', 'Similar to -r but also remove depending packages', Array) do |opt|
  @rerun_with_sudo = true
  @other_options[:remove_all_dep] = true
  @action = :remove
  @action_value = opt
end
opts.on('--rp', '=PACKAGES', 'Similar to -r but also remove prerequisites', Array) do |opt|
  @rerun_with_sudo = true
  @other_options[:remove_all_prereq] = true
  @action = :remove
  @action_value = opt
end
opts.on('--ra', 'Remove all packages') do |opt|
  @rerun_with_sudo = true
  @action = :remove
end
opts.on('--verify', '-V', '=NAME', 'Verify packages') do |opt|
  @rerun_with_sudo = true
  @action = :verify
  @action_value = opt
end
opts.on('--start', '=NAME', 'Start the init script for the specified package', Array) do |opt|
  @rerun_with_sudo = true
  @action = :execute_init
  @init_options[:packages] = opt
  @init_options[:cmd] = 'start'
end
opts.on('--stop', '=NAME', 'Stop the init script for the specified package', Array) do |opt|
  @rerun_with_sudo = true
  @action = :execute_init
  @init_options[:packages] = opt
  @init_options[:cmd] = 'stop'
end
opts.on('--restart', '=NAME', 'Restart the init script for the specified package', Array) do |opt|
  @rerun_with_sudo = true
  @action = :execute_init
  @init_options[:packages] = opt
  @init_options[:cmd] = 'restart'
end
opts.on('--reload', '=NAME', 'Reload the init script for the specified package', Array) do |opt|
  @rerun_with_sudo = true
  @action = :execute_init
  @init_options[:packages] = opt
  @init_options[:cmd] = 'reload'
end
opts.on('--start-all', 'Start the init scripts for all packages') do |opt|
  @rerun_with_sudo = true
  @action = :execute_init
  @init_options[:cmd] = 'start'
end
opts.on('--stop-all', 'Stop the init script for all packages') do |opt|
  @rerun_with_sudo = true
  @action = :execute_init
  @init_options[:cmd] = 'stop'
end
opts.on('--restart-all', 'Restart the init script for all packages') do |opt|
  @rerun_with_sudo = true
  @action = :execute_init
  @init_options[:cmd] = 'restart'
end
opts.on('--reload-all', 'Reload the init script for all packages') do |opt|
  @rerun_with_sudo = true
  @action = :execute_init
  @init_options[:cmd] = 'reload'
end
opts.on('--exec-init', '=NAME', 'Execute init scripts for the specified packages', Array) do |opt|
  @rerun_with_sudo = true
  @init_options[:packages] = opt
  @action = :execute_init
end
opts.on('--init-script', '=NAME', 'What init scripts to execute', Array) do |opt|
  @rerun_with_sudo = true
  @init_options[:scripts] = opt
end
opts.on('--init-cmd', '=CMD', 'Invoke the specified init script command') do |opt|
  @rerun_with_sudo = true
  @init_options[:cmd] = opt
end
opts.on('--query', '-q', '=NAMES', 'List installed packages', Array) do |opt|
  # People mistype -qa instead of --qa frequently
  if opt == ['a']
    warn "NOTE: tpkg -qa queries for a pkg named 'a', you probably want --qa for all pkgs"
  end
  @action = :query_installed
  @action_value = opt
end
opts.on('--qa', 'List all installed packages') do |opt|
  @action = :query_installed
end
opts.on('--qi', '=NAME', 'Info for packages') do |opt|
  @action = :query_info
  @action_value = opt
end
opts.on('--ql', '=NAME', 'List files in installed packages') do |opt|
  @action = :query_list_files
  @action_value = opt
end
opts.on('--qf', '=FILE', 'List the package that owns a file') do |opt|
  @action = :query_who_owns_file
  @action_value = opt
end
opts.on('--qv', '=NAME', 'List available packages') do |opt|
  @action = :query_available
  @action_value = opt
end
opts.on('--qva', 'List all available packages') do |opt|
  @action = :query_available
end
opts.on('--qr', '=NAME', 'List installed packages that require package') do |opt|
  @action = :query_requires
  @action_value = opt
end
opts.on('--qd', '=NAME', 'List the packages that package depends on') do |opt|
  @action = :query_depends
  @action_value = opt
end
opts.on('--qld', '=NAME', 'Similar to --qd, but only look at local packages') do |opt|
  @action = :query_local_depends
  @action_value = opt
end

opts.on('--dw', '=INTEGER', 'Number of workers for deploying') do |opt|
  @worker_count = opt.to_i
  @deploy_params = @deploy_params - ['--dw', @worker_count, "--dw=#{opt}"]
end
opts.on('--qX', '=FILENAME', 'Display tpkg.xml or tpkg.yml of the given package') do |opt|
  @action = :query_tpkg_metadata
  @action_value = opt
end
opts.on('--qenv', "Display machine's information") do |opt|
  @action = :query_env
end
opts.on('--source', '=NAME', 'Sources where packages are located', Array) do |opt|
  @tpkg_options["sources"] = opt
end
opts.on('-n', '--no-prompt', 'No confirmation prompts') do |opt|
  @prompt = opt
  Tpkg::set_prompt(@prompt)
end
opts.on('--quiet', 'Reduce informative but non-essential output') do |opt|
  @quiet = opt
end
opts.on('--no-sudo', 'No calls to sudo for operations that might need root') do |opt|
  @sudo = opt
end
opts.on('--lock-force', 'Force the removal of an existing lockfile') do |opt|
  @lockforce = opt
end
opts.on('--force-replace', 'Replace conflicting pkgs with the new one(s)') do |opt|
  @other_options[:force_replace] = opt
end
opts.on('--force', 'Force the execution of a given task') do |opt|
  @force = opt
end
opts.on('--use-ssh-key', 'Use ssh key for deploying instead of password') do |opt|
  @deploy_options["use-ssh-key"] = opt
  @deploy_params = @deploy_params - ['--use-ssh-key']
end
opts.on('--deploy-as', '=USERNAME', 'What username to use for deploying to remote server') do |opt|
  @deploy_options["deploy-as"] = opt
  @deploy_params = @deploy_params - ['--deploy-as']
end
opts.on('--compress', '=[TYPE]', 'Compress files when making packages') do |opt|
  @compress = opt
end
opts.on('--debug', 'Print lots of messages about what tpkg is doing') do |opt|
  @debug = opt
  Tpkg::set_debug(@debug)
end
opts.on('--version', 'Show tpkg version') do |opt|
  puts Tpkg::VERSION
  exit
end
opts.on_tail("-h", "--help", "Show this message") do
  puts opts
  exit
end

leftovers = opts.parse(ARGV)

# Rerun with sudo if necessary, unless it's a deploy, then
# we don't need to run with sudo on this machine. It will run with sudo
# on the remote machine
if @rerun_with_sudo && !@deploy
  rerun_with_sudo_if_necessary
end

# Display a usage message if the user did not specify a valid action to perform.
if !@action
  puts opts
  exit
end

#
# Figure out base directory, sources and other configuration
#

def instantiate_tpkg(options = {})
  base = Tpkg::DEFAULT_BASE
  sources = options["sources"] || []
  report_server = nil
  
  [File.join(Tpkg::CONFIGDIR, 'tpkg.conf'), File.join(ENV['HOME'], ".tpkg.conf")].each do |configfile|
    if File.exist?(configfile)
      IO.foreach(configfile) do |line|
        line.chomp!
        next if (line =~ /^\s*$/);  # Skip blank lines
        next if (line =~ /^\s*#/);  # Skip comments
        line.strip!  # Remove leading/trailing whitespace
        key, value = line.split(/\s*=\s*/, 2)
        if key == 'base'
          # Warn the user, as this could potentially be confusing
          # if they don't realize there's a config file lying
          # around
          base = value
          warn "Using base #{base} from #{configfile}"
        elsif key == 'source'
          sources << value
          puts "Loaded source #{value} from #{configfile}" if (@debug)
        elsif key == 'report_server'
          report_server = value
          puts "Loaded report server #{report_server} from #{configfile}" if (@debug)
        end
      end
    end
  end
  
  if ENV['TPKG_HOME']
    base = ENV['TPKG_HOME']
    # Warn the user, as this could potentially be confusing
    # if they don't realize there's an environment variable set.
    warn "Using base '#{base}' base from $TPKG_HOME"
  end
  
  tpkg = Tpkg.new(:base => base, :sources => sources, :report_server => report_server, :lockforce => @lockforce, :force => @force)
end

passphrase_callback = lambda do | package |
#  ask("Passphrase for #{package}: ", true)
  begin
    system 'stty -echo;' 
    print "Passphrase for #{package}: "
    input = STDIN.gets.chomp
  ensure
    system 'stty echo; echo ""'
  end
  input
end

#
# Do stuff
#
if @deploy
#  puts "Creating deployer with #{@worker_count} number of worker"
  @deploy_options["max-worker"] = @worker_count
  @deploy_options["abort-on-fail"] = false
  Tpkg::deploy(@deploy_params, @deploy_options,  @servers) 
  exit
end

if @action_value.is_a?(Array)
  @action_value.uniq!
end

ret_val = 0
case @action
when :make
  pkgfile = Tpkg::make_package(@action_value, passphrase_callback, {:force => @force, :compress => @compress})
  if pkgfile
    puts "Package is #{pkgfile}"
  else
    puts "Package build aborted or failed"
  end
when :extract
  Tpkg::extract_metadata(@action_value)
when :install
  tpkg = instantiate_tpkg(@tpkg_options)
  ret_val = tpkg.install(@action_value, passphrase_callback, @other_options)
when :upgrade
  tpkg = instantiate_tpkg(@tpkg_options)
  ret_val = tpkg.upgrade(@action_value, passphrase_callback, @other_options)
when :remove
  tpkg = instantiate_tpkg(@tpkg_options)
  ret_val = tpkg.remove(@action_value, @other_options)
when :verify
  result = nil
  # Verify a given .tpkg file
  if File.exist?(@action_value)
    Tpkg::verify_package_checksum(@action_value)
  # Verify an installed pkg
  else
    tpkg = instantiate_tpkg(@tpkg_options)
    results = tpkg.verify_file_metadata(@action_value)
    if results.length == 0
      puts "No package found"
    end
    success = true
    results.each do | file, errors |
      if errors.length == 0
        puts "#{file}: Passed"
      else
        puts "#{file}: Failed (Reasons: #{errors.join(", ")})"
        success = false
      end
    end 
    puts "Package verification failed" unless success 
  end
when :execute_init
  tpkg = instantiate_tpkg(@tpkg_options)
  if @init_options[:cmd].nil?
    raise "You didn't specify what init command to run"
  end
  ret_val = tpkg.execute_init(@init_options)
when :query_installed
  tpkg = instantiate_tpkg(@tpkg_options)
  req = nil
  matches = []
  if @action_value
    @action_value.each do | value |
      req = Tpkg::parse_request(value, tpkg.installed_directory)
      match = tpkg.installed_packages_that_meet_requirement(req)

      # If the user requested specific packages and we found no matches
      # then exit with a non-zero value to indicate failure.  This allows
      # command-line syntax like "tpkg -q foo || tpkg -i foo" to ensure
      # that a package is installed.
      ret_val = 1 if match.empty?
      matches |= match
    end
  else
    matches = tpkg.installed_packages_that_meet_requirement(req)
  end

  if !@quiet
    matches.sort(&Tpkg::SORT_PACKAGES).each do |pkg|
      puts pkg[:metadata][:filename]
    end
  end
when :query_info
  metadatas = nil
  if File.exist?(@action_value)
    metadatas = [Tpkg::metadata_from_package(@action_value)]
  else
    tpkg = instantiate_tpkg(@tpkg_options)
    metadatas = []
    requirements = []
    packages = {}
    tpkg.parse_requests([@action_value], requirements, packages)
    packages.each do | name, pkgs |
      pkgs.each do | pkg |
        metadatas << pkg[:metadata]
      end
    end
  end
  already_displayed = {}
  metadatas.each do |metadata|
    next if already_displayed[metadata[:filename]]
    already_displayed[metadata[:filename]] = true
    [:name, :version, :package_version, :operatingsystem, :architecture, :maintainer, :description, :bugreporting].each do |field|
      metadata[field] = 'any' if field == :operatingsystem && metadata[field].nil?
      metadata[field] = 'any' if field == :architecture && metadata[field].nil?
      if metadata[field]
        if metadata[field].kind_of?(Array)
          puts "#{field}: #{metadata[field].join(',')}"
        else
          puts "#{field}: #{metadata[field]}"
        end
      end
    end
    if metadata[:dependencies] 
      puts "This package depends on other packages, use --qd/--qld to view the dependencies."
    end
    puts "================================================================================"
  end
when :query_list_files
  tpkg = instantiate_tpkg(@tpkg_options)
  pkgfiles = nil
  if File.exist?(@action_value)
    fip = Tpkg::files_in_package(@action_value)
    tpkg.normalize_paths(fip)
    puts "#{pkgfile}:"
    fip[:normalized].each { |file| puts file }
  else
    pkgfiles = []
    metadatas = []
    requirements = []
    packages = {}

    req = Tpkg::parse_request(@action_value)
    pkgs = tpkg.installed_packages_that_meet_requirement(req)
    if pkgs.nil? or pkgs.empty?
      ret_val = 1
      puts "Could not find any installed packages that meet the request \"#{@action_value}\""
    end
    pkgs.each do | pkg |
      pkgfiles << pkg[:metadata][:filename] 
    end

    files = tpkg.files_for_installed_packages(pkgfiles)
    files.each do |pkgfile, fip|
      puts "#{pkgfile}:"
      fip[:normalized].each { |file| puts file }
    end
  end
when :query_who_owns_file
  tpkg = instantiate_tpkg(@tpkg_options)
  tpkg.files_for_installed_packages.each do |pkgfile, fip|
    fip[:normalized].each do |file|
      if file == @action_value
        puts "#{file}: #{pkgfile}"
      end
    end
  end
when :query_available
  tpkg = instantiate_tpkg(@tpkg_options)
  req = nil
  if @action_value
    req = Tpkg::parse_request(@action_value)
  end
  tpkg.available_packages_that_meet_requirement(req).each do |pkg|
    next if pkg[:source] == :native_installed
    next if pkg[:source] == :native_available
    puts "#{pkg[:metadata][:filename]} (#{pkg[:source]})"
  end
when :query_requires
  tpkg = instantiate_tpkg(@tpkg_options)

  # parse the request
  requirements = [] 
  packages = {}
  tpkg.parse_requests([@action_value], requirements, packages)

  # get dependencies of all installed packages
  dependencies = {}
  tpkg.metadata_for_installed_packages.each do |metadata|
    dependencies[metadata[:filename]] = metadata[:dependencies]
  end

  # check to see if the any required dependencies match with what the
  # user specified in the request
  packages.each do |name, pkgs|
    pkgs.each do |pkg|
      next if pkg[:source] != :currently_installed
      puts "The following package(s) require #{pkg[:metadata][:filename]}:"
      dependencies.each do | requiree, deps |
        next if deps.nil?
        deps.each do | dep | 
          if Tpkg::package_meets_requirement?(pkg, dep)
            puts "  #{requiree}"
          end
        end
      end
    end
  end
when :query_local_depends
  tpkg = instantiate_tpkg(@tpkg_options)
  req = Tpkg::parse_request(@action_value)
  pkgs = tpkg.installed_packages_that_meet_requirement(req)
  pkgs.each do | pkg |
    puts pkg[:metadata][:filename] + ':'
    if pkg[:metadata][:dependencies]
      pkg[:metadata][:dependencies].each do |req|
        puts "  Requires #{req[:name]}"
        req.each do |field, value|
          next if field == :name
          puts "    #{field}: #{value}"
         end
       end
    end
  end
when :query_depends
  tpkg = instantiate_tpkg(@tpkg_options)
  requirements = []
  packages = {}
  tpkg.parse_requests([@action_value], requirements, packages)
  packages.each do |name, pkgs|
    already_displayed = {}
    pkgs.each do |pkg|
      # parse_requests returns both installed and available packages.
      # The same package may show up in both, skip any duplicates.
      next if already_displayed[pkg[:filename]]
      already_displayed[pkg[:metadata][:filename]] = true
      puts pkg[:metadata][:filename] + ':'
      if pkg[:metadata][:dependencies]
        pkg[:metadata][:dependencies].each do |req|
          puts "  Requires #{req[:name]}"
          req.each do |field, value|
            next if field == :name
            puts "    #{field}: #{value}"
          end
        end
      end
    end
  end
when :query_tpkg_metadata
  tpkg = instantiate_tpkg(@tpkg_options)
  if File.exist?(@action_value)
    puts Tpkg::extract_tpkg_metadata_file(@action_value)
  elsif File.exists?(File.join(tpkg.installed_directory, @action_value))
    puts Tpkg::extract_tpkg_metadata_file(File.join(tpkg.installed_directory, @action_value))
  else
    puts "File #{@action_value} doesn't exist."
  end
when :query_env
  puts "Operating System: #{Tpkg::get_os}"
  puts "Architecture: #{Tpkg::get_arch}"
end
exit ret_val