#! /usr/bin/env python
# -*- coding: utf-8 -*-

# opsi-convert is part of the desktop management solution opsi
# (open pc server integration) http://www.opsi.org
# Copyright (C) 2007-2016 uib GmbH <info@uib.de>

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.

# This program is distributed 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
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""
opsi-convert converts opsi-backends.

:copyright: uib GmbH <info@uib.de>
:author: Jan Schneider <j.schneider@uib.de>
:author: Niko Wenselowski <n.wenselowski@uib.de>
:license: GNU Affero General Public License version 3
"""

import fcntl
import getpass
import os
import re
import struct
import sys
import termios

from OPSI.Logger import LOG_DEBUG, LOG_ERROR, LOG_NONE, Logger
from OPSI.Types import forceUnicode, forceHostId, forceUnicodeLower
from OPSI.Util import getfqdn
from OPSI.Util.Message import ProgressObserver
from OPSI.Backend.BackendManager import BackendManager
from OPSI.Backend.JSONRPC import JSONRPCBackend
from OPSI.Backend.Replicator import BackendReplicator

try:
	import argparse
except ImportError:
	from OPSI.Util import argparse

__version__ = '4.0.7.6'

logLevel = LOG_NONE
logger = Logger()
logger.setConsoleLevel(logLevel)
logger.setConsoleFormat('%M')
logger.setFileFormat('%D [%L] %M')
logger.setConsoleColor(True)


class ProgressNotifier(ProgressObserver):
	def __init__(self, backendReplicator):
		self.usedWidth = 120
		self.currentProgressSubject = backendReplicator.getCurrentProgressSubject()
		self.overallProgressSubject = backendReplicator.getOverallProgressSubject()
		self.currentProgressSubject.attachObserver(self)
		self.overallProgressSubject.attachObserver(self)
		self.logSubject = logger.getMessageSubject()
		self.logSubject.attachObserver(self)
		logger.setMessageSubjectLevel(LOG_ERROR)
		self.error = None

	def displayProgress(self):
		usedWidth = self.usedWidth
		try:
			tty = os.popen('tty').readline().strip()
			fd = open(tty)
			terminalWidth = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234'))[1]
			if usedWidth > terminalWidth:
				usedWidth = terminalWidth
			fd.close()
		except Exception:
			pass

		if logLevel <= LOG_NONE:
			sys.stdout.write("\033[2A")

		if self.error:
			sys.stdout.write("\033[K")
			print u"Error occurred: %s" % self.error
			sys.stdout.write("\033[K")
		self.error = None
		for subject in self.overallProgressSubject, self.currentProgressSubject:
			text = u''
			if subject is self.overallProgressSubject:
				text = u'Overall progress'
			else:
				text = subject.getTitle()
			barlen = usedWidth - 62
			filledlen = int("%0.0f" % (barlen * subject.getPercent() / 100))
			bar = u'=' * filledlen + u' ' * (barlen - filledlen)
			percent = '%0.2f%%' % subject.getPercent()
			print '%35s : %8s [%s] %5s/%-5s' % (text, percent, bar, subject.getState(), subject.getEnd())

	def progressChanged(self, subject, state, percent, timeSpend, timeLeft, speed):
		self.displayProgress()

	def messageChanged(self, subject, message):
		if subject == self.logSubject:
			self.error = message
		self.displayProgress()


def main():
	global logLevel

	parser = argparse.ArgumentParser(
		description="Convert an opsi database into an other.",
		epilog="""
The backends can either be the name of a backend as defined
in /etc/opsi/backends (file, mysql, ...) or the the url of an opsi
configuration service in the form of http(s)://<user>@<host>:<port>/rpc""")
	parser.add_argument('--version', '-V', action='version', version=__version__)
	parser.add_argument('--quiet', '-q', action='store_true', default=False,
						help="do not show progress")
	parser.add_argument('--verbose', '-v',
						dest="logLevel", default=LOG_NONE, action="count",
						help="increase verbosity (can be used multiple times)")
	parser.add_argument('--clean-destination', '-c', dest="cleanupFirst",
						default=False, action='store_true',
						help="clean destination database before writing")
	parser.add_argument('--with-audit-data', '-a', dest="audit",
						action='store_true', default=False,
						help="including software/hardware inventory")
	parser.add_argument('-s', metavar="OLD SERVER ID", dest="oldServerId",
						help="use destination host as new server")
	parser.add_argument('--log-file', '-l', dest="logFile",
						help="Log to this file. The loglevel will be DEBUG.")
	parser.add_argument('readBackend', metavar="source", help="Backend to read data from.")
	parser.add_argument('writeBackend', metavar="destination", help="Backend to write data to.")

	args = parser.parse_args()

	logLevel = args.logLevel
	logger.setConsoleLevel(logLevel)
	logger.setFileLevel(logLevel)
	if args.logFile:
		logger.setLogFile(args.logFile)

		if logLevel > LOG_DEBUG:
			logger.setFileLevel(logLevel)
		else:
			logger.setFileLevel(LOG_DEBUG)

	cleanupFirst = args.cleanupFirst
	progress = not args.quiet
	convertAudit = args.audit

	if args.oldServerId:
		newServerId = getfqdn(conf='/etc/opsi/global.conf')
		oldServerId = forceHostId(args.oldServerId)
	else:
		newServerId = None
		oldServerId = None

	readBackend = forceUnicode(args.readBackend)
	writeBackend = forceUnicode(args.writeBackend)

	# Define read/write backend
	read = {
		'username': u'',
		'password': u'',
		'address': u'',
		'backend': u''
	}
	write = {
		'username': u'',
		'password': u'',
		'address': u'',
		'backend': u''
	}

	logger.comment(u"Converting from backend '%s' to backend '%s'." % (readBackend, writeBackend))

	logger.debug(u"Parsing read backend")
	parseBackend(read, readBackend)
	logger.debug(u"Settings for read-backend: {0}".format(read))

	logger.debug(u"Parsing write backend")
	parseBackend(write, writeBackend)
	logger.debug(u"Settings for write-backend: {0}".format(write))
	if write['address'] and write['username'] and newServerId:
		match = re.search('^(\w+://)([^@]+)@([^:]+:\d+/.*)$', writeBackend)
		newServerId = match.group(3).split(':')[0]
		if re.search('^[\d\.]+$', newServerId):
			# Is an ip address
			newServerId = getfqdn(name=newServerId)
			if re.search('^[\d\.]+$', newServerId):
				raise ValueError(u"Cannot resolve '%s'" % newServerId)

	if newServerId:
		try:
			newServerId = forceHostId(newServerId)
		except Exception:
			raise ValueError(u"Bad server-id '%s' for new server" % newServerId)

	sanityCheck(read, write)

	logger.debug(u"Creating BackendManager instance for reading")
	bmRead = createBackend(read)

	logger.debug(u"Creating BackendManager instance for writing")
	bmWrite = createBackend(write)

	backendReplicator = BackendReplicator(
		readBackend=bmRead,
		writeBackend=bmWrite,
		newServerId=newServerId,
		oldServerId=oldServerId,
		cleanupFirst=cleanupFirst
	)
	if progress:
		ProgressNotifier(backendReplicator)
		print ""
	backendReplicator.replicate(audit=convertAudit)


def parseBackend(config, backendString):
	"Parse the string and write the results into config."

	match = re.search('^(\w+://)', backendString)
	if match:
		logger.debug(u"Read-backend seems to be an URL")
		match = re.search('^(\w+://)([^@]+)@([^:]+:\d+/.*)$', backendString)
		if match:
			config['backend'] = 'JSONRPC'
			config['address'] = match.group(1) + match.group(3)
			config['username'] = match.group(2)
		else:
			raise ValueError(u"Bad source URL '%s'" % backendString)
	else:
		logger.debug(u"Assuming {0!r} is a backend name.".format(backendString))
		config['backend'] = backendString


def sanityCheck(read, write):
	if read['backend'] and write['backend']:
		if read['backend'] == write['backend']:
			if read['backend'] == 'JSONRPC':
				if read['address'] == write['address']:
					raise ValueError(u"Source and destination backend are the same.")
			else:
				raise ValueError(u"Source and destination backend are the same.")


def createBackend(config):
	logger.debug(u"Creating backend instance")
	if config['address'] and not config['password']:
		logger.comment(u"Connecting to {address} as {username}".format(**config))
		config['password'] = getpass.getpass()

	config = cleanBackendConfig(config)

	try:
		backend = BackendManager(backendConfigDir=u'/etc/opsi/backends', **config)
	except Exception as error:
		logger.logException(error)
		if forceUnicodeLower(config['backend']) == u'jsonrpc':
			logger.debug(u"Creating a JSONRPC backend through BackendManager failed.")
			logger.debug(u"Trying with a direct connection.")
			backend = JSONRPCBackend(
				deflate=True,
				application='opsi-convert v{0}'.format(__version__),
				**config
			)
		else:
			raise

	return backend


def cleanBackendConfig(config):
	cleanedConfig = dict(config)
	keysToRemove = set()
	for key, value in config.items():
		if not value:
			keysToRemove.add(key)

	for key in keysToRemove:
		del cleanedConfig[key]

	return cleanedConfig


if __name__ == "__main__":
	try:
		main()
	except SystemExit:
		pass
	except Exception as e:
		logger.setConsoleLevel(LOG_ERROR)
		logger.logException(e)
		print >> sys.stderr, u"ERROR: %s" % e
		sys.exit(1)
