"""
Definition of DC config options and their management including I/O.

Importing this reads the configuration, which influences almost everything
in DaCHS, including the way certain items are constructed.  Hence, this
must be imported as early as humanly possible, in particular before
anything trying to talk to the database runs, because this is where
the database access profiles (well: the DSNs) are read.
"""

#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 functools
import io
import os
import re
import shlex
import sys
from configparser import NoOptionError

# You can't import base or anything from base here, as that would
# import sqlsupport, and sqlsupport needs config initialised to
# even come up.

from gavo import utils
from gavo.utils import fancyconfig
from gavo.utils.fancyconfig import (StringConfigItem, #noflake: exported names
	EnumeratedConfigItem, IntConfigItem, PathConfigItem, ListConfigItem,
	BooleanConfigItem, Section, DefaultSection, MagicSection,
	PathRelativeConfigItem, ParseError, SetConfigItem, ExpandedPathConfigItem)

from gavo.utils.dachstypes import (Any, Callable, Dict, IO, List, NoReturn,
	Optional, TextIO, Union, cast)

fancyconfig.BAD_CONFIG_ITEM_JUST_WARNS = True

defaultSettingsPath = "/etc/gavo.rc"


class RootRelativeConfigItem(PathRelativeConfigItem):
	baseKey = "rootDir"
	typedesc = "path relative to rootDir"


class WebRelativeConfigItem(PathRelativeConfigItem):
	baseKey = "webDir"
	typedesc = "path relative to webDir"


class RelativeURL(StringConfigItem):
	"""A configuration item that is interpreted relative to
	the server's root URL.
	"""

	_value = ""
	typedesc = "URL fragment relative to the server's root"

	def _getValue(self) -> str:
		if (self._value.startswith("http://")
				or self._value.startswith("https://")
				or self._value.startswith("/")):
			return self._value
		if self.parent is not None:
			return self.parent.get("web", "nevowRoot")+self._value
		raise NotImplementedError("Unparented RelativeURL?")

	def _setValue(self, val: str) -> None:
		self._value = val
	
	value = property(_getValue, _setValue)


class EatTrailingSlashesItem(StringConfigItem):
	"""is a config item that must not end with a slash.  A trailing slash
	on input is removed.
	"""

	typedesc = "path fragment"

	def _parse(self, val: str) -> str:
		return StringConfigItem._parse(self, val).rstrip("/")


class EnsureTrailingSlashesItem(StringConfigItem):
	"""A config item that must end with a slash.  If no slash is present
	on input, it is added.
	"""

	typedesc = "path fragment"

	def _parse(self, val: str) -> str:
		val = StringConfigItem._parse(self, val)
		if val is not None and not val.endswith("/"):
			val = val+"/"
		return val


class AuthorityConfigItem(StringConfigItem):
	"""an IVOA Identifers-compatible authority.
	"""
	def _parse(self, val: str) -> str:
		val = StringConfigItem._parse(self, val)
		authorityRE = "[a-zA-Z0-9][a-zA-Z0-9._~-]{2,}$"
		if not re.match(authorityRE, val):
			raise fancyconfig.BadConfigValue("[ivoa]authority must match %s"
				" ('more than three normal characters')"%authorityRE)
		return val


class Error(utils.Error):
	pass

class ProfileParseError(Error):
	pass


class DBProfile:
	"""A parsed form of the postgres connection string.
	"""
	profileName = "anonymous"
	name = None
	host = ""
	port = None
	database = ""
	user = ""
	password = ""
	sslmode = "allow"  # or disable, allow, prefer, require, verify-*)")

	def __init__(self, **kwargs: Any) -> None:
		for k, v in kwargs.items():
			setattr(self, k, v)

	def getArgs(self) -> Dict[str, str]:
		"""returns a dictionary suitable as keyword arguments to psycopg2's
		connect.
		"""
		res = {}
		for key in ["database", "user", "password", "host", "port", "sslmode"]:
			if getattr(self, key):
				res[key] = getattr(self, key)
		if list(res.keys())==["sslmode"]:
			raise utils.logOldExc(utils.StructureError("Insufficient information"
			" to connect to the database in profile '%s'."%(
				self.profileName)))
		return res

	@property
	def roleName(self) -> str:
		"""returns the database role used by this profile.

		This normally is user, but in the special case of the empty user,
		we return the logged users' name.
		"""
		if self.user:
			return self.user
		else:
			try:
				return os.getlogin()
			except os.error:
				raise utils.ReportableError("Profiles without user= are only"
					" allowed when DaCHS is called interactively.")


_StateFunction = Callable[[str], Callable[[str], Callable]]

class ProfileParser:
	r"""A parser for database access profiles.

	The profiles are specified in simple text files that have a shell-like
	syntax.  Each line either contains an assignment (x=y) or is of the
	form ``<command> <arg>*``.  Recognized commands include:

		- include f -- read instructions from file f, searched along profilePath

	>>> p = ProfileParser()
	>>> p.parse(None, "x", "host=foo.bar\n").host
	'foo.bar'
	>>> p.parse(None, "x", "") is not None
	True
	>>> p.parse(None, "x", "host=\n").host
	''
	>>> p.parse(None, "x", "=bla\n")
	Traceback (most recent call last):
	gavo.base.config.ProfileParseError: "x", line 1: invalid identifier '='
	>>> p.parse(None, "x", "host=bla")
	Traceback (most recent call last):
	gavo.base.config.ProfileParseError: "x", line 1: unexpected end of file (missing line feed?)
	"""
	profileKeys = set(["host", "port", "database", "user", "password",
		"sslmode"])

	def __init__(self, sourcePath: List[str] = ["."]) -> None:
		self.commands = {
			"include": self._state_include,
		}
		self.sourcePath = sourcePath
	
	def parse(self,
			profileName: Optional[str],
			sourceName: Optional[str],
			stream: Optional[Union[str, IO]] = None) -> DBProfile:
		"""parses a profile input and returns a new DBProfile instance.

		You can pass in a stream (a file open for reading or a string containing
		profile material, but that's really mainly test instrumentation.
		Usually, you will instead pass in sourceName, which is then searched
		in the sourcePath.

		stream, if an open file, will be closed by the function.

		The parsing itself works by state function returning the next state
		function; each state function is a Callable accepting a token and
		returning a new state function.
		"""
		self.tokenStack: List[str] = []
		self.stateFun: _StateFunction = self._state_init
		if stream is None:
			if sourceName is None:
				raise NotImplementedError(
					"parse needs either a sourceName or a stream")
			else:
				sourceName = self._resolveSource(sourceName)
			stream = open(sourceName)
		elif isinstance(stream, str):
			stream = io.StringIO(stream)

		stream = cast(TextIO, stream)
		self.parser = shlex.shlex(stream, sourceName, posix=True)
		self.parser.whitespace = " \t\r"
		self.profile = DBProfile(name=profileName)
		while True:
			tok = self.parser.get_token()
			if not tok:
				break
			self._feed(tok)

		if self.stateFun!=self._state_init:
			self._raiseError("unexpected end of file (missing line feed?)")
		if profileName:
			self.profile.profileName = profileName

		stream.close()
		return self.profile

	def _raiseError(self, msg: str) -> NoReturn:
		# in bookworm, mypy gets error_leader's signature wrong.
		# the type:ignore can probably be dropped later.
		raise utils.logOldExc(
			ProfileParseError(self.parser.error_leader()+msg)) # type:ignore
	
	def _state_init(self, token: str) -> _StateFunction:
		"""the initial state of the parser: wait for a command or an assignement.
		"""
		if token in self.commands:
			return self.commands[token]
		elif token=="\n":
			return self._state_init

		if not re.match(r"[A-Za-z][\w]+$", token):
			self._raiseError("invalid identifier %s"%repr(token))
		self.tokenStack.append(token)
		return self._state_waitForEqual

	def _resolveSource(self, fName: str) -> str:
		"""returns a path for a profile fragment in fName.

		It resolves the name along sourcePath.
		"""
		for dir in self.sourcePath:
			fqName = os.path.join(dir, fName)
			if os.path.exists(fqName):
				return fqName
		raise ProfileParseError("Requested db profile %s does not exist"%
			repr(fName))

	def _state_include(self, token: str) -> _StateFunction:
		"""the state in which the parser expects a profile fragment name.
		"""
		if token=="\n":
			fName = "".join(self.tokenStack)
			self.tokenStack = []
			fName = self._resolveSource(fName)
			self.parser.push_source(open(fName), fName)
			return self._state_init
		else:
			self.tokenStack.append(token)
			return self._state_include

	def _state_eol(self, token: str) -> _StateFunction:
		"""the state in which a statement has been successfully parsed.
		"""
		if token!="\n":
			self._raiseError("expected end of line")
		return self._state_init

	def _state_waitForEqual(self, token: str) -> _StateFunction:
		"""the state when the lvalue of an assingment has been parsed.
		"""
		if token!="=":
			self._raiseError("expected '='")
		return self._state_rval
	
	def _state_rval(self, token: str) -> _StateFunction:
		"""the state when an rvalue of an assignment is parsed.
		"""
		if token=="\n":
			key = self.tokenStack.pop(0)
			val = "".join(self.tokenStack)
			self.tokenStack = []
			if not key in self.profileKeys:
				self._raiseError("unknown setting %s"%repr(key))
			setattr(self.profile, key, val)
			return self._state_init
		else:
			self.tokenStack.append(token)
			return self._state_rval

	def _feed(self, token: str) -> None:
		"""processes token by driving the parser into the next state.
		"""
		self.stateFun = self.stateFun(token)


class Configuration(fancyconfig.Configuration):
	"""A container for settings.

	It is a fancyconfig.Configuration with the addition of making the
	attributes shared at the class level to ward against multiple imports
	(which may happen if config is imported in a weird way).

	In addition, this class handles the access to database profiles.
	"""
	__sharedState: Dict[str,Any] = {}

	def __init__(self, *items: fancyconfig.Section):
		self.__dict__ = self.__sharedState
		fancyconfig.Configuration.__init__(self, *items)

	def _getProfileParser(self) -> ProfileParser:
		if not hasattr(self, "__profileParser"):
			self.__profileParser = ProfileParser(
				self.get("db", "profilePath"))
		return self.__profileParser

	@functools.cache
	def getDBProfile(self, profileName: str) -> DBProfile:
		# remains of retired profile name mapping infrastructure
		if profileName=='admin':
			profileName = 'feed'

		try:
			return self._getProfileParser().parse(profileName, profileName)
		except utils.NoConfigItem:
			raise ProfileParseError("Undefined DB profile: %s"%profileName)


_config = Configuration(
	DefaultSection('Paths and other general settings.',
		ExpandedPathConfigItem("rootDir", default="/var/gavo", description=
			"Path to the root of the DC file (all other paths may be"
			" relative to this"),
		RootRelativeConfigItem("configDir", default="etc",
			description="Path to the DC's non-ini configuration (e.g., DB profiles)"),
		RootRelativeConfigItem("inputsDir", default="inputs",
			description="Path to the DC's data holdings"),
		RootRelativeConfigItem("cacheDir", default="cache",
			description="Path to the DC's persistent scratch space"),
		RootRelativeConfigItem("logDir", default="logs",
			description="Path to the DC's logs (should be local)"),
		RootRelativeConfigItem("tempDir", default="tmp",
			description="Path to the DC's scratch space (should be local)"),
		RootRelativeConfigItem("webDir", default="web",
			description="Path to the DC's web related data (docs, css, js,"
				" templates...)"),
		RootRelativeConfigItem("stateDir", default="state",
			description="Path to the DC's state information (last imported,...)"),
		RootRelativeConfigItem("uwsWD", default="state/uwsjobs",
			description="Directory to keep uws jobs in.  This may need lots"
				" of space if your users do large queries"),
		EnumeratedConfigItem("logLevel", options=["info", "warning",
			"debug", "error"], description="How much should be logged?"),
		StringConfigItem("platform", description="Platform string (can be"
			" empty if inputsDir is only accessed by identical machines)"),
		StringConfigItem("gavoGroup", description="Name of the unix group that"
			" administers the DC", default="gavo"),
		StringConfigItem("defaultProfileName", description="Deprecated"
			" and ignored.", default=""),
		StringConfigItem("group", description="Name of the group that may write"
			" into the log directory", default="gavo"),
		PathConfigItem("xsdclasspath", description="Classpath necessary"
			" to validate XSD using an xsdval java class.  You want GAVO's"
			" VO schemata collection for this.  Deprecated, we're now using"
			" libxml2 for validation.", default="None"),
		StringConfigItem("sendmail", default="sendmail -t",
			description="Command that reads a mail from stdin, taking the"
				"recipient address from the mail header, and transfers the"
				" mail (this is for sending mails to the administrator)."
				" This command is processed by a shell (generally running as"
				" the server user), so you can do tricks if necessary."),
		StringConfigItem("maintainerAddress", default="",
			description="An e-mail address to send reports and warnings to;"
				" this could be the same as contact.email; in practice, it is"
				" shown in more technical circumstances, so it's adviable"
				" to have a narrower distribution here."),
		SetConfigItem("future", default="",
			description="A set of strings naming experimental features"
				" to enable in the server.  You only want to touch this"
				" if you are working on proving proposed improvements."),
		SetConfigItem("rdblacklist", default="",
			description="File names of blacklisted RDs.  These are matched"
				" against the ends of file names, so be careful you do not"
				" over-block; in general, you should use something like"
				" '/resname/q.rd' or so.  You can also blacklist RD ids"
				" (which are compared literally). Separate multiple entries"
				" by commas."),
		),

	Section('web', 'Settings related to serving content to the web.',
		StringConfigItem("serverURL", default="http://localhost:8080",
			description="URL fragment used to qualify relative URLs where"
			" necessary.  Note that this must contain the port the server is"
			" accessible under from the outside if that is not 80; nonstandard"
			" ports are not supported for https. If you offer"
			" both http and https, use your preferred protocol here."
			"  If you change this on an existing server, you *must* run"
			" dachs pub ALL to update internal and external links."),
		ListConfigItem("alternateHostnames", default="",
			description="A comma-separated list of hostnames this server is"
				" also known under.  Only set this if you're running https."
				" With this, you can handle a situation where your data"
				" centre can be reached as both example.org and www.example.org."),
		StringConfigItem("bindAddress", default="127.0.0.1", description=
			"Interface to bind to"),
		IntConfigItem("serverPort", default="8080",
			description="Port to bind the http port of the server to; https,"
			" if present at all, is always on 443."),
		BooleanConfigItem("adaptProtocol", default="True",
			description="Adapt internal absolute links to http/https by"
			" request method used.  You must switch this off if running"
			" behind a reverse proxy."),
		StringConfigItem("user", default="gavo", description="Run server as"
			" this user."),
		EnsureTrailingSlashesItem("nevowRoot", default="/",
			description="Path fragment to the server's root for operation off the"
				" server's root; this must end with a slash (setting this"
				" will currently break essentially the entire web interface."
				" If you must use it, contact the authors and we will fix things.)"),
		StringConfigItem("realm", default="X-Unconfigured",
			description="Authentication realm to be used (currently,"
			" only one, server-wide, is supported)"),
		WebRelativeConfigItem("templateDir", default="templates",
			description="webDir-relative location of global nevow templates"),
		StringConfigItem("adminpasswd", default="",
			description="Password for online administration, leave empty to disable"),
		StringConfigItem("sitename", "Unnamed data center",
			"A short name for your site"),
		IntConfigItem("sqlTimeout", "15",
			"Default timeout for non-TAP database queries (which can in"
			" general be overridden by users setting _TIMEOUT)."),
		WebRelativeConfigItem("previewCache", "previewcache",
			"Webdir-relative directory to store cached previews in"),
		WebRelativeConfigItem("favicon", "None",
			"Webdir-relative path to a favicon; this overrides the default of"
			" a scaled version of the logo."),
		BooleanConfigItem("enableTests", "False",
			"Enable test pages (don't if you don't know why)"),
		IntConfigItem("maxPreviewWidth", "300", "Ignored, only present"
			" for backward compatibility"),
		ListConfigItem("graphicMimes",
			"image/fits,image/jpeg,application/x-votable+xml;content=datalink,"
			"application/fits",
			"Media types considered as graphics (for SIAP, mostly)"),
		IntConfigItem("maxUploadSize",
			"20000000",
			"Maximal size of (mainly TAP) file uploads in async requests in bytes;"
			" sync requests use maxSyncUploadSize."),
		IntConfigItem("maxSyncUploadSize",
			"500000",
			"Maximal size of file uploads for synchronous TAP in bytes."),
		ListConfigItem("preloadRDs", "", "RD ids to preload at the server"
			" start.  Load time of RDs listed here goes against startup time,"
			" so only do this for RDs that have execute children"
			" that should run regularly.  For everything else consider"
			" preloadPublishedRDs."),
		BooleanConfigItem("preloadPublishedRDs", "False", "Preload"
			" all RDs of services you've published.  This is mainly helpful"
			" when code in such RDs might, for instance, lock and such"
			" failures don't suddenly occur in operation."),
		BooleanConfigItem("jsSource", "False", "If True, Javascript"
			" will not be minified on delivery (this is for debugging)"),
		StringConfigItem("operatorCSS", "", "URL of an operator-specific"
			" CSS.  This is included as the last item and can therefore"
			" override rules in the distributed CSS."),
		StringConfigItem("corsOriginPat", "", "A regular expression"
			" for URLs from which to authorise cross-origin requests."
			" This is matched, i.e., the RE must account for the whole URL"
			r" including the schema. Example: https?://example\.com/apps/.*."),
		IntConfigItem("serverFDLimit", "4000",
			"A hard limit of the number of file handles DaCHS should try to"
			" set.  This will only take effect if DaCHS is started as root."
			"  Otherwise, DaCHS will just adjust its soft limit to the"
			" external limit irrespective of this setting."),
		EnumeratedConfigItem("logFormat", "default", "Log format to use."
			"  Default doesn't log IPs, user agents, or referrers and"
			" thus should be ok in terms of not processing personal data,"
			" which in turn means you probably don't have to declare anything"
			" in EU jurisdictions.", options=["default", "combined"]),
		StringConfigItem("root", "__system__/services/root/fixed",
			"The path of the the root page of the data center.  The default"
			" renders the root.html template.  On single-service installations,"
			" you could direct this to that single service, or you could"
			" use the ADQL query form."),
		BooleanConfigItem("ignore-uir-header", "False",
			"Do not honour clients' upgrade-insecure-requests headers if"
			" https is enabled.  Outside of development or testing environments,"
			" there is generally little reason to use this.")
	),

	Section('adql', "(ignored, only left for backward compatibility)",
		IntConfigItem("webDefaultLimit", "2000",
			"(ignored, only present for backwards compatibility; use"
			" [async]defaultMAXREC instead."),
	),

	Section('async', "Settings concerning TAP, UWS, and friends",
		IntConfigItem("defaultExecTimeSync", "60", "Timeout"
			" for synchronous TAP/UWS jobs, in seconds."),
		IntConfigItem("defaultExecTime", "3600", "Default timeout"
			" for UWS jobs, in seconds"),
		IntConfigItem("maxTAPRunning", "2", "Maximum number of"
			" TAP jobs running at a time"),
		IntConfigItem("maxUserUWSRunningDefault", "2", "Maximum number of"
			" user UWS jobs running at a time"),
		IntConfigItem("defaultLifetime", "172800", "Default"
			" time to destruction for UWS jobs, in seconds"),
		IntConfigItem("defaultMAXREC", "20000",
			"Default match limit for ADQL queries via the UWS/TAP"),
		IntConfigItem("hardMAXREC", "20000000",
			"Hard match limit (i.e., users cannot raise MAXREC or TOP beyond that)"
			" for ADQL queries via the UWS/TAP"),
		StringConfigItem("csvDialect", "excel", "CSV dialect as defined"
			" by the python csv module used when writing CSV files."),
		IntConfigItem("maxSlowPollWait", "300", "Maximal time a UWS 1.1-WAIT"
			" request will delay the response.  This should be smaller than"
			" what you have as timeout on outgoing connections."),
	),

	Section('ui', "Settings concerning the local user interface",
	),

	Section('db', 'Settings concerning database access.',
		IntConfigItem("indexWorkMem",
			"2000",
			"Megabytes of memory to give to postgres while making indices."
			"  Set to roughly half your RAM when you have big tables."),
		StringConfigItem("interface", "psycopg2", "Don't change"),
		PathConfigItem("profilePath", "~/.gavo:$configDir",
			"Path for locating DB profiles"),
		StringConfigItem("msgEncoding", "utf-8", "Encoding of the"
			" messages coming from the database"),
		SetConfigItem("maintainers", "admin", "Name(s) of profiles"
			" that should have full access to gavo imp-created tables by default"),
		SetConfigItem("queryProfiles", "trustedquery", "Name(s) of profiles that"
			" should be able to read gavo imp-created tables by default"),
		SetConfigItem("adqlProfiles", "untrustedquery", "Name(s) of profiles that"
			" get access to tables opened for ADQL"),
		IntConfigItem("defaultLimit", "100", "Default match limit for DB queries"),
		ListConfigItem("managedExtensions",
			"pg_sphere",
			"Name(s) of postgres extensions gavo upgrade -e should watch"),
		BooleanConfigItem("dumpSystemTables",
			"False",
			"Dump the tables from //users and //system to"
			" stateDir/system_tables once a day?"),
		IntConfigItem("poolSize", "2",
			"Number of connections in DaCHS' postgres connection pools."),
		BooleanConfigItem("q3cPlannerHack",
			"True",
			"Disable seqscanning when running queries using q3c.  This"
			" used to unconditionally help in ancient times but may actually"
			" hurt with newer postgreses."),
		EnumeratedConfigItem("splitSpatialIndex", "pgsphere",
			"Type of spatial index created for lat/lon pairs in TAP uploads"
			" (and potentially later other things).",
			options=["q3c", "pgsphere"])
	),
	
	Section('ivoa', 'The interface to the Greater VO.',
		AuthorityConfigItem("authority", "x-unregistred",
			"The authority id for this DC; this has *no* leading ivo://"),
		IntConfigItem("dalDefaultLimit", "10000",
			"Default match limit on SCS/SSAP/SIAP queries"),
		IntConfigItem("dalHardLimit", "1000000",
			"Hard match limit on SCS/SSAP/SIAP queries (be careful: due to the"
			" way these protocols work, the results cannot be streamed, and"
			" the results have to be kept in memory; 1e7 rows requiring 1k"
			" of memory each add up to 10 Gigs...)"),
		IntConfigItem("oaipmhPageSize", "500",
			"Default number of records per page in the OAI-PMH interface"),
		EnumeratedConfigItem("votDefaultEncoding", "binary",
			"Default 'encoding' for VOTables in many places (like the DAL"
			" responses; this can be user-overridden using"
			" the _TDENC local HTTP parameter.", options=["binary", "td"]),
		EnumeratedConfigItem("sdmVersion", "1",
			"Obsolete (SDM version 2 is shelved).  Don't use.",
			options=["1", "2"]),
		EnumeratedConfigItem("VOSITableDetail",
			"max",
			"Default level of detail to return on the VOSI endpoint (change"
			" to min when you have more than 100 or tables).",
			options=["min", "max"]),
		BooleanConfigItem("registerAlternative",
			"False",
			"Give access URLs for the alternative protocol (https when"
			" serverURL is http and vice versa) as a mirrorURL?  If you're"
			" listening to HTTPS, this is probably a good idea."),
		ListConfigItem("validOAISets",
			"ivo_managed, vosi, local",
			"Comma-separated list of OAI sets DaCHS should accept for publication."
			"  This must always include ivo_managed and vosi, and you will"
			" want local if you run a web interface."),
		IntConfigItem("userTableDays", "7",
			"Number of days that user-uploaded TAP tables are kept before"
			" garbage collection"),
		BooleanConfigItem("materialiseObscore",
			"False",
			"Make ivoa.obscore a materialised view.  This may help"
			" if you have large, complex obscore contributions and"
			" postgres chooses bad query planes for them (since 2.12.3)."),
	),
)

def loadConfig() -> None:
	"""reads the configuration from /etc/gavo.rc and ~/.gavorc.

	This manipulates the global _config object.

	Actually, you can override these locations in GAVOSETTINGS and GAVOCUSTOM.
	But you'll confuse everyone if you do that in production, so only
	do that if it *really* helps somewhere else (as perhaps in containers
	when you want to everything writable in one place).

	This also manipulates the environment such that libraries with
	configuration files fetch them from our configDir.
	"""
	try:
		fancyconfig.readConfiguration(_config,
			os.environ.get("GAVOSETTINGS", "/etc/gavo.rc"),
			os.environ.get("GAVOCUSTOM",
				os.path.join(os.environ.get("HOME", "/no_home"), ".gavorc")))
	except fancyconfig.ConfigError as ex:
		# This is usually not be protected by top-level exception catcher
		sys.exit("Bad configuration item in %s.  %s"%(
			ex.fileName, str(ex)))
	
	# also set XDG directories so astropy and friends look for their
	# configuration in DaCHS' directories
	os.environ["XDG_CONFIG_HOME"] = str(_config.get("configDir"))
	os.environ["XDG_CACHE_HOME"] = str(_config.get("cacheDir"))
	try:
		from astropy.config import reload_config # type: ignore
		reload_config("astropy")
	except Exception:
		# astropy panics here if it can't write to its home.  But then
		# the reload failure probably doesn't matter either, so let's
		# just ignore it
		pass

	# also configure matplotlib to pull their configuration from
	# DaCHS' configuration (rather than any random matplotlibrc people
	# may have left).
	os.environ["MATPLOTLIBRC"] = str(_config.get("configDir"))

loadConfig()


if "GAVO_INPUTSDIR" in os.environ:
	_config.set("inputsDir", os.environ["GAVO_INPUTSDIR"])

get = _config.get
set = _config.set
getitem = _config.getitem

getDBProfile = _config.getDBProfile


def getRSTReference(underlineChar="-") -> str:
	"""returns a ReStructuredText reference of configuration items.

	This will have one RST section per configuration section; to let
	you adapt that to an embedding document, you can pass the character
	to use for headline underlines in underlineChar.
	"""
	return fancyconfig.makeTxtDocs(_config, underlineChar=underlineChar)


def main(): # pragma: no cover
	try:
		if len(sys.argv)==1:
			print(getRSTReference)
			sys.exit(0)
		elif len(sys.argv)==2:
			item = _config.getitem(sys.argv[1])
		elif len(sys.argv)==3:
			item = _config.getitem(sys.argv[1], sys.argv[2])
		else:
			sys.stderr.write("Usage: %s [<sect> <key> | <key>]\n")
			sys.exit(1)
	except NoOptionError:
		print("")
		sys.exit(2)
	print(item.getAsString())


def _test(): # pragma: no cover
	import doctest
	doctest.testmod()


if __name__=="__main__": # pragma: no cover
	_test()
