#!/usr/bin/python3

# New user registration program

import argparse
import gettext
import gi
import json
import os
import socket
import subprocess
import sys
import threading
import time

gi.require_version('Gdk', '3.0')
gi.require_version('Gtk', '3.0')
from gi.repository import Gdk, GdkPixbuf, Gio, GLib, Gtk

gettext.bindtextdomain('puavo-user-registration', '/usr/share/locale')
gettext.textdomain('puavo-user-registration')
_tr = gettext.gettext

sys.path.append('/usr/share/puavo-user-registration')

from logger import log

import utils
import privacy_policy
from network_thread import NetworkThread
from page_welcome   import PageWelcome
from page_network   import PageNetwork
from page_account   import PageAccount
from page_login     import PageLogin
from page_complete  import PageComplete

# ==============================================================================
# ==============================================================================


class NewUserRegistrationWindow:
    def __init__(self, application, settings):
        self.application = application

        self.data_dir = settings['data_dir']
        self.devel_mode = settings['devel_mode']
        self.machine_password = settings['machine_password']
        self.organisation_domain = settings['organisation_domain']
        self.username = None
        self.password = None

        self.local_login_done = False

        self.page_definitions = [
            (PageWelcome),
            (PageNetwork),
            (PageLogin),
            (PageAccount),
            (PageComplete),
        ]

        self.all_pages = []
        self.current_page = None
        self.current_page_num = None

        # Unfortunately, we're still plagued by the same problem
        # the previous version had: we have to adjust font sizes
        # and other scales for various screen sizes.

        # The entry font sizes WILL vary slightly if you keep
        # clicking prev/next. I don't know why :-(

        try:
            screen_width = Gdk.Screen.get_default().width()
        except Exception as e:
            log.warning('unable to determine the screen width: %s', str(e))
            screen_width = 1366

        try:
            self.hostname = open('/etc/puavo/hostname', 'rb') \
                                   .read().decode('utf-8').strip()
        except Exception as e:
            log.fatal('could not read /etc/puavo/hostname')
            return

        if self.devel_mode:
            screen_width = 1366
            screen_height = 768

        if screen_width >= 1920:
            css_variables = {
                'background_image': 'background_1920.png',
                'entry_font_size': '24px',
                'gtk_dialog_font_scale': '192%',
                'main_font_scale': '144%',
            }
        elif screen_width >= 1600:
            css_variables = {
                'background_image': 'background_1920.png',
                'entry_font_size': '20px',
                'gtk_dialog_font_scale': '160%',
                'main_font_scale': '120%',
            }
        elif screen_width >= 1366:
            css_variables = {
                'background_image': 'background_1600.png',
                'entry_font_size': '17px',
                'gtk_dialog_font_scale': '136%',
                'main_font_scale': '102%',
            }
        elif screen_width >= 1024:
            css_variables = {
                'background_image': 'background_1366.png',
                'entry_font_size': '12px',
                'gtk_dialog_font_scale': '102%',
                'main_font_scale': '76%',
            }
        else:
            css_variables = {
                'background_image': 'background_1024.png',
                'entry_font_size': '8px',
                'gtk_dialog_font_scale': '64%',
                'main_font_scale': '48%',
            }

        css_variables['background_image'] = \
            os.path.join(self.data_dir, css_variables['background_image'])

        try:
            # Load the main UI file
            self.main_builder = Gtk.Builder()
            self.main_builder.set_translation_domain('puavo-user-registration')
            self.main_builder.add_from_file(os.path.join(self.data_dir,
                                                         'main_window.glade'))

            # Load custom CSS
            css = open(os.path.join(self.data_dir, 'main_window.css')).read()

            for key, value in css_variables.items():
                css = css.replace('$(' + key + ')', str(value))

            style_provider = Gtk.CssProvider()
            style_provider.load_from_data(bytes(css, 'utf-8'))

            Gtk.StyleContext.add_provider_for_screen(
                Gdk.Screen.get_default(),
                style_provider,
                Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)

            # Setup event handling
            self.main_builder.connect_signals(self)

        except Exception as e:
            log.fatal('%s', str(e), exc_info=True)
            return

        self.main_window = self.main_builder.get_object('main_window')
        self.main_window.set_application(self.application)
        self.main_window.set_icon_name('user-info')

        if self.devel_mode:
            self.main_window.set_size_request(screen_width, screen_height)
        else:
            self.main_window.fullscreen()
            self.main_window.set_skip_taskbar_hint(True)
            self.main_window.set_skip_pager_hint(True)
            self.main_window.set_deletable(False)
            self.main_window.set_decorated(False)

        # If we have a privacy policy, show the button
        self.__privacy_policy_file = \
            os.path.realpath(os.path.join(self.data_dir, 'privacy_policy.html'))

        if os.path.exists(self.__privacy_policy_file):
            log.info('privacy policy file "%s" exists',
                     self.__privacy_policy_file)
            self.main_builder.get_object('privacy_policy').show()

        # Try to set the lower corner logo
        self.__logo_file = os.path.realpath(os.path.join(self.data_dir, 'lower_logo.png'))

        if os.path.exists(self.__logo_file):
            log.info('lower corner logo file "%s" exists', self.__logo_file)

            logo = self.main_builder.get_object('logo')

            try:
                pixbuf = \
                    GdkPixbuf.Pixbuf.new_from_file_at_scale(
                        filename=self.__logo_file,
                        width=-1,
                        height=32,
                        preserve_aspect_ratio=True)

                logo.set_from_pixbuf(pixbuf)
                logo.show()
            except Exception as e:
                logo.hide()
                log.error('could not set the lower corner logo image: %s',
                          str(e), exc_info=True)

        self.main_title = self.main_builder.get_object('main_title')

        self.desktop_button = self.main_builder.get_object('go_to_desktop')

        middle_box = self.main_builder.get_object('middle_box')
        middle_box.show_all()

        # Build the child pages
        try:
            for ctor in self.page_definitions:
                page = ctor(self, self.main_window, middle_box, self.data_dir,
                    self.main_builder)
                self.all_pages.append(page)
                if ctor == PageAccount:
                    self.account_page = page
                elif ctor == PageComplete:
                    self.complete_page = page
                elif ctor == PageLogin:
                    self.login_page = page
        except Exception as e:
            log.error('%s', str(e), exc_info=True)
            self.main_window.destroy()
        else:
            # Activate the initial page
            self.goto_page_by_num(0)

        self.start_primary_user_lookup()

        self.main_window.show()


    def check_host(self, conn):
        machine_data = self.get_machine_data()
        json_data = json.dumps(machine_data, ensure_ascii=False)
        headers = { 'Content-type': 'application/json', }
        conn.request('POST',
                     '/register_user/check_host',
                     body=bytes(json_data, 'utf-8'),
                     headers=headers)


    def start_primary_user_lookup(self):
        self.pul_thread_event = threading.Event()
        self.pul_thread = NetworkThread(self.check_host,
                                        self.pul_thread_event)
        self.pul_thread.daemon = True
        self.pul_thread.start()

        # This will interpret the server's response
        GLib.idle_add(self.pul_idle_func,
                      self.pul_thread_event,
                      self.pul_thread)


    def pul_idle_func(self, event, thread):
        if event.is_set():
            try:
                if thread.response['failed'] or thread.response['code'] != 200:
                    raise Exception('looking up primary user failed')

                server_data = json.loads(thread.response['data'])
                log.info('got server response: %s' % server_data)
                if type(server_data) != dict:
                    raise Exception('server did not return a dict')
                if server_data['status'] != 'device_already_in_use':
                    return False
                if server_data['primary_user'] == None:
                    return False
                if type(server_data['primary_user']) != str:
                    raise Exception('server did not return primary user as string')
                log.info('this host has primary user set to %s',
                         server_data['primary_user'])

                self.login_page.set_primary_user(server_data['primary_user'])
                if self.current_page == self.account_page:
                    self.goto_page(self.login_page)

            except Exception as e:
                log.error('error when looking up primary user: %s', str(e))

            return False

        time.sleep(0.05)

        return True


    def close(self, *args):
        self.main_window.destroy()


    def go_to_desktop_clicked(self, *args):
        dialog = Gtk.MessageDialog(parent=self.main_window,
            message_type=Gtk.MessageType.WARNING,
            buttons=Gtk.ButtonsType.OK_CANCEL, text=_tr('Go to desktop?'))

        dialog.format_secondary_text( \
            _tr('Are you sure you do not want to create a user account?  ' \
                'Without a user account, your files will be lost after your ' \
                'desktop session ends.'))

        response = dialog.run()
        dialog.destroy()

        if response == Gtk.ResponseType.OK:
            self.main_window.destroy()


    # Show the privacy policy dialog
    def privacy_policy_clicked(self, *args):
        dlg = privacy_policy.PrivacyPolicy(self.main_window, self.__privacy_policy_file)
        dlg.run()
        dlg.destroy()


    # Called from account creation page when the process is complete.
    # Saves the username and password, so they can be used.
    def save_user_details(self, username, password):
        # These variables are undefined unless this method is called,
        # so if someone calls finish_registration() without calling
        # this method first, the process ends in a fatal error.
        self.username = username
        self.password = password

        # tell complete page that we created an account
        self.complete_page.set_account_created()


    def login_locally(self):
        if self.devel_mode:
            log.info('skipping local login with username="%s", password="%s"' \
                       ' due to devel mode',
                     self.username, self.password)
            return True

        if self.local_login_done:
            return True

        if not (self.username and self.password):
            raise Exception(_tr('internal error: username and password missing'))

        log.info('local login: trying username="%s"', self.username)

        cmd = ['sudo', '-n', '/usr/lib/puavo-ltsp-client/firstlogin-to-account',
               self.username]

        proc = subprocess.Popen(cmd,
                                stdin=subprocess.PIPE,
                                stdout=subprocess.DEVNULL,
                                stderr=subprocess.DEVNULL)

        proc.stdin.write(self.password.encode())
        proc.stdin.close()
        proc.wait()

        if proc.returncode == 0:
            log.info('login locally with username="%s" was successful',
                     self.username)
            self.local_login_done = True
            return True

        return False


    def disable_registration_window(self):
        cmd = ['sudo', '-n',
               '/usr/sbin/puavo-conf-local',
               'puavo.xsessions.user_registration.enabled',
               'false']

        log.info('disabling registration window')

        if not self.devel_mode:
            subprocess.check_call(cmd)


    # Called from the "complete" page to finish up the operation
    def finish_registration(self):
        log.info('finishing up the registration process')

        error_msg = ''
        login_ok = False
        try:
            login_ok = self.login_locally()
            if not login_ok:
                raise Exception('login failed after registration')
        except Exception as e:
            log.fatal('%s', str(e), exc_info=True)
            error_msg \
              = _tr('Could not do initial login with new user credentials.' \
                    '  Please remember your username "%s" and password for the first login.' \
                    '  Contact support if there are problems.') % self.username
        if error_msg:
            utils.show_error_message(self.main_window, _tr('Error'), error_msg)

        # disable registration window only in case we had a successful login
        if login_ok:
            try:
                self.disable_registration_window()
            except Exception as e:
                log.error('%s', str(e), exc_info=True)
                utils.show_error_message( \
                    self.main_window,
                    _tr('Error'),
                    _tr('Could not disable the registration tool.  Please contact support.'))


    def reboot(self):
        try:
            log.info('rebooting now')
            if not self.devel_mode:
                subprocess.run(['systemctl', 'reboot', '-i'])
            self.main_window.destroy()
        except Exception as e:
            log.error('%s', str(e), exc_info=True)
            utils.show_error_message( \
                self.main_window,
                _tr('Reboot'),
                _tr('Could not reboot.'))


    def login_locally_from_loginpage(self, username, password):
        if password == '':
            return
        self.username = username
        self.password = password

        try:
            login_ok = self.login_locally()
            if not login_ok:
                utils.show_error_message(self.main_window, _tr('Error'),
                                        _tr('Bad username and/or password'))
            else:
                self.goto_page(self.complete_page)
        except Exception as e:
            log.warning('unexpected error in local login: %s', str(e))
            utils.show_error_message(self.main_window, _tr('Error'),
                _tr('An unexpected error occurred'))


    # Returns the machine data needed on the accounts page, when sending
    # data to the server
    def get_machine_data(self):
        data = {
            'organisation_domain': self.organisation_domain,
            'password': self.machine_password
        }

        if self.devel_mode:
            # This machine does not exist anywhere, development purposes only
            data['dn'] = 'puavoId=37,ou=Devices,ou=Hosts,dc=edu,dc=hogwarts,dc=net'
            data['hostname'] = 'laptop1'
        else:
            data['dn'] = open('/etc/puavo/ldap/dn', 'rb').read().decode('utf-8').strip()
            data['hostname'] = self.hostname

        return data


    def previous_page(self):
        next_page_num = self.current_page_num - 1

        if next_page_num < 0:
            next_page_num = len(self.page_definitions) - 1

        self.goto_page_by_num(next_page_num)


    def next_page(self):
        next_page_num = (self.current_page_num + 1) % len(self.page_definitions)
        self.goto_page_by_num(next_page_num)


    def goto_page(self, page):
        if page == self.current_page:
            return

        # Hide the previous page first
        if self.current_page is not None:
            self.current_page.deactivate()

        self.current_page = page

        # Update the main title
        title = self.current_page.get_main_title()

        if title is None:
            self.main_title.hide()
        else:
            self.main_title.set_label(str(title))
            self.main_title.show()

        # Some pages disable the "Go to desktop" button
        self.desktop_button.set_sensitive(self.current_page.enable_desktop_button())

        # Switch the page
        self.current_page.activate()


    def goto_page_by_num(self, page_num):
        self.current_page_num = page_num
        self.goto_page(self.all_pages[page_num])


class UserRegistrationApplication(Gtk.Application):
    def __init__(self, application_id, flags, settings):
        Gtk.Application.__init__(self, application_id=application_id, flags=flags)
        self.connect('activate', self.new_window, settings)


    def new_window(self, *args):
        NewUserRegistrationWindow(self, args[1])


# ==============================================================================
# ==============================================================================


# Handle command-line arguments
parser = argparse.ArgumentParser(
    formatter_class=argparse.RawDescriptionHelpFormatter)

parser.add_argument('--devel',
                    action='store_true',
                    required=False,
                    help='enable development mode')

args = parser.parse_args()

devel_mode = args.devel

settings = {
    'devel_mode': devel_mode,
}

try:
    if devel_mode:
        # This password is a randomly generated string, the "machine"
        # does not actually exist
        settings['machine_password'] = '7nj7am5d09kug3ed4g24z3zx3x6pek09ybrdbfrd'
        settings['organisation_domain'] = 'hogwarts.puavo.net'
    else:
        get_password_cmd = ['sudo', '-n', 'cat', '/etc/puavo/ldap/password']
        settings['machine_password'] = subprocess.check_output(get_password_cmd).decode('utf-8').strip()
        settings['organisation_domain'] = open('/etc/puavo/domain', 'rb').read().decode('utf-8').strip()
except subprocess.CalledProcessError:
    errmsg = _tr('You do not have the required permissions to use this tool.')
    utils.show_error_message(None, _tr('No permissions'), errmsg)
    exit(1)

if devel_mode:
    settings['data_dir'] = './data'
else:
    settings['data_dir'] = '/usr/share/puavo-user-registration/data'

try:
    app = UserRegistrationApplication('opinsys.new_user_registration',
                                      Gio.ApplicationFlags.FLAGS_NONE,
                                      settings)

    app.run()
except Exception as e:
    log.fatal('TOP-LEVEL EXCEPTION CAUGHT')
    log.fatal('%s', str(e), exc_info=True)
    exit(1)
