require 'fog/brightbox'
require 'fog/compute'
require 'fog/brightbox/oauth2'

module Fog
  module Compute
    class Brightbox < Fog::Service

      API_URL = "https://api.gb1.brightbox.com/"

      # Client credentials
      requires :brightbox_client_id, :brightbox_secret

      # API endpoint settings
      recognizes :brightbox_auth_url, :brightbox_api_url

      # User credentials (still requires client details)
      recognizes :brightbox_username, :brightbox_password, :brightbox_account

      # Cached tokens
      recognizes :brightbox_access_token, :brightbox_refresh_token

      # Automatic token management
      recognizes :brightbox_token_management

      # Excon connection settings
      recognizes :persistent

      model_path 'fog/brightbox/models/compute'
      collection  :accounts
      model       :account
      collection  :applications
      model       :application
      collection  :api_clients
      model       :api_client
      collection  :servers
      model       :server
      collection  :server_groups
      model       :server_group
      collection  :firewall_policies
      model       :firewall_policy
      collection  :firewall_rules
      model       :firewall_rule
      collection  :flavors
      model       :flavor
      collection  :images
      model       :image
      collection  :load_balancers
      model       :load_balancer
      collection  :zones
      model       :zone
      collection  :cloud_ips
      model       :cloud_ip
      collection  :users
      model       :user

      request_path 'fog/brightbox/requests/compute'
      request :activate_console_server
      request :add_listeners_load_balancer
      request :add_nodes_load_balancer
      request :add_servers_server_group
      request :apply_to_firewall_policy
      request :remove_firewall_policy
      request :create_api_client
      request :create_application
      request :create_cloud_ip
      request :create_firewall_policy
      request :create_firewall_rule
      request :create_image
      request :create_load_balancer
      request :create_server
      request :create_server_group
      request :destroy_api_client
      request :destroy_application
      request :destroy_cloud_ip
      request :destroy_firewall_policy
      request :destroy_firewall_rule
      request :destroy_image
      request :destroy_load_balancer
      request :destroy_server
      request :destroy_server_group
      request :get_account
      request :get_api_client
      request :get_application
      request :get_authenticated_user
      request :get_cloud_ip
      request :get_firewall_policy
      request :get_firewall_rule
      request :get_image
      request :get_interface
      request :get_load_balancer
      request :get_scoped_account
      request :get_server
      request :get_server_group
      request :get_server_type
      request :get_user
      request :get_zone
      request :list_accounts
      request :list_api_clients
      request :list_applications
      request :list_cloud_ips
      request :list_firewall_policies
      request :list_images
      request :list_load_balancers
      request :list_server_groups
      request :list_server_types
      request :list_servers
      request :list_users
      request :list_zones
      request :map_cloud_ip
      request :move_servers_server_group
      request :remove_listeners_load_balancer
      request :remove_nodes_load_balancer
      request :remove_servers_server_group
      request :reset_ftp_password_account
      request :reset_ftp_password_scoped_account
      request :reset_secret_api_client
      request :reset_secret_application
      request :shutdown_server
      request :snapshot_server
      request :start_server
      request :stop_server
      request :unmap_cloud_ip
      request :update_account
      request :update_api_client
      request :update_application
      request :update_cloud_ip
      request :update_firewall_rule
      request :update_image
      request :update_load_balancer
      request :update_scoped_account
      request :update_server
      request :update_server_group
      request :update_user

      module Shared
        include Fog::Brightbox::OAuth2

        # Creates a new instance of the Brightbox Compute service
        #
        # @note If you create service using just a refresh token when it
        #   expires the service will no longer be able to authenticate.
        #
        # @param [Hash] options
        # @option options [String] :brightbox_api_url   Override the default (or configured) API endpoint
        # @option options [String] :brightbox_auth_url  Override the default (or configured) API authentication endpoint
        # @option options [String] :brightbox_client_id Client identifier to authenticate with (overrides configured)
        # @option options [String] :brightbox_secret    Client secret to authenticate with (overrides configured)
        # @option options [String] :brightbox_username  Email or user identifier for user based authentication
        # @option options [String] :brightbox_password  Password for user based authentication
        # @option options [String] :brightbox_account   Account identifier to scope this connection to
        # @option options [String] :connection_options  Settings to pass to underlying {Fog::Connection}
        # @option options [Boolean] :persistent         Sets a persistent HTTP {Fog::Connection}
        # @option options [String] :brightbox_access_token  Sets the OAuth access token to use rather than requesting a new token
        # @option options [String] :brightbox_refresh_token Sets the refresh token to use when requesting a newer access token
        # @option options [String] :brightbox_token_management Overide the existing behaviour to request access tokens if expired (default is `true`)
        #
        def initialize(options)
          # Currently authentication and api endpoints are the same but may change
          @auth_url            = options[:brightbox_auth_url]  || Fog.credentials[:brightbox_auth_url] || API_URL
          @auth_connection     = Fog::Connection.new(@auth_url)

          @api_url             = options[:brightbox_api_url]   || Fog.credentials[:brightbox_api_url]  || API_URL
          @connection_options  = options[:connection_options]  || {}
          @persistent          = options[:persistent]          || false
          @connection          = Fog::Connection.new(@api_url, @persistent, @connection_options)

          # Authentication options
          client_id            = options[:brightbox_client_id] || Fog.credentials[:brightbox_client_id]
          client_secret        = options[:brightbox_secret]    || Fog.credentials[:brightbox_secret]

          username             = options[:brightbox_username]  || Fog.credentials[:brightbox_username]
          password             = options[:brightbox_password]  || Fog.credentials[:brightbox_password]
          @configured_account  = options[:brightbox_account]   || Fog.credentials[:brightbox_account]
          # Request account can be changed at anytime and changes behaviour of future requests
          @scoped_account      = @configured_account

          credential_options   = {:username => username, :password => password}
          @credentials         = CredentialSet.new(client_id, client_secret, credential_options)

          # If existing tokens have been cached, allow continued use of them in the service
          @credentials.update_tokens(options[:brightbox_access_token], options[:brightbox_refresh_token])

          @token_management    = options.fetch(:brightbox_token_management, true)
        end

        # Sets the scoped account for future requests
        # @param [String] scoped_account Identifier of the account to scope request to
        def scoped_account=(scoped_account)
          @scoped_account = scoped_account
        end

        # This returns the account identifier that the request should be scoped by
        # based on the options passed to the request and current configuration
        #
        # @param [String] options_account Any identifier passed into the request
        #
        # @return [String, nil] The account identifier to scope the request to or nil
        def scoped_account(options_account = nil)
          [options_account, @scoped_account].compact.first
        end

        # Resets the scoped account back to intially configured one
        def scoped_account_reset
          @scoped_account = @configured_account
        end

        # Returns the scoped account being used for requests
        #
        # * For API clients this is the owning account
        # * For User applications this is the account specified by either +account_id+
        #   option on the service or the +brightbox_account+ setting in your configuration
        #
        # @return [Fog::Compute::Brightbox::Account]
        #
        def account
          account_data = get_scoped_account.merge(:service => self)
          Fog::Compute::Brightbox::Account.new(account_data)
        end

        # Returns true if authentication is being performed as a user
        # @return [Boolean]
        def authenticating_as_user?
          @credentials.user_details?
        end

        # Returns true if an access token is set
        # @return [Boolean]
        def access_token_available?
          !! @credentials.access_token
        end

        # Returns the current access token or nil
        # @return [String,nil]
        def access_token
          @credentials.access_token
        end

        # Returns the current refresh token or nil
        # @return [String,nil]
        def refresh_token
          @credentials.refresh_token
        end

        # Requests a new access token
        #
        # @return [String] New access token
        def get_access_token
          begin
            get_access_token!
          rescue Excon::Errors::Unauthorized, Excon::Errors::BadRequest
            @credentials.update_tokens(nil, nil)
          end
          @credentials.access_token
        end

        # Requests a new access token and raises if there is a problem
        #
        # @return [String] New access token
        # @raise [Excon::Errors::BadRequest] The credentials are expired or incorrect
        #
        def get_access_token!
          response = request_access_token(@auth_connection, @credentials)
          update_credentials_from_response(@credentials, response)
          @credentials.access_token
        end

        # Returns an identifier for the default image for use
        #
        # Currently tries to find the latest version Ubuntu LTS (i686) widening
        # up to the latest, official version of Ubuntu available.
        #
        # Highly recommended that you actually select the image you want to run
        # on your servers yourself!
        #
        # @return [String, nil]
        def default_image
          return @default_image_id unless @default_image_id.nil?
          @default_image_id = Fog.credentials[:brightbox_default_image] || select_default_image
        end

      private

        # This makes a request of the API based on the configured setting for
        # token management.
        #
        # @param [Hash] options Excon compatible options
        # @see https://github.com/geemus/excon/blob/master/lib/excon/connection.rb
        #
        # @return [Hash] Data of response body
        #
        def make_request(options)
          if @token_management
            managed_token_request(options)
          else
            authenticated_request(options)
          end
        end

        # This request checks for access tokens and will ask for a new one if
        # it receives Unauthorized from the API before repeating the request
        #
        # @param [Hash] options Excon compatible options
        #
        # @return [Excon::Response]
        def managed_token_request(options)
          begin
            get_access_token unless access_token_available?
            response = authenticated_request(options)
          rescue Excon::Errors::Unauthorized
            get_access_token
            response = authenticated_request(options)
          end
        end

        # This request makes an authenticated request of the API using currently
        # setup credentials.
        #
        # @param [Hash] options Excon compatible options
        #
        # @see https://github.com/geemus/excon/blob/master/lib/excon/connection.rb
        #
        # @return [Excon::Response]
        def authenticated_request(options)
          headers = options[:headers] || {}
          headers.merge!("Authorization" => "OAuth #{@credentials.access_token}", "Content-Type" => "application/json")
          options[:headers] = headers
          # TODO This is just a wrapper around a call to Excon::Connection#request
          #   so can be extracted from Compute by passing in the connection,
          #   credentials and options
          @connection.request(options)
        end
      end

      # The Mock Service allows you to run a fake instance of the Service
      # which makes no real connections.
      #
      # @todo Implement
      #
      class Mock
        include Shared

        def request(method, path, expected_responses, parameters = {})
          _request
        end

        def request_access_token(connection, credentials)
          _request
        end

      private

        def _request
          raise Fog::Errors::MockNotImplemented
        end

        def select_default_image
          "img-mockd"
        end
      end

      # The Real Service actually makes real connections to the Brightbox
      # service.
      #
      class Real
        include Shared

        # Makes an API request to the given path using passed options or those
        # set with the service setup
        #
        # @todo Standard Fog behaviour is to return the Excon::Response but
        #   this was unintentionally changed to be the Hash version of the
        #   data in the body. This loses access to some details and should
        #   be corrected in a backwards compatible manner
        #
        # @overload request(params)
        #   @param [Hash] params Excon compatible options
        #   @option params [String] :body text to be sent over a socket
        #   @option params [Hash<Symbol, String>] :headers The default headers to supply in a request
        #   @option params [String] :host The destination host's reachable DNS name or IP, in the form of a String
        #   @option params [String] :path appears after 'scheme://host:port/'
        #   @option params [Fixnum] :port The port on which to connect, to the destination host
        #   @option params [Hash]   :query appended to the 'scheme://host:port/path/' in the form of '?key=value'
        #   @option params [String] :scheme The protocol; 'https' causes OpenSSL to be used
        #   @return [Excon::Response]
        #   @see https://github.com/geemus/excon/blob/master/lib/excon/connection.rb
        #
        # @overload request(method, path, expected_responses, params = {})
        #   @param [String] method HTTP method to use for the request
        #   @param [String] path   The absolute path for the request
        #   @param [Array<Fixnum>] expected_responses HTTP response codes that have been successful
        #   @param [Hash] params Keys and values for JSON
        #   @option params [String] :account_id The scoping account if required
        #   @deprecated #request with multiple arguments is deprecated
        #     since it is inconsistent with original fog version.
        #   @return [Hash]
        def request(*args)
          if args.size == 1
            authenticated_request(*args)
          else
            Fog::Logger.deprecation("#request with multiple parameters is deprecated, use #wrapped_request instead [light_black](#{caller.first})[/]")
            wrapped_request(*args)
          end
        end

        # Makes a request but with seperated arguments and parses the response to a hash
        #
        # @note #wrapped_request is the non-standard form of request introduced by mistake
        #
        # @param [String] method HTTP method to use for the request
        # @param [String] path   The absolute path for the request
        # @param [Array<Fixnum>] expected_responses HTTP response codes that have been successful
        # @param [Hash]  parameters Keys and values for JSON
        # @option parameters [String] :account_id The scoping account if required
        #
        # @return [Hash]
        def wrapped_request(method, path, expected_responses, parameters = {})
          _wrapped_request(method, path, expected_responses, parameters)
        end

      private

        # Wrapped request is the non-standard form of request introduced by mistake
        #
        # @param [String] method HTTP method to use for the request
        # @param [String] path   The absolute path for the request
        # @param [Array<Fixnum>] expected_responses HTTP response codes that have been successful
        # @param [Hash]  parameters Keys and values for JSON
        # @option parameters [String] :account_id The scoping account if required
        #
        # @return [Hash]
        def _wrapped_request(method, path, expected_responses, parameters = {})
          request_options = {
            :method   => method.to_s.upcase,
            :path     => path,
            :expects  => expected_responses
          }

          # Select the account to scope for this request
          account = scoped_account(parameters.fetch(:account_id, nil))
          if account
            request_options[:query] = { :account_id => account }
          end

          request_options[:body] = Fog::JSON.encode(parameters) unless parameters.empty?

          response = make_request(request_options)

          # FIXME We should revert to returning the Excon::Request after a suitable
          # configuration option is in place to switch back to this incorrect behaviour
          unless response.body.empty?
            Fog::JSON.decode(response.body)
          else
            response
          end
        end

        # Queries the API and tries to select the most suitable official Image
        # to use if the user chooses not to select their own.
        def select_default_image
          return @default_image_id unless @default_image_id.nil?

          all_images = list_images
          official_images = all_images.select {|img| img["official"] == true}
          ubuntu_lts_images = official_images.select {|img| img["name"] =~ /Ubuntu.*LTS/}
          ubuntu_lts_i686_images = ubuntu_lts_images.select {|img| img["arch"] == "i686"}

          if ubuntu_lts_i686_images.empty?
            # Accept other architectures
            if ubuntu_lts_images.empty?
              # Accept non-LTS versions of Ubuntu
              unsorted_images = official_images.select {|img| img["name"] =~ /Ubuntu/}
            else
              unsorted_images = ubuntu_lts_images
            end
          else
            unsorted_images = ubuntu_lts_i686_images
          end

          # Get the latest and use it's ID for the default image
          @default_image_id = unsorted_images.sort {|a,b| a["created_at"] <=> b["created_at"]}.first["id"]
        rescue
          nil
        end
      end

    end
  end
end
