#!/usr/bin/env ruby

bin_dir = File.expand_path("..", __FILE__)
lib_dir = File.expand_path("../lib", bin_dir)

$LOAD_PATH.unshift(lib_dir)
$LOAD_PATH.uniq!

OAUTH_SERVER_PORT = 12736

require 'rubygems'
require 'optparse'
require 'faraday'
require 'faraday/utils'
require 'webrick'
require 'google/api_client/version'
require 'google/api_client'

ARGV.unshift('--help') if ARGV.empty?

module Google
  class APIClient
    class CLI
      # Used for oauth login
      class OAuthVerifierServlet < WEBrick::HTTPServlet::AbstractServlet
        attr_reader :verifier

        def do_GET(request, response)
          $verifier ||= Addressable::URI.unencode_component(
            request.request_uri.to_s[/\?.*oauth_verifier=([^&$]+)(&|$)/, 1] ||
            request.request_uri.to_s[/\?.*code=([^&$]+)(&|$)/, 1]
          )
          response.status = WEBrick::HTTPStatus::RC_ACCEPTED
          # This javascript will auto-close the tab after the
          # verifier is obtained.
          response.body = <<-HTML
<html>
  <head>
    <script>
      function closeWindow() { 
        window.open('', '_self', '');
        window.close();
      }
      setTimeout(closeWindow, 10);
    </script>
  </head>
  <body>
    You may close this window.
  </body>
</html>
HTML
          # Eww, hack!
          server = self.instance_variable_get('@server')
          server.stop if server
        end
      end

      # Initialize with default parameter values
      def initialize(argv)
        @options = {
          :command => 'execute',
          :rpcname => nil,
          :verbose => false
        }
        @argv = argv.clone
        if @argv.first =~ /^[a-z0-9][a-z0-9_-]*$/i
          self.options[:command] = @argv.shift
        end
        if @argv.first =~ /^[a-z0-9_-]+\.[a-z0-9_\.-]+$/i
          self.options[:rpcname] = @argv.shift
        end
      end

      attr_reader :options
      attr_reader :argv

      def command
        return self.options[:command]
      end

      def rpcname
        return self.options[:rpcname]
      end

      def parser
        @parser ||= OptionParser.new do |opts|
          opts.banner = "Usage: google-api " +
            "(execute <rpcname> | [command]) [options] [-- <parameters>]"

          opts.separator "\nAvailable options:"

          opts.on(
              "--scope <scope>", String, "Set the OAuth scope") do |s|
            options[:scope] = s
          end
          opts.on(
              "--client-id <key>", String,
                "Set the OAuth client id or key") do |k|
            options[:client_credential_key] = k
          end
          opts.on(
              "--client-secret <secret>", String,
                "Set the OAuth client secret") do |s|
            options[:client_credential_secret] = s
          end
          opts.on(
              "--api <name>", String,
              "Perform discovery on API") do |s|
            options[:api] = s
          end
          opts.on(
              "--api-version <id>", String,
              "Select api version") do |id|
            options[:version] = id
          end
          opts.on(
              "--content-type <format>", String,
              "Content-Type for request") do |f|
            # Resolve content type shortcuts
            case f
            when 'json'
              f = 'application/json'
            when 'xml'
              f = 'application/xml'
            when 'atom'
              f = 'application/atom+xml'
            when 'rss'
              f = 'application/rss+xml'
            end
            options[:content_type] = f
          end
          opts.on(
              "-u", "--uri <uri>", String,
              "Sets the URI to perform a request against") do |u|
            options[:uri] = u
          end
          opts.on(
              "--discovery-uri <uri>", String,
              "Sets the URI to perform discovery") do |u|
            options[:discovery_uri] = u
          end
          opts.on(
              "-m", "--method <method>", String,
              "Sets the HTTP method to use for the request") do |m|
            options[:http_method] = m
          end
          opts.on(
              "--requestor-id <email>", String,
              "Sets the email address of the requestor") do |e|
            options[:requestor_id] = e
          end

          opts.on("-v", "--verbose", "Run verbosely") do |v|
            options[:verbose] = v
          end
          opts.on("-h", "--help", "Show this message") do
            puts opts
            exit
          end
          opts.on("--version", "Show version") do
            puts "google-api-client (#{Google::APIClient::VERSION::STRING})"
            exit
          end

          opts.separator(
            "\nAvailable commands:\n" +
            "    oauth-1-login   Log a user into an API with OAuth 1.0a\n" +
            "    oauth-2-login   Log a user into an API with OAuth 2.0 d10\n" +
            "    list            List the methods available for an API\n" +
            "    execute         Execute a method on the API\n" +
            "    irb             Start an interactive client session"
          )
        end
      end

      def parse!
        self.parser.parse!(self.argv)
        symbol = self.command.gsub(/-/, "_").to_sym
        if !COMMANDS.include?(symbol)
          STDERR.puts("Invalid command: #{self.command}")
          exit(1)
        end
        self.send(symbol)
      end

      def client
        require 'signet/oauth_1/client'
        require 'yaml'
        require 'irb'
        config_file = File.expand_path('~/.google-api.yaml')
        authorization = nil
        if File.exist?(config_file)
          config = open(config_file, 'r') { |file| YAML.load(file.read) }
        else
          config = {}
        end
        if config["mechanism"]
          authorization = config["mechanism"].to_sym
        end

        client = Google::APIClient.new(:authorization => authorization)

        case authorization
        when :oauth_1
          if client.authorization &&
              !client.authorization.kind_of?(Signet::OAuth1::Client)
            STDERR.puts(
              "Unexpected authorization mechanism: " +
              "#{client.authorization.class}"
            )
            exit(1)
          end
          config = open(config_file, 'r') { |file| YAML.load(file.read) }
          client.authorization.client_credential_key =
            config["client_credential_key"]
          client.authorization.client_credential_secret =
            config["client_credential_secret"]
          client.authorization.token_credential_key =
            config["token_credential_key"]
          client.authorization.token_credential_secret =
            config["token_credential_secret"]
        when :oauth_2
          if client.authorization &&
              !client.authorization.kind_of?(Signet::OAuth2::Client)
            STDERR.puts(
              "Unexpected authorization mechanism: " +
              "#{client.authorization.class}"
            )
            exit(1)
          end
          config = open(config_file, 'r') { |file| YAML.load(file.read) }
          client.authorization.scope = options[:scope]
          client.authorization.client_id = config["client_id"]
          client.authorization.client_secret = config["client_secret"]
          client.authorization.access_token = config["access_token"]
          client.authorization.refresh_token = config["refresh_token"]
        else
          # Dunno?
        end

        if options[:discovery_uri]
          if options[:api] && options[:version]
            client.register_discovery_uri(
              options[:api], options[:version], options[:discovery_uri]
            )
          else
            STDERR.puts(
              'Cannot register a discovery URI without ' +
              'specifying an API and version.'
            )
            exit(1)
          end
        end

        return client
      end

      def api_version(api_name, version)
        v = version
        if !version
          if client.preferred_version(api_name)
            v = client.preferred_version(api_name).version
          else
            v = 'v1'
          end
        end
        return v
      end

      COMMANDS = [
        :oauth_1_login,
        :oauth_2_login,
        :list,
        :execute,
        :irb,
        :fuzz
      ]

      def oauth_1_login
        require 'signet/oauth_1/client'
        require 'launchy'
        require 'yaml'
        if options[:client_credential_key] &&
            options[:client_credential_secret]
          config = {
            "mechanism" => "oauth_1",
            "scope" => options[:scope],
            "client_credential_key" => options[:client_credential_key],
            "client_credential_secret" => options[:client_credential_secret],
            "token_credential_key" => nil,
            "token_credential_secret" => nil
          }
          config_file = File.expand_path('~/.google-api.yaml')
          open(config_file, 'w') { |file| file.write(YAML.dump(config)) }
          exit(0)
        else
          $verifier = nil
          server = WEBrick::HTTPServer.new(
            :Port => OAUTH_SERVER_PORT,
            :Logger => WEBrick::Log.new,
            :AccessLog => WEBrick::Log.new
          )
          server.logger.level = 0
          trap("INT") { server.shutdown }

          server.mount("/", OAuthVerifierServlet)

          oauth_client = Signet::OAuth1::Client.new(
            :temporary_credential_uri =>
              'https://www.google.com/accounts/OAuthGetRequestToken',
            :authorization_uri =>
              'https://www.google.com/accounts/OAuthAuthorizeToken',
            :token_credential_uri =>
              'https://www.google.com/accounts/OAuthGetAccessToken',
            :client_credential_key => 'anonymous',
            :client_credential_secret => 'anonymous',
            :callback => "http://localhost:#{OAUTH_SERVER_PORT}/"
          )
          oauth_client.fetch_temporary_credential!(:additional_parameters => {
            :scope => options[:scope],
            :xoauth_displayname => 'Google API Client'
          })

          # Launch browser
          Launchy::Browser.run(oauth_client.authorization_uri.to_s)

          server.start
          oauth_client.fetch_token_credential!(:verifier => $verifier)
          config = {
            "scope" => options[:scope],
            "client_credential_key" =>
              oauth_client.client_credential_key,
            "client_credential_secret" =>
              oauth_client.client_credential_secret,
            "token_credential_key" =>
              oauth_client.token_credential_key,
            "token_credential_secret" =>
              oauth_client.token_credential_secret
          }
          config_file = File.expand_path('~/.google-api.yaml')
          open(config_file, 'w') { |file| file.write(YAML.dump(config)) }
          exit(0)
        end
      end

      def oauth_2_login
        require 'signet/oauth_2/client'
        require 'launchy'
        require 'yaml'
        if !options[:client_credential_key] ||
            !options[:client_credential_secret]
          STDERR.puts('No client ID and secret supplied.')
          exit(1)
        end
        if options[:access_token]
          config = {
            "mechanism" => "oauth_2",
            "scope" => options[:scope],
            "client_id" => options[:client_credential_key],
            "client_secret" => options[:client_credential_secret],
            "access_token" => options[:access_token],
            "refresh_token" => options[:refresh_token]
          }
          config_file = File.expand_path('~/.google-api.yaml')
          open(config_file, 'w') { |file| file.write(YAML.dump(config)) }
          exit(0)
        else
          $verifier = nil
          logger = WEBrick::Log.new
          logger.level = 0
          server = WEBrick::HTTPServer.new(
            :Port => OAUTH_SERVER_PORT,
            :Logger => logger,
            :AccessLog => logger
          )
          trap("INT") { server.shutdown }

          server.mount("/", OAuthVerifierServlet)

          oauth_client = Signet::OAuth2::Client.new(
            :authorization_uri =>
              'https://www.google.com/accounts/o8/oauth2/authorization',
            :token_credential_uri =>
              'https://www.google.com/accounts/o8/oauth2/token',
            :client_id => options[:client_credential_key],
            :client_secret => options[:client_credential_secret],
            :redirect_uri => "http://localhost:#{OAUTH_SERVER_PORT}/",
            :scope => options[:scope]
          )

          # Launch browser
          Launchy.open(oauth_client.authorization_uri.to_s)

          server.start
          oauth_client.code = $verifier
          oauth_client.fetch_access_token!
          config = {
            "mechanism" => "oauth_2",
            "scope" => options[:scope],
            "client_id" => oauth_client.client_id,
            "client_secret" => oauth_client.client_secret,
            "access_token" => oauth_client.access_token,
            "refresh_token" => oauth_client.refresh_token
          }
          config_file = File.expand_path('~/.google-api.yaml')
          open(config_file, 'w') { |file| file.write(YAML.dump(config)) }
          exit(0)
        end
      end

      def list
        api_name = options[:api]
        unless api_name
          STDERR.puts('No API name supplied.')
          exit(1)
        end
        client = Google::APIClient.new(:authorization => nil)
        if options[:discovery_uri]
          if options[:api] && options[:version]
            client.register_discovery_uri(
              options[:api], options[:version], options[:discovery_uri]
            )
          else
            STDERR.puts(
              'Cannot register a discovery URI without ' +
              'specifying an API and version.'
            )
            exit(1)
          end
        end
        version = api_version(api_name, options[:version])
        api = client.discovered_api(api_name, version)
        rpcnames = api.to_h.keys
        puts rpcnames.sort.join("\n")
        exit(0)
      end

      def execute
        client = self.client

        # Setup HTTP request data
        request_body = ''
        input_streams, _, _ = IO.select([STDIN], [], [], 0)
        request_body = STDIN.read || '' if input_streams
        headers = []
        if options[:content_type]
          headers << ['Content-Type', options[:content_type]]
        elsif request_body
          # Default to JSON
          headers << ['Content-Type', 'application/json']
        end

        if options[:uri]
          # Make request with URI manually specified
          uri = Addressable::URI.parse(options[:uri])
          if uri.relative?
            STDERR.puts('URI may not be relative.')
            exit(1)
          end
          if options[:requestor_id]
            uri.query_values = uri.query_values.merge(
              'xoauth_requestor_id' => options[:requestor_id]
            )
          end
          method = options[:http_method]
          method ||= request_body == '' ? 'GET' : 'POST'
          method.upcase!
          response = client.execute(:http_method => method, :uri => uri.to_str, 
            :headers => headers, :body => request_body)
          puts response.body
          exit(0)
        else
          # Make request with URI generated from template and parameters
          if !self.rpcname
            STDERR.puts('No rpcname supplied.')
            exit(1)
          end
          api_name = options[:api] || self.rpcname[/^([^\.]+)\./, 1]
          version = api_version(api_name, options[:version])
          api = client.discovered_api(api_name, version)
          method = api.to_h[self.rpcname]
          if !method
            STDERR.puts(
              "Method #{self.rpcname} does not exist for " +
              "#{api_name}-#{version}."
            )
            exit(1)
          end
          parameters = self.argv.inject({}) do |accu, pair|
            name, value = pair.split('=', 2)
            accu[name] = value
            accu
          end
          if options[:requestor_id]
            parameters['xoauth_requestor_id'] = options[:requestor_id]
          end
          begin
            result = client.execute(
              :api_method => method,
              :parameters => parameters,
              :merged_body => request_body,
              :headers => headers
            )
            puts result.response.body
            exit(0)
          rescue ArgumentError => e
            puts e.message
            exit(1)
          end
        end
      end

      def irb
        $client = self.client
        # Otherwise IRB will misinterpret command-line options
        ARGV.clear
        IRB.start(__FILE__)
      end

      def fuzz
        STDERR.puts('API fuzzing not yet supported.')
        if self.rpcname
          # Fuzz just one method
        else
          # Fuzz the entire API
        end
        exit(1)
      end

      def help
        puts self.parser
        exit(0)
      end
    end
  end
end

Google::APIClient::CLI.new(ARGV).parse!