# Copyright 2016 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.


require "google/cloud/errors"
require "google/cloud/datastore/credentials"
require "google/cloud/datastore/version"
require "google/cloud/datastore/v1"
require "google/gax/errors"

module Google
  module Cloud
    module Datastore
      ##
      # @private Represents the GAX Datastore service, including all the API
      # methods.
      class Service
        attr_accessor :project, :credentials, :host, :timeout, :client_config

        ##
        # Creates a new Service instance.
        def initialize project, credentials, host: nil, timeout: nil,
                       client_config: nil
          @project = project
          @credentials = credentials
          @host = host || V1::DatastoreClient::SERVICE_ADDRESS
          @timeout = timeout
          @client_config = client_config || {}
        end

        def channel
          require "grpc"
          GRPC::Core::Channel.new host, nil, chan_creds
        end

        def chan_creds
          return credentials if insecure?
          require "grpc"
          GRPC::Core::ChannelCredentials.new.compose \
            GRPC::Core::CallCredentials.new credentials.client.updater_proc
        end

        def service
          return mocked_service if mocked_service
          @service ||= V1::DatastoreClient.new(
            credentials: channel,
            timeout: timeout,
            client_config: client_config,
            lib_name: "gccl",
            lib_version: Google::Cloud::Datastore::VERSION
          )
        end
        attr_accessor :mocked_service

        def insecure?
          credentials == :this_channel_is_insecure
        end

        ##
        # Allocate IDs for incomplete keys.
        # (This is useful for referencing an entity before it is inserted.)
        def allocate_ids *incomplete_keys
          execute do
            service.allocate_ids project, incomplete_keys,
                                 options: default_options
          end
        end

        ##
        # Look up entities by keys.
        def lookup *keys, consistency: nil, transaction: nil
          read_options = generate_read_options consistency, transaction

          execute do
            service.lookup project, keys,
                           read_options: read_options, options: default_options
          end
        end

        # Query for entities.
        def run_query query, namespace = nil, consistency: nil, transaction: nil
          gql_query = nil
          if query.is_a? Google::Datastore::V1::GqlQuery
            gql_query = query
            query = nil
          end
          read_options = generate_read_options consistency, transaction
          if namespace
            partition_id = Google::Datastore::V1::PartitionId.new(
              namespace_id: namespace
            )
          end

          execute do
            service.run_query project,
                              partition_id,
                              read_options: read_options,
                              query: query,
                              gql_query: gql_query,
                              options: default_options
          end
        end

        ##
        # Begin a new transaction.
        def begin_transaction read_only: nil, previous_transaction: nil
          if read_only
            transaction_options = Google::Datastore::V1::TransactionOptions.new
            transaction_options.read_only = \
              Google::Datastore::V1::TransactionOptions::ReadOnly.new
          end
          if previous_transaction
            transaction_options ||= \
              Google::Datastore::V1::TransactionOptions.new
            rw = Google::Datastore::V1::TransactionOptions::ReadWrite.new(
              previous_transaction: previous_transaction.encode("ASCII-8BIT")
            )
            transaction_options.read_write = rw
          end
          execute do
            service.begin_transaction project,
                                      transaction_options: transaction_options
          end
        end

        ##
        # Commit a transaction, optionally creating, deleting or modifying
        # some entities.
        def commit mutations, transaction: nil
          mode =  transaction.nil? ? :NON_TRANSACTIONAL : :TRANSACTIONAL
          execute do
            service.commit project, mode, mutations, transaction: transaction,
                                                     options: default_options
          end
        end

        ##
        # Roll back a transaction.
        def rollback transaction
          execute do
            service.rollback project, transaction, options: default_options
          end
        end

        def inspect
          "#{self.class}(#{@project})"
        end

        protected

        def generate_read_options consistency, transaction
          if consistency == :eventual
            return Google::Datastore::V1::ReadOptions.new(
              read_consistency: :EVENTUAL
            )
          elsif consistency == :strong
            return Google::Datastore::V1::ReadOptions.new(
              read_consistency: :STRONG
            )
          elsif transaction
            return Google::Datastore::V1::ReadOptions.new(
              transaction: transaction
            )
          end
          nil
        end

        def default_headers
          { "google-cloud-resource-prefix" => "projects/#{@project}" }
        end

        def default_options
          Google::Gax::CallOptions.new kwargs: default_headers
        end

        def execute
          yield
        rescue Google::Gax::GaxError => e
          # GaxError wraps BadStatus, but exposes it as #cause
          raise Google::Cloud::Error.from_error(e.cause)
        end
      end
    end
  end
end
