require "open-uri"

# A record of installation of a Farmware.
# Useful for restoring a device after a re-flash.
class FarmwareInstallation < ApplicationRecord
  belongs_to :device
  validates :url, url: true
  validates_uniqueness_of :url, { scope: :device }
  validates_presence_of :device
  # Prevent malice when fetching a farmware manifest
  MAX_JSON_SIZE = 5000
  OTHER_PROBLEM = "Unknown error: %s"
  # Keep a dictionary of known errors if fetching
  # the `package` attr raises a runtime error.
  KNOWN_PROBLEMS = {
    KeyError => "Farmware manifest must have a `package` field that is a string.",
    OpenURI::HTTPError => "The server is online, but the URL could not be opened.",
    SocketError => "The server at the provided appears to be offline.",
    Net::OpenTimeout => "A timeout error occurred.",
    JSON::ParserError => "Expected Farmware manifest to be valid JSON, " \
                         "but it is not. Consider using a JSON validator.",
    ActiveRecord::ValueTooLong => "The name of the package is too long.",
    Errno::ECONNREFUSED => "Could not connect to the server at the provided URL.",
  }

  # Downloads the farmware manifest JSON file in a background
  # worker, updating the `package` column if possible.
  def force_package_refresh!
    self.delay.infer_package_name_from_url
  end

  # A lot of things can go wrong when fetching
  # a package name in a background worker.
  def maybe_recover_from_fetch_error(error)
    known_error = KNOWN_PROBLEMS[error.class]
    description = known_error || (OTHER_PROBLEM % error.class)
    update!(package_error: description, package: nil)
    unless known_error.present?
      raise error
    end
  end

  # SLOW I/O BOUND STUFF! Don't run this on the
  # main thread!
  def infer_package_name_from_url
    string_io = URI.parse(url).open
    string = string_io.read(MAX_JSON_SIZE)
    json = JSON.parse(string)
    pkg_name = json.fetch("package")
    update!(package: pkg_name, package_error: nil)
  rescue => error
    maybe_recover_from_fetch_error(error)
  end
end
