# Encoding: utf-8
#
# Copyright 2018 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# GoogleAds client class to configure settings and fetch services.

require 'googleauth'

require 'google/ads/google_ads/patches'
require 'google/ads/google_ads/config'
require 'google/ads/google_ads/field_mask_util'
require 'google/ads/google_ads/lookup_util'
require 'google/ads/google_ads/interceptors/logging_interceptor'
require 'google/ads/google_ads/factories'
require 'google/ads/google_ads/errors'
require 'google/ads/google_ads/service_lookup'
require 'google/ads/google_ads/deprecation'
require 'google/ads/google_ads/status_decoder'

require 'grpc'

require 'logger'
require 'json'
require 'openssl'
require 'signet/oauth_2/client'
require 'delegate'

module Google
  module Ads
    module GoogleAds
      class GoogleAdsClient

        DEFAULT_CONFIG_FILENAME = "google_ads_config.rb".freeze
        SCOPE = "https://www.googleapis.com/auth/adwords".freeze

        MAX_MESSAGE_LENGTH = "grpc.max_receive_message_length".freeze
        MAX_METADATA_SIZE = "grpc.max_metadata_size".freeze

        attr_reader :logger
        # Allow setting the lookup_util manually for users who use it before creating the client.
        attr_writer :lookup_util

        def initialize(config_path = nil, &block)
          if block_given?
            @config = Google::Ads::GoogleAds::Config.new

            yield @config
          else
            if config_path.nil?
              config_path = ENV.fetch("GOOGLE_ADS_CONFIGURATION_FILE_PATH",
                File.join(ENV['HOME'], DEFAULT_CONFIG_FILENAME))
            end

            unless File.exist?(config_path)
              raise ArgumentError,
                  sprintf('No configuration file found at location "%s"',
                  config_path)
            end
            file = File.read(config_path)

            eval_result = eval(file, binding, config_path)
            unless eval_result.instance_of?(Google::Ads::GoogleAds::Config)
              raise ArgumentError, sprintf(
                  'Configuration file did not produce expected type ' +
                  'Google::Ads::GoogleAds::Config, got "%s" instead',
                  eval_result.class
              )
            end
            @config = eval_result
          end

          begin
            @logger = create_default_logger
          rescue
            STDERR.puts(
                "Could not create default logger. Check your config file.")
          end
        end

        def configure(&block)
          yield @config
        end

        def load_environment_config
          # Generic variables
          @config.refresh_token = ENV.fetch("GOOGLE_ADS_REFRESH_TOKEN", @config.refresh_token)
          @config.client_id = ENV.fetch("GOOGLE_ADS_CLIENT_ID", @config.client_id)
          @config.client_secret = ENV.fetch("GOOGLE_ADS_CLIENT_SECRET", @config.client_secret)
          @config.keyfile = ENV.fetch("GOOGLE_ADS_JSON_KEY_FILE_PATH", @config.keyfile)
          @config.impersonate = ENV.fetch("GOOGLE_ADS_IMPERSONATED_EMAIL", @config.impersonate)
          @config.developer_token = ENV.fetch("GOOGLE_ADS_DEVELOPER_TOKEN", @config.developer_token)
          @config.login_customer_id = ENV.fetch("GOOGLE_ADS_LOGIN_CUSTOMER_ID", @config.login_customer_id)
          @config.linked_customer_id = ENV.fetch("GOOGLE_ADS_LINKED_CUSTOMER_ID", @config.linked_customer_id)
          @config.api_endpoint = ENV.fetch("GOOGLE_ADS_ENDPOINT", @config.api_endpoint)

          # Client library-specific variables
          @config.log_level = ENV.fetch("GOOGLE_ADS_RUBY_LOG_LEVEL", @config.log_level)
          @config.http_proxy = ENV.fetch("GOOGLE_ADS_RUBY_HTTP_PROXY", @config.http_proxy)
        end

        # Return a service for the provided entity type. For example, passing
        # :Campaign will return an instantiated CampaignServiceClient.
        #
        # Raises ArgumentError if no service can be found for the provided type.
        def service
          ServiceLookup.new(
            lookup_util,
            @logger,
            @config,
            make_channel,
            endpoint,
            deprecator,
          ).call
        end

        def endpoint
          target.split(":443").first
        end

        def target
          default_target = "googleads.googleapis.com:443"
          target = @config.api_endpoint || default_target
        end

        def make_channel
          channel_args = {
            MAX_MESSAGE_LENGTH => 64*1024*1024,
            MAX_METADATA_SIZE => 16*1024*1024,
          }

          if !@config.use_insecure_channel
            call_creds = GRPC::Core::CallCredentials.new(get_credentials)
            chan_creds = GRPC::Core::ChannelCredentials.new.compose(call_creds)
            GRPC::Core::Channel.new(target, channel_args, chan_creds)
          else
            GRPC::Core::Channel.new(target, channel_args, :this_channel_is_insecure)
          end
        end

        def patch_lro_headers(class_to_return, headers)
          PatchLROHeaders.new(class_to_return, headers).call
        end

        # Return a resource or common entity for the provided entity type. For
        # example, passing :Campaign will return an instantiated Campaign.
        #
        # Raises ArgumentError if no entity can be found for the provided type.
        def resource
          Factories.version_alternate_for(:resources)
        end

        def operation
          Factories.version_alternate_for(:operations)
        end

        def enum
          Factories.version_alternate_for(:enums)
        end

        # Returns a reference to the FieldMaskUtil class for ease of access.
        def field_mask()
          Google::Ads::GoogleAds::FieldMaskUtil
        end

        # Returns a reference to the PathLookupUtil to generate resource names.
        def path(version = default_api_version)
          lookup_util.path(version)
        end

        # Set the logger to use. This will only take effect on services fetched
        # after setting this value.
        def logger=(logger)
          @logger = logger
        end

        # Decode a partial failure error from a response.
        # See Google::Ads::GoogleAds::PartialFailureErrorDecoder for full
        # documentation.
        def decode_partial_failure_error(pfe)
          StatusDecoder.decode(pfe)
        end

        # Identical to decoding a partial failure error as above, but
        # duplicated so the client code is easier to follow.
        def decode_warning(warning)
          StatusDecoder.decode(warning)
        end

        private

        def get_credentials
          if @config.authentication
            @config.authentication
          elsif @config.keyfile
            get_service_account_credentials
          else
            get_updater_proc
          end
        end

        # Provides the service a method by which to obtain an access token to
        # authenticate API requests.
        def get_updater_proc
          Signet::OAuth2::Client.new(
            token_credential_uri: "https://www.googleapis.com/oauth2/v3/token",
            client_id: @config.client_id,
            client_secret: @config.client_secret,
            refresh_token: @config.refresh_token,
            scope: [SCOPE]
          ).updater_proc
        end

        # Provides a Google::Auth::Credentials initialized with a keyfile
        # specified in the config.
        def get_service_account_credentials
          keyfile = File.read(@config.keyfile)
          keyfile = JSON.parse(keyfile)
          Signet::OAuth2::Client.new(
            token_credential_uri: "https://accounts.google.com/o/oauth2/token",
            audience: "https://accounts.google.com/o/oauth2/token",
            issuer: keyfile.fetch("client_email"),
            signing_key: OpenSSL::PKey::RSA.new(keyfile.fetch("private_key")),
            person: @config.impersonate,
            scope: [SCOPE],
          ).updater_proc
        end

        # Create the default logger, useful if the user hasn't defined one.
        def create_default_logger()
          if @config.logger.nil?
            logger = Logger.new(@config.log_target)
            logger.level = Logger.const_get(@config.log_level)
            logger
          else
            @config.logger
          end
        end

        def lookup_util
          @lookup_util ||= Google::Ads::GoogleAds::LookupUtil.new
        end

        def default_api_version
          Google::Ads::GoogleAds.default_api_version
        end

        def deprecate(deprecation)
          deprecator.deprecate(deprecation)
        end

        def deprecator
          Google::Ads::GoogleAds::Deprecation.new(
            @config.treat_deprecation_warnings_as_errors,
            @config.warn_on_all_deprecations,
          )
        end
      end
    end
  end
end
