#!/usr/bin/python2.7
# -*- coding: utf-8 -*-
#
# Manage the OX listener queue
#
# Copyright (C) 2017 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 re
import sys
import pickle
import shutil
import argparse
import datetime
import subprocess
from collections import OrderedDict
from pick import pick


BACKUP_PATH = '/var/spool/univention-directory-listener_backup'


class OXQueueFile(object):
	def __init__(self, filename, path, ox_module, options_str=None, dn=None):
		self.filename = filename
		self.path = path
		self.ox_module = ox_module
		self.options_str = options_str
		self.dn = dn


class OXQueueFilesHandling(object):
	queue_paths = OrderedDict((
		('context', '/var/spool/univention-directory-listener/ox-context'),
		('domain', '/var/spool/univention-directory-listener/ox-domain'),
		('groups', '/var/spool/univention-directory-listener/ox-groups'),
		('resource', '/var/spool/univention-directory-listener/ox-resource'),
		('user', '/var/spool/univention-directory-listener/ox-user'),
	))

	def __init__(self, **kwargs):
		self.filter = kwargs['regex']
		self.filenames_only = kwargs['filenames_only']
		self.wide = kwargs['wide']
		self.pickle_files = dict()
		self.backup_path = os.path.join(BACKUP_PATH, datetime.datetime.now().strftime('%Y-%m-%d_%H:%M:%S'))

	def log(self, msg):
		if not self.filenames_only:
			print(msg)

	def _run_cmd(self, cmd):
		cmd_proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
		cmd_out, cmd_err = cmd_proc.communicate()
		if cmd_proc.returncode:
			self.log('Command {!r} returned with exit code {!r}. stdout={!r} stderr={!r}'.format(
				cmd, cmd_proc.returncode, cmd_out, cmd_err))

	def start_listener(self):
		self.log('Starting univention-directory-listener...')
		self._run_cmd(['systemctl', 'start', 'univention-directory-listener.service'])

	def stop_listener(self):
		self.log('Stopping univention-directory-listener...')
		self._run_cmd(['systemctl', 'stop', 'univention-directory-listener.service'])

	def archive(self):
		self.stop_listener()
		self.log('Moving files to {}:'.format(self.backup_path))
		try:
			if not os.path.isdir(BACKUP_PATH):
				os.mkdir(BACKUP_PATH, 0o700)
			if not os.path.isdir(self.backup_path):
				os.mkdir(self.backup_path, 0o700)

			for a_file in self.pickle_files_sorted:
				self.log('  {}'.format(a_file.path))
				target_dir = os.path.join(self.backup_path, 'ox-{}'.format(a_file.ox_module))
				if not os.path.isdir(target_dir):
					os.mkdir(target_dir, 0o700)
				shutil.move(a_file.path, target_dir)
		finally:
			self.start_listener()
		self.log('Done.')
		return 0

	@staticmethod
	def cancel():
		return 0

	def delete(self):
		self.stop_listener()
		try:
			for a_file in self.pickle_files_sorted:
				self.log('Deleting {}...'.format(a_file.path))
				os.remove(a_file.path)
		finally:
			self.start_listener()
		self.log('Done.')
		return 0

	def collect_pickle_files(self):
		for ox_module, queue_path in self.queue_paths.items():
			if not os.path.isdir(queue_path):
				continue
			for filename in [f for f in os.listdir(queue_path) if f != 'lock.tmp']:
				path = os.path.join(queue_path, filename)
				try:
					with open(path, 'rb') as fp:
						data = pickle.load(fp)
				except (IOError, KeyError):
					self.log('Not a or broken pickle file: {!r}'.format(path))
					continue
				if not all(hasattr(data, attr) for attr in ('action', 'dn', 'new', 'old', 'timestamp')):
					self.log('Not a or broken queue file: {!r}'.format(path))
					continue
				if self.filter and not self.filter.match(data.dn):
					continue
				name = data.new.get('uid') or data.old.get('uid')
				if not name:
					name = data.new.get('cn') or data.old.get('cn')
				name = name[0]
				date = datetime.datetime.fromtimestamp(float(data.timestamp))
				if self.wide:
					dn_s = ' | {}'.format(data.dn)
				else:
					dn_s = ''
				options_str = '{} | {:8} | {} | {:18} | {}{}'.format(
					date.strftime('%Y-%m-%d %H:%M:%S'),
					ox_module,
					data.action or ' ',
					name,
					data.timestamp,
					dn_s
				)
				self.pickle_files[filename] = OXQueueFile(
					filename=filename,
					path=path,
					ox_module=ox_module,
					options_str=options_str,
					dn=data.dn
				)
		return self.pickle_files

	def list(self):
		if self.filenames_only:
			print(' '.join(f.path for f in self.pickle_files_sorted))
		else:
			print('len(f.dn) for f in self.pickle_files.values()={!r}'.format([len(f.dn) for f in self.pickle_files.values()]))
			dn_len = max(len(f.dn) for f in self.pickle_files.values())
			print('        Date        |  Module  | * |        Name        |       Timestamp{}'.format(
				'       |{}DN'.format(' ' * (dn_len / 2 - 2)) if self.wide else ''
			))
			print('-' * (82 + dn_len if self.wide else 79))
			print('\n'.join(f.options_str for f in self.pickle_files_sorted))
			print('\n(*) The command (a/m/d/n) is only available for user and group objects.')
		return 0

	@property
	def pickle_files_sorted(self):
		return [self.pickle_files[filename] for filename in sorted(self.pickle_files.keys())]


def parse_cmdline():
	parser = argparse.ArgumentParser(
		description='Manage the OX listener queue',
		epilog='An interactive mode will be started, when no action (archive/delete/list) is chosen.'
	)
	parser.add_argument("--archive", help='Move queue files to {}.'.format(BACKUP_PATH), action='store_true')
	parser.add_argument("--delete", help='Delete queue files.', action='store_true')
	parser.add_argument("--list", help='Print a list of queue files and related information.', action='store_true')
	parser.add_argument('--filter', help="Optional regular expression to limit actions to only those DNs that match (interactive and non-interactive), e.g. 'uid=alice,' or '^cn=.*,ou=myschool,'. Use single tick quoting to prevent shell expansion.")
	parser.add_argument('--wide', help='Also show the DN (very long lines).', action='store_true')
	parser.add_argument('--filenames-only', help='Option to use together with "--list": print a space separated list of filenames, no description.', action='store_true')
	args = parser.parse_args()

	if args.filenames_only and not args.list:
		parser.error('Option "--filenames-only" can only be used together with "--list".')
	if sum((int(args.archive), int(args.delete), int(args.list))) > 1:
		parser.error('Options --archive, --delete and --list cannot be used at the same time.')
	if args.filter:
		try:
			args.regex = re.compile(args.filter)
		except re.error as exc:
			parser.error('Error compiling filter as a regular expression: {}'.format(exc))
	else:
		args.regex = None
	args.interactive = not any((args.archive, args.delete, args.list))
	return args


def main():
	args = parse_cmdline()
	handler = OXQueueFilesHandling(**args.__dict__)
	handler.collect_pickle_files()

	if not handler.pickle_files:
		handler.log('No queue files found.')
		return 0

	if not args.interactive and args.archive:
		return handler.archive() or 0
	if not args.interactive and args.delete:
		return handler.delete() or 0
	if not args.interactive and args.list:
		handler.list()
		return 0

	file_options = OrderedDict((f.options_str, f) for f in handler.pickle_files_sorted)
	title = 'Please choose the files you want to archive or remove (press SPACE to mark,\nENTER to continue):'
	try:
		selected = pick(file_options.keys(), title, multi_select=True)
	except KeyboardInterrupt:
		return 1

	if not selected:
		handler.log('Nothing selected.')
		return 0

	selected_files = [file_options[x] for x in (s[0] for s in selected)]
	for k, v in handler.pickle_files.items():
		if v not in selected_files:
			del handler.pickle_files[k]

	title = 'Please choose the the operation you wish to perform on the\nselected files:\n    {}'.format(
		'\n    '.join(f.path for f in handler.pickle_files_sorted)
	)
	action_options = OrderedDict((
		('Archive in {}'.format(handler.backup_path), handler.archive),
		('Delete', handler.delete),
		('Cancel', handler.cancel),
	))
	try:
		option, index = pick(action_options.keys(), title)
	except KeyboardInterrupt:
		return 1
	return action_options[option]() or 0


if __name__ == '__main__':
	sys.exit(main())
