"""
DC administration interface.
"""

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

from gavo import base
from gavo import utils
from gavo import rscdef
from gavo import rscdesc  #noflake: for cache registration
from gavo.protocols import creds
from gavo.protocols import uws
from gavo.utils import Arg, exposedFunction, makeCLIParser


@exposedFunction([
	Arg("user", help="the user name"),
	Arg("password", help="a password for the user"),
	Arg("remarks", help="optional remarks", 
		default="", nargs='?')],
	help="add a user/password pair and a matching group to the DC server")
def adduser(querier, args):
	try:
		creds.addUser(querier.connection, args.user, args.password, args.remarks)
	except base.IntegrityError:
		raise base.ui.logOldExc(base.ReportableError("User %s already exists."
			"  Use 'changeuser' command to edit."%args.user))


@exposedFunction([
	Arg("user", help="the user name to remove")],
	help="remove a user from the DC server")
def deluser(querier, args):
	rowsAffected = creds.delUser(querier.connection, args.user)
	if not rowsAffected:
		sys.stderr.write("Warning: No rows deleted while deleting user %s\n"%
			args.user)


@exposedFunction([
	Arg("user", help="the user name"),
	Arg("password", help="a password for the user"),
	Arg("remarks", help="optional remarks", 
		default="", nargs='?')],
	help="change remarks and/or password for a DC user")
def changeuser(querier, args):
	creds.changeUser(querier.connection, 
		args.user, args.password, args.remarks)


@exposedFunction([
	Arg("user", help="a user name"),
	Arg("group", help="the group to add the user to")],
	help="add a user to a group")
def addtogroup(querier, args):
	try:
		creds.addToGroup(querier.connection, args.user, args.group)
	except Exception:
		raise base.ui.logOldExc(base.ReportableError(
			"User %s does not exist."%args.user))


@exposedFunction([
	Arg("user", help="a user name"),
	Arg("group", help="the group to remove the user from")],
	help="remove a user from a group")
def delfromgroup(querier, args):
	if not creds.removeFromGroup(querier.connection, args.user, args.group):
		sys.stderr.write("Warning: No rows deleted while deleting user"
			" %s from group %s\n"%(args.user, args.group))


@exposedFunction(help="list users known to the DC")
def listusers(querier, args):
	data = list(querier.connection.query("SELECT username, groupname, remarks"
		" FROM dc.users NATURAL JOIN dc.groups ORDER BY username"))
	curUser = None
	for user, group, remark in data:
		if user!=curUser:
			print("\n%s (%s) --"%(user, remark), end=' ')
			curUser = user
		print(group, end=' ')
	print()


@exposedFunction([
	Arg("-f", help="also remove all jobs in ERROR and ABORTED states (only use"
		" if you are sure what you are doing).", action="store_true",
		dest="includeFailed"),
	Arg("-p", help="also remove all jobs in PENDING states (only use"
		" if you are sure what you are doing).", action="store_true",
		dest="includeForgotten"),
	Arg("--all", help="remove all jobs (this is extremely unfriendly."
		"  Don't use this on public UWSes)", action="store_true",
		dest="includeAll"),
	Arg("--nuke-completed", help="also remove COMPLETEd jobs (this is"
		" unfriendly.  Don't do this on public UWSes).", action="store_true",
		dest="includeCompleted"),],
	help="remove expired UWS jobs")
def cleantap(querier, args):
	from gavo.protocols import tap
	tap.WORKER_SYSTEM.cleanupJobsTable(includeFailed=args.includeFailed,
		includeCompleted=args.includeCompleted,
		includeAll=args.includeAll,
		includeForgotten=args.includeForgotten)


@exposedFunction([
	Arg("jobId", help="id of the job to abort"),
	Arg("helpMsg", help="A helpful message to add to the abort message")],
	help="manually abort a TAP job and send some message to a user")
def tapabort(querier, args):
	from gavo.protocols import tap

	tap.WORKER_SYSTEM.changeToPhase(args.jobId, uws.ERROR, 
			"Job aborted by an administrator, probably because the query\n"
			" should be written differently to be less of a resource hog.\n"
			"  Here's what the administrator had to say:\n\n"+args.helpMsg+
			"\n\nIf you have further questions, just send a mail to "+
			base.getMetaText(base.caches.getRD("//tap").getById("run"), 
				"contact.email"))


@exposedFunction([Arg(help="rd#table-id of the table containing the"
	" products that should get cached previews", dest="tableId"),
	Arg("-w", type=str,
		help="width to compute the preview for", dest="width", default="200"),],
	help="Precompute previews for the product interface columns in a table.")
def cacheprev(querier, args):
	from gavo.protocols.products import PreviewCacheManager, getProductForRAccref
	from twisted.internet import reactor

	td = base.resolveId(None, args.tableId)
	rows = querier.queryToDicts(
		td.getSimpleQuery(["accref", "mime"]))

	def runNext(token):
		try:
			row = next(rows)
			res = PreviewCacheManager.getPreviewFor(
				getProductForRAccref(row["accref"]))

			if getattr(res, "result", None): # don't add a callback on a 
					# fired deferred or you'll exhaust the stack
				reactor.callLater(0.1, runNext, "continue")
			else:
				res.addCallback(runNext)
			return res
		except StopIteration:
			pass
		except:
			import traceback
			traceback.print_exc()
		reactor.stop()
		return ""

	reactor.callLater(0, runNext, "startup")
	reactor.run()


@exposedFunction([Arg(help="rd#table-id of the table to look at",
	dest="tableId")],
	help="Make suggestions for UCDs of columns not having one (based"
	" on their descriptions; this uses a GAVO web service).")
def suggestucds(querier, args):
	from gavo import api
	from gavo import votable
	
	apiURL = "http://dc.zah.uni-heidelberg.de/ucds/ui/ui/api"

	def getMatches(description):
		res = utils.urlopenRemote(apiURL, {"description": description})
		data, metadata = votable.load(res)
		if metadata:
			return list(metadata.iterDicts(data))
		else:
			return []
	
	td = api.getReferencedElement(args.tableId, forceType=api.TableDef)
	for col in td:
		if (not col.ucd or col.ucd=="XXX") and col.description:
			try:
				res = [(row["score"], row["ucd"]) 
					for row in getMatches(col.description)]
				res.sort()
				res.reverse()
				print(col.name)
				for score, ucd in res:
					print("  ", ucd)
			except IOError:
				# remote failure, guess it's "no matches" (TODO: distinguish)
				pass
			

@exposedFunction([Arg(help="rd#table-id of the table of interest", 
	dest="tableId")],
	help="Show the statements to create the indices on a table.")
def indexStatements(querier, args):
	import re
	td = base.resolveId(None, args.tableId)
	for ind in td.indices:
		print("\n".join(re.sub(r"\s+", " ", s) for s in ind.iterCode()))


@exposedFunction([Arg(help="rd#exec-id of the execute element to run (note:"
		" the title won't work, you have to give the thing an id to use adm exec).",
	dest="execId")],
	help="Execute the contents of an RD execute element.  You must"
	" give that element an explicit id in order to make this work.")
def execute(querier, args):
	from gavo.user import logui
	logui.LoggingUI(base.ui)
	execEl = base.resolveCrossId(args.execId, rscdef.Execute)
	execEl.callable().join()


@exposedFunction([Arg(help="Package resource path"
	" (like '/inputs/__system__/scs.rd); for system RDs, the special"
	" //rd-id syntax is supported.",
	dest="path")],
	help="Dump the source of a distribution file; this is useful when you want"
	" to override them and you are running DaCHS from a zipped egg")
def dumpDF(querier, args):
	import pkg_resources
	if args.path.startswith("//"):
		args.path = "inputs/__system__"+args.path[1:]+".rd"
	try:
		with pkg_resources.resource_stream('gavo', "resources/"+args.path) as f:
			sys.stdout.buffer.write(f.read())
	except FileNotFoundError:
		raise base.ReportableError("No such distribution file: %s\n"%args.path)


@exposedFunction([Arg(help="XML file", dest="path", nargs='+')],
	help="Validate a file against built-in VO schemas and with built-in"
		" schema validator.")
def xsdValidate(querier, args):
	from gavo.helpers import testtricks
	rtval = 0

	for path in args.path:
		print(path, end=" -- ")
		try:
			with open(path, "rb") as f:
				msgs = testtricks.getXSDErrors(f.read())
		except Exception as ex:
			msgs = [str(ex)]
		if not msgs:
			print("valid")
		else:
			print(msgs)
			rtval = 1

	return rtval


@exposedFunction([Arg(help="IVOID to mark as deleted", dest="ivoid")],
	help="Add a registry entry for a deleted record with IVOID.  This"
		" should only be necessary if you accidentally manually removed"
		" records from your dc.resources table.")
def makedelrec(querier, args):
	from gavo import registry

	authority, path = registry.parseIdentifier(args.ivoid)
	if authority not in registry.getManagedAuthorities():
		raise base.ReportableError("You can only declare ivo ids from your"
			" own authority as deleted.")

	registry.makeDeletedRecord(args.ivoid, querier.connection)


@exposedFunction([], help="Update the TAP_SCHEMA metadata for all"
	" RDs mentioned in TAP_SCHEMA.")
def updateTAPSchema(querier, args):
	from gavo.protocols import tap
	for rdId, in querier.connection.query(
			"select sourcerd from TAP_SCHEMA.tables"):
		try:
			rd = base.caches.getRD(rdId)
			tap.publishToTAP(rd, querier.connection)
		except base.NotFoundError as msg:
			base.ui.notifyWarning("Stale records in TAP_SCHEMA: %s"%msg)


@exposedFunction([Arg(help="Password to hash", dest="pw")], 
	help="Hash a password (typically for [web]adminpasswd)")
def hashPasswd(querier, args):
	from gavo.protocols import creds
	print(creds.hashPassword(args.pw))


def main():
	with base.AdhocQuerier(lambda: base.getDBConnection("feed")) as querier:
		args = makeCLIParser(globals()).parse_args()
		retval = args.subAction(querier, args) or 0
	
	sys.exit(retval)
