# frozen_string_literal: true

require 'etc'
require 'openssl'

autoload :GitlabSDK, 'gitlab-sdk'
autoload :Sentry, 'sentry-ruby'
autoload :SnowplowTracker, 'snowplow-tracker'

module GDK
  module Telemetry
    ANALYTICS_APP_ID = 'e2e967c0-785f-40ae-9b45-5a05f729a27f'
    ANALYTICS_BASE_URL = 'https://collector.prod-1.gl-product-analytics.com'
    # Track events emitted on CI in https://gitlab.com/gitlab-org/quality/tooling/gdk-playground/
    CI_ANALYTICS_APP_ID = '6a31192c-6567-40a3-9413-923abc790f05'

    SENTRY_DSN = 'https://4e771163209528e15a6a66a6e674ddc3@new-sentry.gitlab.net/38'
    PROMPT_TEXT = <<~TEXT.chomp
      To improve GDK, GitLab would like to collect basic error and usage information, including your platform and architecture.

      Would you like to send telemetry anonymously to GitLab? [y/n]
    TEXT
    FLUSH_TIMEOUT_SECONDS = 3

    def self.with_telemetry(command)
      return yield unless telemetry_enabled?

      start = Process.clock_gettime(Process::CLOCK_MONOTONIC)

      err = nil
      begin
        result = yield
      rescue StandardError => e
        err = e
        result = false
      ensure
        duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
      end

      send_telemetry(result, command, duration: duration)

      result
    ensure
      raise err if err
    end

    def self.send_telemetry(success, command, duration:)
      # This is tightly coupled to GDK commands and returns false when the system call exits with a non-zero status.
      status = success ? 'Finish' : 'Failed'

      client.identify(GDK.config.telemetry.username)
      client.track("#{status} #{command} #{ARGV}", payload.merge(duration: duration))
    end

    def self.flush_events(async: false)
      Timeout.timeout(FLUSH_TIMEOUT_SECONDS) do
        client.flush_events(async: async)
      end
    rescue Timeout::Error
      GDK::Output.warn(
        "Could not flush telemetry events within #{FLUSH_TIMEOUT_SECONDS} seconds. Is #{telemetry_host} blocked or unreachable?"
      )
    end

    def self.environment
      GDK.config.telemetry.environment
    end

    def self.version_manager
      return 'asdf' unless GDK.config.asdf.opt_out?
      return 'mise' if GDK.config.mise.enabled?

      'none'
    end

    def self.session_id
      @session_id ||= SecureRandom.uuid
    end

    def self.client
      return @client if @client

      default_app_id = ENV['CI'] ? CI_ANALYTICS_APP_ID : ANALYTICS_APP_ID

      app_id = ENV.fetch('GITLAB_SDK_APP_ID', default_app_id)
      host = telemetry_host

      SnowplowTracker::LOGGER.level = Logger::WARN
      SnowplowTracker::LOGGER.extend LoggerWithoutBacktrace

      at_exit do
        # Flush all pending events synchronously before exit.
        GDK::Telemetry.flush_events
      end

      @client = GitlabSDK::Client.new(app_id: app_id, host: host, buffer_size: 10)
    end

    def self.telemetry_host
      ENV.fetch('GITLAB_SDK_HOST', ANALYTICS_BASE_URL)
    end

    def self.init_sentry
      return if Sentry.configuration

      Sentry.init do |config|
        config.dsn = SENTRY_DSN
        config.breadcrumbs_logger = [:sentry_logger]
        config.traces_sample_rate = 1.0
        config.logger.level = Logger::WARN

        # Pseudonymize server name using checksum for username and hostname
        config.server_name = OpenSSL::Digest::SHA256.hexdigest([GDK.config.telemetry.username, config.server_name].join(':'))[0, 16]

        config.before_send = lambda do |event, hint|
          exception = hint[:exception]

          # Workaround for using fingerprint to make certain errors distinct.
          # See https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2842#note_1927103517
          event.transaction = exception.message if exception.is_a?(Shellout::ShelloutBaseError)

          event
        end
      end

      Sentry.set_user(username: GDK.config.telemetry.username)
    end

    def self.capture_exception(message, attachment: nil)
      return unless telemetry_enabled?

      if message.is_a?(Exception)
        exception = message.dup
      else
        exception = StandardError.new(message)
        exception.set_backtrace(caller)
      end

      # Drop the caller GDK::Telemetry.capture_exception to make errors distinct.
      exception.set_backtrace(exception.backtrace.drop(1)) if exception.backtrace

      init_sentry

      Sentry.configure_scope do |scope|
        scope.set_context('gdk', payload)
      end

      Sentry.add_attachment(**attachment) if attachment
      Sentry.capture_exception(exception)
    end

    def self.telemetry_enabled?
      return false if ENV['GDK_TELEMETRY'] == '0'

      GDK.config.telemetry.enabled
    end

    def self.payload
      {
        session_id: session_id,
        environment: environment,
        platform: GDK::Machine.platform,
        architecture: GDK::Machine.architecture,
        version_manager: version_manager,
        team_member: team_member?,
        enabled_services: enabled_services,
        cpu_count: Etc.nprocessors
      }
    end

    # Returns true if the user has configured a @gitlab.com email for git.
    #
    # This should only be used for telemetry and NEVER for authentication.
    def self.team_member?
      return @team_member if defined?(@team_member)

      @team_member = Shellout.new(%w[git config --get user.email])
              .run.include?('@gitlab.com')

      return @team_member if @team_member

      @team_member =
        if GDK::Machine.macos?
          # See https://handbook.gitlab.com/handbook/security/corporate/systems/jamf/setup/
          Shellout
            .new(%w[profiles status -type enrollment])
            .execute(display_output: false, display_error: false)
            .read_stdout.include?('gitlab.jamfcloud.com')
        elsif GDK::Machine.linux?
          # GitLab hostname standard
          hostname_match = -> { /\A\S+--\d+-\w+\z$/.match?(Etc.uname[:nodename]) }
          file_contains = ->(file, regexp) { File.exist?(file) && File.foreach(file).any?(regexp) }

          hostname_match.call ||
            !!file_contains.call(Pathname(Dir.home).join('.config/zoomus.conf'), /gitlab\.zoom\.us/)
        else
          false
        end
    end

    def self.enabled_services
      GDK::ToolVersionsUpdater.enabled_services
    end

    def self.update_settings(answer)
      enabled = answer == 'y'

      if enabled != GDK.config.telemetry.enabled
        GDK.config.bury!('telemetry.enabled', enabled)
        changes_made = true
      end

      if enabled
        username = GDK.config.telemetry.username
        anonymized = /\A\h{32}\z/.match?(username)

        unless anonymized
          GDK.config.bury!('telemetry.username', SecureRandom.hex)
          GDK::Output.info('Telemetry username has been anonymized.')
          changes_made = true
        end
      end

      GDK.config.save_yaml! if changes_made
    end

    # To reduce noise skip long backtraces for all system call errors like "connection refused".
    module LoggerWithoutBacktrace
      def warn(message)
        message.set_backtrace([]) if message.is_a?(SystemCallError)

        super
      end
    end
  end
end
