"""
Default error displays for the data center and error helper code.

Everything in here must render synchronuosly.

You probably should not construct anything in this module directly
but rather just raise the appropriate exceptions from svcs.
"""

#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.


import urllib.parse

from twisted.python import failure
from twisted.web import server
from twisted.web import template
from twisted.web.template import tags as T

from gavo import base
from gavo import svcs
from gavo import utils
from gavo.formal import nevowc
from gavo.web import common


class _Silence(Exception):
	"""A sentinel used by Silence.
	"""

def Silence():
	"""returns a failure.Failure that cases this module to not
	touch request at all.

	You want to return this from errbacks that already have spit
	out their error message.
	"""
	return failure.Failure(exc_value=_Silence("Keep quiet"))


class ErrorPage(nevowc.TemplatedPage, common.CommonRenderers):
	"""A base for error handling pages.

	The idea is that you set the "handles" class attribute to
	the exception you handle.  The exception has to match exactly, i.e.,
	no isinstancing is done.

	You also must set status to the HTTP status code the error should
	return.

	All error pages have a failure attribute that's a twisted failure
	with all the related mess (e.g., tracebacks).

	You have the status and message data methods.

	In custom exceptions, you can attach stan in htmlMessage, and ErrorPages
	will show that as the error message.
	"""
	handles = None
	status = 500
	titleMessage = "Unspecified Error"
	beforeMessage = "We're sorry, but something didn't work out:"
	afterMessage = T.p["This generic text shouldn't be here.  The"
		" child class should override afterMessage."]
	_footer = "delete this when done"

	def __init__(self, error):
		self.flr = error

	def data_status(self, request, tag):
		return str(self.status)

	@template.renderer
	def beforemessage(self, request, tag):
		return tag[self.beforeMessage]

	@template.renderer
	def aftermessage(self, request, tag):
		return tag[self.afterMessage]

	@template.renderer
	def message(self, request, tag):
		if hasattr(self.flr.value, "htmlMessage"):
			return tag(class_="errmsg")[self.flr.value.htmlMessage]
		else:
			return tag(class_="errmsg")[self.flr.getErrorMessage()]

	@template.renderer
	def hint(self, request, tag):
		if (hasattr(self, "flr")
				and hasattr(self.flr.value, "hint")
				and self.flr.value.hint):
			return tag[T.strong["Hint: "],
				self.flr.value.hint]
		return ""

	@template.renderer
	def rdlink(self, request, tag):
		if hasattr(self.flr.value, "rd") and self.flr.value.rd:
			rdURL = base.makeAbsoluteURL("/browse/%s"%
				self.flr.value.rd.sourceId)
			return T.p(class_="rdbacklink")["Also see the ",
				T.a(href=rdURL)["resources provided by this RD"],
				"."]
		return ""
	
	@template.renderer
	def titlemessage(self, request, tag):
		return tag["%s -- %s"%(
			base.getConfig("web", "sitename"), self.titleMessage)]
	
	@template.renderer
	def footer(self, request, tag):
		contactMail = str(base.getGlobalMeta("contact.email"))
		return tag[
			T.hr,
			T.address[T.a(href="mailto:%s"%contactMail)[contactMail]]]
	
	def render(self, request):
		request.setResponseCode(self.status)
		try:
			request.write(nevowc.flattenSync(self._getDoc(request)))
		except Exception as msg:
			base.ui.notifyError("Error while writing error page: %s"%msg)
			# something went wrong while serialising the error page
			request.write(b"Ouch.  Error while writing the error page.  Sorry.\r\n"
				b"\nPlease complain.\r\n")
		request.finish()
		return server.NOT_DONE_YET
	
	loader = common.doctypedStan(T.html[
		T.head(render="commonhead")[
			T.title(render="titlemessage")],
		T.body[
			T.img(src="/static/img/logo_medium.png", class_="headlinelogo",
				style="position:absolute;right:5pt"),
			T.h1[
				T.transparent(render="titlemessage"),
				" (",
				nevowc.addNevowAttributes(
					T.transparent(render="string"), data="status"),
				")"],
			T.p(render="beforemessage"),
			T.div(class_="errors", render="message"),
			T.div(render="aftermessage"),
			T.div(class_="hint", render="hint")[
				T.p("The error message also has a hint, which may in particular"
					" be useful to the service operator.  Be sure to include it"
					" with your report:")],
			T.transparent(render="footer")]])


class NotFoundPage(ErrorPage):
	handles = svcs.UnknownURI
	status = 404
	titleMessage = "Not Found"
	beforeMessage = ("We're sorry, but the resource you"
		" requested could not be located.")
	afterMessage = [
			T.p["If this message resulted from following a link from ",
				T.strong["within the data center"],
				", you have discovered a bug, and we would be"
				" extremely grateful if you could notify us."],
			T.p["If you got here following an ",
				T.strong["external link"],
				", we would be"
				" grateful for a notification as well.  We will ask the"
				" external operators to fix their links or provide"
				" redirects as appropriate."],
			T.p["In either case, you may find whatever you were looking"
				" for by inspecting our ",
				T.a(href="/")["list of published services"], "."],
			T.p(render="rdlink")]


class NotFoundPageWithFancyMessage(NotFoundPage):
	"""A NotFoundPage with a message that's taken from a piece of stan.
	"""
	handles = None

	def __init__(self, message):
		self.fancyMessage = message
	
	@template.renderer
	def message(self, request, tag):
		return tag[self.fancyMessage]
	
	@template.renderer
	def rdlink(self, request, tag):
		return ""


class OtherNotFoundPage(NotFoundPage):
	handles = base.NotFoundError


class RDNotFoundPage(NotFoundPage):
	handles = base.RDNotFound


class ForbiddenPage(ErrorPage):
	handles = svcs.ForbiddenURI
	status = 403
	titleMessage = "Forbidden"
	beforeMessage = "We're sorry, but the resource you requested is forbidden."
	afterMessage = T.div[
		T.p["This usually means you tried to use a renderer on a service"
			" that does not support it.  If you did not come up with the"
			" URL in question yourself, complain fiercely to the staff of ",
			T.transparent(render="getconfig")["[web]sitename"],
			"."],
		T.p(render="rdlink")]


class RedirectBase(ErrorPage):
	def render(self, request):
		# add request arguments if they are not already included in the
		# URL we're redirecting to:
		self.destURL = utils.bytify(self.flr.value.dest)
		if b'?' not in self.destURL:
			args = urllib.parse.urlparse(request.uri).query
			if args:
				self.destURL = self.destURL+b"?"+utils.bytify(args)
		request.setHeader("location", self.destURL)
		return ErrorPage.render(self, request)
	
	@template.renderer
	def destLink(self, request, tag):
		return tag(href=self.destURL)
	
	afterMessage = T.p["You should not see this page -- either your browser or"
				" our site is broken.  Complain."]


class RedirectPage(RedirectBase):
	handles = svcs.WebRedirect
	status = 301
	titleMessage = "Moved Permanently"
	beforeMessage = ["The resource you requested is available from a ",
				T.a(render="destLink")[
			 		"different URL"],
				"."]


class FoundPage(RedirectBase):
	handles = svcs.Found
	status = 302
	titleMessage = "Found"
	beforeMessage = ["The resource you requested can be found at ",
				T.a(render="destLink")[
			 		T.transparent(render="destLink")],
				"."]


class SeeOtherPage(RedirectBase):
	handles = svcs.SeeOther
	status = 303
	titleMessage = "See Other"
	beforeMessage = ["Please turn to a ",
				T.a(render="destLink")[
			 		"different URL"],
				" to go on."]


class AuthenticatePage(ErrorPage):
	handles = svcs.Authenticate
	status = 401
	titleMessage = "Authentication Required"

	def render(self, request):
		request.setHeader('WWW-Authenticate',
			'Basic realm="%s"'%str(self.flr.value.realm))
		return ErrorPage.render(self, request)
	
	loader = svcs.loadSystemTemplate("unauth.html")


class BadMethodPage(ErrorPage):
	handles = svcs.BadMethod
	status = 405
	titleMessage = "Bad Method"
	beforeMessage = (
		"You just tried to use some HTTP method to access this resource"
		" that this resource does not support.  This probably means that"
		" this resource is for exclusive use for specialized clients.")
	afterMessage = T.p["You may find whatever you were really looking"
				" for by inspecting our ",
				T.a(href="/")["list of published services"],
				"."]


class UploadTooLargePage(ErrorPage):
	handles = svcs.RequestEntityTooLarge
	status = 413
	titleMessage = "Upload too big"


class NotAcceptable(ErrorPage):
	handles = base.DataError
	status = 406
	titleMessage = "Not Acceptable"
	beforeMessage = ("The server cannot generate the data you requested."
				"  The associated message is:")
	afterMessage = ""


class ErrorDisplay(ErrorPage):
	handles = base.ReportableError
	status = 500
	titleMessage = "Error"
	beforeMessage = ("A piece of code failed:")
	afterMessage = [T.p["Problems of this sort usually mean we considered"
		" the possibility of something like this happening; if the above"
		" doesn't give you sufficient hints to fix the problem, please"
		" complain to the address given below."],
		T.p(render="hint")]


class NullPage(ErrorPage):
	handles = _Silence
	def render(self, request):
		pass


# HTML mess for last-resort type error handling.
errorTemplate = (
		'<body><div style="position:fixed;left:4px;top:4px;'
		'visibility:visible;overflow:visible !important;'
		'max-width:600px !important;z-index:500">'
		'<div style="border:2px solid red;'
		'width:400px !important;background:white">'
		'%s'
		'</div></div></body></html>')

def _formatFailure(flr):
	res = errorTemplate%(
		"<h1>Internal Error</h1><p>A(n)"
		" %s exception occurred.  The"
		" accompanying message is: '%s'</p>"
		"<p>If you are seeing this, it is always a bug in our code"
		" or the data descriptions, and we would be extremely grateful"
		" for a report at"
		" %s</p>"%(flr.value.__class__.__name__,
			common.escapeForHTML(flr.getErrorMessage()),
			str(base.getGlobalMeta("contact.email"))))
	return res.encode("ascii", "ignore")


class InternalServerErrorPage(ErrorPage):
	"""A catch-all page served when no other error page seemed responsible.
	"""
	handles = base.Error  # meaningless, no isinstance done here
	status = 500
	titleMessage = "Uncaught Exception"
	beforeMessage = T.p["Your action has caused a(n) ",
				nevowc.addNevowAttributes(
					T.span(render="unicode"), data="excname"),
				" exception to occur.  As additional info, the failing code"
				" gave:"],
	afterMessage = T.p["This is always a bug in our software, and we would really"
				" be grateful for a report to the contact address below,"
				" preferably with a description of what you were trying to do,"
				" including any data pieces if applicable.  Thanks."]

	def data_excname(self, request, tag):
		return self.flr.value.__class__.__name__

	def renderInnerException(self, request):
		"""called when rendering already has started.

		We don't know where we're sitting, so we try to break out as well
		as we can.
		"""
		request.setResponseCode(500)  # probably too late, but log still profits.
		data = _formatFailure(self.flr)
		if isinstance(data, str):
			data = data.encode("utf-8", "ignore")
		request.write(data)
		request.finish()
		return server.NOT_DONE_YET

	def render(self, request):
		base.ui.notifyFailure(self.flr,
			"Returning an internal server error!")
		base.ui.notifyInfo("Arguments of failed request: %s"%
			repr(request.args)[:2000])

		if getattr(self.flr.value, "hint", None):
			base.ui.notifyDebug("Exception hint: %s"%self.flr.value.hint)

		# Hack to work around excessive chattiness of t.w.FlattenerError
		# (for something in an error page)
		if hasattr(self.flr.value, "_roots"):
			self.flr.value._roots = ''
			self.flr.value._traceback = ''

		if getattr(request, "startedWriting", False):
			# exception happened while rendering a page.
			return self.renderInnerException(request)
		else:
			return ErrorPage.render(self, request)


getErrorPage = utils.buildClassResolver(
	baseClass=ErrorPage,
	objects=list(globals().values()),
	instances=False,
	key=lambda obj: obj.handles,
	default=InternalServerErrorPage)


def getDCErrorPage(flr):
	"""returns a t.w resource for a twisted failure.
	"""
	if flr is None:
		flr = failure.Failure()
	return getErrorPage(flr.value.__class__)(flr)


def renderDCErrorPage(flr, request):
	"""renders a resource a twisted failure.

	This finishes request itself.  It returns t.w.server.NOT_DONE_YET
	because of that, so you can write return renderDCErrorPage from
	a render method (or similar).
	"""
	if getattr(request, "channel", "just testing") is None:
		# the remote end has hung up; let's forget things
		return
	res = getDCErrorPage(flr)
	return res.render(request)

common.produceErrorDocument = renderDCErrorPage
