module Setup
  class BaseOauthProvider < AuthorizationProvider

    abstract_class true

    def self.response_type_enum
      ['code']
    end

    def self.token_method_enum
      %w(POST GET)
    end

    def self.refresh_token_strategy_enum
      [
        'Google v4',
        'Intuit Reconnect API V1',
        'Lazada REST API',
        'custom',
        'default',
        'none'
      ]
    end

    build_in_data_type
      .referenced_by(:namespace, :name)
      .and_polymorphic(properties: {
        response_type: {
          enum: response_type_enum
        },
        token_method: {
          enum: token_method_enum
        },
        refresh_token_strategy: {
          enum: refresh_token_strategy_enum
        }
      })

    field :response_type, type: String
    field :token_endpoint, type: String
    field :token_method, type: String

    field :refresh_token_strategy, type: String, default: :none.to_s
    belongs_to :refresh_token_algorithm, class_name: Setup::Algorithm.to_s, inverse_of: nil

    validates_presence_of :name, :response_type, :authorization_endpoint, :token_endpoint, :token_method
    validates_inclusion_of :response_type, in: ->(provider) { provider.response_type_enum }
    validates_inclusion_of :token_method, in: ->(provider) { provider.token_method_enum }
    validates_inclusion_of :refresh_token_strategy, in: ->(provider) { provider.refresh_token_strategy_enum }

    before_save do
      case refresh_token_strategy
        when :custom
          errors.add(:refresh_token_algorithm, "can't be blank") unless refresh_token_algorithm
        when :none
          if refresh_token_algorithm
            errors.add(:refresh_token_algorithm, 'not allowed')
            self.refresh_token_algorithm = nil
          end
      end
      abort_if_has_errors
    end

    def response_type_enum
      self.class.response_type_enum
    end

    def token_method_enum
      self.class.token_method_enum
    end

    def refresh_token_strategy_enum
      self.class.refresh_token_strategy_enum
    end

    def refresh_token(authorization)
      if authorization.authorized?
        send(refresh_token_strategy.tr(' ', '_').underscore + '_refresh_token', authorization)
        authorization.access_token
      else
        fail "#{authorization.custom_title} not yet authorized"
      end
    end

    def is_time_to_refresh?(authorization, refresh_delay = Cenit.delay_time_for_token_refresh.seconds)
      authorization.authorized_at.nil? || (
        (authorization.authorized_at + authorization.token_span.to_i.seconds - refresh_delay) < Time.now
      )
    end

    def none_refresh_token(authorization)
    end

    def custom_refresh_token(authorization)
      if (alg = refresh_token_algorithm)
        alg.run(authorization)
        authorization.save
      else
        fail "Refresh token algorithm is missing on #{custom_title}"
      end
    end

    def default_refresh_token(authorization)
      if (refresh_token = authorization.refresh_token) && is_time_to_refresh?(authorization)
        fail 'Missing client configuration' unless (client = authorization.client)
        http_response = HTTMultiParty.post(
          authorization.token_endpoint,
          headers: {
            'Content-Type' => 'application/x-www-form-urlencoded'
          }.merge(client.conformed_request_token_headers(template_parameters = authorization.template_parameters_hash)),
          query: client.conformed_request_token_parameters(template_parameters),
          body: {
            grant_type: :refresh_token,
            refresh_token: refresh_token,
            client_id: authorization.client.get_identifier,
            client_secret: authorization.client.get_secret
          }.to_param
        )
        body = JSON.parse(http_response.body)
        if http_response.code == 200
          update_data = {
            authorized_at: Time.now,
            token_type: body['token_type'],
            access_token: body['access_token'],
            token_span: body['expires_in']
          }
          if (refresh_token = body['refresh_token'])
            update_data[:refresh_token] = refresh_token
          end
          authorization.update!(update_data)
        else
          fail "(response code #{http_response.code} - #{body['error']}) #{body['error_description']}"
        end
      end
    rescue Exception => ex
      Setup::SystemNotification.create_from(ex, "refreshing token for #{authorization.custom_title}")
      raise "Error refreshing token for #{authorization.custom_title}: #{ex.message}"
    end

    def google_v4_refresh_token(authorization)
      if (refresh_token = authorization.refresh_token) && is_time_to_refresh?(authorization)
        fail 'Missing client configuration' unless authorization.client
        post = Setup::Connection.post('https://www.googleapis.com/oauth2/v4/token')
        http_response = post.submit(
          headers: { 'Content-Type' => 'application/x-www-form-urlencoded' },
          body: {
            grant_type: :refresh_token,
            refresh_token: refresh_token,
            client_id: authorization.client.get_identifier,
            client_secret: authorization.client.get_secret
          }.to_param,
          verbose_response: true
        )[:response]
        body = JSON.parse(http_response.body)
        if http_response.code == 200
          authorization.authorized_at = Time.now
          authorization.token_type = body['token_type']
          authorization.access_token = body['access_token']
          authorization.token_span = body['expires_in']
          authorization.save
        else
          fail "(response code #{http_response.code} - #{body['error']}) #{body['error_description']}"
        end
      end
    rescue Exception => ex
      raise "Error refreshing token for #{authorization.custom_title}: #{ex.message}"
    end

    def intuit_reconnect_api_v1_refresh_token(authorization)
      return unless authorization.authorized_at + 151.days < Time.now
      url = 'https://appcenter.intuit.com/api/v1/connection/reconnect'
      response = Setup::Connection.get(url).with(authorization).submit(http_proxy_address: Cenit.http_proxy_address,
                                                                       http_proxy_port: Cenit.http_proxy_port)
      xml_doc = Nokogiri::XML(response)
      if (oauth_token = xml_doc.root.element_children.detect { |e| e.name == 'OAuthToken' }) &&
         (oauth_token_secret = xml_doc.root.element_children.detect { |e| e.name == 'OAuthTokenSecret' })
        authorization.access_token = oauth_token.content
        authorization.access_token_secret = oauth_token_secret.content
        authorization.save
      else
        msg = 'An error occurs'
        if (error_message = xml_doc.root.element_children.detect { |e| e.name == 'ErrorMessage' })
          msg = error_message.content
        end
        if (error_code = xml_doc.root.element_children.detect { |e| e.name == 'ErrorCode' })
          msg += " (Error code #{error_code.content})"
        end
        fail msg
      end
    rescue Exception => ex
      raise "Error refreshing token for #{authorization.custom_title}: #{ex.message}"
    end

    def lazada_rest_api_refresh_token(authorization)
      if is_time_to_refresh?(authorization, Cenit.delay_time_for_lazada_token_refresh)
        client = authorization.client
        fail 'Missing client configuration' unless client
        fail 'Missing OAuth provider configuration' unless authorization.provider
        token_endpoint_uri = URI.parse(authorization.token_endpoint)
        refresh_endpoint_uri = URI.parse('/rest/auth/token/refresh')
        refresh_endpoint_uri.scheme = token_endpoint_uri.scheme
        refresh_endpoint_uri.host = token_endpoint_uri.host
        get = Setup::Connection.get(refresh_endpoint_uri.to_s)
        params = { 'refresh_token' => authorization.refresh_token.to_s }
        Setup::LazadaAuthorization.sign_params(client, '/auth/token/refresh', params)
        http_response = get.submit(parameters: params, verbose_response: true)[:response]
        body = JSON.parse(http_response.body)
        if http_response.code == 200 && body['access_token']
          authorization.authorized_at = Time.now
          if (token_type = body['token_type'])
            authorization.token_type = token_type
          end
          authorization.access_token = body['access_token']
          authorization.token_span = body['expires_in']
          if (refresh_token = body['refresh_token'])
            authorization.refresh_token = refresh_token
          end
          authorization.save
        else
          fail body['message']
        end
      end
    rescue Exception => ex
      raise "Error refreshing token for #{authorization.custom_title}: #{ex.message}"
    end

    def client(name)
      Setup::OauthClient.where(provider_id: id, name: name).first
    end
  end
end
