# ideally encrypt and decrypt would happen at the blob/service level
# however, there isn't really a great place to define encryption settings there
# instead, we encrypt and decrypt at the attachment level,
# and we define encryption settings at the model level
class Lockbox
  module ActiveStorageExtensions
    module Attached
      protected

      def encrypted?
        # could use record_type directly
        # but record should already be loaded most of the time
        !Utils.encrypted_options(record, name).nil?
      end

      def encrypt_attachable(attachable)
        options = Utils.encrypted_options(record, name)
        box = Utils.build_box(record, options, record.class.table_name, name)

        case attachable
        when ActiveStorage::Blob
          raise NotImplementedError, "Not supported"
        when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
          attachable = {
            io: StringIO.new(box.encrypt(attachable.read)),
            filename: attachable.original_filename,
            content_type: attachable.content_type
          }
        when Hash
          attachable = {
            io: StringIO.new(box.encrypt(attachable[:io].read)),
            filename: attachable[:filename],
            content_type: attachable[:content_type]
          }
        when String
          raise NotImplementedError, "Not supported"
        else
          nil
        end

        attachable
      end

      def rebuild_attachable(attachment)
        {
          io: StringIO.new(attachment.download),
          filename: attachment.filename,
          content_type: attachment.content_type
        }
      end
    end

    module AttachedOne
      def attach(attachable)
        attachable = encrypt_attachable(attachable) if encrypted?
        super(attachable)
      end

      def rotate_encryption!
        raise "Not encrypted" unless encrypted?

        attach(rebuild_attachable(self)) if attached?

        true
      end
    end

    module AttachedMany
      def attach(*attachables)
        if encrypted?
          attachables =
            attachables.flatten.collect do |attachable|
              encrypt_attachable(attachable)
            end
        end

        super(attachables)
      end

      def rotate_encryption!
        raise "Not encrypted" unless encrypted?

        # must call to_a - do not change
        previous_attachments = attachments.to_a

        attachables =
          previous_attachments.map do |attachment|
            rebuild_attachable(attachment)
          end

        ActiveStorage::Attachment.transaction do
          attach(attachables)
          previous_attachments.each(&:purge)
        end

        attachments.reload

        true
      end
    end

    module Attachment
      extend ActiveSupport::Concern

      def download
        result = super

        options = Utils.encrypted_options(record, name)
        if options
          result = Utils.build_box(record, options, record.class.table_name, name).decrypt(result)
        end

        result
      end

      def mark_analyzed
        if Utils.encrypted_options(record, name)
          blob.update!(metadata: blob.metadata.merge(analyzed: true))
        end
      end

      included do
        after_save :mark_analyzed
      end
    end
  end
end