"""
A renderer to do RD-based maintainance.
"""

#c Copyright 2008-2021, 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.


import sys
import traceback

from twisted.web import template
from twisted.web.template import tags as T

from gavo import base
from gavo import formal
from gavo import stc
from gavo import svcs
from gavo.protocols import creds
from gavo.web import common
from gavo.web import grend


class AdminRenderer(formal.ResourceWithForm, 
		grend.CustomTemplateMixin,
		grend.ServiceBasedPage):
	"""A renderer allowing to block and/or reload services.

	This renderer could really be attached to any service since
	it does not call it, but it usually lives on //services/overview.  
	It will always require authentication.

	It takes the id of the RD to administer from the path segments
	following the renderer name.

	By virtue of builtin vanity, you can reach the admin renderer
	at /seffe, and thus you can access /seffe/foo/q to administer
	the foo/q RD.
	"""
	name = "admin"
	customTemplate = svcs.loadSystemTemplate("admin.html")
	clientRD = None
	# set below when RD loading failed.
	reloadExc = None
	reloadTB = None

	def form_setDowntime(self, request):
		form = formal.Form()
		form.addField("scheduled", formal.String(),
			label="Schedule downtime for",
			description="Note that this is purely informative.  The server"
				" will not take down the services at this point in time."
				" Leave empty to cancel.  This will also be cleared on a"
				" reload.")
		form.addAction(self.setDowntime, label="Ok")
		form.data = {
			"scheduled": base.getMetaText(self.clientRD, "_scheduledDowntime")}
		return form

	def setDowntime(self, request, form, data):
		scheduleFor = data.get("scheduled")
		if scheduleFor is None:
			self.clientRD.delMeta("_scheduledDowntime")
		else:
			try:
				stc.parseISODT(scheduleFor)  # check syntax
				self.clientRD.setMeta("_scheduledDowntime", scheduleFor)
			except stc.STCLiteralError: # bad date syntax
				raise base.ui.logOldExc(
					formal.FieldError("Doesn't look like ISO", "scheduleFor"))

	def form_adminOps(self, request):
		form = formal.Form()
		if hasattr(self.clientRD, "currently_blocked"):
			label = "Unblock"
		else:
			label = "Block"
		form.addAction(self.toggleBlock, label=label, name="block")
		form.addAction(self.reloadRD, label="Reload RD", name="submit")
		return form

	def toggleBlock(self, request, form, data):
		if hasattr(self.clientRD, "currently_blocked"):
			delattr(self.clientRD, "currently_blocked")
		else:
			self.clientRD.currently_blocked = True

	def reloadRD(self, request, form, data):
		base.caches.clearForName(self.clientRD.sourceId)

	def data_blockstatus(self, request, tag):
		if hasattr(self.clientRD, "currently_blocked"):
			return "blocked"
		return "not blocked"

	def data_services(self, request, tag):
		"""returns a sequence of service items belonging to clientRD, sorted
		by id.
		"""
		return sorted(self.clientRD.services, key=lambda svc: svc.id)

	@template.renderer
	def svclink(self, request, tag):
		"""renders a link to a service info with a service title.
		
		data must be an item returned from data_services.
		"""
		data = tag.slotData
		return tag(href=data.getURL("info"))[base.getMetaText(data, "title")]

	@template.renderer
	def rdId(self, request, tag):
		return tag[self.clientRD.sourceId]

	@template.renderer
	def ifexc(self, request, tag):
		"""render children if there was an exception during RD load.
		"""
		if self.reloadExc is None:
			return ""
		else:
			return tag

	@template.renderer
	def exc(self, request, tag):
		return tag[repr(self.reloadExc)]
	
	@template.renderer
	def traceback(self, request, tag):
		return tag[self.reloadTB]

	def render(self, request):
		# naked renderer means admin services itself
		if self.clientRD is None:
			self.clientRD = base.caches.getRD("__system__/services")
		return common.runAuthenticated(request, "admin", 
			super(AdminRenderer, self).render, request)

	def _extractDamageInfo(self):
		"""called when reload of RD failed; leaves exc. info in some attributes.
		"""
		type, value = sys.exc_info()[:2]
		self.reloadExc = value
		self.reloadTB = traceback.format_exc()

	# the getChild here is actually the constructor, as it were --
	# each request gets a new AdminRender by web.root
	def getChild(self, name, request):
		segments = request.popSegments(name)
		rdId = "/".join(segments)
		try:
			self.clientRD = base.caches.getRD(rdId)
			if hasattr(self.clientRD, "getRealRD"):
				self.clientRD = self.clientRD.getRealRD()

			self.metaCarrier = self.clientRD
			self.macroPackage = self.clientRD
		except base.RDNotFound:
			raise base.ui.logOldExc(
				svcs.UnknownURI("No such resource descriptor: %s"%rdId))
		except Exception: # RD is botched.  Clear cache and give an error
			base.caches.clearForName(rdId)
			self._extractDamageInfo()
		return self

	defaultLoader =  common.doctypedStan(
		T.html[
			T.head[
				T.title["Missing Template"]],
			T.body[
				T.p["Admin services are only available with a admin.html template"]]
		])


class AuthcheckRenderer(grend.ServiceBasedPage):
	"""A renderer that lets people can use to see if a username/password
	name is valid on this service (and if there's auth on at all).

	It returns text/plain, with 403 with a "NO" payload if the pair doesn't 
	work, and 200 with a "PROBABLY YES" payload if it does.

	On an unrestricted service, this check is if password matches username.
	That's mainly for services like our UWS that let people put in
	gratuitous auth.

	This is (at least for now) highly experimental and will presumably go away
	again.  If you find you need it, let the authors know.

	And if we keep it, we should probably add some sort of rate limiting to it.
	"""
	name = "authcheck"
	checkedRenderer = False
	openRenderer = True

	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 render(self, request):
		if request.getUser():
			return self._renderWithUser(request)

		else:
			return self._renderWithoutUser(request)

	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-authenticated-id", request.getUser())
			return b"PROBABLY YES"

		else:
			self._addChallenge(request)
			request.setResponseCode(401)
			return b"NO"
	
	def _renderWithoutUser(self, request):
		"""creates a response when a request does not contain credentials.

		Two cases:

		* open service: return a 200 but annouce optional auth for the 
		  sake of services that may support gratuitous auth.
		* closed service: return 401 and a challenge.
		"""
		self._addChallenge(request)
		request.setHeader("content-type", "text/plain")

		if self.service.limitTo is None:
			return b"PROBABLY YES"

		request.setResponseCode(401)
		return b"NO"

