require "net/ssh"
require "shellwords"
require "etc"

module Soloist
  class RemoteError < RuntimeError; end

  class Remote
    attr_reader :user, :host, :key, :timeout, :stdout, :stderr, :exitstatus
    attr_writer :connection

    def self.from_uri(uri, key = "~/.ssh/id_rsa")
      parsed = URI.parse("ssh://#{uri}")
      new(parsed.user || Etc.getlogin, parsed.host, key)
    end

    def initialize(user, host, key, options = {})
      @user = user
      @host = host
      @key = File.expand_path(key)
      @timeout = options[:timeout] || 10000
    end

    def backtick(command)
      @stdout = ""
      @stderr = ""
      exec(command)
      @stdout
    end

    def system(command)
      @stdout = STDOUT
      @stderr = STDERR
      exec(command)
      exitstatus
    end

    def system!(*command)
      system(*command).tap do |status|
        raise RemoteError.new("#{command.join(" ")} exited #{status}") unless status == 0
      end
    end

    def upload(from, to, opts = "--exclude .git")
      Kernel.system("rsync -e 'ssh -i #{key}' -avz --delete #{from} #{user}@#{host}:#{to} #{opts}")
    end

    private
    def connection
      @connection ||= Net::SSH.start(host, user, :keys => [key], :timeout => timeout)
    end

    def exec(*command)
      connection.open_channel do |channel|
        channel.exec(*command) do |stream, success|
          raise RemoteError.new("Could not run #{command.join(" ")}") unless success
          stream.on_data { |_, data| stdout << data }
          stream.on_extended_data { |_, type, data| stderr << data }
          stream.on_request("exit-status") { |_, data| @exitstatus = data.read_long }
        end
      end
      connection.loop
      @exitstatus ||= 0
    end
  end
end
