# frozen_string_literal: true

module SignedUrlHelper
  SIGNED_S3_URL_VALID_FOR_MINIMUM = 10.minutes
  SIGNED_S3_URL_VALID_FOR_MAXIMUM = 3.hours
  SIGNED_VIDEO_URL_VALID_FOR = 12.hours

  # Warning: Changing the value of this will invalidate a lot of cached files in CloudFlare
  CF_WORKER_CACHE_KEY = "caIWHGT4Qhqo6KoxDMNXwQ" # Generated by SecureRandom.urlsafe_base64
  private_constant :CF_WORKER_CACHE_KEY

  def signed_download_url_for_s3_key_and_filename(s3_key, s3_filename, is_video: false, expires_in: nil, cache_group: nil)
    content_length, public_url_path = Rails.cache.fetch("content_length_public_url_path_#{S3_BUCKET}_#{s3_key}") do
      obj = Aws::S3::Resource.new.bucket(S3_BUCKET).object(s3_key)
      [obj.content_length, obj.public_url]
    rescue Aws::S3::Errors::NotFound => e
      raise e.exception("Key = #{s3_key}")
    end
    public_url_path = URI.parse(public_url_path).path
    s3_path = public_url_path.to_s.sub(%r{^/#{Regexp.escape(S3_BUCKET)}}o, "") # remove the S3 bucket name portion
    s3_path = s3_path.sub(%r{^/}, "") # remove the leading slash as the distribution URL will always end in a slash
    download_url = "#{file_download_host(content_length)}#{s3_path}?"
    download_url += "cache_group=#{CGI.escape(cache_group)}&" if cache_group.present?
    download_url += "response-content-disposition=attachment"
    download_url += "&cache_key=#{cf_cache_key(s3_filename)}" if file_needs_cache_key?(s3_filename)
    expires_in ||= is_video ? SIGNED_VIDEO_URL_VALID_FOR : signed_url_validity_time_for_file_size(content_length)

    if is_cloudflare_cacheable?(content_length)
      signed_cloudflare_url(download_url, expires_in)
    else
      signed_cloudfront_url(download_url, is_video:, expires_in:)
    end
  end

  # Public: Signs the cloudfront url or path and returns the result.
  #
  # url - The url or path of the resource on cloudfront.
  def signed_cloudfront_url(url, is_video: false, expires_in: nil)
    raise "CLOUDFRONT_PRIVATE_KEY is not set" unless CLOUDFRONT_PRIVATE_KEY

    expires_in = SIGNED_VIDEO_URL_VALID_FOR if is_video && expires_in.nil?
    expires_in = SIGNED_S3_URL_VALID_FOR_MINIMUM if expires_in.nil?
    expires_at = (Time.current + expires_in).to_i # AWS times are in UTC
    policy = %({"Statement":[{"Resource":"#{url}","Condition":{"DateLessThan":{"AWS:EpochTime":#{expires_at}}}}]}) # no spaces allowed
    signature = Base64.strict_encode64(CLOUDFRONT_PRIVATE_KEY.sign(OpenSSL::Digest::SHA1.new, policy))
    signature.tr("+=/", "-_~") # stolen from AWS's perl script
    url_separator = url.include?("?") ? "&" : "?"
    "#{url}#{url_separator}Expires=#{expires_at}&Signature=#{signature}&Key-Pair-Id=#{CLOUDFRONT_KEYPAIR_ID}"
  end

  private
    def signed_url_validity_time_for_file_size(file_size)
      return SIGNED_S3_URL_VALID_FOR_MINIMUM unless file_size
      valid_for = [(file_size / 1_024 / 50).seconds, SIGNED_S3_URL_VALID_FOR_MINIMUM].max
      [valid_for, SIGNED_S3_URL_VALID_FOR_MAXIMUM].min
    end

    def is_cloudflare_cacheable?(content_length)
      content_length.to_i < CLOUDFLARE_CACHE_LIMIT
    end

    def signed_cloudflare_url(url, expires_in)
      expires_at = (Time.current + expires_in).to_i.to_s
      token_data = URI.parse(url).path + "/" + expires_at
      signature = Base64.encode64(OpenSSL::HMAC.digest(OpenSSL::Digest.new("sha256"),
                                                       GlobalConfig.get("CLOUDFLARE_HMAC_KEY"),
                                                       token_data)).strip
      signature = CGI.escape(signature)

      token = "#{expires_at}-#{signature}"
      url + "&verify=#{token}"
    end

    def file_download_host(content_length)
      if is_cloudflare_cacheable?(content_length)
        FILE_DOWNLOAD_DISTRIBUTION_URL
      else
        CLOUDFRONT_DOWNLOAD_DISTRIBUTION_URL
      end
    end

    def file_needs_cache_key?(s3_filename)
      s3_filename.present? && cf_cache_key(s3_filename).present?
    end

    def cf_cache_key(s3_filename)
      cf_worker_cache_extensions_and_keys[File.extname(s3_filename).downcase]
    end

    def cf_worker_cache_extensions_and_keys
      extensions = %w[.jpg .jpeg .png .epub .brushset .scrivtemplate .zip .tpl]
      # Default cache keys
      cache_key_for_extensions = extensions.inject({}) do |hash, ext|
        hash[ext] = CF_WORKER_CACHE_KEY

        hash
      end

      # Dynamically set or override cache keys by adding them to Redis.
      #
      # Steps to set a new cache key for an extension:
      # 1) Add the extension and its new cache key to the invalidated cache keys list
      #    $redis.hset(RedisKey.cf_cache_invalidated_extensions_and_cache_keys, ".mp3", Digest::SHA1.hexdigest("2020-10-08"))
      # 2) Delete the Rails cache for the app to read from Redis again.
      #    Rails.cache.delete("set_cf_worker_cache_keys_from_redis")
      cache_key_for_extensions = Rails.cache.fetch("set_cf_worker_cache_keys_from_redis") do
        cache_key_for_extensions.merge($redis.hgetall(RedisKey.cf_cache_invalidated_extensions_and_cache_keys))
      end

      # We can also override cache keys of specific extensions here. Instead of using the dates directly in URL, we can use
      # its hexdigest to hide that info but we would know when it was changed.
      # Example:
      #   cache_key_for_extensions[".zip"] = Digest::SHA1.hexdigest("2020-10-08")

      cache_key_for_extensions
    end
end
