module Mirrors
  class MirrorError < Exception; end

  def ssh_open_pipe(mirror, command, timeout = 30)
    remote_user_host = "#{mirror[:user]}@#{mirror[:host]}"
    ret = nil
    IO.popen("/usr/bin/ssh -o ServerAliveInterval=30 -o Compression=no -o BatchMode=yes -o ConnectTimeout=#{timeout} #{remote_user_host} '#{command}'") do |f|
      ret = yield(f)
    end
    if ($CHILD_STATUS & 0xFF) != 0
      raise MirrorError, "Command \"%s\" to %s exited with signal %i" % [ command, mirror[:host], $CHILD_STATUS & 0xFF ]
    end
    if ($CHILD_STATUS >> 8) != 0
      raise MirrorError, "Command \"%s\" to %s exited with status %i" % [ command, mirror[:host], $CHILD_STATUS >> 8 ]
    end
    ret
  end
  module_function :ssh_open_pipe

  def filter_mirror_list(options)
    results = []
    CONFIG["mirrors"].each do |mirror|
      next if options[:previews_only] != mirror[:previews_only]
      results << mirror
    end

    results
  end
  module_function :filter_mirror_list

  def create_mirror_paths(dirs, options = {})
    dirs = dirs.uniq

    target_mirrors = filter_mirror_list(options)
    target_mirrors.each do |mirror|
      remote_user_host = "#{mirror[:user]}@#{mirror[:host]}"
      remote_dirs = []
      dirs.each do |dir|
        remote_dirs << mirror[:data_dir] + "/" + dir
      end

      # Create all directories in one go.
      system("/usr/bin/ssh", "-o", "Compression=no", "-o", "BatchMode=yes",
             remote_user_host, "mkdir -p #{remote_dirs.uniq.join(" ")}")
    end
  end
  module_function :create_mirror_paths

  # Copy a file to all mirrors.  file is an absolute path which must be
  # located in public/data; the files will land in the equivalent public/data
  # on each mirror.
  #
  # If options[:previews_only] is true, copy only to :previews_only mirrors; otherwise
  # copy only to others.
  #
  # Because we have no mechanism for indicating that a file is only available on
  # certain mirrors, if any mirror fails to upload, MirrorError will be thrown
  # and the file should be treated as completely unwarehoused.
  def copy_file_to_mirrors(file, options = {})
    # CONFIG[:data_dir] is equivalent to our local_base.
    local_base = "#{Rails.root}/public/data/"
    options = { timeout: 30 }.merge(options)

    if file[0, local_base.length] != local_base
      raise "Invalid filename to mirror: \"%s" % file
    end

    expected_md5 = Moebooru::Hasher.compute_one(file, :md5)

    target_mirrors = filter_mirror_list(options)
    target_mirrors.each do |mirror|
      remote_user_host = "#{mirror[:user]}@#{mirror[:host]}"
      remote_filename = "#{mirror[:data_dir]}/#{file[local_base.length, file.length]}"

      # Tolerate a few errors in case of communication problems.
      retry_count = 0

      begin
        # Check if the file is already mirrored before we spend time uploading it.
        # Linux needs md5sum; FreeBSD needs md5 -q.
        actual_md5 = Mirrors.ssh_open_pipe(mirror,
                                           "if [ -f #{remote_filename} ]; then (which md5sum >/dev/null) && md5sum #{remote_filename} || md5 -q #{remote_filename}; fi",
                                           timeout = options[:timeout]) { |f| f.gets }
        if actual_md5 =~ /^[0-9a-f]{32}/
          actual_md5 = actual_md5.slice(0, 32)
          if expected_md5 == actual_md5
            next
          end
        end

        unless system("/usr/bin/scp", "-pq", "-o", "Compression no", "-o", "BatchMode=yes",
                      "-o", "ConnectTimeout=%i" % timeout,
                      file, "#{remote_user_host}:#{remote_filename}")
          raise MirrorError, "Error copying #{file} to #{remote_user_host}:#{remote_filename}"
        end

        # Don't trust scp; verify the files.
        actual_md5 = Mirrors.ssh_open_pipe(mirror, "if [ -f #{remote_filename} ]; then (which md5sum >/dev/null) && md5sum #{remote_filename} || md5 -q #{remote_filename}; fi") { |f| f.gets }
        if actual_md5 !~ /^[0-9a-f]{32}/
          raise MirrorError, "Error verifying #{remote_user_host}:#{remote_filename}: #{actual_md5}"
        end

        actual_md5 = actual_md5.slice(0, 32)

        if expected_md5 != actual_md5
          raise MirrorError, "Verifying #{remote_user_host}:#{remote_filename} failed: got #{actual_md5}, expected #{expected_md5}"
        end
      rescue MirrorError
        retry_count += 1
        raise if retry_count == 3

        retry
      end
    end
  end
  module_function :copy_file_to_mirrors

  # Return a URL prefix for a file.  If not warehoused, always returns the main
  # server.  If a seed is specified, seeds the server selection; otherwise, each
  # IP will always use the same server.
  #
  # If :zipfile is set, ignore mirrors with the :nozipfile flag.
  if CONFIG["image_store"] == :remote_hierarchy
    def select_main_image_server
      return CONFIG["url_base"] if !CONFIG["image_servers"] || CONFIG["image_servers"].empty?
      raise 'CONFIG["url_base"] is set incorrectly; please see config/default_config.rb' if CONFIG["image_servers"][0].class == String

      CONFIG["image_servers"][0][:server]
    end

    def select_image_server(is_warehoused, seed = 0, options = {})
      return CONFIG["url_base"] if !CONFIG["image_servers"] || CONFIG["image_servers"].empty?
      raise 'CONFIG["url_base"] is set incorrectly; please see config/default_config.rb' if CONFIG["image_servers"][0].class == String

      unless is_warehoused
        # return CONFIG["url_base"]
        return CONFIG["image_servers"][0][:server]
      end

      mirrors = CONFIG["image_servers"]
      if options[:preview]
        mirrors =  mirrors.select do |mirror|
          mirror[:nopreview] != true
        end
      end

      unless options[:preview]
        mirrors =  mirrors.select do |mirror|
          mirror[:previews_only] != true
        end
      end

      if options[:zipfile]
        mirrors =  mirrors.select do |mirror|
          mirror[:nozipfile] != true
        end
      end

      raise "No usable mirrors" if mirrors.empty?

      total_weights = 0
      mirrors.each { |s| total_weights += s[:traffic] }

      seed += Thread.current["danbooru-ip_addr_seed"] || 0
      seed %= total_weights

      server = nil
      mirrors.each do |s|
        w = s[:traffic]
        if seed < w
          server = s
          break
        end

        seed -= w
      end
      server ||= mirrors[0]

      # If this server has aliases, and an ID has been specified, pick a random server alias
      # with the ID as a seed so we always use the same server name for the same image.
      if !options[:id].nil? && !server[:aliases].nil? && options[:use_aliases]
        server_count = server[:aliases].length + 1
        server_index = options[:id] % server_count
        if server_index == 0
          return server[:server]
        else
          return server[:aliases][server_index - 1]
        end
      end

      server[:server]
    end
  else
    def select_main_image_server
      CONFIG["url_base"]
    end

    def select_image_server(_is_warehoused, _seed = 0, _options = {})
      CONFIG["url_base"]
    end
  end
  module_function :select_main_image_server
  module_function :select_image_server
end
