# frozen_string_literal: true

class ActivityPub::ProcessActivityService < BaseService
  include JsonLdHelper
  include DomainControlHelper

  def call(body, actor, **options)
    @account = actor
    @json    = original_json = JSON.parse(body)
    @options = options

    return unless @json.is_a?(Hash)

    ActivityPub::OpenTelemetry.decorate_current_span(payload: @json)

    # Ideally, we should treat all ActivityPub payloads as proper JSON-LD.
    # However, some implementations do not produce valid JSON-LD, and processing JSON-LD is pretty expensive.
    # Therefore, we only process activities as JSON-LD if they make use of JSON-LD signatures.
    # The reasons are that this is a strong indication that they are valid JSON-LD documents,
    # and that Linked Data Signature verification requires JSON-LD processing to begin with.
    # Doing JSON-LD compaction early ensures we know what we are working with, and avoids
    # issues like attacks involving swapping external context documents between processing steps.
    begin
      @json = compact(@json) if @json['signature'].is_a?(Hash)
      if unsupported_jsonld_features?(@json)
        Rails.logger.debug { "JSON-LD document for #{value_or_id(@json['actor'])} contains unsupported JSON-LD features" }
        @json = original_json.without('signature')
      end
    rescue JSON::LD::JsonLdError => e
      Rails.logger.debug { "Error when compacting JSON-LD document for #{value_or_id(@json['actor'])}: #{e.message}" }
      @json = original_json.without('signature')
    end

    return unless supported_context?(@json)

    if different_actor?
      # This has been relayed by a different account.
      # Record where it's coming from and try to verify proof of the activity.

      @options[:relayed_through_actor] = @account

      # Linked Data Signature verification
      @account = actor_from_verified_ld_signature

      # If Linked Data Signature verification failed, throw away the signature
      # as other parts of the code use its presence as an indication of whether
      # to forward the activity (don't throw away compaction though, it is still useful)
      @json = @json.without('signature') if @account.nil?

      # TODO: in the future, we might extend our forwarding rules to allow activities with
      # FEP-8b32 Object Integrity Proofs to be forwarded.
      # This would require keeping the original JSON around and changing forwarding logic in
      # a few places. This is not worth it right now since FEP-8b32 is not widely supported,
      # but could be worth doing in the future.
      @account ||= actor_from_verified_object_integrity_proof(original_json)
    end

    return if !@account.is_a?(Account) || different_actor? || suspended_actor? || @account.local?

    if @json['signature'].present?
      # We have verified the signature, but in the compaction step above, might
      # have introduced incompatibilities with other servers that do not
      # normalize the JSON-LD documents (for instance, previous Mastodon
      # versions), so skip redistribution if we can't get a safe document.
      patch_for_forwarding!(original_json, @json)
      @json.delete('signature') unless safe_for_forwarding?(original_json, @json)
    end

    activity = ActivityPub::Activity.factory(@json, @account, **@options)
    activity&.perform
  rescue JSON::ParserError
    nil
  end

  private

  def different_actor?
    @json['actor'].present? && value_or_id(@json['actor']) != @account.uri
  end

  def suspended_actor?
    @account.suspended? && !activity_allowed_while_suspended?
  end

  def activity_allowed_while_suspended?
    %w(Delete Reject Undo Update).include?(@json['type'])
  end

  def actor_from_verified_ld_signature
    return unless @json['signature'].is_a?(Hash)
    return if domain_not_allowed?(@json['signature']['creator'])

    ActivityPub::LinkedDataSignature.new(@json).verify_actor!
  rescue JSON::LD::JsonLdError, RDF::WriterError => e
    Rails.logger.debug { "Could not verify LD-Signature for #{value_or_id(@json['actor'])}: #{e.message}" }
    nil
  end

  def actor_from_verified_object_integrity_proof(original_json)
    return unless original_json['proof'].present? && original_json['actor'] == @json['actor']
    return if domain_not_allowed?(@json['proof']['verificationMethod'])

    # Verification is done on the original JSON without a signature
    ActivityPub::ObjectIntegrityProof.new(original_json.without('signature')).verify_actor!
  end
end
