"""
A core for validating IVOIDs.

To make tests, add a function test_<whatever>(uriRef, msgs)
adding to msgs as appropriate.  uriRef is an rfc3986.URIReference
instance.

Use the msgs.add method exclusively to produce messages, and use
terms from the enums below exclusively for major and minor.

tests will be run in alphabetical order.
"""

import re
import urllib.parse

import rfc3986

from gavo import base
from gavo import rsc
from gavo.svcs import core


class AbortRun(Exception):
	pass


def enum(vals):
	g = globals()
	for index, v in enumerate(vals.split()):
		g[v] = index+1


# major error codes
enum("ERR_URI ERR_VALIDATOR ERR_SCHEME ERR_AUTHORITY ERR_RESKEY"
	" ERR_QUERY ERR_FRAGMENT ERR_REGISTRY")
# minor error codes
enum("CODE_BROKEN CODE_BADCHAR CODE_BADVAL CODE_MISSING")


def mc(major, minor):
	"""makes a machine readable error code from major and minor error codes.
	"""
	return "%d.%d"%(major, minor)


class Core(core.Core):
	def run(self, service, inputTable, queryMeta):
		tuples = validateURI(inputTable.getParam("uri"))
		res = rsc.TableForDef(self.outputTable)
		for t in tuples:
			res.addTuple(t)
		return res


class Messages(list):
	"""a container for diagnostics.

	This is essentially to have the convenience method add.
	"""
	def add(self, type, major, minor, message):
		if major:
			code = mc(major, minor)
		else:
			code = None
		self.append((type, message, code))


def getResType(ivoid):
	"""returns the res_type of ivoid in the registry.

	ivoid must not have local parts in Identifiers parlance.

	This raises a KeyError if ivoid cannot be resolved
	"""
	with base.getTableConn() as conn:
		res = list(conn.query(
			"select res_type from rr.resource where ivoid=%(ivoid)s",
			{'ivoid': ivoid}))
		if not res:
			raise KeyError(ivoid)
		return res[0][0]
			

def validateURI(uri):
	"""returns rows of (type, message, code) for a validation of the
	string URI.
	"""
	msgs = Messages()
	parsed = None
	try:
		parsed = rfc3986.uri_reference(uri)
	except Exception as msg:
		msgs.add("ERROR", ERR_URI, CODE_BROKEN,
			"URI cannot be parsed(%s)"%str(msg))

	if re.match("[^ -~]+", uri):
		msgs.add("ERROR", ERR_URI, CODE_BADCHAR,
			"Non-ASCII characters are not allowed in ivoids")

	if parsed:
		for name, test in iterTests():
			try:
				test(parsed, msgs)
			except AbortRun:
				break
			except Exception as msg:
				msgs.add("ERROR", ERR_VALIDATOR, CODE_BROKEN,
				"URI made a test %s bomb (%s) Please report this."%(
					name, str(msg)))
	
	return msgs


def iterTests():
	g = globals()
	testNames = sorted(n for n in g if n.startswith("test_"))
	for n in testNames:
		yield n, g[n]


def test_000uriValid(uri, msgs):
	if not uri.is_valid():
		msgs.add("ERROR", ERR_URI, CODE_BROKEN,
			"URI is not RFC3986-valid. Bailing out")
		raise AbortRun()


def test_001classify(uri, msgs):
	if uri.fragment or uri.query:
		msgs.add("INFO", None, None, "Validating as a full ivoid")
		return

	if uri.path:
		msgs.add("INFO", None, None,
			"Validating as a Registry reference")
		return

	if uri.authority:
		msgs.add("INFO", None, None, "Validating as an authority ID")
		return
	
	msgs.add("ERROR", ERR_AUTHORITY, CODE_MISSING, "authority part is"
		" mandatory for ivoids")


def test_010scheme(uri, msgs):
	if not uri.scheme:
		msgs.add("ERROR", ERR_SCHEME, CODE_MISSING,
			"IVOID scheme must not be ivo (rather than empty)")
		return
	if not uri.scheme.lower()=="ivo":
		msgs.add("ERROR", ERR_SCHEME, CODE_BADVAL,
			"IVOID scheme must be IVO")


def test_020authority(uri, msgs):
	if not uri.authority:
		msgs.add("ERROR", ERR_AUTHORITY, CODE_MISSING,
			"Empty authorities are not allowed in ivoids")
		return

	if not re.match("[a-z0-9][a-z0-9._~-]*$", uri.authority.lower()):
		msgs.add("ERROR", ERR_AUTHORITY, CODE_BADCHAR,
			"Authority must start with a-z and only contain unreserved characters")
	
	if len(uri.authority)<3:
		msgs.add("ERROR", ERR_AUTHORITY, CODE_BADVAL,
			"Authority must be at least three characters long")
	

def test_030resourcekey(uri, msgs):
	if not uri.path:
		return

	if re.search("[!$&'()]", uri.path):
		msgs.add("WARNING", None, None, "Sub-delimiter found in resource"
			" key; this ivoid must only be used with specialised clients")
	
	if re.search(r"/\.{0,2}/", uri.path):
		msgs.add("ERROR", ERR_RESKEY, CODE_BADVAL, "Empty segments or those"
			" with only dots are forbidden in resource keys")

	if uri.path.endswith("/"):
		msgs.add("ERROR", ERR_RESKEY, CODE_BADVAL, "Trailing slashes are"
			" forbidden for Registry references")
	
	if '%' in uri.path:
		msgs.add("ERROR", ERR_RESKEY, CODE_BADCHAR, "Percent-encoded"
			" characters are forbidden in resource keys.")
	

def test_040query(uri, msgs):
	if not uri.query:
		return

	decoded = urllib.parse.unquote(uri.query)
	if "%" in decoded:
		msgs.add("ERROR", ERR_QUERY, CODE_BADVAL,
			"Invalid percent encoding in query part")

	if '\ufffd' in decoded:
		msgs.add("WARNING", None, None,
			"Percent-encoded characters in query part are invalid utf-8")


def test_050fragment(uri, msgs):
	if not uri.fragment:
		return

	decoded = urllib.parse.unquote(uri.fragment)
	if "%" in decoded:
		msgs.add("ERROR", ERR_FRAGMENT, CODE_BADVAL,
			"Invalid percent encoding in fragment part")
	
	if '\ufffd' in decoded:
		msgs.add("WARNING", None, None,
			"Percent-encoded characters in fragment part are invalid utf-8")


def test_060inRegistry(uri, msgs):
	ivoid = "%s://%s"%(uri.scheme, uri.authority)
	if uri.path:
		ivoid = ivoid+uri.path
		checkAuthority = False
	else:
		checkAuthority = True

	try:
		resType = getResType(ivoid)
		if checkAuthority and resType!="vg:authority":
			msgs.add("ERROR", ERR_REGISTRY, CODE_BADVAL,
				"IVOID is an authority id but registry record is not vg:authority")
	except KeyError:
		msgs.add("ERROR", ERR_REGISTRY, CODE_MISSING,
			"Registry reference missing in registry")


def test_ZZsummary(uri, msgs):
	valid = True
	for type, _, _ in msgs:
		if type=="ERROR":
			valid = False
	
	if valid:
		msgs.add("INFO", None, None, "ivo-id is valid")
	else:
		msgs.add("INFO", None, None, "ivo-id is invalid")


if __name__=="__main__":
	print(validateURI("]://&%!"))
