"""
DC administration interface.
"""

#c Copyright 2008-2025, 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 pathlib
import re
import sys

from gavo import base
from gavo import utils
from gavo import rscdef
from gavo import rscdesc  #noflake: for cache registration
from gavo import svcs
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, data={"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"], row["is_valid"])
					for row in getMatches(col.description)]
				res.sort()
				res.reverse()
				print(col.name)
				for score, ucd, is_valid in res:
					if is_valid:
						print("  ", ucd)
					else:
						print(f"   (invalid: {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 = rscdef.getReferencedElement(args.tableId, forceType=rscdef.TableDef)
	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 = rscdef.getReferencedElement(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 (deprecated; use"
	" dachs imp //tap refresh instead)")
def updateTAPSchema(querier, args):
	from gavo import rsc
	rsc.makeData(
		base.resolveCrossId("//tap#refresh"),
		connection=querier.connection)


@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 _getConstantPrefix(path):
	"""returns the segments of path without a wildcard.
	"""
	segs = []
	for seg in path.split("/"):
		if "*" in seg or "?" in seg:
			break
		segs.append(seg)
	return "/".join(segs)


@exposedFunction([Arg(help="Id of a data element importing what you want to"
		" turn into a HiPS", dest="dataId"),
		Arg(help="Minimal Order to generate (0 is full sky, 4 is an area about"
			" 4 degrees in diameter)", dest="minOrder")],
	help="Write a Hipsgen parameter file to stdout")
def hipsgen(querier, args):
	data = rscdef.getReferencedElement(args.dataId,
		forceType=rscdef.DataDescriptor)

	for pat in data.sources.patterns:
		print("in={}".format(_getConstantPrefix(pat)))
	
	print("minOrder={}".format(args.minOrder))

	for m in data.rd.iterMeta("creator.name"):
		print("creator: {}".format(m.getContent("text")))
		break

	# now find some publication we can piggyback on; we'll take the
	# first we find.
	from gavo.registry import publication
	for dest, rec in publication.RDRscRecGrammar(None).parse(data.rd):
		if dest=="resources":
			print("id={}".format(rec["ivoid"]))
			break

	print("status=public clonable")
	print("title={}".format(base.getMetaText(data, "title")))
	print("out=hips")


def _normalizeWhitespace(s):
	return re.sub(r"\s+", " ", s)


def _getBoundsFromIntervals(intervals):
	return (min((i[0] for i in intervals), default=None),
		max((i[1] for i in intervals), default=None))


def _getHipsFillers(svc):
	"""returns a dictionary of HiPS propertes keys to values fillable
	from svcs.
	"""
	res = {}

	for metaKey, hipsKey in [
			("description", "obs_description"),
			("creator.name", "hips_creator"),
			("source", "bib_reference"),
			("rights", "obs_copyright"),
			("coverage.waveband", "obs_regime"),
			("rights.rightsURI", "obs_copyright_url"),]:
		for m in svc.rd.iterMeta(metaKey):
			res[hipsKey] = _normalizeWhitespace(m.getContent(
				"text", macroPackage=svc.rd))
			break

	res["hips_service_url"] = svc.getURL("hips")

	minTime, maxTime = _getBoundsFromIntervals(svc.rd.coverage.temporal)
	minE, maxE = _getBoundsFromIntervals(svc.rd.coverage.spectral)
	if minE:
		maxLambda = base.PLANCK_H*base.LIGHT_C/minE
	if maxE:
		minLambda = base.PLANCK_H*base.LIGHT_C/maxE

	for hipsKey, val in [
			("t_min", minTime),
			("t_max", maxTime),
			("em_min", minLambda),
			("em_max", maxLambda)]:
		if val is not None:
			res[hipsKey] = val
	
	return res


def _editProp(propLn, fillers):
	"""edits a hips properties template line with fillers if appropriate.

	What we edit is lines commented out with keys present in fillers.
	Unedited lines are returned stripped but otherwise unchanged.
	"""
	propLn = propLn.strip()
	mat = re.match(r"(#?)(\w+)\s+=\s+(.*)", propLn)
	if not mat:
		raise base.ReportableError(f"Invalid properties line: {propLn}")
	
	isTemplate, key, value = mat.groups()
	if isTemplate and key in fillers:
		propLn = "{:21s}= {}".format(key, fillers[key])
	
	return propLn


@exposedFunction([Arg(help="Reference to the HiPS-serving service",
		dest="svcId"),
	Arg("-d", "--hipsdir", help="Directory the HiPS is stored in",
		type=pathlib.Path, dest="hipsDir", default="hips")],
	help="Fill out the templated elements in hipsDir's properties file.")
def hipsfill(querier, args):
	svc = rscdef.getReferencedElement(args.svcId,
		forceType=svcs.Service)
	fillers = _getHipsFillers(svc)

	propFile = args.hipsDir/"properties"
	newLines = []
	with open(propFile, "r", encoding="utf-8") as f:
		for ln in f:
			newLines.append(_editProp(ln, fillers))
	
	newContent = "\n".join(newLines).encode("utf-8")+b"\n"
	with open(propFile, "wb") as f:
		f.write(newContent)


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)
