#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Univention Management Console
#
# Like what you see? Join us!
# https://www.univention.com/about-us/careers/vacancies/
#
# Copyright 2006-2023 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

import ast
import locale
import os
import pprint
import sys
from argparse import ArgumentParser, FileType, Namespace  # noqa: F401
from datetime import datetime
from getpass import getpass

import notifier
import six
from six.moves import input

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


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('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()
