#!/usr/bin/env luajit
local args = {...}
verbosity = -1
local logFile
local html = false
--[[ Ordered table iterator, allow to iterate on the natural order of the keys of a table.
    From http://lua-users.org/wiki/SortedIteration
  ]]
function __genOrderedIndex( t )
    local orderedIndex = {}
    for key in pairs(t) do
        table.insert( orderedIndex, key )
    end
    table.sort( orderedIndex )
    return orderedIndex
end
function orderedNext(t, state)
    -- Equivalent of the next function, but returns the keys in the alphabetic
    -- order. We use a temporary ordered key table that is stored in the
    -- table being iterated.
    local key = nil
    --print("orderedNext: state = "..tostring(state) )
    if state == nil then
        -- the first time, generate the index
        t.__orderedIndex = __genOrderedIndex( t )
        key = t.__orderedIndex[1]
    else
        -- fetch the next value
        for i = 1,table.getn(t.__orderedIndex) do
            if t.__orderedIndex[i] == state then
                key = t.__orderedIndex[i+1]
            end
        end
    end
    if key then
        return key, t[key]
    end
    -- no more value to return, cleanup
    t.__orderedIndex = nil
    return
end
function orderedPairs(t)
    -- Equivalent of the pairs() function on tables. Allows to iterate
    -- in order
    return orderedNext, t, nil
end
-- Use this to dump a table to a string, with HTML.
dumpTableHTML = function (table, space, name)
    local r = name .. "\n"
    r = r .. dumpTableHTMLSub(table, space .. " ")
    r = r .. space .. ""
    return r
end
dumpTableHTMLSub = function (table, space)
    local r = ""
    for k, v in orderedPairs(table) do 
	if type(v) == "table" then
	    if " " == space then
		r = r ..           space .. dumpTableHTML(v, space, k .. "
\n"
	    else
		r = r .. "" .. space .. dumpTableHTML(v, space, k .. "\n"
	    end
	else
	    r = r .. space .. "" .. k .. "\n"
	end
    end
    return r
end
local checkFile = function(f)
    local h, e = io.open(f, "r")
    if nil == h then return false else h:close();  return true end
end
local plurals = function(e, w)
    local result = ""
    if 1 == e then
	result = e .. " error"
    elseif e ~= 0 then
	result = e .. " errors"
    end
    if ("" ~= result) and html then result = "" .. result .. "" end
--    result = " " .. result
    if 0 < w then
	if 0 < e then result = result .. ", " end
	if 1 == w then 
	    result = result .. w .. " warning"
	else
	    result = result .. w .. " warnings"
	end
	if ("" ~= result) and html then result = "" .. result .. "" end
--	result = " " .. result
    end
    if "" ~= result then result = " (" .. result .. ")" end
    return result
end
local results = {}
local log = function(v, t, s, prot, test, host)
    local x = ""
    if nil == prot then prot = "" end
    if nil ~= test then x = x .. test else test = "" end
    if nil ~= host then
	if #x > 0 then x = x .. " " end
	x = x .. host
    end
    if #x > 0 then
	t = t .. "(" .. x ..  ")"
    end
    if v <= verbosity then
	if 3 <= verbosity then t = os.date() .. " " .. t end
	print(t .. ": " .. s)
    end
    if nil ~= logFile then
	logFile:write(os.date() .. " " .. t .. ": " .. s .. "\n")
	logFile:flush()
    end
end
local D = function(s) log(3,  "DEBUG   ", s) end
local I = function(s) log(2,  "INFO    ", s) end
local W = function(s, p, t, h) log(1,  "WARNING ", s, p, t, h) end
local E = function(s, p, t, h) log(0,  "ERROR   ", s, p, t, h) end
local C = function(s) log(-1, "CRITICAL", s) end
local mirrors = loadfile("results/mirrors.lua")()
local revDNS = function(dom, IP)
    if "deb.devuan.org" ~= dom then
	if nil ~= mirrors["deb.devuan.org"] then
	    if nil ~= mirrors["deb.devuan.org"].IPs["deb.roundr.devuan.org"][IP] then
		if html then
		    return "DNS-RR"
		else
		    return "DNS-RR"
		end
	    end
	end
    else
	for k, v in pairs(mirrors) do
	    if "deb.devuan.org" ~= k then
		local IPs = v.IPs
		for i, u in pairs(IPs) do
		    if "table" == type(u) then
			for h, t in pairs(u) do
			    if IP == h then return k end
			end
		    else
			if IP == i then return k end
		    end
		end
	    end
	end
    end
    return ""
end
local faulty = ""
local status = function(host, results, typ)
    local result = ""
    local e = 0
    local w = 0
    local s = nil ~= mirrors[host].Protocols[typ]
    local to = results.timeout
    if ('http' ~= typ) and ('https' ~= typ) and ('ftp' ~= typ) and ('rsync' ~= typ) then s = true end
    if nil ~= results[typ] then
	e = results[typ].errors
	w = results[typ].warnings
--[[
	for k, v in pairs(results[typ]) do
	    if "table" == type(v) then
		if 0 <= v.errors   then e = e + v.errors   else to = true end
		if 0 <= v.warnings then w = w + v.warnings else to = true end
	    end
	end
]]
    else
	for k, v in pairs(results) do
	    if "table" == type(v) then
		for i, u in pairs(v) do
		    if "table" == type(u) then
			if typ == i then
			    if 0 <= u.errors   then e = e + u.errors   end
			    if 0 <= u.warnings then w = w + u.warnings end
			end
		    end
		end
	    end
	end
    end
    if to then
	result = "[TIMEOUT"
	if not s then result = result .. "*" end
	if html then
	    if s then
		result = "[TIMEOUT"
	    else
		result = "[TIMEOUT*"
	    end
	end
    elseif 0 < e then
	result = "[FAILED"
	if not s then result = result .. "*" end
	if html then
	    if s then
		result = "[FAILED"
	    else
		result = "[FAILED*"
	    end
	end
	if html then
	    faulty = faulty .. host .. " (" .. typ .. ")
\n"
	else
	    faulty = faulty .. host .. " (" .. typ .. ")\n"
	end
    else
	result = "[OK"
	if not s then result = result .. "*" end
	if html then
	    if s then
		result = "[OK"
	    else
		result = "[OK*"
	    end
	end
    end
    return result .. plurals(e, w) .. "]"
end
local collate = function(host, ip, results)
    local f = "results/" .. host .. "_" .. ip .. ".lua"
    local rfile, e = io.open(f, "r")
    if nil == rfile then I("opening " .. f .. " file - " .. e) else
	rfile:close()
	local rs = loadfile(f)()
	for k, v in pairs(rs) do
	    if "table" == type(v) then
		if "speed" == k then
		    if v.min < results.speed.min then results.speed.min = v.min end
		    if v.max > results.speed.max then results.speed.max = v.max end
		else
		    for i, u in pairs(v) do
			if "table" == type(u) then
			    for h, t in pairs(u) do
				local a = results[k][h]
				if nil == a then a = 0 end
				results[k][h] = a + t
			    end
			else
			    local a = results[k][i]
			    if nil == a then a = 0 end
			    results[k][i] = a + u
			end
		    end
		end
	    elseif "timeout" ~= k then
		local a = results[k]
		if nil == a then a = 0 end
		results[k] = a + v
	    end
	end
    end
    return results
end
local m = {}
local logCount = function(domain, ip)
    local nm = "LOG_" .. domain
    local log = ""
    local extra = ""
    if nil ~= ip then nm = nm .. "_" .. ip end
    nm = nm .. ".html"
    local rfile, e = io.open("results/" .. nm, "r")
    if nil ~= rfile then
	local errors = 0
	local warnings = 0
	for l in rfile:lines() do
	    if nil ~= l:match(">ERROR ") then errors = errors + 1 end
	    if nil ~= l:match(">WARNING ") then warnings = warnings + 1 end
	end
	rfile:close()
	if html then
	    if nil == ip then
		log = "" .. domain .. ""
	    else
		log = "" .. ip .. ""
	    end
	end
	log = log .. plurals(errors, warnings)
    end
    return log
end
html = false
local email, e = io.open("results/Report-email.txt", "w+")
if nil == email then C("opening mirrors file - " .. e) else
    email:write( "Dear Mirror Admins,\n\n" ..
		"This is the status of the mirror servers in the Devuan package mirror network.\n\n" ..
		"EXPERIMENTAL CODE - double check all results you see here, and read the logs if it's important." ..
		"The full list of Devuan package mirrors is available at the URL:\n\n" ..
		" https://pkgmaster.devuan.org/mirror_list.txt\n\n" ..
		'Please contact "mirrors@devuan.org" if any of the information \nin the file above needs to be amended. \n\n' ..
		"The full results of the mirror checking is available at the URL:\n\n" ..
		" https://sledjhamr.org/apt-panopticon/results/Report-web.html\n\n" ..
		"Due to the nature of the tests, some errors or warnings will be \ncounted several times.  " ..
		"Refer to the logs on the web page for details.\n\n" ..
		"Please see below the current status of the Devuan Package Mirror \nnetwork:\n\n" ..
		"==== package mirror status " .. os.date("!%Y-%m-%d %H:%M") .. " GMT ====\n" ..
		"[skip] means that the test hasn't been written yet.\n\n")
    for k, v in orderedPairs(mirrors) do
	local results = loadfile("results/" .. k .. ".lua")()
	email:write(k .. "....\n")
	local IPs = v.IPs
	for i, u in pairs(IPs) do
	    if "table" == type(u) then
		for h, t in pairs(u) do
		    results = collate(k, h, results)
		end
	    else
		results = collate(k, i, results)
	    end
	end
	local ftp	= "[skip]"
	local http	= status(k, results, "http")
	local https	= status(k, results, "https")
	local rsync	= "[skip]"
	local dns	= ""
	local protocol	= status(k, results, "Protocol")
	local sanity	= status(k, results, "URLSanity")
	local integrity = status(k, results, "Integrity")
	local updated	= status(k, results, "Updated")
	-- DNS-RR test.
	if ("deb.devuan.org" ~= k) and (nil ~= mirrors["deb.devuan.org"]) then
	    for l, w in pairs(mirrors[k].IPs) do
		if type(w) == "table" then
		    for i, u in pairs(w) do
			if nil ~= mirrors["deb.devuan.org"].IPs["deb.roundr.devuan.org"][i] then
			    local log = logCount("deb.devuan.org", i)
			    if "" ~= log then
				if "" == dns then dns = " " else dns = dns .. "  " end
				dns = dns .. logCount("deb.devuan.org", i)
			    else
				if "" == dns then dns = " " else dns = dns .. "  " end
				dns = dns .. i
			    end
			end
		    end
		else
		    if nil ~= mirrors["deb.devuan.org"].IPs["deb.roundr.devuan.org"][l] then
			local log = logCount("deb.devuan.org", l)
			if "" ~= log then
			    if "" == dns then dns = " " else dns = dns .. "  " end
			    dns = dns .. log
			else
			    if "" == dns then dns = " " else dns = dns .. "  " end
			    dns = dns .. l
			end
		    end
		end
	    end
	    if "" == dns then dns = "[no]" end
	    dns = " DNS-RR: " .. dns
	end
	email:write(	"      ftp: " .. ftp .. " http: " .. http .. " https: " .. https .." rsync: " .. rsync .. "\n" ..
			"     " .. dns .. "\n" ..
			"      Protocol: " .. protocol .. " URL-sanity: " .. sanity .. " Integrity: " .. integrity .. "\n" ..
			"      Updated: " .. updated .. "\n")
    end
    email:write( "\n==== faulty mirrors: ====\n" .. faulty)
    email:write( "\n-------------------------\n\n" ..
		"* This means that this protocol isn't actually supported, but the test was run ayway.\n\n" ..
		"Thanks for your precious help in ensuring that Devuan GNU+Linux \nremains a universal, stable, dependable, free operating system.\n\n" ..
		"You can get the source code from https://sledjhamr.org/cgit/apt-panopticon/about/ .\n\n" ..
		"Love\n\n" ..
		"The Dev1Devs\n\n")
    email:close()
end
results = {}
m = {}
faulty = ""
html = true
local web, e = io.open("results/Report-web.html", "w+")
if nil == web then C("opening mirrors file - " .. e) else
    web:write( "apt-panopticon results\n" ..
		'' ..
		"Welcome to the apt-panopticon results page.
\n" ..
		"This is the status of the mirror servers in the Devuan package mirror network.
\n" ..
		"EXPERIMENTAL CODE - double check all results you see here, and read the logs if it's important.
" ..
		"The full list of Devuan package mirrors is available at the URL: " ..
		"https://pkgmaster.devuan.org/mirror_list.txt
\n" ..
		"Due to the nature of the tests, some errors or warnings will be counted several times.   " ..
		"The links in the table and DNS list go to the detailed testing logs.
\n\n" ..
		"
\n==== package mirror status " .. os.date("!%Y-%m-%d %H:%M") .. " GMT ====
\n" ..
		"[FAILED] or [OK]" ..
		" means the tested thing is supported for that mirror.
\n" ..
		"[FAILED*] or [OK*]" ..
		" means the tested thing is unsupported for that mirror, but might have been tested anyway.
\n" ..
		"[TIMEOUT] or [TIMEOUT]" ..
		" means the server had too many timeouts, and tests where aborted, so there is no result for this test.
" ..
		"The DNS round robin (DNS-RR) column shows the IPs for that mirror, or [no] if it isn't part of the DNS-RR.   " ..
		"The IPs link to the testing log for that IP accessed via the DNS-RR.   " ..
		"deb.devuan.org is the DNS-RR itself, so it doesn't get tested directly.
\n" ..
		"The time in the Updated column is how often the mirror updates itself.
" ..
		"Mirrors with a grey background are not active (though may be usable as part of the DNS-RR).
\n" ..
		"[skip] means that the test hasn't been written yet.
\n" ..
		"\n|  | FTP | HTTP | HTTPS | RSYNC | DNS round robin" ..
		" | Protocol | URL sanity | Integrity | Updated | Speed range | 
|---|
\n"
		)
    for k, v in orderedPairs(mirrors) do
	local results = loadfile("results/" .. k .. ".lua")()
	local active = ""
	if "yes" == v.Active then
	    web:write("     | " .. k .. "")
	else
	    if nil == v.Active then active = 'nil' else active = v.Active end
	    web:write(" | 
|---|
| " .. k .. "")
	end
	local IPs = v.IPs
	for i, u in pairs(IPs) do
	    if "table" == type(u) then
		for h, t in pairs(u) do
		    results = collate(k, h, results)
		end
	    else
		results = collate(k, i, results)
	    end
	end
	local ftp	= "[skip]"
	local http	= status(k, results, "http")
	local https	= status(k, results, "https")
	local rsync	= "[skip]"
	local dns	= ""
	local protocol	= status(k, results, "Protocol")
	local sanity	= status(k, results, "URLSanity")
	local integrity = status(k, results, "Integrity")
	local updated	= status(k, results, "Updated")
	local rate = v.Rate
	if nil ~= rate then updated = updated .. ' ' .. rate end
	local min	= tonumber(results.speed.min)
	local max	= tonumber(results.speed.max)
	local spd	= ''
	-- DNS-RR test.
	if ("deb.devuan.org" ~= k) and (nil ~= mirrors["deb.devuan.org"]) then
	    for l, w in pairs(mirrors[k].IPs) do
		if type(w) == "table" then
		    for i, u in pairs(w) do
			if nil ~= mirrors["deb.devuan.org"].IPs["deb.roundr.devuan.org"][i] then
			    local log = logCount("deb.devuan.org", i)
			    if "" ~= log then
				if "" == dns then dns = " " else dns = dns .. "   " end
				dns = dns .. logCount("deb.devuan.org", i)
			    else
				if "" == dns then dns = " " else dns = dns .. "   " end
				dns = dns .. "" .. i .. ""
			    end
			end
		    end
		else
		    if nil ~= mirrors["deb.devuan.org"].IPs["deb.roundr.devuan.org"][l] then
			local log = logCount("deb.devuan.org", l)
			if "" ~= log then
			    if "" == dns then dns = " " else dns = dns .. "   " end
			    dns = dns .. log
			else
			    if "" == dns then dns = " " else dns = dns .. "   " end
			    dns = dns .. "" .. l .. ""
			end
		    end
		end
	    end
	    if "" == dns then dns = "[no]" end
	    if 0 == max then
		spd = ' |  | '
	    else
		spd = string.format(' | %d -> | %d', min, max)
	    end
	end
	web:write(" | " .. ftp .. " | " .. http .. " | " .. https .. " | " .. rsync .. " | " .. dns ..
		    " | " .. protocol .. " | " .. sanity ..
		    " | " .. integrity .. " | " .. updated .. "" .. spd .. " | 
|---|
\n")
	if "" ~= active then
	    web:write("| " .. active .. " | 
\n")
	end
    end
    web:write( "
\n
\n==== faulty mirrors: ====
\n" .. faulty)
    web:write( "
\n
\n==== DNS and logs: ====
\n")
    for k, v in pairs(mirrors) do
	local log = k
	local n = {}
	log = logCount(k)
	mirrors[k].Protocols = nil
	mirrors[k].FQDN = nil
	mirrors[k].Active = nil
	mirrors[k].Rate = nil
	mirrors[k].BaseURL = nil
	mirrors[k].Country = nil
	mirrors[k].Bandwidth = nil
	for l, w in pairs(mirrors[k].IPs) do
	    if type(w) == "table" then
		n[l] = {}
		for i, u in pairs(w) do
		    local log = logCount(k, i)
		    if "" == log then n[l][i] = u else n[l][log .. " " .. revDNS(k, i)] = u end
		end
	    else
		local log = logCount(k, l)
		if "" == log then n[l] = w else n[log .. " " .. revDNS(k, l)] = w end
	    end
	end
	m[log .. " DNS entries -"] = n
    end
    web:write(	"This lists each mirror, and the DNS entries for that mirror.   " ..
		"The links point to the testing log files for " .. logCount("apt-panopticon") .. " for each domain name / IP combination that was tested.   " ..
		"If a mirror has a CNAME, that CNAME is listed along with that CNAMEs DNS entries.   " ..
		"deb.devuan.org is the DNS round robin, which points to the mirrors that are part of the DNS-RR.   " ..
		"If an IP is part of the DNS-RR, it is marked with 'DNS-RR'   " ..
		"pkgmaster.devuan.org is the master mirror, all the others sync to it.   " ..
		"
\n"
		)
    web:write(dumpTableHTML(m, "", ""))
    web:write( "\n
\n
\n\n" ..
		"The email report.   " ..
		"All the logs and other output.   " ..
		"You can get the source code here.
" ..
		"\n")
    web:close()
end