#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Univention Management Console
#
# Copyright 2006-2022 Univention GmbH
#
# https://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
# <https://www.gnu.org/licenses/>.

from __future__ import print_function

from argparse import ArgumentParser, FileType, Namespace  # noqa: F401
from getpass import getpass

from datetime import datetime
import os
import ast
import pprint
import locale
import sys

import six
from six.moves import input
import notifier

import univention.management.console.protocol as umcp
from univention.management.console.protocol.client import Client
from univention.management.console.log import log_init

try:
	from typing import NoReturn, Optional, Sequence  # noqa: F401
except ImportError:
	pass


class ClientExit(Exception):
	pass


class CLI_Client(Client):

	def __init__(self, options):  # type: (Namespace) -> None
		if options.unix_socket and os.path.exists(options.unix_socket):
			super(CLI_Client, self).__init__(unix=options.unix_socket, ssl=False)
		else:
			super(CLI_Client, self).__init__(servername=options.server, port=options.port)

		self.__wait = None  # type: Optional[umcp.Request]
		self._options = options
		self.__started = None  # type: Optional[datetime]
		self.language = (locale.getlocale()[0] or locale.getdefaultlocale()[0] or 'en-US').replace('_', '-')

		try:
			self.connect()
		except umcp.NoSocketError:
			print("The UMC server %s could not be contacted." % options.server, file=sys.stderr)
			sys.exit(1)

		self.signal_connect('response', self._response)
		self.signal_connect('authenticated', self._authenticated)
		self.signal_connect('closed', self._closed)
		self.signal_connect('error', self._error)
		if options.command:
			self.__wait = self.create(options.command, options.arguments, options)
		if options.authenticate:
			if self._options.timing:
				self.__started = datetime.now()
			self.authenticate(options.username, options.password)
		else:
			if self.__wait:
				self.request(self.__wait)
				self.__wait = None
		if options.exit:
			if not self._options.quiet:
				print('existing without waiting for response', file=sys.stderr)
			sys.exit(0)

	def authenticate(self, username, password, new_password=None):  # type: (str, str, str) -> None
		authRequest = umcp.Request('AUTH')
		authRequest.http_method = 'POST'
		authRequest.body['username'] = username
		authRequest.body['password'] = password
		authRequest.body['new_password'] = new_password
		authRequest.headers['Accept-Language'] = self.language
		authRequest.headers['X-Requested-With'] = 'XMLHttpRequest'
		return super(CLI_Client, self).authenticate(authRequest)

	def create(self, command, args, options, flavor=None):  # type: (str, Sequence[str], Namespace, Optional[str]) -> umcp.Request
		msg = umcp.Request(command.upper())
		msg.http_method = 'POST'
		msg.cookies['Cookie'] = 'UMCSessionId=00000000-0000-0000-0000-000000000000'
		msg.headers['X-XSRF-Protection'] = '00000000-0000-0000-0000-000000000000'
		msg.headers['Accept-Language'] = self.language
		msg.headers['X-Requested-With'] = 'XMLHttpRequest'
		msg.arguments = args
		if options.flavor is not None:
			msg.flavor = options.flavor
		if options.list_options and not options.eval_option:
			msg.options = options.options
		elif options.filename:
			msg.body = options.filename.read()
			msg.mimetype = options.mimetype
		elif options.eval_option:
			if not options.list_options:
				msg.options = ast.literal_eval(options.options[0])
			else:
				msg.options = [ast.literal_eval(x) for x in options.options]
		else:
			for opt in options.options:
				key, value = opt.split('=', 1)
				if ':' in key:
					try:
						value = {'bool': bool, 'int': int, 'str': str, 'unicode': six.text_type}[key.split(':', 1)[0]](value)
						key = key.split(':', 1)[1]
					except KeyError:
						pass
				msg.options[key] = value
		return msg

	def _error(self, error):  # type: (umcp.Response) -> NoReturn
		print("Error: %s" % (error,), file=sys.stderr)
		sys.exit(1)

	def _closed(self):  # type: () -> NoReturn
		print("Error: The connection to UMC was closed unexpectedly. Make sure the server is running", file=sys.stderr)
		sys.exit(1)

	def print_timing(self, response=None):  # type: (Optional[umcp.Response]) -> None
		if self._options.timing:
			return
		if self.__started is not None:
			finished = datetime.now()
			diff = finished - self.__started
			if response is not None:
				print('>>> Timing:', response.command, ' '.join(response.arguments))
			else:
				print('>>> Timing (Authentication):')
			print(' Request send at', self.__started)
			print(' Response received at', finished)
			print(' Elapsed time', diff)

		self.__started = None

	def _authenticated(self, success, response):  # type: (bool, umcp.Response) -> None
		self.print_timing()
		if success:
			if self.__wait:
				if self._options.timing:
					self.__started = datetime.now()
				self.request(self.__wait)
				self.__wait = None
		else:
			print('error: authentication failed', file=sys.stderr)
			sys.exit(1)

	def _response(self, msg):  # type: (umcp.Response) -> NoReturn
		self.print_timing(msg)
		if self._options.quiet:
			raise ClientExit(msg.status)
		print('Response: %s' % msg.command)
		print('  data length   : %4d' % len(str(msg)))
		print('  message length: %4d' % msg._length)
		print('  ---')
		if msg.arguments:
			if self._options.prettyprint:
				print('  ARGUMENTS: %s' % pprint.pformat(msg.arguments))
			else:
				print('  ARGUMENTS: %s' % ' '.join(msg.arguments))
		print('MIMETYPE   : %s' % msg.mimetype)
		if msg.mimetype == umcp.MIMETYPE_JSON:
			print('  STATUS   : %d' % msg.status)
			if msg.options:
				if self._options.prettyprint:
					print('  OPTIONS  : %s' % pprint.pformat(msg.options, indent=2))
				else:
					if isinstance(msg.options, (list, tuple)):
						print('  OPTIONS  : %s' % ', '.join(str(x) for x in msg.options))
					else:
						print('  OPTIONS  : %s' % ' '.join(['%s=%s' % (k, v) for k, v in msg.options.items()]))
			print('  MESSAGE  : %s' % msg.message)
			if msg.error:
				print('  ERROR    : %r' % (msg.error,))
			result = msg.result
			if not result:
				result = msg.body
			if self._options.prettyprint:
				print('  RESULT   : %s' % pprint.pformat(result, indent=2))
			else:
				print('  RESULT   : %s' % result)
			if msg.status is not None:
				raise ClientExit(msg.status)
		else:
			print('BODY    : %s' % str(msg.body))
		raise ClientExit()


def parse_args(argv=sys.argv):  # type: (Sequence[str]) -> Namespace
	parser = ArgumentParser()
	group = parser.add_argument_group('General')
	group.add_argument(
		'-d', '--debug', type=int, default=0,
		help='if given than debugging is activated and set to the specified level [default: %(default)s]')
	group.add_argument(
		'-q', '--quiet', action='store_true',
		help='if given no output is generated')
	group.add_argument(
		'-r', '--pretty-print', action='store_true', dest='prettyprint',
		help='if given the output will be printed out using pretty print')
	group.add_argument(
		'-t', '--timing', action='store_true',
		help='if given the amount of time required for the UMCP request is measured. -q will not suppress the output')
	parser.add_argument_group(group)

	group = parser.add_argument_group('Connection')
	group.add_argument(
		'-n', '--no-auth', action='store_false', dest='authenticate',
		help='if given the client do not try to authenticate first')
	group.add_argument(
		'-p', '--port', type=int, default=6670,
		help='defines the port to connect to [default: %(default)s]')
	group.add_argument(
		'-P', '--password',
		help='set password for authentication')
	group.add_argument(
		'-y', '--password_file', type=FileType("r"),
		help='read password for authentication from given file')
	group.add_argument(
		'-s', '--server', default='localhost',
		help='defines the host of the UMC daemon to connect to [default: %(default)s]')
	group.add_argument(
		'-u', '--unix-socket',
		help='defines the filename of the UNIX socket')
	group.add_argument(
		'-U', '--username',
		help='set username for authentication')
	group.add_argument(
		'-x', '--exit', action='store_true',
		help='if given, the client send the request to the server and exits directly after it without waiting for the response')
	parser.add_argument_group(group)

	group = parser.add_argument_group('Request arguments')
	group.add_argument(
		'-e', '--eval-option', action='store_true',
		help='if set the only given option is evalulated as python code')
	group.add_argument(
		'-f', '--flavor',
		help='set the required flavor')
	group.add_argument(
		'-F', '--filename', type=FileType("r"),
		help='if given the content of the file is send as the body of the UMCP request. Additionally the mime type must be given')
	group.add_argument(
		'-l', '--list-options', action='store_true',
		help='if set all specified options will be assembled in a list')
	group.add_argument(
		'-m', '--mimetype', default=umcp.MIMETYPE_JSON,
		help='defines the mime type of the UMCP request body [default: %(default)s].')
	group.add_argument(
		'-o', '--option', default=[], action='append', dest='options',
		help='append an option to the request')
	parser.add_argument_group(group)

	prog = os.path.basename(sys.argv[0])
	command = prog[4:] if prog.startswith("umc-") and prog != "umc-client" else ""
	if command:
		parser.set_defaults(command=command)
	else:
		parser.add_argument('command', help="UMC command")
	parser.add_argument('arguments', nargs='*', default=[], help="UMC command arguments")

	arguments = parser.parse_args()

	return arguments


def main():  # type: () -> None
	arguments = parse_args()

	notifier.init(notifier.GENERIC)
	try:
		locale.setlocale(locale.LC_ALL, "")
	except locale.Error:
		pass

	log_init('/dev/stderr', arguments.debug)

	if arguments.authenticate:
		if not arguments.username:
			arguments.username = input('Username:')
		if arguments.password_file:
			arguments.password = arguments.password_file.read().strip()
		if not arguments.password:
			arguments.password = getpass('Password:')

	try:
		CLI_Client(arguments)
	except Exception as exc:
		print('An fatal error occurred: %s' % (exc,), file=sys.stderr)
		sys.exit(1)

	try:
		notifier.loop()
	except ClientExit as exit:
		if exit.args:
			exitcode = int(exit.args[0])
			if 200 <= exitcode < 300:
				exitcode = 0
			else:
				exitcode = 1  # dooh, sys.exit returns a short, mapping 400 to 144
			sys.exit(exitcode)
		sys.exit(0)


if __name__ == '__main__':
	main()
