# frozen_string_literal: true

#  Copyright 2020-2021 Couchbase, Inc.
#
#  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
#
#      http://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 "couchbase/collection"
require "couchbase/query_options"
require "couchbase/analytics_options"

module Couchbase
  # The scope identifies a group of collections and allows high application density as a result.
  class Scope
    attr_reader :bucket_name
    attr_reader :name

    alias inspect to_s

    # @param [Couchbase::Backend] backend
    # @param [String] bucket_name name of the bucket
    # @param [String] scope_name name of the scope
    def initialize(backend, bucket_name, scope_name)
      @backend = backend
      @bucket_name = bucket_name
      @name = scope_name
    end

    # Opens the default collection for this scope
    #
    # @param [String] collection_name name of the collection
    #
    # @return [Collection]
    def collection(collection_name)
      Collection.new(@backend, @bucket_name, @name, collection_name)
    end

    # Performs a query against the query (N1QL) services.
    #
    # The query will be implicitly scoped using current bucket and scope names.
    #
    # @see Options::Query#scope_qualifier
    # @see Cluster#query
    #
    # @param [String] statement the N1QL query statement
    # @param [Options::Query] options the custom options for this query
    #
    # @example Select first ten hotels from travel sample dataset
    #   scope.query("SELECT * FROM `travel-sample` WHERE type = $type LIMIT 10",
    #                Options::Query(named_parameters: {type: "hotel"}, metrics: true))
    #
    # @return [QueryResult]
    def query(statement, options = Options::Query::DEFAULT)
      resp = @backend.document_query(statement, options.to_backend(scope_name: @name, bucket_name: @bucket_name))

      Cluster::QueryResult.new do |res|
        res.meta_data = Cluster::QueryMetaData.new do |meta|
          meta.status = resp[:meta][:status]
          meta.request_id = resp[:meta][:request_id]
          meta.client_context_id = resp[:meta][:client_context_id]
          meta.signature = JSON.parse(resp[:meta][:signature]) if resp[:meta][:signature]
          meta.profile = JSON.parse(resp[:meta][:profile]) if resp[:meta][:profile]
          meta.metrics = Cluster::QueryMetrics.new do |metrics|
            if resp[:meta][:metrics]
              metrics.elapsed_time = resp[:meta][:metrics][:elapsed_time]
              metrics.execution_time = resp[:meta][:metrics][:execution_time]
              metrics.sort_count = resp[:meta][:metrics][:sort_count]
              metrics.result_count = resp[:meta][:metrics][:result_count]
              metrics.result_size = resp[:meta][:metrics][:result_size]
              metrics.mutation_count = resp[:meta][:metrics][:mutation_count]
              metrics.error_count = resp[:meta][:metrics][:error_count]
              metrics.warning_count = resp[:meta][:metrics][:warning_count]
            end
          end
          meta.warnings = resp[:warnings].map { |warn| Cluster::QueryWarning.new(warn[:code], warn[:message]) } if resp[:warnings]
        end
        res.instance_variable_set(:@rows, resp[:rows])
      end
    end

    # Performs an analytics query
    #
    # The query will be implicitly scoped using current bucket and scope names.
    #
    # @param [String] statement the N1QL query statement
    # @param [Options::Analytics] options the custom options for this query
    #
    # @example Select name of the given user
    #   scope.analytics_query("SELECT u.name AS uname FROM GleambookUsers u WHERE u.id = $user_id ",
    #                          Options::Analytics(named_parameters: {user_id: 2}))
    #
    # @return [AnalyticsResult]
    def analytics_query(statement, options = Options::Analytics::DEFAULT)
      resp = @backend.document_analytics(statement, options.to_backend(scope_name: @name, bucket_name: @bucket_name))

      Cluster::AnalyticsResult.new do |res|
        res.transcoder = options.transcoder
        res.meta_data = Cluster::AnalyticsMetaData.new do |meta|
          meta.status = resp[:meta][:status]
          meta.request_id = resp[:meta][:request_id]
          meta.client_context_id = resp[:meta][:client_context_id]
          meta.signature = JSON.parse(resp[:meta][:signature]) if resp[:meta][:signature]
          meta.profile = JSON.parse(resp[:meta][:profile]) if resp[:meta][:profile]
          meta.metrics = Cluster::AnalyticsMetrics.new do |metrics|
            if resp[:meta][:metrics]
              metrics.elapsed_time = resp[:meta][:metrics][:elapsed_time]
              metrics.execution_time = resp[:meta][:metrics][:execution_time]
              metrics.result_count = resp[:meta][:metrics][:result_count]
              metrics.result_size = resp[:meta][:metrics][:result_size]
              metrics.error_count = resp[:meta][:metrics][:error_count]
              metrics.warning_count = resp[:meta][:metrics][:warning_count]
              metrics.processed_objects = resp[:meta][:metrics][:processed_objects]
            end
          end
          res[:warnings] = resp[:warnings].map { |warn| Cluster::AnalyticsWarning.new(warn[:code], warn[:message]) } if resp[:warnings]
        end
        res.instance_variable_set(:@rows, resp[:rows])
      end
    end

    # Performs a Full Text Search (FTS) query
    #
    # @param [String] index_name the name of the search index
    # @param [SearchQuery] query the query tree
    # @param [Options::Search] options the query tree
    #
    # @example Return first 10 results of "hop beer" query and request highlighting
    #   cluster.search_query("travel_index", Cluster::SearchQuery.match_phrase("green"),
    #                        Options::Search(
    #                          limit: 10,
    #                          collections: ["landmark", "hotel"]
    #                          fields: %w[name],
    #                          highlight_style: :html,
    #                          highlight_fields: %w[name description]
    #                        ))
    #
    # @return [SearchResult]
    def search_query(index_name, query, options = Options::Search::DEFAULT)
      resp = @backend.document_search(@bucket_name, @name, index_name, JSON.generate(query), {}, options.to_backend)
      convert_search_result(resp, options)
    end

    # Performs a request against the Full Text Search (FTS) service.
    #
    # @param [String] index_name the name of the search index
    # @param [SearchRequest] search_request the request
    # @param [Options::Search] options the custom options for this search request
    #
    # @return [SearchResult]
    def search(index_name, search_request, options = Options::Search::DEFAULT)
      encoded_query, encoded_req = search_request.to_backend
      resp = @backend.document_search(@bucket_name, @name, index_name, encoded_query, encoded_req, options.to_backend(show_request: false))
      convert_search_result(resp, options)
    end

    # @return [Management::ScopeSearchIndexManager]
    def search_indexes
      Management::ScopeSearchIndexManager.new(@backend, @bucket_name, @name)
    end

    private

    # @api private
    def convert_search_result(resp, options)
      SearchResult.new do |res|
        res.meta_data = SearchMetaData.new do |meta|
          meta.metrics.max_score = resp[:meta_data][:metrics][:max_score]
          meta.metrics.error_partition_count = resp[:meta_data][:metrics][:error_partition_count]
          meta.metrics.success_partition_count = resp[:meta_data][:metrics][:success_partition_count]
          meta.metrics.took = resp[:meta_data][:metrics][:took]
          meta.metrics.total_rows = resp[:meta_data][:metrics][:total_rows]
          meta.errors = resp[:meta_data][:errors]
        end
        res.rows = resp[:rows].map do |r|
          SearchRow.new do |row|
            row.transcoder = options.transcoder
            row.index = r[:index]
            row.id = r[:id]
            row.score = r[:score]
            row.fragments = r[:fragments]
            unless r[:locations].empty?
              row.locations = SearchRowLocations.new(
                r[:locations].map do |loc|
                  SearchRowLocation.new do |location|
                    location.field = loc[:field]
                    location.term = loc[:term]
                    location.position = loc[:position]
                    location.start_offset = loc[:start_offset]
                    location.end_offset = loc[:end_offset]
                    location.array_positions = loc[:array_positions]
                  end
                end,
              )
            end
            row.instance_variable_set(:@fields, r[:fields])
            row.explanation = JSON.parse(r[:explanation]) if r[:explanation]
          end
        end
        if resp[:facets]
          res.facets = resp[:facets].each_with_object({}) do |(k, v), o|
            facet = case options.facets[k]
                    when SearchFacet::SearchFacetTerm
                      SearchFacetResult::TermFacetResult.new do |f|
                        f.terms =
                          if v[:terms]
                            v[:terms].map do |t|
                              SearchFacetResult::TermFacetResult::TermFacet.new(t[:term], t[:count])
                            end
                          else
                            []
                          end
                      end
                    when SearchFacet::SearchFacetDateRange
                      SearchFacetResult::DateRangeFacetResult.new do |f|
                        f.date_ranges =
                          if v[:date_ranges]
                            v[:date_ranges].map do |r|
                              SearchFacetResult::DateRangeFacetResult::DateRangeFacet.new(r[:name], r[:count], r[:start_time], r[:end_time])
                            end
                          else
                            []
                          end
                      end
                    when SearchFacet::SearchFacetNumericRange
                      SearchFacetResult::NumericRangeFacetResult.new do |f|
                        f.numeric_ranges =
                          if v[:numeric_ranges]
                            v[:numeric_ranges].map do |r|
                              SearchFacetResult::NumericRangeFacetResult::NumericRangeFacet.new(r[:name], r[:count], r[:min], r[:max])
                            end
                          else
                            []
                          end
                      end
                    else
                      next # ignore unknown facet result
                    end
            facet.name = v[:name]
            facet.field = v[:field]
            facet.total = v[:total]
            facet.missing = v[:missing]
            facet.other = v[:other]
            o[k] = facet
          end
        end
      end
    end
  end
end
