"""
The SSAP core and supporting code.

"""

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

from gavo import base
from gavo import rsc
from gavo import rscdef
from gavo import svcs
from gavo.protocols import datalink
from gavo.svcs import outputdef


MS = base.makeStruct


class SSADescriptor(datalink.ProductDescriptor):
	"""SSA descriptors have ssaRow and limits attributes.

	These both reference SSA results.  ssaRow is the first result of
	the query, which also provides the accref.  limits is a table.Limits
	instance for the total result set.

	Warning: limits will be None if this is constructed with fromSSARow.
	"""
	ssaRow = None

	@classmethod
	def fromSSARow(cls, ssaRow, paramDict):
		"""returns a descriptor from a row in an ssa table and
		the params of that table.

		Don't use this; the limits attribute will be {} for these.
		"""
		paramDict.update(ssaRow)
		ssaRow = paramDict
		res = cls.fromAccref(ssaRow["ssa_pubDID"], ssaRow['accref'])
		res.ssaRow = ssaRow
		res.pubDID = ssaRow["ssa_pubDID"]
		res.limits = {}
		return res
	
	@classmethod
	def fromSSAResult(cls, ssaResult):
		"""returns a descriptor from an SSA query result (an InMemoryTable
		instance).
		"""
		if not ssaResult.rows:
			raise base.NotFoundError("any", "row", "ssa result for SODA block"
				" generation")
		res = cls.fromSSARow(ssaResult.rows[0], ssaResult.getParamDict())
		res.limits = ssaResult.getLimits()
		res.pubDID = None
		return res


def _combineRowIntoOne(ssaRows):
	"""makes a "total row" from ssaRows.

	In the resulting row, minima and maxima are representative of the
	whole result set, and enumerated columns are set-valued.

	This is useful when generating parameter metadata.
	"""
	# XXX TODO: shouldn't this be made more generic by using something like
	# in user.info?
	if not ssaRows:
		raise base.ReportableError("Datalink meta needs at least one result row")

	totalRow = ssaRows[0].copy()
	totalRow["mime"] = set([totalRow["mime"]])
	calibs = set()

	for row in ssaRows[1:]:
		if row["ssa_specstart"]<totalRow["ssa_specstart"]:
			totalRow["ssa_specstart"] = row["ssa_specstart"]
		if row["ssa_specend"]>totalRow["ssa_specend"]:
			totalRow["ssa_specend"] = row["ssa_specend"]
		totalRow["mime"].add(row["mime"])
		calibs.add(row.get("ssa_fluxcalib", None))
	
	totalRow["collect_calibs"] = set(c for c in calibs if c is not None)
	return totalRow


def getDatalinkCore(dlSvc, ssaTable):
	"""returns a datalink core adapted for ssaTable.

	dlSvc is the datalink service, ssaTable a non-empty SSA result table.
	"""
	allowedRendsForStealing = ["dlget"] #noflake: for stealVar downstack
	desc = SSADescriptor.fromSSAResult(ssaTable)
	return dlSvc.core.adaptForDescriptors(
		svcs.getRenderer("dlget"), [desc], None)


class SSAPCore(svcs.DBCore):
	"""A core doing SSAP queries.

	This core knows about metadata queries, version negotiation, and
	dispatches on REQUEST.  Thus, it may return formatted XML data
	under certain circumstances.

	Interpreted Properties:

	* previews: If set to "auto", the core will automatically add a preview
	  column and fill it with the URL of the products-based preview.  Other
	  values are not defined.
	"""
	name_ = "ssapCore"

	outputTableXML = """
		<outputTable verbLevel="30">
			<property name="virtual">True</property>
			<FEED source="//ssap#coreOutputAdditionals"/>
		</outputTable>"""

	previewColumn = base.parseFromString(svcs.OutputField,
		'<outputField name="preview" type="text"'
		' ucd="meta.ref.url;meta.preview" tablehead="Preview"'
		' description="URL of a preview for the dataset"'
		' select="NULL" displayHint="type=product" verbLevel="15"/>')


	############### Implementation of the service operations

	def onElementComplete(self):
		super().onElementComplete()
		if self.getProperty("previews", default=False)=="auto":
			self.outputTable.columns.append(self.previewColumn)

	def _run_getTargetNames(self, service, inputTable, queryMeta):
		with base.getTableConn()  as conn:
			table = rsc.TableForDef(self.queriedTable, create=False,
				connection=conn)
			destTD = base.makeStruct(outputdef.OutputTableDef,
				parent_=self.queriedTable.parent,
				id="result", onDisk=False,
				columns=[self.queriedTable.getColumnByName("ssa_targname")])
			res = table.getTableForQuery(destTD, "", distinct=True)
			res.noPostprocess = True
			return res
	
	def _addPreviewLinks(self, resultTable):
		for row in resultTable:
			if "preview" in row and row["preview"] is None:
				row["preview"] = row["accref"]+"?preview=True"

	def _run_queryData(self, service, inputTable, queryMeta):
		limitThroughTOP = inputTable.getParam("TOP")
		if limitThroughTOP and limitThroughTOP<queryMeta["dbLimit"]:
			queryMeta["dbLimit"] = limitThroughTOP

		res = svcs.DBCore.run(self, service, inputTable, queryMeta)
		if self.getProperty("previews", default=False)=="auto":
			self._addPreviewLinks(res)

		if service.hasProperty("datalink"):
			# backwards compatibility: The datalink property on the service
			# can name a datalink service.  We don't want that any more, but
			# for now copy it to the queried table (where it should be)
			# XXX Deprecate around version 1.3
			try:
				res.getMeta("_associatedDatalinkService", raiseOnFail=True)
			except base.NoMetaKey:
				# No datalink metadata on the table yet, generate one from the property.
				res.addMeta(
					"_associatedDatalinkService.serviceId",
					service.getProperty("datalink"))
				res.addMeta(
					"_associatedDatalinkService.idColumn",
					"ssa_pubDID")

		return res

	################ the main dispatcher

	def run(self, service, inputTable, queryMeta):
		defaultRequest = service.getProperty("defaultRequest", "")
		requestType = (inputTable.getParam("REQUEST") or defaultRequest).upper()

		if requestType=="QUERYDATA":
			return self._run_queryData(service, inputTable, queryMeta)

		elif requestType=="GETTARGETNAMES":
			return self._run_getTargetNames(service, inputTable, queryMeta)

		else:
			raise base.ValidationError("Missing or invalid value for REQUEST.",
				"REQUEST",
				hint="According to SSAP, you can only have queryData here;"
					" as an extension, this service also understands getTargetNames.")


_VIEW_COLUMNS_CACHE = []

def iterViewColumns(context):
	"""returns a list of column objects for building the SSA view
	mixin's columns.

	The argument is the DaCHS RD parse context.

	This is probably only useful for the //ssap#view mixin.  The argument
	is that mixin's context.  This could go if we drop the hcd and mixc
	mixins and instead have a normal STREAM with the columns, as it's really
	only necessary to make columns from the stupid params and remove their
	defaults.

	This will always return the same column objects -- don't change them.
	"""
	if not _VIEW_COLUMNS_CACHE:
		dontCopyAtts = frozenset(["value", "content_"])
		protoTable = context.getById("instance")

		_VIEW_COLUMNS_CACHE.append([MS(rscdef.Column,
			**c.getCopyableAttributes(ignoreKeys=dontCopyAtts))
			for c in itertools.chain(
				protoTable.columns, protoTable.params)])

	return _VIEW_COLUMNS_CACHE[0]
