# frozen_string_literal: true

module WopiUtil
  require 'open-uri'

  # Used for timestamp
  UNIX_EPOCH_IN_CLR_TICKS = 621355968000000000
  CLR_TICKS_PER_SECOND = 10000000

  DISCOVERY_TTL = 1.day
  DISCOVERY_TTL.freeze

  # For more explanation see this:
  # http://stackoverflow.com/questions/11888053/
  # convert-net-datetime-ticks-property-to-date-in-objective-c
  def convert_to_unix_timestamp(timestamp)
    Time.zone.at((timestamp - UNIX_EPOCH_IN_CLR_TICKS) / CLR_TICKS_PER_SECOND)
  end

  def get_action(extension, action)
    discovery = current_wopi_discovery
    discovery[:actions].find { |i| i[:extension] == extension && i[:action] == action }
  end

  def current_wopi_discovery
    initialize_discovery
  end

  # Verifies if proof from headers, X-WOPI-Proof/X-WOPI-OldProof was encrypted
  # with this discovery public key (two key possible old/new)
  def wopi_verify_proof(token, timestamp, signed_proof, signed_proof_old, url)
    discovery = current_wopi_discovery
    token_length = [token.length].pack('>N').bytes
    timestamp_bytes = [timestamp.to_i].pack('>Q').bytes.reverse
    timestamp_length = [timestamp_bytes.length].pack('>N').bytes
    url_length = [url.length].pack('>N').bytes

    expected_proof = token_length + token.bytes +
                     url_length + url.upcase.bytes +
                     timestamp_length + timestamp_bytes

    key = generate_key(discovery[:proof_key_mod], discovery[:proof_key_exp])
    old_key = generate_key(discovery[:proof_key_old_mod], discovery[:proof_key_old_exp])

    # Try all possible combiniations
    try_verification(expected_proof, signed_proof, key) ||
      try_verification(expected_proof, signed_proof_old, key) ||
      try_verification(expected_proof, signed_proof, old_key)
  end

  private

  # Currently only saves Excel, Word and PowerPoint view and edit actions
  def initialize_discovery
    Rails.cache.fetch(:wopi_discovery, expires_in: DISCOVERY_TTL) do
      @doc = Nokogiri::XML(Kernel.open(ENV['WOPI_DISCOVERY_URL']))
      discovery_json = {}
      key = @doc.xpath('//proof-key')
      discovery_json[:proof_key_mod] = key.xpath('@modulus').first.value
      discovery_json[:proof_key_exp] = key.xpath('@exponent').first.value
      discovery_json[:proof_key_old_mod] = key.xpath('@oldmodulus').first.value
      discovery_json[:proof_key_old_exp] = key.xpath('@oldexponent').first.value
      discovery_json[:actions] = []

      @doc.xpath('//app').each do |app|
        app_name = app.xpath('@name').first.value
        next unless %w(Excel Word PowerPoint WopiTest).include?(app_name)

        icon = app.xpath('@favIconUrl').first.value

        app.xpath('action').each do |action|
          action_name = action.xpath('@name').first.value
          next unless %w(view edit editnew embedview wopitest).include?(action_name)

          action_json = {}
          action_json[:icon] = icon
          action_json[:action] = action_name
          action_json[:extension] = action.xpath('@ext').first.value
          action_json[:urlsrc] = action.xpath('@urlsrc').first.value
          discovery_json[:actions].push(action_json)
        end
      end
      discovery_json
    end
  rescue StandardError => e
    Rails.logger.warn 'WOPI: initialization failed: ' + e.message
    e.backtrace.each { |line| Rails.logger.error line }
  end

  # Generates a public key from given modulus and exponent
  def generate_key(modulus, exponent)
    mod = Base64.decode64(modulus).unpack1('H*').to_i(16)
    exp = Base64.decode64(exponent).unpack1('H*').to_i(16)

    seq = OpenSSL::ASN1::Sequence.new([OpenSSL::ASN1::Integer.new(mod),
                                       OpenSSL::ASN1::Integer.new(exp)])
    OpenSSL::PKey::RSA.new(seq.to_der)
  end

  # Verify if decrypting signed_proof with public_key equals to expected_proof
  def try_verification(expected_proof, signed_proof_b64, public_key)
    signed_proof = Base64.decode64(signed_proof_b64)
    public_key.verify(OpenSSL::Digest::SHA256.new, signed_proof,
                      expected_proof.pack('c*'))
  end

  def create_wopi_file_activity(current_user, started_editing)
    action = if started_editing
               t('activities.wupi_file_editing.started')
             else
               t('activities.wupi_file_editing.finished')
             end
    if @assoc.class == Step
      default_step_items =
        { step: @asset.step.id,
          step_position: { id: @asset.step.id, value_for: 'position_plus_one' },
          asset_name: { id: @asset.id, value_for: 'file_name' },
          action: action }
      if @protocol.in_module?
        project = @protocol.my_module.experiment.project
        team = project.team
        type_of = :edit_wopi_file_on_step
        message_items = { my_module: @protocol.my_module.id }
      else
        type_of = :edit_wopi_file_on_step_in_repository
        project = nil
        team = @protocol.team
        message_items = { protocol: @protocol.id }
      end
      message_items = default_step_items.merge(message_items)
      Activities::CreateActivityService
        .call(activity_type: type_of,
              owner: current_user,
              subject: @protocol,
              team: team,
              project: project,
              message_items: message_items)
    elsif @assoc.class == Result
      Activities::CreateActivityService
        .call(activity_type: :edit_wopi_file_on_result,
              owner: current_user,
              subject: @asset.result,
              team: @my_module.experiment.project.team,
              project: @my_module.experiment.project,
              message_items: {
                result: @asset.result.id,
                asset_name: { id: @asset.id, value_for: 'file_name' },
                action: action
              })
    end
  end
end
