"""
A renderer that allows fancy uploads to the lightmeter database.

The main trick here is that entries in the stations table translate
into upload URLs.
"""

import grp
import gzip
import io
import os
import re
import zipfile

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

from gavo import api
from gavo import formal
from gavo import svcs
from gavo import web


def compress(data, fName):
	compr = io.BytesIO()
	g = gzip.GzipFile(fName, "wb", 5, compr)
	g.write(data)
	g.close()
	return compr.getvalue()


class UploadPage(formal.ResourceWithForm, web.ServiceBasedPage):
	name = "custom"

	def __init__(self, request, service, stationRecord):
		super(UploadPage, self).__init__(request, service)
		self.uploadedName = None
		self.stationRecord = stationRecord

	def crash(self, failure, request):
		api.ui.notifyFailure(failure, "In lightmeter upload")
		formal.ResourceWithForm.crash(self, failure, request)

	def submitAction(self, request, form, data):
		srcName, srcFile = data["inFile"]
		action = self._writeData
		if data.get("remove", False):  # hack for tests, not to be published.
			action = self._removeOne
		return threads.deferToThread(action, srcName, srcFile
			).addCallback(self._reDisplay, request
			).addErrback(self._flagError, request)

	def _computeFullPath(self, srcName):
		uploadPath = os.path.join(self.service.rd.resdir,
			self.service.rd.getProperty("uploadDir"),
			self.stationRecord["stationId"])
		if not os.path.exists(uploadPath):
			os.makedirs(uploadPath)
			os.chmod(uploadPath, 0o770)
			os.chown(uploadPath, -1, grp.getgrnam("gavo")[2])
		srcName = re.sub(r".*\\", "",
			os.path.basename(srcName))
		return os.path.join(uploadPath, srcName)

	_singleFileExtensions = set([".txt", ".csv", ".txt.gz", ".csv.gz",
		"skyglow.gz"])
	_archiveExtensions = set([".zip"])

	def _writeData(self, srcName, srcFile):
		for ext in self._singleFileExtensions:
			if srcName.endswith(ext):
				return self._writeDataSingleFile(srcName, srcFile)
		for ext in self._archiveExtensions:
			if srcName.endswith(ext):
				return self._writeDataArchive(srcName, srcFile)
		raise api.ValidationError("%s is not a valid upload name since"
			" it does not end with a recognized extenion (one of %s)"%(
				srcName,
				", ".join(self._singleFileExtensions|self._archiveExtensions)),
			colName="inFile")

	def _writeDataArchiveReal(self, srcName, srcFile):
		dupes, cumSize = [], 0
		archive = zipfile.ZipFile(srcFile, "r")
		for info in archive.infolist():
			try:
				self._writeDataSingleFile(info.filename,
					io.BytesIO(archive.read(info.filename)))
				cumSize += info.file_size
			except api.ValidationError:
				dupes.append(info.filename)
		if dupes:
			raise api.ValidationError("The following files could not be"
				" written: %s; presumably the were already uploaded."%', '.join(dupes),
				colName="inFile")
		self.uploadedName = srcName
		self.uploadedSize = cumSize

	def _writeDataArchive(self, srcName, srcFile):
		try:
			self._writeDataArchiveReal(srcName, srcFile)
		except zipfile.error:
			raise api.ValidationError("Something was wrong with the zip file: %s"%
				str(zipfile.error), colName="inFile")

	def _writeDataSingleFile(self, srcName, srcFile):
		content = srcFile.read()
		if not srcName.endswith(".gz"):
			content = compress(content.replace(b"\r", b""), srcName)
			srcName = srcName+".gz"

		targetName = self._computeFullPath(srcName)
		if os.path.exists(targetName):
			raise api.ValidationError("File %s already exists.  If you want to"
				" overwrite it, contact the operators"%srcName, colName="inFile")
		with open(targetName, "wb") as f:
			f.write(content)
		self.uploadedName = srcName
		self.uploadedSize = os.path.getsize(targetName)

	def _reDisplay(self, result, request):
		args = request.strargs
		if "__nevow_form__" in args:
			del args["__nevow_form__"]
		return self

	def _flagError(self, failure, request):
		failure.printTraceback()
		if isinstance(failure.value, (formal.FormError, formal.FieldError)):
			self.uploadform.errors.add(failure.value)
		else:
			self.uploadform.errors.add(formal.FormError(str(failure.value)))
		return self

	@template.renderer
	def title(self, request, tag):
		return tag["Upload data from %s"%self.stationRecord["stationId"]]

	@template.renderer
	def location(self, request, tag):
		return tag["%s (%s, %s)"%(self.stationRecord["fullname"],
			self.stationRecord["long"], self.stationRecord["lat"])]

	@template.renderer
	def uploadinfo(self, request, tag):
		if self.uploadedName is None:
			return ""
		elif self.uploadedSize<0:
			return tag["File %s removed."%self.uploadedName]
		else:
			return tag["File %s uploaded, %d bytes."%(
				self.uploadedName, self.uploadedSize)]

	def form_upload(self, request):
		# an attribute since that makes error handling more convenient.
		self.uploadform = formal.Form()
		self.uploadform.addField("inFile", formal.File(required=True),
			# broken, not worth fixing. formal.FileUploadWidget,
			label="Source File", description="Lightmeter data file.")
		self.uploadform.addField("remove", formal.Boolean(missing=False),
			label="Remove this file", widgetFactory=formal.Hidden)
		self.uploadform.addAction(self.submitAction, label="Upload")
		return self.uploadform

	def _removeOne(self, fName, ignored):
		"""removes a file; this is an internal hack for testing.

		This can't handle archives, etc.  For users, deletions should
		be done by the admins.
		"""
		if not fName.endswith(".gz"):
			fName = fName+".gz"
		destPath = self._computeFullPath(fName)
		if not os.path.exists(destPath):
			raise api.ValidationError("File '%s' does not exist and thus cannot"
				" be deleted."%fName, colName="inFile")
		os.unlink(destPath)
		self.uploadedSize = -1 # hack, see render_uploadinfo
		self.uploadedName = fName

	loader = template.TagLoader(T.html[
		T.head[
			T.title(render="title"),
			T.transparent(render="commonhead"),
		],
		T.body(render="withsidebar")[
			T.h1(render="title"),
			T.p(render="uploadinfo"),
			T.p["Upload data from ",
				T.transparent(render="location"),
				".  The data format is inferred from the extension; legal values"
				" are .csv for CSV data and .txt for Skysensor format, plus"
				" their gzipped variants with a .gz extension added."],
			T.p["You can also upload zip archives containing either text"
				" or CSV data"],
			T.transparent(render="form upload"),
		]
	])


class MainPage(web.ServiceBasedPage):
	name = "custom"
	_rdId = "lightmeter/q"
	_accTableId = "accesskeys"
	_statTableId = "stations"

	@classmethod
	def isBrowseable(cls, service):
		return True

	def getChild(self, name, request):
		segments = request.popSegments(name)

		if len(segments)!=2:
			raise svcs.UnknownURI("Can't upload to there")

		accTd = api.getRD(self._rdId).getById(self._accTableId)
		statTd = api.getRD(self._rdId).getById(self._statTableId)

		with api.getTableConn() as conn:
			accTable = api.TableForDef(accTd, connection=conn)
			data = list(accTable.iterQuery(accTd,
				"stationId=%(stationId)s AND accessKey=%(accessKey)s",
				{"stationId": segments[0], "accessKey": segments[1]}))
			if len(data)!=1:
				raise svcs.UnknownURI("No such upload facility.  Probably either your"
					" access key or your stationId is wrong.  If you cannot find"
					" anything wrong, please contact the administrators.")
		
			statTable = api.TableForDef(statTd, connection=conn)
			data = list(statTable.iterQuery(statTd,
				"stationId=%(stationId)s", {"stationId": segments[0]}))
		return UploadPage(request, self.service, data[0])
	
	loader = template.TagLoader(T.html[
		T.head[
			T.title["Lightmeter Upload Facility"],
			T.transparent(render="commonhead"),
		],
		T.body(render="withsidebar")[
			T.h1["Station ID required"],
			T.p["To upload data from your lightmeter, use the upload URL"
				" supplied on registration or ask the operators.",]
		]
	])
