# == Schema Information
#
# Table name: bill_signatures
#
#  id         :bigint           not null, primary key
#  digest     :string
#  created_at :datetime         not null
#  updated_at :datetime         not null
#
class BillSignature < ApplicationRecord
  has_many :dossier_operation_logs

  has_one_attached :serialized
  has_one_attached :signature

  validate :check_bill_digest
  validate :check_serialized_bill_contents
  validate :check_signature_contents

  def self.build_with_operations(operations, day)
    bill = new(dossier_operation_logs: operations)

    bill.serialize_operations(day)

    bill
  end

  def serialize_operations(day)
    serialized.attach(
      io: StringIO.new(operations_bill_json),
      filename: "demarches-simplifiees-operations-#{day.to_date.iso8601}.json",
      content_type: 'application/json',
      # we don't want to run virus scanner on this file
      metadata: { virus_scan_result: ActiveStorage::VirusScanner::SAFE }
    )

    self.digest = operations_bill_digest
  end

  def operations_bill
    dossier_operation_logs.map { |op| [op.id.to_s, op.digest] }.to_h
  end

  def operations_bill_json
    operations_bill.to_json
  end

  def operations_bill_digest
    Digest::SHA256.hexdigest(operations_bill_json)
  end

  def set_signature(signature, day)
    self.signature.attach(
      io: StringIO.new(signature),
      filename: "demarches-simplifiees-signature-#{day.to_date.iso8601}.der",
      content_type: 'application/x-x509-ca-cert',
      # we don't want to run virus scanner on this file
      metadata: { virus_scan_result: ActiveStorage::VirusScanner::SAFE }
    )
  end

  # Validations
  def check_bill_digest
    if digest != operations_bill_digest
      errors.add(:digest)
    end
  end

  def check_serialized_bill_contents
    if !serialized.attached?
      errors.add(:serialized, :blank)
      return
    end

    if JSON.parse(read_serialized) != operations_bill
      errors.add(:serialized)
    end
  end

  def check_signature_contents
    if !signature.attached?
      errors.add(:signature, :blank)
      return
    end

    timestamp_signature_date = ASN1::Timestamp.signature_time(read_signature)
    if timestamp_signature_date > Time.zone.now
      errors.add(:signature, :invalid_date)
    end

    timestamp_signed_digest = ASN1::Timestamp.signed_digest(read_signature)
    if timestamp_signed_digest != digest
      errors.add(:signature)
    end
  end

  def read_signature
    read_attachment('signature')
  end

  def read_serialized
    read_attachment('serialized')
  end

  private

  def read_attachment(attachment)
    if attachment_changes[attachment]
      io = io_for_changes(attachment_changes[attachment])
      if io.present?
        io.rewind
        io.read
      end
    elsif serialized.attached?
      serialized.download
    end
  end

  def io_for_changes(attachment_changes)
    attachable = attachment_changes.attachable
    case attachable
    when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
      attachable.open
    when Hash
      attachable.fetch(:io)
    end
  end
end