module Specinfra
  module Backend
    class Ssh < Exec
      def run_command(cmd, opt={})
        cmd = build_command(cmd)
        cmd = add_pre_command(cmd)

        if get_config(:ssh_without_env)
          ret = ssh_exec!(cmd)
        else
          ret = with_env do
            ssh_exec!(cmd)
          end
        end

        ret[:stdout].gsub!(/\r\n/, "\n")
        ret[:stdout].gsub!(/\A\n/, "") if sudo?

        if @example
          @example.metadata[:command] = cmd
          @example.metadata[:stdout]  = ret[:stdout]
        end

        CommandResult.new ret
      end

      def send_file(from, to)
        scp_upload!(from, to)
      end

      def send_directory(from, to)
        scp_upload!(from, to, :recursive => true)
      end

      def build_command(cmd)
        cmd = super(cmd)
        if sudo?
          cmd = "#{sudo} -p '#{prompt}' #{cmd}"
        end
        cmd
      end

      private
      def prompt
        'Password: '
      end

      def with_env
        env = get_config(:env) || {}
        env[:LANG] ||= 'C'

        ssh_options = get_config(:ssh_options) || {}
        ssh_options[:send_env] ||= []

        env.each do |key, value|
          key = key.to_s
          ENV["_SPECINFRA_#{key}"] = ENV[key];
          ENV[key] = value
          ssh_options[:send_env] << key
        end

        yield
      ensure
        env.each do |key, value|
          key = key.to_s
          ENV[key] = ENV.delete("_SPECINFRA_#{key}");
        end
      end

      def create_ssh
        Net::SSH.start(
          get_config(:host),
          get_config(:ssh_options)[:user],
          get_config(:ssh_options)
        )
      end

      def create_scp
        ssh = get_config(:ssh)
        if ssh.nil?
          ssh = create_ssh
        end
        Net::SCP.new(ssh)
      end

      def scp_upload!(from, to, opt={})
        if get_config(:scp).nil?
          set_config(:scp, create_scp)
        end

        tmp = File.join('/tmp', File.basename(to))
        scp = get_config(:scp)
        scp.upload!(from, tmp, opt)
        run_command(command.get(:move_file, tmp, to))
      end

      def ssh_exec!(command)
        stdout_data = ''
        stderr_data = ''
        exit_status = nil
        exit_signal = nil
        retry_prompt = /^Sorry, try again/

        if get_config(:ssh).nil?
          set_config(:ssh, create_ssh)
        end

        ssh = get_config(:ssh)
        ssh.open_channel do |channel|
          if get_config(:sudo_password) or get_config(:request_pty)
            channel.request_pty do |ch, success|
              abort "Could not obtain pty " if !success
            end
          end
          channel.exec("#{command}") do |ch, success|
            abort "FAILED: couldn't execute command (ssh.channel.exec)" if !success
            channel.on_data do |ch, data|
              if data.match retry_prompt
                abort "Wrong sudo password! Please confirm your password on #{get_config(:host)}."
              elsif data.match /^#{prompt}/
                channel.send_data "#{get_config(:sudo_password)}\n"
              # When pty is allocated and the name of the target host
              # cannot be resolved, this error is injected into stdout.
              # So exclude this error message.
              elsif ! data.match /^sudo: unable to resolve host/
                stdout_data += data
                @stdout_handler.call(data) if @stdout_handler
              end
            end

            channel.on_extended_data do |ch, type, data|
              if data.match /you must have a tty to run sudo/
                abort 'Please write "set :request_pty, true" in your spec_helper.rb or other appropriate file.'
              end

              if data.match /^sudo: no tty present and no askpass program specified/
                abort 'Please set sudo password to Specinfra.configuration.sudo_password.'
              else
                stderr_data += data
                @stderr_handler.call(data) if @stderr_handler
              end
            end

            channel.on_request("exit-status") do |ch, data|
              exit_status = data.read_long
            end

            channel.on_request("exit-signal") do |ch, data|
              exit_signal = data.read_long
            end
          end
        end
        ssh.loop
        { :stdout => stdout_data, :stderr => stderr_data, :exit_status => exit_status, :exit_signal => exit_signal }
      end

      def sudo
        if sudo_path = get_config(:sudo_path)
          sudo_path += '/sudo'
        else
          sudo_path = 'sudo'
        end

        sudo_options = get_config(:sudo_options)
        if sudo_options
          sudo_options = sudo_options.shelljoin if sudo_options.is_a?(Array)
          sudo_options = ' ' + sudo_options
        end

        "#{sudo_path.shellescape}#{sudo_options}"
      end

      def sudo?
        user = get_config(:ssh_options)[:user]
        disable_sudo = get_config(:disable_sudo)
        user != 'root' && !disable_sudo
      end
    end
  end
end
