"""
VOSI renderers.

These are really three different renderers for each service.  IVOA wants
it this way (in effect, since they are supposed to be three different
capabilities).
"""

#c Copyright 2008-2024, the GAVO project <gavo@ari.uni-heidelberg.de>
#c
#c This program is free software, covered by the GNU GPL.  See the
#c COPYING file in the source distribution.


from twisted.internet import threads
from twisted.web import resource
from twisted.web import server

from gavo import base
from gavo import registry
from gavo import svcs
from gavo import utils
from gavo.base import meta
from gavo.registry import capabilities
from gavo.registry import tableset
from gavo.registry.model import VS
from gavo.protocols import creds
from gavo.utils.stanxml import Element, registerPrefix, schemaURL, xsiPrefix
from gavo.web import grend
from gavo.web import weberrors


registerPrefix("avl", "http://www.ivoa.net/xml/VOSIAvailability/v1.0",
	schemaURL("VOSIAvailability-v1.0.xsd"))
registerPrefix("cap", "http://www.ivoa.net/xml/VOSICapabilities/v1.0",
	schemaURL("VOSICapabilities-v1.0.xsd"))
registerPrefix("vtm", "http://www.ivoa.net/xml/VOSITables/v1.0",
	schemaURL("VOSITables-v1.0.xsd"))


class VOSIRenderMixin(object):
	"""A mixin furnishing the basic rendering methods for VOSI resources.

	You will have to provide a method _getTree(request) returning some
	xmlstan.
	"""
	def render(self, request):
		request.setHeader("content-type", "text/xml")
		threads.deferToThread(self._getTree, request
			).addCallback(self._shipout, request
			).addCallback(request.finishCallback
			).addErrback(weberrors.renderDCErrorPage, request)
		return server.NOT_DONE_YET

	def _shipout(self, response, request):
		utils.xmlwrite(response, request,
			prolog="<?xml-stylesheet href='/static/xsl/vosi.xsl' type='text/xsl'?>")


class VOSIRenderer(VOSIRenderMixin, grend.ServiceBasedPage):
	"""An abstract base for renderers handling VOSI requests.

	All of these return some sort of XML and are legal on all services.

	The actual documents returned are defined in _getTree(request)->deferred
	firing stanxml.
	"""
	checkedRenderer = False

	def _getTree(self, request):  # pragma: no cover
		raise ValueError("_getTree has not been overridden.")


############ The VOSI data models (no better place for it yet)


class AVL(object):
	"""The container for elements from the VOSI availability schema.
	"""
	class AVLElement(Element):
		_prefix = "avl"
	
	class availability(AVLElement):
		_additionalPrefixes = xsiPrefix
	class available(AVLElement): pass
	class upSince(AVLElement): pass
	class downAt(AVLElement): pass
	class backAt(AVLElement): pass
	class note(AVLElement): pass


class CAP(object):
	"""The container for element from the VOSI capabilities schema.
	"""
	class CAPElement(Element):
		_prefix = "cap"
	
	class capabilities(CAPElement):
		_mayBeEmpty = True


class VTM(object):
	"""The container for element from the VOSI tableset schema.
	"""
	class VTMElement(Element):
		_prefix = "vtm"
	
	class tableset(VTMElement):
		_mayBeEmpty = True

	class table(VTMElement, VS.table):
		_mayBeEmpty = True
		_local = False



SF = meta.stanFactory

_availabilityBuilder = meta.ModelBasedBuilder([
	('available', SF(AVL.available)),
	('upSince', SF(AVL.upSince)),
	('_scheduledDowntime', SF(AVL.downAt)),
	('backAt', SF(AVL.backAt)),
	('availability_note', SF(AVL.note)),
	('crash_this', lambda: "invalid, for testing"),
	])


############ The actual VOSI renderers

class VOSIAvailabilityRenderer(VOSIRenderer):
	"""A renderer for a VOSI availability endpoint.

	An endpoint with this renderer is automatically registered for
	every service.	The answers can be configured using the admin
	renderer.
	"""
	name = "availability"

	def _getTree(self, request):
		root = AVL.availability[
			_availabilityBuilder.build(self.service)]
		return root


class VOSICapabilityRenderer(VOSIRenderer):
	"""A renderer for a VOSI capability endpoint.

	An endpoint with this renderer is automatically registered for
	every service.	The responses contain information on what renderers
	("interfaces") are available for a service and what properties they have.

	This also doubles as a canary for authentication, which is why there
	are the somewhat complicated things in render; cf.
	https://wiki.ivoa.net/twiki/bin/view/IVOA/SSO_next
	"""
	name = "capabilities"

	def _getTree(self, request):
		root = CAP.capabilities()

		for pub in self.service.getPublicationsForSet(None):
			try:
				root = root[capabilities.getCapabilityElement(pub)]
			except Exception as msg:
				base.ui.notifyError("Error while creating VOSI capability"
					" for %s: %s"%(
					self.service.getURL(pub.render, absolute=False),
					msg))

		return root

	def render(self, request):
		if request.getUser():
			return self._renderWithUser(request)

		else:
			return self._renderWithoutUser(request)

	def _addChallenge(self, request):
		"""adds a www-authenticate header to request.
		"""
		# We may have to figure out a way to admit different realms here,
		# but for now just use our global realm
		realm = base.getConfig("web", "realm")
		request.setHeader('WWW-Authenticate',
			'Basic realm="%s"'%realm)

	def _renderWithUser(self, request):
		"""creates a response when a request contains some credentials.

		Four cases:

		* bad credentials: return a 401 with an auth challenge.
		* open service, good credentials: return 200 with the authenticated id
		* closed service, good credentials: return 200 with the authenticated id
		* closed service, good credentials but not authorised:
		  return 401 with a challenge.
		"""
		groups = creds.getGroupsForUser(request.getUser(), request.getPassword())

		if self.service.limitTo is None:
			isAuth = bool(groups)
		else:
			isAuth = self.service.limitTo in groups

		request.setHeader("content-type", "text/plain")
		if isAuth:
			request.setHeader("x-vo-authenticated", request.getUser())

		else:
			self._addChallenge(request)
			request.setResponseCode(401)

		return super().render(request)
	
	def _renderWithoutUser(self, request):
		"""creates a response when a request does not contain credentials.

		Two cases:

		* open service: return a 200 but announce optional auth for the
		  sake of services that may support gratuitous auth.
		* closed service: return 401 and a challenge.
		"""
		self._addChallenge(request)
		if self.service.limitTo is not None:
			request.setResponseCode(401)
		return super().render(request)

	

class VOSITableResponse(VOSIRenderMixin, resource.Resource):
	"""A resource building a VOSI tableset for a single table.

	This is returned as a child resource of VOSITablesetRenderer.
	"""
	def __init__(self, tableDef):
		self.tableDef = tableDef
		resource.Resource.__init__(self)
	
	def _getTree(self, request):
		return tableset.getTableForTableDef(self.tableDef, [],
			rootElement=VTM.table)

	def getChild(self, request, name):
		raise svcs.UnknownURI("VOSI tables resources have no children")


class VOSITablesetRenderer(VOSIRenderer):
	"""A renderer for a VOSI table metadata endpoint.

	An endpoint with this renderer is automatically registered for
	every service.	The responses contain information on the tables
	exposed by a given service.
	"""
	name = "tableMetadata"
	aliases = frozenset(["tables"])

	def _getTree(self, request):
		detail = request.strargs.get("detail",
			[base.getConfig("ivoa", "vositabledetail")])[0]

		root = registry.getTablesetForService(self.service,
			rootElement=VTM.tableset, suppressBodies=detail=="min")
		return root
	
	def getChild(self, name, request):
		if name==b"":
			raise svcs.WebRedirect(self.service.getURL("tableMetadata"))

		tableName = name.decode("utf-8").lower()

		for td in self.service.getTableSet():
			if td.getQName().lower()==tableName:
				return VOSITableResponse(td)

		raise svcs.UnknownURI("No table %s on this service."%tableName)
