module Koala
  module Facebook
    class OAuth
      attr_reader :app_id, :app_secret, :oauth_callback_url
      def initialize(app_id, app_secret, oauth_callback_url = nil)
        @app_id = app_id
        @app_secret = app_secret
        @oauth_callback_url = oauth_callback_url
      end

      def get_user_info_from_cookie(cookie_hash)
        # Parses the cookie set by the official Facebook JavaScript SDK.
        #
        # cookies should be a Hash, like the one Rails provides
        #
        # If the user is logged in via Facebook, we return a dictionary with the
        # keys "uid" and "access_token". The former is the user's Facebook ID,
        # and the latter can be used to make authenticated requests to the Graph API.
        # If the user is not logged in, we return None.
        #
        # Download the official Facebook JavaScript SDK at
        # http://github.com/facebook/connect-js/. Read more about Facebook
        # authentication at http://developers.facebook.com/docs/authentication/.

        if fb_cookie = cookie_hash["fbs_" + @app_id.to_s]
          # remove the opening/closing quote
          fb_cookie = fb_cookie.gsub(/\"/, "")

          # since we no longer get individual cookies, we have to separate out the components ourselves
          components = {}
          fb_cookie.split("&").map {|param| param = param.split("="); components[param[0]] = param[1]}

          # generate the signature and make sure it matches what we expect
          auth_string = components.keys.sort.collect {|a| a == "sig" ? nil : "#{a}=#{components[a]}"}.reject {|a| a.nil?}.join("")
          sig = Digest::MD5.hexdigest(auth_string + @app_secret)
          sig == components["sig"] && (components["expires"] == "0" || Time.now.to_i < components["expires"].to_i) ? components : nil
        end
      end
      alias_method :get_user_info_from_cookies, :get_user_info_from_cookie

      def get_user_from_cookie(cookies)
        if info = get_user_info_from_cookies(cookies)
          string = info["uid"]
        end
      end
      alias_method :get_user_from_cookies, :get_user_from_cookie

      # URLs

      def url_for_oauth_code(options = {})
        # for permissions, see http://developers.facebook.com/docs/authentication/permissions
        permissions = options[:permissions]
        scope = permissions ? "&scope=#{permissions.is_a?(Array) ? permissions.join(",") : permissions}" : ""
        display = options.has_key?(:display) ? "&display=#{options[:display]}" : ""
        
        callback = options[:callback] || @oauth_callback_url
        raise ArgumentError, "url_for_oauth_code must get a callback either from the OAuth object or in the options!" unless callback

        # Creates the URL for oauth authorization for a given callback and optional set of permissions
        "https://#{GRAPH_SERVER}/oauth/authorize?client_id=#{@app_id}&redirect_uri=#{callback}#{scope}#{display}"
      end

      def url_for_access_token(code, options = {})
        # Creates the URL for the token corresponding to a given code generated by Facebook
        callback = options[:callback] || @oauth_callback_url
        raise ArgumentError, "url_for_access_token must get a callback either from the OAuth object or in the parameters!" unless callback
        "https://#{GRAPH_SERVER}/oauth/access_token?client_id=#{@app_id}&redirect_uri=#{callback}&client_secret=#{@app_secret}&code=#{code}"
      end

      def get_access_token_info(code, options = {})
        # convenience method to get a parsed token from Facebook for a given code
        # should this require an OAuth callback URL?
        get_token_from_server({:code => code, :redirect_uri => @oauth_callback_url}, false, options)
      end

      def get_access_token(code, options = {})
        # upstream methods will throw errors if needed
        if info = get_access_token_info(code, options)
          string = info["access_token"]
        end
      end

      def get_app_access_token_info(options = {})
        # convenience method to get a the application's sessionless access token
        get_token_from_server({:type => 'client_cred'}, true, options)
      end

      def get_app_access_token(options = {})
        if info = get_app_access_token_info(options)
          string = info["access_token"]
        end
      end

      # Originally provided directly by Facebook, however this has changed
      # as their concept of crypto changed. For historic purposes, this is their proposal:
      # https://developers.facebook.com/docs/authentication/canvas/encryption_proposal/
      # Currently see https://github.com/facebook/php-sdk/blob/master/src/facebook.php#L758
      # for a more accurate reference implementation strategy.
      def parse_signed_request(input)
        encoded_sig, encoded_envelope = input.split('.', 2)
        signature = base64_url_decode(encoded_sig).unpack("H*").first
        envelope = MultiJson.decode(base64_url_decode(encoded_envelope))

        raise "SignedRequest: Unsupported algorithm #{envelope['algorithm']}" if envelope['algorithm'] != 'HMAC-SHA256'

        # now see if the signature is valid (digest, key, data)
        hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, @app_secret, encoded_envelope.tr("-_", "+/"))
        raise 'SignedRequest: Invalid signature' if (signature != hmac)

        return envelope
      end

      # from session keys
      def get_token_info_from_session_keys(sessions, options = {})
        # fetch the OAuth tokens from Facebook
        response = fetch_token_string({
          :type => 'client_cred',
          :sessions => sessions.join(",")
        }, true, "exchange_sessions", options)

        # Facebook returns an empty body in certain error conditions
        if response == ""
          raise APIError.new({
            "type" => "ArgumentError",
            "message" => "get_token_from_session_key received an error (empty response body) for sessions #{sessions.inspect}!"
          })
        end

        MultiJson.decode(response)
      end

      def get_tokens_from_session_keys(sessions, options = {})
        # get the original hash results
        results = get_token_info_from_session_keys(sessions, options)
        # now recollect them as just the access tokens
        results.collect { |r| r ? r["access_token"] : nil }
      end

      def get_token_from_session_key(session, options = {})
        # convenience method for a single key
        # gets the overlaoded strings automatically
        get_tokens_from_session_keys([session], options)[0]
      end

      protected

      def get_token_from_server(args, post = false, options = {})
        # fetch the result from Facebook's servers
        result = fetch_token_string(args, post, "access_token", options)

        # if we have an error, parse the error JSON and raise an error
        raise APIError.new((MultiJson.decode(result)["error"] rescue nil) || {}) if result =~ /error/

        # otherwise, parse the access token
        parse_access_token(result)
      end

      def parse_access_token(response_text)
        components = response_text.split("&").inject({}) do |hash, bit|
          key, value = bit.split("=")
          hash.merge!(key => value)
        end
        components
      end

      def fetch_token_string(args, post = false, endpoint = "access_token", options = {})
        Koala.make_request("/oauth/#{endpoint}", {
          :client_id => @app_id,
          :client_secret => @app_secret
        }.merge!(args), post ? "post" : "get", {:use_ssl => true}.merge!(options)).body
      end

      # base 64
      # directly from https://github.com/facebook/crypto-request-examples/raw/master/sample.rb
      def base64_url_decode(str)
        str += '=' * (4 - str.length.modulo(4))
        Base64.decode64(str.tr('-_', '+/'))
      end
    end
  end
end