#!/usr/bin/ruby
### Copyright 2016 Pixar
###
###    Licensed under the Apache License, Version 2.0 (the "Apache License")
###    with the following modification; you may not use this file except in
###    compliance with the Apache License and the following modification to it:
###    Section 6. Trademarks. is deleted and replaced with:
###
###    6. Trademarks. This License does not grant permission to use the trade
###       names, trademarks, service marks, or product names of the Licensor
###       and its affiliates, except as required to comply with Section 4(c) of
###       the License and to reproduce the content of the NOTICE file.
###
###    You may obtain a copy of the Apache License at
###
###        http://www.apache.org/licenses/LICENSE-2.0
###
###    Unless required by applicable law or agreed to in writing, software
###    distributed under the Apache License with the above modification is
###    distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
###    KIND, either express or implied. See the Apache License for the specific
###    language governing permissions and limitations under the Apache License.
###
###

# == Synopsis
#  d3admin - a commandline tool for creating and maintaining d3 pkgs in Casper
#
# == Usage
#   d3admin [options] action target....
#
#   For help use: d3admin help
#
# == Author
#   Chris Lasell <d3@pixar.com>
#
# == Copyright
#   Copyright (c) 2016 Pixar Animation Studios.

############
# Modules, Libraries, etc
############

# Load libraries
require 'd3'



######################
# The Script Object
######################
class App

  ### Setup
  def initialize

    ### parse the commandline
    parse_commandline

    # can't  be run as root other than help, search or report - no keychain, among other things
    raise "d3admin can't make server changes as root" if JSS.superuser? and (not @action =~ /^[hsr]/)

  end #initialize

  ### Run
  def run

    if @action == "help"
      show_help @options.helptype
      return
    end

    # run config if it hasn't ever run
    unless D3::Admin::Prefs.prefs[:last_config]
      puts
      puts "********  INITIAL D3ADMIN CONFIGURATION  ********"
      puts
      config
      # but dont run it again if that's the action chosen
      return if @action =~ /^c/
    end

    ### our admin is us
    @admin = ENV['USER']

    # admin can't be a badmin
    if D3.badmins.include? @admin and D3::Admin::ACTIONS_NEEDING_ADMIN.include? @action
      raise D3::PermissionError, "d3admin cannot do '#{@action}' as #{@admin}."
    end

    # config before connecting
    if @action =~ /^c/
      config
      return
    end

    ### connect to the server, prompting for info as needed
    D3::Admin::Auth.connect

    case @action

      when /^a/ then
        add_pilot_package

      when /^e/ then
        edit_package

      when /^l/ then
        make_package_live

      when /^d/ then
        delete_package

      when /^i/ then
        show_package_info

      when /^s/ then
        search

      when /^r/ then
        show_report

      else
        raise ArgumentError, "#{D3::Admin::Help::USAGE}\nUnknown action, must be one of #{D3::Admin::ACTIONS.join(', ')}"

    end # case


  end #run

  ### Parse the command line
  ###
  ### @return [void]
  ###
  def parse_commandline

    # Debugging file? if so, always set debug.
    ARGV << "--debug" if D3::DEBUG_FILE.exist?

    # this holds everything that comes from the commandline
    # or from prompting the user
    @options = OpenStruct.new

    # cli option defaults
    @options.helptype = :help

    # --status can be given multiple times, or can be a comma-separated list.
    # the results end up in this array
    @options.status = []

    ### see Admin::CLIOpts
    opt_arry = D3::Admin::OPTIONS.values.map{|o| o[:cli] }
    opts = GetoptLong.new *opt_arry

    opts.each do |opt, arg|
      case opt

      #general
        when '--help'
          @action = "help"
          @options.helptype = :help
          return
        when '--extended-help'
          @action = "help"
          @options.helptype = :extended_help
          return
        when '--debug'
          D3::Admin.debug = true

        when '--d3-version'
          @action = "help"
          @options.helptype = :show_d3_version
          return

        when '--walkthru'
          @options.walkthru = true
        when '--auto-confirm'
          @options.auto_confirm = true

      # search and report
        when '--status'
          @options.status +=  arg.split(/,\s*/)
        when '--queue'
          @options.report_q = true
        when '--frozen'
          @options.report_frozen = true
        when  '--computers'
          @options.report_computers = true
        when '--groups'
          @options.search_groups = true
      # add/edit
        when '--import'
          @options.import = true
          @options.import_from = arg.empty? ? nil : arg
        when '--no-inherit'
          @options.no_inherit = true
        when '--basename'
          @options.basename = arg
        when '--version'
          @options.version = arg
        when '--revision'
          @options.revision = arg
        when '--description'
          @options.description = arg
        when '--package-name'
          @options.package_name = arg
        when '--filename'
          @options.filename = arg
        when '--edition'
          @options.edition = arg
        when '--source-path'
          @options.source_path = arg
        when '--dmg'
          @options.package_build_type = 'd'
        when '--preserve-owners'
          @options.pkg_preserve_owners = 'y'
        when '--pkg-id'
          @options.pkg_identifier = arg
        when '--workspace'
          @options.workspace = arg
        when '--pre-install'
          @options.pre_install = arg
        when '--post-install'
          @options.post_install = arg
        when '--pre-remove'
          @options.pre_remove = arg
        when '--post-remove'
          @options.post_remove = arg
        when '--auto-groups'
          @options.auto_groups = arg
        when '--excluded-groups'
          @options.excluded_groups = arg
        when '--prohibiting-process'
          @options.prohibiting_process = arg
        when '--cpu_type'
          @options.cpu_type = arg
        when '--category'
          @options.category = arg

        when '--reboot'
          # dft is no, so if arg is empty or /^n(o)$/i, it should be nil,
          # otherwise must be /^y(es)$/i
          if arg.empty? or arg =~ /^no?$/i
            @options.reboot =  "n"
          elsif arg =~ /^y(es)?$/i
            @options.reboot =  "y"
          else
            raise ArgumentError, "--reboot must be 'y' or 'n'"
          end

        when '--remove-first'
          # dft is no, so if arg is empty or /^n(o)$/i, it should be nil,
          # otherwise must be /^y(es)$/i
          if arg.empty? or arg =~ /^no?$/i
            @options.remove_first =  "n"
          elsif arg =~ /^y(es)?$/i
            @options.remove_first =  "y"
          else
            raise ArgumentError, "--remove-first must be 'y' or 'n'"
          end

        when '--removable'
          # dft is yes, so if arg is empty or /^y(es)$/i, it should be nil,
          # otherwise must be  /^n(o)$/i
          if arg.empty? or arg =~ /^y(es)?$/i
            @options.removable =  "y"
          elsif arg =~ /^no?$/i
            @options.removable =  "n"
          else
            raise ArgumentError, "--removable must be 'y' or 'n'"
          end

        when '--oses'
          @options.oses = arg
        when '--expiration'
          @options.expiration = arg
        when '--expiration-path', '--expiration_paths'
          @options.expiration_paths = arg

      # delete
        when '--keep-scripts'
          @options.keep_scripts = 'y'
        when '--keep-in-jss'
          @options.keep_in_jss = 'y'

      end # case
    end # opts.each

    # the action is always the first thing in ARGV
    # after the options have been removed
    action_arg = ARGV.shift

    unless action_arg
      raise ArgumentError, "#{D3::Admin::Help::USAGE}\nAction, must be one of #{D3::Admin::ACTIONS.join(', ')}"
    end

    # one or more letters is all we need to specify the action...
    @action = D3::Admin::ACTIONS.select{|a| a.start_with? action_arg}.first

    unless @action and D3::Admin::ACTIONS.include? @action
      raise ArgumentError, "#{D3::Admin::Help::USAGE}\nAction, must be one of #{D3::Admin::ACTIONS.join(', ')}"
    end

    # anything remaining in ARGV is one or more targets
    # to work on.
    @targets = ARGV

  end # parse_commandline

  ### Show the help message
  ###
  ### @return [void]
  ###
  def show_help (type)
    text = case type
           when :help
            D3::Admin::Help.help_text
           when :extended_help
             D3::Admin::Help.extended_help_text
           when :show_d3_version
             d3_version_text
           end
    D3.less_text text
    return true
  end #show help

  ### the d3 version text to spew
  ###
  ### @return [String] the text
  def d3_version_text
  <<-ENDVERS
D3 module version: #{D3::VERSION}
JSS module version: #{JSS::VERSION}
ENDVERS
  end

  ### Add a new pilot package from an existing
  ### JSS package
  def import_pilot_pkg_from_jss

    if @options.walkthru
      @options.import_from = D3::Admin::Interactive.get_value :import unless @options.import_from
      @options.version = D3::Admin::Interactive.get_value :version
      @options.revision = D3::Admin::Interactive.get_value :revision
    else
      raise JSS::MissingDataError, "A version must be provided with --version" unless @options.version
      raise JSS::MissingDataError, "A revision must be provided with --revision" unless @options.revision
    end # if walk thru

    imported_pkg = D3::Package.import( @options.import_from,
      basename: @options.basename,
      version: @options.version,
      revision: @options.revision,
      dist_pw: D3::Admin::Auth.rw_credentials(:dist)[:password]
      )
    imported_pkg.admin = @admin
    edit_package imported_pkg
  end

  ### Add a new package to d3 for piloting
  ### In this method, the following are OpenStructs:
  ### - @options holds the value from the commandline
  ### - default_options holds the default values, either as
  ###     defined in D3::Admin::OPTIONS or inherited
  ###     from an older package
  ### - new_package_options holds the validated values for
  ###     making the new package
  ###
  ### @return [void]
  ###
  def add_pilot_package

    ## we must have a basename before going farther
    @options.basename = @targets.first
    if @options.walkthru
      @options.basename ||= D3::Admin::Interactive.get_basename
    else
      raise ArgumentError, "A basename must be provided as a target. Use -H for help" unless @options.basename
    end

    if @options.import
      import_pilot_pkg_from_jss
      return
    end

    puts "Adding a new pilot package to d3..."

    # this holds the validated data used for making the new pkg
    new_package_options = OpenStruct.new

    # now figure out our default values..
    default_options = D3::Admin::Add.get_default_options(@options.basename, @options.no_inherit)

    # get all new package options into new_package_options
    #  via walkthru ...
    if @options.walkthru
      new_package_options = D3::Admin::Add.loop_thru_add_walkthru default_options

    # via the commandline
    else

      # if we were given a new version but not a new revision,
      # set the new revision to 1
      if @options[:version] and  @options[:version] !=  default_options[:version]
        @options[:revision] ||= 1
        @options[:edition] ||=  "#{@options.basename}-#{@options[:version]}-#{@options[:revision]}"
        @options.package_name ||= @options[:edition]
        @options.filename ||= "#{@options[:edition]}.#{default_options.package_build_type}"

      end

      # for any options that weren't given on the commandline, use the defaults
      default_options.each_pair { |opt, val| @options[opt] ||= val }


      new_package_options = D3::Admin::Add.add_pilot_cli(@options)

      ##########################

    end # if @options.walkthru

    new_package_options.edition = "#{new_package_options.basename}-#{new_package_options.version}-#{new_package_options.revision}"

    # make a summary of the new values to display for confirmation, if no walkthru
    unless @options.walkthru
      lines = []

      opts_to_confirm = D3::Admin::Add::NEW_PKG_OPTIONS
      opts_to_confirm += D3::Admin::Add::BUILD_OPTIONS if new_package_options.package_build_type
      opts_to_confirm += D3::Admin::Add::PKG_OPTIONS if new_package_options.package_build_type == :pkg

      #D3::Admin::Add::NEW_PKG_OPTIONS.each do |opt|
      opts_to_confirm.each do |opt|
        lbl = D3::Admin::OPTIONS[opt][:label]
        if D3::Admin::OPTIONS[opt][:display_conversion]
          disp = D3::Admin::OPTIONS[opt][:display_conversion].call(new_package_options[opt])
        else
          disp = new_package_options[opt]
        end
        lines <<  "#{lbl}: #{disp}"
      end
      settings_conf_disp = "\n******* New d3 Package Settings *******\n"
      settings_conf_disp += "Edition: #{ new_package_options.edition}\n"
      settings_conf_disp += "Basename: #{ new_package_options.basename}\n"
      settings_conf_disp +=  lines.join "\n"
      settings_conf_disp +=  "\n"
      puts settings_conf_disp
    end

    # Get confirmation
    confirm "Create a new package '#{new_package_options.edition}' in d3\nwith settings shown above."

    D3::Admin::Add.add_new_package new_package_options

    puts "Done!"
    puts "To pilot it, run 'sudo d3 install #{new_package_options.edition}' on a test machine."
    puts "To make it live, run 'd3admin live #{new_package_options.edition}' on your machine."

  end # add pilot

  ### Make a package live.
  ###
  ### @return [void]
  ###
  def make_package_live

    pkg_id = get_pkg_from_cli_or_prompt

    if pkg_id.nil?
      puts "No edition given to make live or no matching package found"
      return
    end
    pkg = D3::Package.new id: pkg_id

    if pkg.status == :live
      puts "Doh, '#{pkg.edition}' is already live"
      return
    end

    # is this a rollback?
    rollback_warning = ""
    if pkg.skipped? or pkg.deprecated?
      rollback_warning = "\n\nWARNING: You're rolling back to an older edition!\n"
      rollback_warning += "ALL non-frozen installs of '#{pkg.basename}' will be downgraded to this edition! \n"
    end

    # is the prev. live pkg in use in any policies?
    policy_warning = ""
    pkg_id_being_deprecated = D3::Package.basenames_to_live_ids[pkg.basename]
    if pkg_id_being_deprecated
      outgoing_pkg = D3::Package.new(id: pkg_id_being_deprecated)
      pols_used_by_old_pkg  = outgoing_pkg.policy_ids
      unless pols_used_by_old_pkg.empty?
        names = pols_used_by_old_pkg.map{|pid| JSS::Policy.map_all_ids_to(:name)[pid]}.join(', ')
        policy_warning = "\n\nWARNING: the current live package is in use by these Casper Policies:\n"
        policy_warning += " #{names}\n"
        policy_warning += "You might want to update them to use the new live package."
      end # unless empty
    end # if pkg_id_being_deprecated

    confirm "Make #{pkg.edition} live for basename '#{pkg.basename}'#{rollback_warning}#{policy_warning}" , ""

    pkg.make_live @admin

    puts "Done!"
    puts "New installs of basename '#{pkg.basename}' will get #{pkg.version}-#{pkg.revision}"
    puts "Existing installs will be updated at the next d3 sync."
  end

  ### Edit an existing or importing package
  ###
  ### @param pkg[D3::Package, nil] A possibly unsaved D3::Package to edit.
  ###   This is usually used for importing JSS pkgs into d3.
  ###
  ### @return [void]
  ###
  def edit_package (pkg=nil)

    unless pkg
      pkg_id = get_pkg_from_cli_or_prompt
      pkg = pkg_id ? D3::Package.new(id: pkg_id) : nil
    end
    if pkg.nil?
      puts "No targets given to edit or no matching package found"
      return
    end


    if @options.walkthru
      changes_to_make = D3::Admin::Edit.loop_thru_editing_walkthru pkg
    else

      changes_to_make = D3::Admin::Edit.validate_cli_edits (@options)
    end # if @options.walkthru

    # Confirm the edition is still good if vers or rev changed
    changes_to_make = D3::Admin::Edit.check_new_edition pkg, changes_to_make

    # Confirm the auto and excl groups don't conflict with each other
    D3::Admin::Edit.check_for_new_group_overlaps pkg, changes_to_make


    # exit if no changes on edit
    if changes_to_make.empty? and not @options.import
        puts "No changes to make!"
        return
    end

    # confirm
    if @options.import
      confirm_heading = "Import #{pkg.edition}, JSS id #{pkg.id}, into d3"
      conf_deets = "with these non-default settings..."
    else
      confirm_heading = "Make changes to #{pkg.edition}, JSS id #{pkg.id}"
      conf_deets = "Here are the changes..."
    end

    D3::Admin::Edit::EDITING_OPTIONS.each do |opt|
      next unless changes_to_make.keys.include? opt
      new_val = changes_to_make[opt]
      opt_def = D3::Admin::OPTIONS[opt]
      label = opt_def[:label]
      if opt_def[:display_conversion]
        new_val_display = opt_def[:display_conversion].call(new_val)
      else
        new_val_display = new_val
      end
      new_val_display = new_val_display.to_s.empty? ? 'none' : new_val_display
      conf_deets += "\n#{label}: #{new_val_display}"
    end
    confirm confirm_heading, conf_deets

    # Save if importing
    pkg.save if @options.import

    # make the changes
    D3::Admin::Edit.process_edits pkg, changes_to_make

    puts @options.import ? "Done: the package has been imported to d3." : "Done! Your changes have been saved."

  end # edit_pkg

  ### delete a pkg, optionallay archiving it
  ###
  ### @return [void]
  ###
  def delete_package

    pkg_id = get_pkg_from_cli_or_prompt

    if pkg_id.nil?
      puts "No targets given to delete or no matching package found"
      return
    end

    # check to see if the pkg is missing
    if D3::Package.package_data[pkg_id][:status] == :missing || (not JSS::Package.all_ids.include? pkg_id)
      delete_missing_package pkg_id
      return
    else
      pkg = D3::Package.new id: pkg_id
    end

    got_scripts = (not pkg.script_ids.values.empty?)

    if got_scripts

      if @options.walkthru
        @options.keep_scripts =  D3::Admin::Interactive.get_value(:get_keep_scripts, default = 'n', check_method = :validate_yes_no)

        @options.keep_in_jss = D3::Admin::Interactive.get_value(:get_keep_in_jss, default = 'n', check_method = :validate_yes_no)
      end # if @options.walkthru


      if @options.keep_scripts
        deets = " - Keeping pre- or post- scripts in the JSS"
      else
        deets = " - Deleting pre- or post- scripts not used elsewhere"
      end # if @options.keep_scripts

    else
      deets = " - No pre- or post- scripts to delete"
    end # if got_scripts

    if @options.keep_in_jss
      deets += "\n - Leaving the Casper package in the JSS"
    else
      deets += "\n - Deleting the Casper package as well as the d3 data"
    end

    confirm "DELETE #{pkg.edition}, JSS id #{pkg.id}\nDist.Point Filename: #{pkg.filename}", deets

    if @options.keep_scripts && got_scripts
      puts "Scanning for other packages or policies using  pre- or post- scripts before deleting..."
    end

    script_actions = pkg.delete(
        admin: @admin,
        keep_scripts: @options.keep_scripts,
        keep_in_jss: @options.keep_in_jss,
        rwpw: D3::Admin::Auth.rw_credentials(:dist)[:password]
        )

    unless script_actions.empty?
      puts "Here' what happened to the scripts:"
      puts script_actions.join "\n"
    end
    puts "Done! #{pkg.edition} has been deleted"

  end # delete package

  ### Delete a package that's missing from the JSS
  ###
  ###
  def delete_missing_package (pkgid)
    pkg_data = D3::Package.package_data[pkgid]
    script_ids = [pkg_data[:pre_install_script_id], pkg_data[:post_install_script_id], pkg_data[:pre_remove_script_id], pkg_data[:post_remove_script_id] ].compact

    if script_ids.empty?
      deets = " - No pre- or post- scripts to delete"
    else
      if @options.keep_scripts
        deets = " - Keeping pre- or post- scripts in the JSS"
      else
        deets = " - Deleting pre- or post- scripts not in use elsewhere"
      end # if @options.keep_scripts
    end # if script_ids.empty?

    confirm "DELETE #{pkg_data[:edition]}\n** Already missing from the JSS **", deets

    JSS::DB_CNX.db.query "DELETE FROM #{D3::Database::PACKAGE_TABLE[:table_name]} WHERE package_id = #{pkgid}"
    puts "Package #{pkg_data[:edition]} deleted."

    if (not @options.keep_scripts) && (not script_ids.empty?)
      puts "Scanning for other packages or policies using  pre- or post- scripts before deleting..."
      policy_scripts = D3.policy_scripts
      script_ids.each do |victim_script_id|
        next unless JSS::Script.all_ids.include? victim_script_id

        victim_script_name = JSS::Script.map_all_ids_to(:name)[victim_script_id]
        pol_users = []
        policy_scripts.each do |pol, pol_scripts|
          if pol_scripts.include? victim_script_id
            puts "Script '#{victim_script_name}' in use by policy '#{pol}'"
            pol_users << pol
          end
        end # policy scripts.each
        d3_users = (D3::Package.packages_for_script(victim_script_id) - [pkgid])
        d3_users.each{|pkgid| puts "Script '#{victim_script_name}' in use by d3 edition '#{D3::Package.ids_to_editions[pkgid]}'" }
        if pol_users.empty? and d3_users.empty?
          JSS::Script.new(id: victim_script_id).delete
          puts "Deleted script '#{victim_script_name}'"
        end
      end # do script id
    end # if @options.keep_scripts && (not script_ids.empty?)

  end # delete_missing_package (pkgid)

  ### Get an existing pkg id from the user via prompt (if walkthru) or the
  ### first thing listed in @targets. We don't instantiate the pkg here
  ### because it might be :missing in the JSS.
  ###
  ### @return [Ingeter, nil] an existing d3 pkg id if one was chosen
  ###
  def get_pkg_from_cli_or_prompt
    if @options.walkthru and @targets.first.nil?
      D3::Admin::Interactive.get_value :get_existing_package, nil, :validate_existing_package
    else
      pkg_data = D3::Package.find_package(@targets.first, :hash)
      return pkg_data ? pkg_data[:id] : nil
    end
  end

  ### Get confirmation before making any changes to a pkg.
  ###
  ### This method just taks a string of text to show the user
  ### and shows it below a standard, attention-getting header line
  ### It then waits for the user to type y or n and exits if n was typed.
  ###
  ### @param action[String] a textual representation of what we're confirming
  ###
  ### @param details[String] The details requireing confirmation
  ###
  ###
  def confirm (action, details = nil)

    puts
    puts "*****************************************"
    puts "Ahoy there! You are about to:\n#{action}"
    puts "*****************************************"
    puts details if details

    if @options.auto_confirm
      puts "auto-confirmed! Here we go....."
    else
      puts
      reply = Readline.readline("Are you SURE? (y/n): ", false)
      return true if reply =~ /^y/i
      puts "Cancelled - wise choice!"
      exit 0
    end # if autoconfirm
  end # confirm

  ### show details about an existing package
  ###
  ### @return [void]
  ###
  def show_package_info

    pkg_id = get_pkg_from_cli_or_prompt

    if pkg_id.nil?
      puts "No targets given or no matching package found"
      return
    end

    pkg = D3::Package.new id: pkg_id

    pkg_deets = <<-ENDDEETS
*****************************************
Details about #{pkg.edition}
JSS id: #{pkg.id}, Status: #{pkg.status}
****************************************
ENDDEETS
    pkg_deets += "Basename: #{pkg.basename}\n"
    pkg_deets += "Version: #{pkg.version}\n"
    pkg_deets += "Revision: #{pkg.revision}\n"
    pkg_deets += "Added to on: #{pkg.added_date.strftime "%Y-%m-%d"}\n"
    pkg_deets += "Added by: #{pkg.added_by}\n"
    if pkg.release_date
      pkg_deets += "Released on: #{pkg.release_date.strftime "%Y-%m-%d"}\n"
      pkg_deets += "Released by: #{pkg.released_by}\n"
    end

    done = [:basename, :version, :revision, :added_by, :added_date, :released_by, :release_date]
    D3::Admin::Edit::EDITING_OPTIONS.each do |opt|
      next if done.include? opt
      label = D3::Admin::OPTIONS[opt][:label]
      if D3::Admin::OPTIONS[opt][:display_conversion]
        val_display = D3::Admin::OPTIONS[opt][:display_conversion].call(pkg.send opt)
      else
        val_display =  pkg.send opt
      end
      val_display = val_display.to_s.empty? ? "none" : val_display
      pkg_deets += "#{label}: #{val_display}\n"
    end


    puts pkg_deets
    puts
  end # show_package_info

  ### search for and print a list of packages in d3
  ###
  ### @return [void]
  ###
  def search
    D3::Admin::Report.connect_for_reports

    if @options.walkthru
      # prompt for search type?
        search_for = D3::Admin::Interactive.prompt_for_data(
          desc: "SEARCH PACKAGES BY?\nAre you searching for packages by basename, or by scoped groups?\nEnter 'b' or 'g'",
          prompt: "Basenames or Groups",
          default: 'b',
          required: false
        )
      @options.search_groups = true if search_for =~ /^g/i

      @targets = [D3::Admin::Interactive.get_search_target] if @targets.empty?
      @options.status = D3::Admin::Interactive.get_status_for_filter.split(/,\s*/) if @options.status.empty?
    end #if @options.walkthru

    @options.status = [] if @options.status.include? "all"

    # show all pkgs or group scope
    if @targets.empty? or @targets.include? "all"
      if @options.search_groups
        D3::Admin::Report.list_all_pkgs_with_scope @options.status
      else
        D3::Admin::Report.list_packages nil, @options.status
      end
      return
    end

    # show specific basenames or group scopes
    @targets.each do |search_target|

      # groups?
      if @options.search_groups
        found_groups = JSS::ComputerGroup.all_names.select{|gn| gn =~ /#{search_target}/}
        if found_groups.empty?
          puts "No computer groups in Casper match '#{search_target}'"
        else
          found_groups.each {|fgn|
            D3::Admin::Report.list_scoped_installs fgn, @options.status, :auto
            D3::Admin::Report.list_scoped_installs fgn, @options.status, :excluded
          } # found_groups.each
        end # if found_groups.empty?

      # basenames
      else
        found_basenames = D3::Package.all_basenames.select{|bn| bn =~ /#{search_target}/}
        if found_basenames.empty?
          puts "No basenames in d3 match '#{search_target}'"
        else
          found_basenames.each {|fbn| D3::Admin::Report.list_packages fbn, @options.status }
        end # if found_basenames.empty?
      end # if @options.search_groups
    end # @targets.each do |search_target|
  end # def search

  ### show a report
  ###
  ### @return [void]
  ###
  def show_report
    D3::Admin::Report.connect_for_reports

    # targets are basenames or computer names

    if @options.walkthru

      # prompt for target?
      if @targets.empty?
        one_or_all = D3::Admin::Interactive.prompt_for_data(
          desc: "ALL COMPUTERS?\nAre you reporting for a single computer or all computers?\nEnter 'all' for all computers, '1' for one",
          prompt: "All or 1",
          default: 'all',
          required: false
        )

        # basename report across computers
        if one_or_all == "all"
          @targets = [(D3::Admin::Interactive.get_value :get_basename, nil, :validate_basename)]

        # reporting single computers
        else
          @options.report_computers = true
          computer_id = D3::Admin::Interactive.get_value :get_computer, nil, :validate_computer
          @targets = [JSS::Computer.map_all_ids_to(:name)[computer_id]]
        end
      end # if targets empty

      # prompt for rcpts or puppies
      rcpts_or_pups = D3::Admin::Interactive.prompt_for_data(
          desc: "RECEIPTS OR PUPPIES?\nShould the report list receipts or pending puppytime installs?\nEnter 'r' for receipts, 'p' for puppies",
          prompt: "Receipts or puppies",
          required: false,
          default: 'r'
      )
      @options.report_q  =  rcpts_or_pups =~ /^p/i ?  true : false

      # prompt for status or frozen
      @options.status = D3::Admin::Interactive.get_status_for_filter(:with_frozen).split(/,\s*/) if @options.status.empty?

    end #if @options.walkthru

    @options.status = [] if @options.status.include? "all"

    # pass the --frozen option with the --status options, since
    # its treated like a pseudo-status, and thats where walkthru puts it too
    @options.status << "frozen" if @options.report_frozen

    reporting = @options.report_q ? :puppies : :receipts


    if @targets.empty?
      puts "No basename or computer name given for report"
      return
    end

    @targets.each do |target|

      # report puppies
      if reporting == :puppies
        if @options.report_computers
          D3::Admin::Report.report_single_puppy_queue target, @options.status
        else
          D3::Admin::Report.report_puppy_queues target, @options.status
        end

      # report receipts
      else
        if @options.report_computers
          D3::Admin::Report.report_single_computer_receipts target, @options.status
        else
          D3::Admin::Report.report_basename_receipts target, @options.status
        end

      end # if reporting puppies

      # blank line between reports, for clarity
      puts
    end # targets each
  end # show_report

  ### Run the config, saving hostnames, usernames and pws
  ### as needed
  ###
  ### @param display[Boolean] show the current admin config rather than
  ###   set it.
  ###
  ### @return [void]
  ###
  def config

    if @targets.include? 'display'
      D3::Admin::Prefs.display_config
    else
      D3::Admin::Prefs.config @targets, @options
    end
  end # config


end # class App




### save terminal state incase user interrupts during readline or less
if $stdin.tty?
  stty_save = `stty -g`.chomp
  trap("SIGINT") do
    puts "\nCancelled! Woot!"
    system('stty', stty_save)
    exit 0
  end
end

begin
  app = App.new
  app.run
rescue
  puts "An error occurred: #{$!.class}: #{$!}"
  puts $@ if D3::Admin.debug
  exit 1
ensure
  if D3::Admin::Auth.connected?
    # JSS::DistributionPoint.master_distribution_point.unmount if JSS::DistributionPoint.master_distribution_point.mounted?
    D3::Admin::Auth.disconnect
  end
end # begin