# frozen_string_literal: true

require "json"
require "base64"
require "sentry/envelope"

module Sentry
  class Transport
    PROTOCOL_VERSION = '7'
    USER_AGENT = "sentry-ruby/#{Sentry::VERSION}"
    CLIENT_REPORT_INTERVAL = 30

    # https://develop.sentry.dev/sdk/client-reports/#envelope-item-payload
    CLIENT_REPORT_REASONS = [
      :ratelimit_backoff,
      :queue_overflow,
      :cache_overflow, # NA
      :network_error,
      :sample_rate,
      :before_send,
      :event_processor
    ]

    include LoggingHelper

    attr_reader :rate_limits, :discarded_events, :last_client_report_sent

    # @deprecated Use Sentry.logger to retrieve the current logger instead.
    attr_reader :logger

    def initialize(configuration)
      @logger = configuration.logger
      @transport_configuration = configuration.transport
      @dsn = configuration.dsn
      @rate_limits = {}
      @send_client_reports = configuration.send_client_reports

      if @send_client_reports
        @discarded_events = Hash.new(0)
        @last_client_report_sent = Time.now
      end
    end

    def send_data(data, options = {})
      raise NotImplementedError
    end

    def send_event(event)
      envelope = envelope_from_event(event)
      send_envelope(envelope)

      event
    end

    def send_envelope(envelope)
      reject_rate_limited_items(envelope)

      return if envelope.items.empty?

      data, serialized_items = serialize_envelope(envelope)

      if data
        log_info("[Transport] Sending envelope with items [#{serialized_items.map(&:type).join(', ')}] #{envelope.event_id} to Sentry")
        send_data(data)
      end
    end

    def serialize_envelope(envelope)
      serialized_items = []
      serialized_results = []

      envelope.items.each do |item|
        result = item.to_s

        if result.bytesize > Event::MAX_SERIALIZED_PAYLOAD_SIZE
          if item.payload.key?(:breadcrumbs)
            item.payload.delete(:breadcrumbs)
          elsif item.payload.key?("breadcrumbs")
            item.payload.delete("breadcrumbs")
          end

          result = item.to_s
        end

        if result.bytesize > Event::MAX_SERIALIZED_PAYLOAD_SIZE
          size_breakdown = item.payload.map do |key, value|
            "#{key}: #{JSON.generate(value).bytesize}"
          end.join(", ")

          log_debug("Envelope item [#{item.type}] is still oversized without breadcrumbs: {#{size_breakdown}}")

          next
        end

        serialized_results << result
        serialized_items << item
      end

      data = [JSON.generate(envelope.headers), *serialized_results].join("\n") unless serialized_results.empty?

      [data, serialized_items]
    end

    def is_rate_limited?(item_type)
      # check category-specific limit
      category_delay =
        case item_type
        when "transaction"
          @rate_limits["transaction"]
        when "sessions"
          @rate_limits["session"]
        else
          @rate_limits["error"]
        end

      # check universal limit if not category limit
      universal_delay = @rate_limits[nil]

      delay =
        if category_delay && universal_delay
          if category_delay > universal_delay
            category_delay
          else
            universal_delay
          end
        elsif category_delay
          category_delay
        else
          universal_delay
        end

      !!delay && delay > Time.now
    end

    def generate_auth_header
      now = Sentry.utc_now.to_i
      fields = {
        'sentry_version' => PROTOCOL_VERSION,
        'sentry_client' => USER_AGENT,
        'sentry_timestamp' => now,
        'sentry_key' => @dsn.public_key
      }
      fields['sentry_secret'] = @dsn.secret_key if @dsn.secret_key
      'Sentry ' + fields.map { |key, value| "#{key}=#{value}" }.join(', ')
    end

    def envelope_from_event(event)
      # Convert to hash
      event_payload = event.to_hash
      event_id = event_payload[:event_id] || event_payload["event_id"]
      item_type = event_payload[:type] || event_payload["type"]

      envelope = Envelope.new(
        {
          event_id: event_id,
          dsn: @dsn.to_s,
          sdk: Sentry.sdk_meta,
          sent_at: Sentry.utc_now.iso8601
        }
      )

      envelope.add_item(
        { type: item_type, content_type: 'application/json' },
        event_payload
      )

      client_report_headers, client_report_payload = fetch_pending_client_report
      envelope.add_item(client_report_headers, client_report_payload) if client_report_headers

      envelope
    end

    def record_lost_event(reason, item_type)
      return unless @send_client_reports
      return unless CLIENT_REPORT_REASONS.include?(reason)

      @discarded_events[[reason, item_type]] += 1
    end

    private

    def fetch_pending_client_report
      return nil unless @send_client_reports
      return nil if @last_client_report_sent > Time.now - CLIENT_REPORT_INTERVAL
      return nil if @discarded_events.empty?

      discarded_events_hash = @discarded_events.map do |key, val|
        reason, type = key

        # 'event' has to be mapped to 'error'
        category = type == 'transaction' ? 'transaction' : 'error'

        { reason: reason, category: category, quantity: val }
      end

      item_header = { type: 'client_report' }
      item_payload = {
        timestamp: Sentry.utc_now.iso8601,
        discarded_events: discarded_events_hash
      }

      @discarded_events = Hash.new(0)
      @last_client_report_sent = Time.now

      [item_header, item_payload]
    end

    def reject_rate_limited_items(envelope)
      envelope.items.reject! do |item|
        if is_rate_limited?(item.type)
          log_info("[Transport] Envelope item [#{item.type}] not sent: rate limiting")
          record_lost_event(:ratelimit_backoff, item.type)

          true
        else
          false
        end
      end
    end
  end
end

require "sentry/transport/dummy_transport"
require "sentry/transport/http_transport"
