#!/usr/bin/python2.7
# -*- coding: utf-8 -*-
#
# Copyright (C) 2012-2015 Univention GmbH
#
# http://www.univention.de/
#
# All rights reserved.
#
# The source code of this program is made available
# under the terms of the GNU Affero General Public License version 3
# (GNU AGPL V3) as published by the Free Software Foundation.
#
# Binary versions of this program provided by Univention to you as
# well as other copyrighted, protected or trademarked materials like
# Logos, graphics, fonts, specific documentations and configurations,
# cryptographic keys etc. are subject to a license agreement between
# you and Univention and not subject to the GNU AGPL V3.
#
# In the case you use this program under the terms of the GNU AGPL V3,
# the program is provided in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public
# License with the Debian GNU/Linux or Univention distribution in file
# /usr/share/common-licenses/AGPL-3; if not, see
# <http://www.gnu.org/licenses/>.

import os
import sys
import ldap
import copy
import MySQLdb
import subprocess
from optparse import OptionParser, OptionGroup
from univention.config_registry import ConfigRegistry
import univention.admin.modules
import univention.admin.config
import univention.admin.uldap
try:
	import imp
	ucs_zarafa_cfg = imp.load_source('zarafa-cfg', '/etc/univention/templates/modules/zarafa-cfg.py')
except:
	print 'Could not load /etc/univention/modules/zarafa-cfg.py'
	sys.exit(1)


class ConnectionError(Exception):
	pass


class PreCheckError(Exception):
	pass


def get_value(configRegistry, value):
	"""
	this function loads the correct UCR value by replacing
	UCR placeholders with its current value.
	RE_UCRVAR and RE_FILENAME are defined in the UCR module zarafa-cfg.py
	The function is adapted from the zarafa-cfg module get_line function
	"""
	matches = ucs_zarafa_cfg.RE_UCRVAR.findall(value)
	ucr_value = "error:notfound"
	for key in matches:
		ucr_value = value.replace('@%%@%s@%%@' % key, configRegistry.get(key, ''))
	matches = ucs_zarafa_cfg.RE_FILENAME.findall(value)
	for key in matches:
		try:
			content = open(key, 'r').read()
		except IOError:
			content = ''
		ucr_value = value.replace('@&@%s@&@' % key, content)
	return ucr_value


class ZarafaMigration(object):
	def debug(self, msg):
		if self.options.verbose:
			print >> sys.stderr, msg

	def message(self, msg):
		print >> sys.stderr, msg

	def error(self, msg):
		print >> sys.stderr, msg
		self.ret = 1

	def __init__(self):
		self.options = None
		self.ucr = ConfigRegistry()
		self.config = None
		self.access = None
		self.position = None
		self.ret = 0

	def parse_cmdline(self):
		usage = "%prog [options]"
		parser = OptionParser(usage=usage)
		parser.add_option(
			'--dry-run', '-d',
			action='store_true', dest='dry_run', help='Do not modify objects')
		parser.add_option(
			'--verbose', '-v',
			action='store_true', dest='verbose', help='Enable verbose output')
		parser.add_option(
			'--migrate', '-m',
			action='store_true', dest='migrate', help='Migrate existing contacts and non-active stores')

		group = OptionGroup(parser, "LDAP bind credentials")
		group.add_option(
			'--binddn',
			action='store', dest='binddn', help='LDAP bind dn for UDM CLI operation')
		group.add_option(
			'--bindpwd',
			action='store', dest='bindpwd', help='LDAP bind password for bind dn')
		parser.add_option_group(group)

		(self.options, _args) = parser.parse_args()

	def main(self):
		self.ucr.load()
		if self.ucr.is_true('zarafa/migrateusers', True):
			try:
				self.parse_cmdline()
				self.precheck()
				self.get_ldap_connection()
				self.get_mysql_connection()
				self.migrate()
				if not self.options.dry_run:
					subprocess.call(['/usr/share/zarafa4ucs/db-upgrade-addressbook-entryids-delegate.pl'])
			except (ldap.LDAPError), e:
				if 'info' in e:
					self.error(e.info)
				elif 'desc' in e:
					self.error(e.desc)
			except MySQLdb.MySQLError, e:
				self.error(str(e))
			except PreCheckError:
				self.error('\nError encountered during precheck, exiting')
		else:
			self.debug('zarafa/migrateusers is set to False, exiting')

		return self.ret

	def precheck(self):
		# Check if zarafa is stopped
		try:
			self.debug('Check if Zarafa is running')
			with open(os.devnull, 'w') as devnull:
				retcode = subprocess.call(['/usr/bin/zarafa-stats', '--system'], stdout=devnull, stderr=subprocess.STDOUT)
		except subprocess.CalledProcessError:
			self.error('CalledProcessError while checking if zarafa is running')
			raise PreCheckError()

		if retcode is 0:
			self.error('Zarafa is still running. Stop Zarafa services before continuing')
			self.error('Stop Zarafa services with e.g.')
			self.error('for PROC in /etc/init.d/zarafa-*; do $PROC stop; done')
			raise PreCheckError()

	def get_ldap_connection(self):
		self.config = univention.admin.config.config()
		if self.options.binddn and self.options.bindpwd:
			self.access = univention.admin.uldap.access(
				host=self.ucr["ldap/master"],
				port=int(self.ucr.get('ldap/master/port', '7389')),
				base=self.ucr['ldap/base'],
				binddn=self.options.binddn,
				bindpw=self.options.bindpwd,
				start_tls=1)
			self.position = univention.admin.uldap.position(self.ucr['ldap/base'])
		else:
			self.access, self.position = univention.admin.uldap.getAdminConnection()
		self.debug('ldap connection established')

	def get_mysql_connection(self):
		mysql_host = self.ucr.get('zarafa/cfg/server/mysql_host')
		mysql_port = self.ucr.get('zarafa/cfg/server/mysql_port')
		mysql_user = self.ucr.get('zarafa/cfg/server/mysql_user')
		mysql_password = get_value(self.ucr, self.ucr.get('zarafa/cfg/server/mysql_password')).strip()
		mysql_db = self.ucr.get('zarafa/cfg/server/mysql_database')
		self.dbconn = MySQLdb.connect(user=mysql_user, passwd=mysql_password, db=mysql_db, host=mysql_host, port=int(mysql_port))
		self.cursor = self.dbconn.cursor()

	def migrate(self):
		self.migrate_users()
		if self.options.migrate:
			self.migrate_non_active()
			self.migrate_contacts()

	def migrate_contacts(self):
		self.debug('\nMigrating contacts:')
		contacts_container = "cn=contacts,cn=zarafa,%s" % self.ucr.get('ldap/base')
		for dn, attrs in self.access.search(
			filter='(&(zarafa4ucsRole=contact)(!(univentionObjectFlag=functional)))',
		):
			classes_updated = attrs.get('objectClass', [])
			classes_updated = [x for x in classes_updated if x != 'zarafa-user']
			for objclass in ['zarafa-contact', 'zarafa4ucsObject']:
				if objclass not in classes_updated:
					classes_updated.append(objclass)

			flag_updated = attrs.get('univentionObjectFlag', [])
			flag_updated.append('functional')

			changes = [
				('objectClass', attrs.get('objectClass', []), classes_updated),
				('univentionObjectFlag', attrs.get('univentionObjectFlag', []), flag_updated),
				('univentionObjectType', attrs.get('univentionObjectType', []), ['zarafa/contact']),
				('zarafa4ucsRole', attrs.get('zarafa4ucsRole', []), []),
				('zarafaAdmin', attrs.get('zarafaAdmin', []), []),
				('zarafaSharedStoreOnly', attrs.get('zarafaSharedStoreOnly', []), []),
			]
			try:
				self.debug('Updating %s' % dn)
				if not self.options.dry_run:
					self.access.modify(dn, changes, ignore_license=1)

				# new_dn = 'cn=%s,%s' % (attrs.get('cn', '')[0] , contacts_container)
				part_dn = univention.admin.uldap.explodeDn(dn)[0]
				new_dn = '%s,%s' % (part_dn, contacts_container)
				self.debug('Moving %s to %s' % (dn, new_dn))
				if not self.options.dry_run:
					self.access.rename(dn, new_dn)
			except Exception as ex:
				self.error('Failed to modify %s: %s' % (dn, ex))

	def migrate_non_active(self):
		self.debug('\nMigrating shared stores and non-active users:')
		nonactive_container = "cn=non-active,cn=zarafa,%s" % self.ucr.get('ldap/base')

		for dn, attrs in self.access.search(
			filter='(&(zarafa4ucsRole=store)(!(univentionObjectFlag=functional)))',
		):

			classes_updated = copy.copy(attrs.get('objectClass', []))
			for objclass in ['zarafa-user', 'zarafa4ucsObject']:
				if objclass not in classes_updated:
					classes_updated.append(objclass)

			flag_updated = attrs.get('univentionObjectFlag', [])
			flag_updated.append('functional')

			changes = [
				('objectClass', attrs.get('objectClass', []), classes_updated),
				('univentionObjectFlag', attrs.get('univentionObjectFlag', []), flag_updated),
				('univentionObjectType', attrs.get('univentionObjectType', []), ['zarafa/non-active']),
				('zarafa4ucsRole', attrs.get('zarafa4ucsRole', []), []),
				('zarafaAdmin', attrs.get('zarafaAdmin', []), []),
			]
			try:
				self.debug('Updating %s' % dn)
				if not self.options.dry_run:
					self.access.modify(dn, changes, ignore_license=1)

				part_dn = univention.admin.uldap.explodeDn(dn)[0]
				new_dn = '%s,%s' % (part_dn, nonactive_container)
				self.debug('Moving %s to %s' % (dn, new_dn))
				if not self.options.dry_run:
					self.access.rename(dn, new_dn)

			except Exception as ex:
				self.error('Failed to modify %s: %s' % (dn, ex))

	# change in mysql: zarafa.users.externid from uidNumber to entryUUID
	def migrate_users(self):
		self.debug('\nMigrating zarafa mysql database:')
		for dn, attrs in self.access.search(
				filter=('(&'
					  '(|(zarafa4ucsRole=user)(zarafa4ucsRole=admin)(zarafa4ucsRole=store))'
					  '(!(objectClass=univentionUserTemplate))'
					  '(!(univentionObjectFlag=functional)))'),
			attr=['uid', 'uidNumber', 'entryUUID'],
		):
			if not attrs.get('uid', []):
				self.message('No uid attribute for %s' % dn)
				continue
			username = attrs.get('uid')[0]

			if not attrs.get('uidNumber', []):
				self.message('No uidNumber attribute for %s' % dn)
				continue
			uid_number = attrs.get('uidNumber')[0]

			if not attrs.get('entryUUID', []):
				self.message('No entryUUID attribute for %s' % dn)
				continue
			entryUUID = attrs.get('entryUUID')[0]

			self.debug('Changing externID for %s from %s (uidNumber) to %s (entryUUID)' % (username, uid_number, entryUUID))
			if not self.options.dry_run:
				self.cursor.execute("UPDATE users SET users.externid=%s WHERE users.externid=%s", (entryUUID, uid_number))
				self.dbconn.commit()


def main():
	m = ZarafaMigration()
	m.main()
	sys.exit(m.ret)

if __name__ == '__main__':
	main()
