from cryptobox.core.exceptions import *
import logging
try:
	import validate
except:
	raise CBEnvironmentError("couldn't import 'validate'! Try 'apt-get install python-formencode'.")
import os
import subprocess
try:
	import configobj	## needed for reading and writing of the config file
except:
	raise CBEnvironmentError("couldn't import 'configobj'! Try 'apt-get install python-configobj'.")



class CryptoBoxSettings:

	CONF_LOCATIONS = [
		"./cryptobox.conf",
		"~/.cryptobox.conf",
		"/etc/cryptobox/cryptobox.conf"]
	
	VOLUMESDB_FILE = "cryptobox_volumes.db"
	PLUGINCONF_FILE = "cryptobox_plugins.conf"
	USERDB_FILE = "cryptobox_users.db"


	def __init__(self, config_file=None):
		self.log = logging.getLogger("CryptoBox")
		config_file = self.__getConfigFileName(config_file)
		self.log.info("loading config file: %s" % config_file)
		self.prefs = self.__getPreferences(config_file)
		self.__validateConfig()
		self.__configureLogHandler()
		self.__checkUnknownPreferences()
		self.preparePartition()
		self.volumesDB = self.__getVolumesDatabase()
		self.pluginConf = self.__getPluginConfig()
		self.userDB = self.__getUserDB()
		self.misc_files = self.__getMiscFiles()
	

	def write(self):
		"""
		write all local setting files including the content of the "misc" subdirectory
		"""
		ok = True
		try:
			self.volumesDB.write()
		except IOError:
			self.log.warn("could not save the volume database")
			ok = False
		try:
			self.pluginConf.write()
		except IOError:
			self.log.warn("could not save the plugin configuration")
			ok = False
		try:
			self.userDB.write()
		except IOError:
			self.log.warn("could not save the user database")
			ok = False
		for misc_file in self.misc_files:
			if not misc_file.save():
				self.log.warn("could not save a misc setting file (%s)" % misc_file.filename)
				ok = False
		return ok


	def requiresPartition(self):
		return bool(self.prefs["Main"]["UseConfigPartition"])
	

	def getActivePartition(self):
		settings_dir = self.prefs["Locations"]["SettingsDir"]
		if not os.path.ismount(settings_dir): return None
		for line in file("/proc/mounts"):
			fields = line.split(" ")
			mount_dir = fields[1]
			try:
				if os.path.samefile(mount_dir, settings_dir): return fields[0]
			except OSError:
				pass
		## no matching entry found
		return None


	def mountPartition(self):
		self.log.debug("trying to mount configuration partition")
		if not self.requiresPartition():
			self.log.warn("mountConfigPartition: configuration partition is not required - mounting anyway")
		if self.getActivePartition():
			self.log.warn("mountConfigPartition: configuration partition already mounted - not mounting again")
			return False
		confPartitions = self.getAvailablePartitions()
		if not confPartitions:
			self.log.error("no configuration partitions found - you have to create it first")
			return False
		partition = confPartitions[0]
		proc = subprocess.Popen(
			shell = False,
			stdout = subprocess.PIPE,
			stderr = subprocess.PIPE,
			args = [
				self.prefs["Programs"]["super"],
				self.prefs["Programs"]["CryptoBoxRootActions"],
				"mount",
				partition,
				self.prefs["Locations"]["SettingsDir"]])
		(stdout, stderr) = proc.communicate()
		if proc.returncode != 0:
			self.log.error("failed to mount the configuration partition: %s" % partition)
			self.log.error("output of mount: %s" % (stderr,))
			return False
		self.log.info("configuration partition mounted: %s" % partition)
		return True


	def umountPartition(self):
		if not self.getActivePartition():
			self.log.warn("umountConfigPartition: no configuration partition mounted")
			return False
		proc = subprocess.Popen(
			shell = False,
			stdout = subprocess.PIPE,
			stderr = subprocess.PIPE,
			args = [
				self.prefs["Programs"]["super"],
				self.prefs["Programs"]["CryptoBoxRootActions"],
				"umount",
				self.prefs["Locations"]["SettingsDir"]])
		(stdout, stderr) = proc.communicate()
		if proc.returncode != 0:
			self.log.error("failed to unmount the configuration partition")
			self.log.error("output of mount: %s" % (stderr,))
			return False
		self.log.info("configuration partition unmounted")
		return True
		

	def getAvailablePartitions(self):
		"""returns a sequence of found config partitions"""
		proc = subprocess.Popen(
			shell = False,
			stdout = subprocess.PIPE,
			args = [
				self.prefs["Programs"]["blkid"],
				"-c", os.path.devnull,
				"-t", "LABEL=%s" % self.prefs["Main"]["ConfigVolumeLabel"] ])
		(output, error) = proc.communicate()
		if output:
			return [e.strip().split(":",1)[0] for e in output.splitlines()]
		else:
			return []

	
	def preparePartition(self):
		if self.requiresPartition() and not self.getActivePartition():
			self.mountPartition()


	def __getitem__(self, key):
		"""redirect all requests to the 'prefs' attribute"""
		return self.prefs[key]


	def __getPreferences(self, config_file):
		import StringIO
		config_rules = StringIO.StringIO(self.validation_spec)
		try:
			prefs = configobj.ConfigObj(config_file, configspec=config_rules)
			if prefs:
				self.log.info("found config: %s" % prefs.items())
			else: 
				raise CBConfigUnavailableError("failed to load the config file: %s" % config_file)
		except IOError:
			raise CBConfigUnavailableError("unable to open the config file: %s" % config_file)
		return prefs


	def __validateConfig(self):
		result = self.prefs.validate(CryptoBoxSettingsValidator(), preserve_errors=True)
		error_list = configobj.flatten_errors(self.prefs, result)
		if not error_list: return
		errorMsgs = []
		for sections, key, text in error_list:
			section_name = "->".join(sections)
			if not text:
				errorMsg = "undefined configuration value (%s) in section '%s'" % (key, section_name)
			else:
				errorMsg = "invalid configuration value (%s) in section '%s': %s" % (key, section_name, text)
			errorMsgs.append(errorMsg)
		raise CBConfigError, "\n".join(errorMsgs)


	def __checkUnknownPreferences(self):
		import StringIO
		config_rules = configobj.ConfigObj(StringIO.StringIO(self.validation_spec), list_values=False)
		self.__recursiveConfigSectionCheck("", self.prefs, config_rules)
		
	
	def __recursiveConfigSectionCheck(self, section_path, section_config, section_rules):
		"""should be called by '__checkUnknownPreferences' for every section
		sends a warning message to the logger for every undefined (see validation_spec)
		configuration setting
		"""
		for e in section_config.keys():
			element_path = section_path + e
			if e in section_rules.keys():
				if isinstance(section_config[e], configobj.Section):
					if isinstance(section_rules[e], configobj.Section):
						self.__recursiveConfigSectionCheck(element_path + "->", section_config[e], section_rules[e])
					else:
						self.log.warn("configuration setting should be a value instead of a section name: %s" % element_path)
				else:
					if not isinstance(section_rules[e], configobj.Section):
						pass	# good - the setting is valid
					else:
						self.log.warn("configuration setting should be a section name instead of a value: %s" % element_path)
			else:
				self.log.warn("unknown configuration setting: %s" % element_path)


	def __getPluginConfig(self):
		import StringIO
		plugin_rules = StringIO.StringIO(self.pluginValidationSpec)
		try:
			try:
				pluginConf_file = os.path.join(self.prefs["Locations"]["SettingsDir"], self.PLUGINCONF_FILE)
			except KeyError:
				raise CBConfigUndefinedError("Locations", "SettingsDir")
		except SyntaxError:
			raise CBConfigInvalidValueError("Locations", "SettingsDir", pluginConf_file, "failed to interprete the filename of the plugin config file correctly (%s)" % pluginConf_file)
		## create pluginConf_file if necessary
		if os.path.exists(pluginConf_file):
			pluginConf = configobj.ConfigObj(pluginConf_file, configspec=plugin_rules)
		else:
			pluginConf = configobj.ConfigObj(pluginConf_file, configspec=plugin_rules, create_empty=True)
		## validate and convert values according to the spec
		pluginConf.validate(validate.Validator())
		## check if pluginConf_file file was created successfully?
		if not os.path.exists(pluginConf_file):
			raise CBEnvironmentError("failed to create plugin configuration file (%s)" % pluginConf_file)
		return pluginConf


	def __getVolumesDatabase(self):
		try:
			try:
				conf_file = os.path.join(self.prefs["Locations"]["SettingsDir"], self.VOLUMESDB_FILE)
			except KeyError:
				raise CBConfigUndefinedError("Locations", "SettingsDir")
		except SyntaxError:
			raise CBConfigInvalidValueError("Locations", "SettingsDir", conf_file, "failed to interprete the filename of the volume database correctly (%s)" % conf_file)
		## create conf_file if necessary
		if os.path.exists(conf_file):
			conf = configobj.ConfigObj(conf_file)
		else:
			conf = configobj.ConfigObj(conf_file, create_empty=True)
		## check if conf_file file was created successfully?
		if not os.path.exists(conf_file):
			raise CBEnvironmentError("failed to create volume database file (%s)" % conf_file)
		return conf


	def __getUserDB(self):
		import StringIO, sha
		userDB_rules = StringIO.StringIO(self.userDatabaseSpec)
		try:
			try:
				userDB_file = os.path.join(self.prefs["Locations"]["SettingsDir"], self.USERDB_FILE)
			except KeyError:
				raise CBConfigUndefinedError("Locations", "SettingsDir")
		except SyntaxError:
			raise CBConfigInvalidValueError("Locations", "SettingsDir", userDB_file, "failed to interprete the filename of the users database file correctly (%s)" % userDB_file)
		## create userDB_file if necessary
		if os.path.exists(userDB_file):
			userDB = configobj.ConfigObj(userDB_file, configspec=userDB_rules)
		else:
			userDB = configobj.ConfigObj(userDB_file, configspec=userDB_rules, create_empty=True)
		## validate and set default value for "admin" user
		userDB.validate(validate.Validator())
		## check if userDB file was created successfully?
		if not os.path.exists(userDB_file):
			raise CBEnvironmentError("failed to create user database file (%s)" % userDB_file)
		## define password hash function - never use "sha" directly - SPOT
		userDB.getDigest = lambda password: sha.new(password).hexdigest()
		return userDB


	def __getMiscFiles(self):
		misc_dir = os.path.join(self.prefs["Locations"]["SettingsDir"], "misc")
		if (not os.path.isdir(misc_dir)) or (not os.access(misc_dir, os.X_OK)):
			return []
		return [MiscConfigFile(os.path.join(misc_dir, f), self.log)
			for f in os.listdir(misc_dir)
			if os.path.isfile(os.path.join(misc_dir, f))]


	def __getConfigFileName(self, config_file):
		# search for the configuration file
		import types
		if config_file is None:
			# no config file was specified - we will look for it in the ususal locations
			conf_file_list = [os.path.expanduser(f)
				for f in self.CONF_LOCATIONS
				if os.path.exists(os.path.expanduser(f))]
			if not conf_file_list:
				# no possible config file found in the usual locations
				raise CBConfigUnavailableError()
			config_file = conf_file_list[0]
		else:
			# a config file was specified (e.g. via command line)
			if type(config_file) != types.StringType:
				raise CBConfigUnavailableError("invalid config file specified: %s" % config_file)
			if not os.path.exists(config_file):
				raise CBConfigUnavailableError("could not find the specified configuration file (%s)" % config_file)
		return config_file


	def __configureLogHandler(self):
		try:
			log_level = self.prefs["Log"]["Level"].upper()
			log_level_avail = ["DEBUG", "INFO", "WARN", "ERROR"]
			if not log_level in log_level_avail:
				raise TypeError
		except KeyError:
			raise CBConfigUndefinedError("Log", "Level")
		except TypeError:
			raise CBConfigInvalidValueError("Log", "Level", log_level, "invalid log level: only %s are allowed" % log_level_avail)
		try:
			try:
				log_handler = logging.FileHandler(self.prefs["Log"]["Details"])
			except KeyError:
				raise CBConfigUndefinedError("Log", "Details")
		except IOError:
			raise CBEnvironmentError("could not create the log file (%s)" % self.prefs["Log"]["Details"])
		log_handler.setFormatter(logging.Formatter('%(asctime)s CryptoBox %(levelname)s: %(message)s'))
		cbox_log = logging.getLogger("CryptoBox")
		## remove previous handlers
		cbox_log.handlers = []
		## add new one
		cbox_log.addHandler(log_handler)
		## do not call parent's handlers
		cbox_log.propagate = False
		## 'log_level' is a string -> use 'getattr'
		cbox_log.setLevel(getattr(logging,log_level))
		## the logger named "CryptoBox" is configured now


	validation_spec = """
[Main]
AllowedDevices = list(min=1)
DefaultVolumePrefix = string(min=1)
DefaultCipher = string(default="aes-cbc-essiv:sha256")
ConfigVolumeLabel = string(min=1, default="cbox_config")
UseConfigPartition = integer(min=0, max=1, default=0)
DisabledPlugins = list(default=[])

[Locations]
MountParentDir = directoryExists(default="/var/cache/cryptobox/mnt")
SettingsDir = directoryExists(default="/var/cache/cryptobox/settings")
TemplateDir = directoryExists(default="/usr/share/cryptobox/template")
LangDir = directoryExists(default="/usr/share/cryptobox/lang")
DocDir = directoryExists(default="/usr/share/doc/cryptobox/www-data")
PluginDir = directoryExists(default="/usr/share/cryptobox/plugins")
EventDir = directoryExists(default="/etc/cryptobox/events.d")

[Log]
Level = option("debug", "info", "warn", "error", default="warn")
Destination = option("file", default="file")
Details = string(min=1)

[WebSettings]
Stylesheet = string(min=1)
Language = string(min=1, default="en")

[Programs]
cryptsetup = fileExecutable(default="/sbin/cryptsetup")
mkfs-data = fileExecutable(default="/sbin/mkfs.ext3")
blkid = fileExecutable(default="/sbin/blkid")
blockdev = fileExecutable(default="/sbin/blockdev")
mount = fileExecutable(default="/bin/mount")
umount = fileExecutable(default="/bin/umount")
super = fileExecutable(default="/usr/bin/super")
# this is the "program" name as defined in /etc/super.tab
CryptoBoxRootActions = string(min=1)
		"""

	pluginValidationSpec = """
[__many__]
visibility = boolean(default=None)
requestAuth = boolean(default=None)
rank = integer(default=None)
		"""
	
	userDatabaseSpec = """
[admins]
admin = string(default=d033e22ae348aeb5660fc2140aec35850c4da997)
		"""
	

class CryptoBoxSettingsValidator(validate.Validator):

	def __init__(self):
		validate.Validator.__init__(self)
		self.functions["directoryExists"] = self.check_directoryExists
		self.functions["fileExecutable"] = self.check_fileExecutable
		self.functions["fileWriteable"] = self.check_fileWriteable
	

	def check_directoryExists(self, value):
		dir_path = os.path.abspath(value)
		if not os.path.isdir(dir_path):
			raise validate.VdtValueError("%s (not found)" % value)
		if not os.access(dir_path, os.X_OK):
			raise validate.VdtValueError("%s (access denied)" % value)
		return dir_path
	

	def check_fileExecutable(self, value):
		file_path = os.path.abspath(value)
		if not os.path.isfile(file_path):
			raise validate.VdtValueError("%s (not found)" % value)
		if not os.access(file_path, os.X_OK):
			raise validate.VdtValueError("%s (access denied)" % value)
		return file_path
	

	def check_fileWriteable(self, value):
		file_path = os.path.abspath(value)
		if os.path.isfile(file_path):
			if not os.access(file_path, os.W_OK):
				raise validate.VdtValueError("%s (not found)" % value)
		else:
			parent_dir = os.path.dirname(file_path)
			if os.path.isdir(parent_dir) and os.access(parent_dir, os.W_OK):
				return file_path
			raise validate.VdtValueError("%s (directory does not exist)" % value)
		return file_path



class MiscConfigFile:

	maxSize = 20480

	def __init__(self, filename, logger):
		self.filename = filename
		self.log = logger
		self.load()

	
	def load(self):
		fd = open(self.filename, "rb")
		## limit the maximum size
		self.content = fd.read(self.maxSize)
		if fd.tell() == self.maxSize:
			self.log.warn("file in misc settings directory (%s) is bigger than allowed (%s)" % (self.filename, self.maxSize))
		fd.close()


	def save(self):
		save_dir = os.path.dirname(self.filename)
		## create the directory, if necessary
		if not os.path.isdir(save_dir):
			try:
				os.mkdir(save_dir)
			except IOError:
				return False
		## save the content of the file
		try:
			fd = open(self.filename, "wb")
		except IOError:
			return False
		try:
			fd.write(self.content)
			fd.close()
			return True
		except IOError:
			fd.close()
			return False

