require 'active_support/core_ext/kernel/reporting'
# Silencing warnings because not all versions of GSSAPI support all of the GSSAPI methods
# the gssapi gem attempts to attach to and these warnings are dumped to STDERR.
silence_warnings do
  require 'winrm'
end

module Ridley
  module HostConnector
    # @author Kyle Allan <kallan@riotgames.com>
    class WinRM < HostConnector::Base
      require_relative 'winrm/command_uploader'

      DEFAULT_PORT       = 5985
      EMBEDDED_RUBY_PATH = 'C:\opscode\chef\embedded\bin\ruby'.freeze

      # Execute a shell command on a node
      #
      # @param [String] host
      #   the host to perform the action on
      # @param [String] command
      #
      # @option options [Hash] :winrm
      #   * :user (String) a user that will login to each node and perform the bootstrap command on
      #   * :password (String) the password for the user that will perform the bootstrap (required)
      #   * :port (Fixnum) the winrm port to connect on the node the bootstrap will be performed on (5985)
      #
      # @return [HostConnector::Response]
      def run(host, command, options = {})
        options = options.reverse_merge(winrm: Hash.new)
        options[:winrm].reverse_merge!(port: DEFAULT_PORT)

        command_uploaders = Array.new
        user              = options[:winrm][:user]
        password          = options[:winrm][:password]
        port              = options[:winrm][:port]
        connection        = winrm(host, port, options[:winrm].slice(:user, :password))

        HostConnector::Response.new(host).tap do |response|
          command_uploaders << command_uploader = CommandUploader.new(connection)
          command = get_command(command, command_uploader)

          begin
            log.info "Running WinRM Command: '#{command}' on: '#{host}' as: '#{user}'"

            output = connection.run_cmd(command) do |stdout, stderr|
              if stdout
                response.stdout += stdout
                log.info "[#{host}](WinRM) #{stdout}"
              end

              if stderr
                response.stderr += stderr unless stderr.nil?
                log.info "[#{host}](WinRM) #{stdout}"
              end
            end

            response.exit_code = output[:exitcode]
          rescue ::WinRM::WinRMHTTPTransportError => ex
            response.exit_code = -1
            response.stderr    = ex.message
            return response
          end

          case response.exit_code
          when 0
            log.info "Successfully ran WinRM command on: '#{host}' as: '#{user}'"
          else
            log.info "Successfully ran WinRM command on: '#{host}' as: '#{user}', but it failed"
          end
        end
      ensure
        command_uploaders.map(&:cleanup)
      end

      # Returns the command if it does not break the WinRM command length
      # limit. Otherwise, we return an execution of the command as a batch file.
      #
      # @param  command [String]
      #
      # @return [String]
      def get_command(command, command_uploader)
        if command.length < CommandUploader::CHUNK_LIMIT
          command
        else
          log.debug "Detected a command that was longer than #{CommandUploader::CHUNK_LIMIT} characters. " +
            "Uploading command as a file to the host."
          command_uploader.upload(command)
          command_uploader.command
        end
      end

      # Bootstrap a node
      #
      # @param [String] host
      #   the host to perform the action on
      #
      # @option options [Hash] :winrm
      #   * :user (String) a user that will login to each node and perform the bootstrap command on
      #   * :password (String) the password for the user that will perform the bootstrap (required)
      #   * :port (Fixnum) the winrm port to connect on the node the bootstrap will be performed on (5985)
      #
      # @return [HostConnector::Response]
      def bootstrap(host, options = {})
        context = BootstrapContext::Windows.new(options)

        log.info "Bootstrapping host: #{host}"
        run(host, context.boot_command, options)
      end

      # Perform a chef client run on a node
      #
      # @param [String] host
      #   the host to perform the action on
      #
      # @option options [Hash] :winrm
      #   * :user (String) a user that will login to each node and perform the bootstrap command on
      #   * :password (String) the password for the user that will perform the bootstrap (required)
      #   * :port (Fixnum) the winrm port to connect on the node the bootstrap will be performed on (5985)
      #
      # @return [HostConnector::Response]
      def chef_client(host, options = {})
        run(host, "chef-client", options)
      end

      # Write your encrypted data bag secret on a node
      #
      # @param [String] host
      #   the host to perform the action on
      # @param [String] secret
      #   your organization's encrypted data bag secret
      #
      # @option options [Hash] :winrm
      #   * :user (String) a user that will login to each node and perform the bootstrap command on
      #   * :password (String) the password for the user that will perform the bootstrap (required)
      #   * :port (Fixnum) the winrm port to connect on the node the bootstrap will be performed on (5985)
      #
      # @return [HostConnector::Response]
      def put_secret(host, secret, options = {})
        command = "echo #{secret} > C:\\chef\\encrypted_data_bag_secret"
        run(host, command, options)
      end

      # Execute line(s) of Ruby code on a node using Chef's embedded Ruby
      #
      # @param [String] host
      #   the host to perform the action on
      # @param [Array<String>] command_lines
      #   An Array of lines of the command to be executed
      #
      # @option options [Hash] :winrm
      #   * :user (String) a user that will login to each node and perform the bootstrap command on
      #   * :password (String) the password for the user that will perform the bootstrap (required)
      #   * :port (Fixnum) the winrm port to connect on the node the bootstrap will be performed on (5985)
      #
      # @return [HostConnector::Response]
      def ruby_script(host, command_lines, options = {})
        command = "#{EMBEDDED_RUBY_PATH} -e \"#{command_lines.join(';')}\""
        run(host, command, options)
      end

      private

        # @param [String] host
        # @param [Integer] port
        #
        # @option options [String] :user
        # @option options [String] :password
        #
        # @return [WinRM::WinRMWebService]
        def winrm(host, port, options = {})
          winrm_opts = { disable_sspi: true, basic_auth_only: true }
          winrm_opts[:user] = options[:user]
          winrm_opts[:pass] = options[:password]
          client = ::WinRM::WinRMWebService.new("http://#{host}:#{port}/wsman", :plaintext, winrm_opts)
          client.set_timeout(6000)
          client
        end
    end
  end
end