#!/usr/bin/ruby

require 'date'
require 'net/http'
require 'optparse'
require 'thread'
require 'uri'

def get_dates(base, archs_per_distro, optional=true)
  r = {}
  begin
    r['base'] = get_timestamp(base)
  rescue Net::OpenTimeout, Timeout::Error, ArgumentError, NoMethodError, Errno::EHOSTUNREACH, Errno::ECONNREFUSED, Errno::ECONNRESET, IOError, OpenSSL::SSL::SSLError => e
  end

  archs_per_distro.each{|d, archs|
    r[d] = {}
    archs.each{|a|
      begin
        r[d][a] = get_date(base, d, a)
      rescue Net::OpenTimeout, Timeout::Error, ArgumentError, NoMethodError, Errno::EHOSTUNREACH, Errno::ECONNREFUSED, Errno::ECONNRESET, IOError, OpenSSL::SSL::SSLError => e
        if !optional then
          STDERR.puts "Failed to fetch #{version_url(base, d, a)}"
          raise
        end
      end
    }
  }
  r
end

def get_mirrors
  # TODO Get it from the DB
  mirrors = []
  url = nil
  tier1 = false
  fetch_url("http://mirrors.mageia.org/").each_line{|l|
    if l =~ /rsync.mageia.org/ then
      tier1 = true
      next
    end
    if l=~ /<\/tr>/ && !url.nil? then
      if tier1 then
        mirrors.prepend url
        tier1 = false
      else
        mirrors.append url
      end
      url = nil
      next
    end
    next unless l =~ /https?:.*>http/
    # No need to check twice mirrors available in http + https
    if !url.nil? && url =~ /https:/ && l =~ /https:\/\//
      # Skip http:// if https:// already seen for current mirror
      # If the are in the other order http one will just be replaced
      next
    end
    url = l.sub(/<a href="(http[^"]*)".*\n/, '\1')
    url += "/" unless url =~ /\/$/
  }
  mirrors
end

def fetch_url(url, redirect_limit = 3)
  return if redirect_limit < 0
  if url =~ /^\// then
    open(url){|f|
      return f.read
    }
  else
    uri = URI.parse(url)
    http = Net::HTTP.new(uri.host, uri.port)
    http.open_timeout = 9
    http.read_timeout = 9
    if uri.scheme == 'https' then
      http.use_ssl = true
    end
    # Ruby 1.8.7 doesn't set a default User-Agent which causes at
    # least one mirror to return 403
    response = http.get(uri.path, {'User-Agent' => 'check_mirrors'})
    case response
    when Net::HTTPSuccess then
      return response.body
    when Net::HTTPRedirection then
      location = response['location']
      # Make location absolute if it was not
      if location =~ /:\/\// then
        fetch_url(location, redirect_limit - 1)
      else
        uri.path = location
        fetch_url(uri.to_s, redirect_limit - 1)
      end
    end
  end
end

def timestamp_url(url)
  "#{url}mageia_timestamp"
end

def get_timestamp(url)
  ti = fetch_url(timestamp_url(url)).to_i
  if ti == 0 then
    return nil
  end
  return DateTime.strptime(ti.to_s, '%s')
end

def parse_version(version)
  date = version.sub(/.* (........ ..:..)$/, '\1').rstrip
  DateTime.strptime(date, '%Y%m%d %H:%M')
end

def version_url(url, distrib, arch)
  "#{url}distrib/#{distrib}/#{arch}/VERSION"
end

def get_date(url, distrib, arch)
  return parse_version(fetch_url(version_url(url, distrib, arch)))
end

def format_age(ref_time, time)
  return "  <td class='broken'>X</td>" unless ref_time and time

  diff = ref_time - time
  cls = 'broken'
  if diff == 0 then
    cls = 'ok'
  elsif diff < 0.5 then
    cls = 'almost'
  elsif diff < 2 then
    cls = 'bad'
  end
  if cls == 'ok' then
    return "  <td class='#{cls}'>&nbsp;</td>"
  else
    return "  <td class='#{cls}'>#{time.strftime("%F %R")}</td>"
  end
end

def print_output(archs_per_distro, mirrors, ref_times, times)
  puts "<html><head><title>Mageia Mirror Status #{Time.now.strftime("%F")}</title>
<link rel=\"icon\" type=\"image/png\" href=\"//www.mageia.org/g/favicon.png\">
<style>
td.broken {background-color:#FF0033;}
td.bad {background-color:#FF9933;}
td.almost {background-color:#CCFF66;}
td.ok {background-color:#00FF66;}

td {text-align:center;}
td.name {text-align:left;}

td.sep {width:12px;}
table.legend td {padding:4px;}

th {background-color:#EEEEEE;}
</style>
</head>
<body>"
  puts "Last checked on #{Time.now.strftime("%F %R %Z")}<br/>"
  puts "<table class='legend'><tr><td class='ok'>Up to date</td><td class='almost'>Less than 12h old</td><td class='bad'>Less than 2 days old</td><td class='broken'>Old or broken</td></tr></table>"
  puts "<table><thead>"
  puts "<tr><td/>"
  puts "<td/><th>Base directory</th>"
  archs_per_distro.each{|d, archs|
	 nb_arches = archs.size
	 puts "  <td/><th colspan='#{nb_arches}'>#{d}</th>"
  }
  puts "</tr>"
  puts "<tr><td/><td/><td/>"
  archs_per_distro.each{|d, archs|
	  puts "  <td class='sep' />"
	  archs.each{|a| 
		puts "  <th>#{a}</th>"
	  }
  }
  puts "</tr></thead>"
  puts "<tbody>"
  puts "<tr><td class='name'>Reference</td>"
  puts "  <td class='sep' />"
  puts "  <td>#{!ref_times['base'].nil? ? ref_times['base'].strftime("%F %R") : "?"}</td>"
  archs_per_distro.each{|d, archs|
    puts "  <td class='sep' />"
    archs.each{|a|
      puts "  <td>#{ref_times[d][a].strftime("%F %R")}</td>"
    }
  }
  puts "</tr>"

  mirrors.each{|u|
    puts "<tr><td class='name'><a href='#{u}'>#{u}</a></td>"
    puts "  <td class='sep' />"
    puts format_age(ref_times['base'], times[u]['base'])
    archs_per_distro.each{|d, archs|
      puts "  <td class='sep' />"
      archs.each{|a|
        puts format_age(ref_times[d][a], times[u][d][a])
      }
    }
    puts "</tr>"
  }
  puts "</tbody></table>"
  puts "</body></html>"
end



# Defaults
ref = 'http://repository.mageia.org/'
archs_per_distro = {
	'9' => ['i586', 'x86_64', 'armv7hl', 'aarch64'],
	'8' => ['i586', 'x86_64', 'armv7hl', 'aarch64'],
	'cauldron' => ['i586', 'x86_64', 'armv7hl', 'aarch64']
}
parallel = 8

OptionParser.new {|opts|
  opts.banner = "Usage: #{$0} [options]"
  opts.on("--repository URL",
          "Reference repository. Default: #{ref}") {
    |url| ref = url
  }
  opts.on("--parallel n", Integer,
	  "Max number of parallel connections. Default: #{parallel}") {
    |n| $parallel = n
  }
  opts.on("--output file",
	  "Write output into given file. Default to STDOUT") {
    |f| $stdout.reopen(f, "w")
  }
}.parse!

# Get dates from the reference repository, and fail if some requested distros 
# or archs are missing
ref_times = get_dates(ref, archs_per_distro, false)

# Get the list of mirror URLs to check
mirrors = get_mirrors

workqueue = Queue.new
times = {}

# Create all the thread and have them loop on the work queue
threads = (1..parallel).map{|n|
  Thread.new {
    loop do
      u = workqueue.pop
      break if u == :exit
      times[u] = get_dates(u, archs_per_distro)
    end
  }
}

# Push all mirrors into the queue
mirrors.each{|u|
  workqueue << u
}

# Get all the threads to exit after all the work is done
parallel.times{|i|
  workqueue << :exit
}

# Wait for the threads to exit
threads.each{|t|
  t.join
}

# Generate output
print_output(archs_per_distro, mirrors, ref_times, times)