module Oxidized
  require_relative 'script'
  require 'slop'

  class Script
    class CLI
      attr_accessor :cmd_class
      class CLIError < ScriptError; end
      class NothingToDo < ScriptError; end

      def run
        if @group or @regex or @ostype
          $stdout.sync = true
          nodes = get_hosts
          counter = @threads.to_i
          Signal.trap("CLD")  { counter += 1 }
          nodes.each do |node| 
            Process.wait if counter <= 0
            puts "Forking " + node if @verbose
            counter -= 1
            fork {
              begin
                @host = node
                connect
                if @opts[:commands]
                  puts "Running commands on #{node}:\n#{run_file @opts[:commands]}"
                elsif @cmd
                  puts "Running commands on #{node}:\n#{@oxs.cmd @cmd}"
                end
              rescue => error
                puts "We had the following error on node #{node}:\n#{error}"
              end
            }
          end
          Process.waitall
        else
          connect
          if @opts[:commands]
            puts run_file @opts[:commands]
          elsif @cmd
            puts @oxs.cmd @cmd
          end
        end
      end

      private

      def initialize
        @args, @opts = opts_parse load_dynamic

        Config.load(@opts)
        Oxidized.setup_logger

        if @opts[:commands]
          Oxidized.config.vars.ssh_no_exec = true
        end

        if @cmd_class
          @cmd_class.run :args=>@args, :opts=>@opts, :host=>@host, :cmd=>@cmd
          exit 0
        else
          if @group or @regex or @ostype
            @cmd = @args.shift
          else
            @host = @args.shift
            @cmd  = @args.shift if @args
          end
          @oxs  = nil
          raise NothingToDo, 'no host given' if not @host and not @group and not @ostype and not @regex
          if @dryrun
            puts get_hosts
            exit
          end
          raise NothingToDo, 'nothing to do, give command or -x' if not @cmd and not @opts[:commands]
        end
      end

      def opts_parse cmds
        slop = Slop.new(:help=>true)
        slop.banner 'Usage: oxs [options] hostname [command]'
        slop.on 'm=', '--model',     'host model (ios, junos, etc), otherwise discovered from Oxidized source'
        slop.on 'o=', '--ostype',    'OS Type (ios, junos, etc)'
        slop.on 'x=', '--commands',  'commands file to be sent'
        slop.on 'u=', '--username',  'username to use'
        slop.on 'p=', '--password',  'password to use'
        slop.on 't=', '--timeout',   'timeout value to use'
        slop.on 'e=', '--enable',    'enable password to use'
        slop.on 'c=', '--community', 'snmp community to use for discovery'
        slop.on 'g=', '--group',     'group to run commands on (ios, junos, etc), specified in oxidized db'
        slop.on 'r=', '--threads',   'specify ammount of threads to use for running group', default: '1'
        slop.on       '--regex=',    'run on all hosts that match the regexp'
        slop.on       '--dryrun',    'do a dry run on either groups or regexp to find matching hosts'
        slop.on       '--protocols=','protocols to use, default "ssh, telnet"'
        slop.on 'v',  '--verbose',   'verbose output, e.g. show commands sent'
        slop.on 'd',  '--debug',     'turn on debugging'
        slop.on :terse, 'display clean output'
        cmds.each do |cmd|
          if cmd[:class].respond_to? :cmdline
            cmd[:class].cmdline slop, self
          else
            slop.on cmd[:name], cmd[:description] do
              @cmd_class = cmd[:class]
            end
          end
        end
        slop.parse
        @group = slop[:group]
        @ostype = slop[:ostype]
        @threads = slop[:threads]
        @verbose = slop[:verbose]
        @dryrun= slop[:dryrun]
        @regex = slop[:regex]
        [slop.parse!, slop]
      end

      def connect
        opts = {}
        opts[:host]     = @host
        [:model, :username, :password, :timeout, :enable, :verbose, :community, :protocols].each do |key|
          opts[key] = @opts[key] if @opts[key]
        end
        @oxs = Script.new opts
      end

      def run_file file
        out = ''
        file = file == '-' ? $stdin : File.read(file)
        file.each_line do |line|
          line.chomp!
          # line.sub!(/\\n/, "\n") # treat escaped newline as newline
          out += @oxs.cmd line
        end
        out
      end

      def load_dynamic
        cmds = []
        files = File.dirname __FILE__
        files = File.join files, 'commands', '*.rb'
        files = Dir.glob files
        files.each { |file| require_relative file }
        Script::Command.constants.each do |cmd|
          next if cmd == :Base
          cmd = Script::Command.const_get cmd
          name = cmd.const_get :Name
          desc = cmd.const_get :Description
          cmds << {:class=>cmd, :name=>name, :description=>desc}
        end
        cmds
      end

      def get_hosts
        if @group and @regex
          puts "running list for hosts in group: #{@group} and matching: #{@regex}" if @verbose
          nodes_group = run_group @group
          nodes_regex = run_regex @regex
          return nodes_group & nodes_regex
        elsif @group and @ostype
          puts "running list for hosts in group: #{@group} and matching: #{@ostype}" if @verbose
          nodes_group = run_group @group
          nodes_ostype = run_ostype @ostype
          return nodes_group & nodes_ostype
        elsif @regex
          puts 'running list for hosts matching: ' + @regex if @verbose
          return run_regex @regex
        elsif @ostype
          puts 'running list for hosts matching ostype: ' + @ostype if @verbose
          return run_ostype @ostype
        else
          puts 'running list for hosts in group: ' + @group if @verbose
          return run_group @group
        end
      end

      def run_group group
        Oxidized.mgr = Manager.new
        out = []
        Nodes.new.each do |node|
          next unless group == node.group
          out << node.name
        end
        out
      end

      def run_ostype ostype
        Oxidized.mgr = Manager.new
        out = []
        Nodes.new.each do |node|
          ostype.downcase # need to make sure they are both in lowercase
          nodemodel = node.model.to_s.downcase # need to make sure they are both in lowercase
          next unless nodemodel =~ /#{ostype}/ 
          out << node.name
        end
        out
      end

      def run_regex regex
        Oxidized.mgr = Manager.new
        out = []
        Nodes.new.each do |node|
          next unless node.name =~ /#{regex}/
          out << node.name
        end
        out
      end

    end
  end
end