# Copyright 2024 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/env"
require "google/logging/message"
require "google/logging/structured_formatter"

module Gapic
  ##
  # A mixin module that handles logging setup for a stub.
  #
  module LoggingConcerns
    ##
    # The logger for this object.
    #
    # @return [Logger]
    #
    attr_reader :logger

    ##
    # @private
    # A convenience object used by stub-based logging.
    #
    class StubLogger
      OMIT_FILES = [
        /^#{Regexp.escape __dir__}/
      ].freeze

      def initialize logger: nil, **kwargs
        @logger = logger
        @kwargs = kwargs
      end

      def enabled?
        !!@logger
      end

      def log severity
        return unless @logger
        locations = caller_locations
        @logger.add severity do
          builder = LogEntryBuilder.new(**@kwargs)
          builder.set_source_location_from locations
          yield builder
          builder.build
        rescue StandardError
          # Do nothing
        end
      end

      def info(&)
        log(Logger::INFO, &)
      end

      def debug(&)
        log(Logger::DEBUG, &)
      end

      ##
      # @private
      # Builder for a log entry, passed to {StubLogger#log}.
      #
      class LogEntryBuilder
        def initialize system_name: nil,
                       service: nil,
                       endpoint: nil,
                       client_id: nil
          @system_name = system_name
          @service = service
          @endpoint = endpoint
          @client_id = client_id
          @message = nil
          @caller_locations = caller_locations
          @fields = { "clientId" => @client_id }
        end

        attr_reader :system_name

        attr_reader :service

        attr_reader :endpoint

        attr_accessor :message

        attr_writer :source_location

        attr_reader :fields

        def set name, value
          fields[name] = value
        end

        def set_system_name
          set "system", system_name
        end

        def set_service
          set "serviceName", service
        end

        def set_credentials_fields creds
          creds = creds.client if creds.respond_to? :client
          set "credentialsId", creds.object_id
          set "credentialsType", creds.class.name
          set "credentialsScope", creds.scope if creds.respond_to? :scope
          set "useSelfSignedJWT", creds.enable_self_signed_jwt? if creds.respond_to? :enable_self_signed_jwt?
          set "universeDomain", creds.universe_domain if creds.respond_to? :universe_domain
        end

        def source_location
          @source_location ||= Google::Logging::SourceLocation.for_caller omit_files: OMIT_FILES,
                                                                          locations: @caller_locations
        end

        def set_source_location_from locations
          @caller_locations = locations
          @source_location = nil
        end

        def build
          Google::Logging::Message.from message: message, source_location: source_location, fields: fields
        end
      end
    end

    ##
    # @private
    # Initialize logging concerns.
    #
    def setup_logging logger: :default,
                      stream: nil,
                      formatter: nil,
                      level: nil,
                      system_name: nil,
                      service: nil,
                      endpoint: nil,
                      client_id: nil
      service = LoggingConcerns.normalize_service service
      system_name = LoggingConcerns.normalize_system_name system_name
      logging_env = ENV["GOOGLE_SDK_RUBY_LOGGING_GEMS"].to_s.downcase
      logger = nil if ["false", "none"].include? logging_env
      if logger == :default
        logger = nil
        if ["true", "all"].include?(logging_env) || logging_env.split(",").include?(system_name)
          stream ||= $stderr
          level ||= "DEBUG"
          formatter ||= Google::Logging::StructuredFormatter.new if Google::Cloud::Env.get.logging_agent_expected?
          logger = Logger.new stream, progname: system_name, level: level, formatter: formatter
        end
      end
      @logger = logger
      @stub_logger = StubLogger.new logger: logger,
                                    system_name: system_name,
                                    service: service,
                                    endpoint: endpoint,
                                    client_id: client_id
    end

    # @private
    attr_reader :stub_logger

    class << self
      # @private
      def random_uuid4
        ary = Random.bytes 16
        ary.setbyte 6, ((ary.getbyte(6) & 0x0f) | 0x40)
        ary.setbyte 8, ((ary.getbyte(8) & 0x3f) | 0x80)
        ary.unpack("H8H4H4H4H12").join "-"
      end

      # @private
      def normalize_system_name input
        case input
        when String
          input
        when Class
          input.name.split("::")[..-3]
               .map { |elem| elem.scan(/[A-Z][A-Z]*(?=[A-Z][a-z0-9]|$)|[A-Z][a-z0-9]+/).map(&:downcase).join("_") }
               .join("-")
        else
          "googleapis"
        end
      end

      # @private
      def normalize_service input
        case input
        when String
          input
        when Class
          mod = input.name.split("::")[..-2].inject(Object) { |m, n| m.const_get n }
          if mod.const_defined? "Service"
            mod.const_get("Service").service_name
          else
            name_segments = input.name.split("::")[..-3]
            mod = name_segments.inject(Object) { |m, n| m.const_get n }
            name_segments.join "." if mod.const_defined? "Rest"
          end
        end
      end
    end
  end
end
