# Copyright 3D Control Systems, Inc. All Rights Reserved 2017-2019.
# Built in San Francisco.

# This software is distributed under a commercial license for personal,
# educational, corporate or any other use.
# The software as a whole or any parts of it is prohibited for distribution or
# use without obtaining a license from 3D Control Systems, Inc.

# All software licenses are subject to the 3DPrinterOS terms of use
# (available at https://www.3dprinteros.com/terms-and-conditions/),
# and privacy policy (available at https://www.3dprinteros.com/privacy-policy/)

import os
import simplejson as json
import paths
import logging
import marshal

import config
from http_client import HTTPClient, HTTPClientPrinterAPIV1
from awaitable import Awaitable


class UserLogin(Awaitable):

    NAME = 'user login'
    STORAGE_PATH = os.path.join(paths.CURRENT_SETTINGS_FOLDER, 'stuff.bin')
    DEFAULT_PROFILES_FILE_PATH = os.path.join(os.path.dirname(__file__), 'default_printer_profiles.json')
    STORED_PROFILES_FILE_PATH = os.path.join(paths.CURRENT_SETTINGS_FOLDER, 'printer_profiles.json')

    @staticmethod
    def load_stuffbin(): #TODO rename this to show that auth_tokens can be read here too
        logger = logging.getLogger(__name__)
        try:
            with open(UserLogin.STORAGE_PATH, "rb") as f:
                return marshal.load(f)
        except FileNotFoundError:
            pass
        except Exception as e:
            logger.info("Can't read or decode login storage file: " + str(e))
            try:
                UserLogin.logout()
            except:
                pass

    @staticmethod
    def load_login():
        login, password = None, None
        decoded_data = UserLogin.load_stuffbin()
        if decoded_data:
            if type(decoded_data) == tuple or type(decoded_data) == list:
                login = decoded_data[0]
                password = decoded_data[1]
                if type(login) == bytes:
                    login = str(login, 'latin1')
                    password = str(password, 'latin1')
            elif type(decoded_data) == dict:
                login = decoded_data.get('login')
                password = decoded_data.get('password')
            else:
                logger = logging.getLogger(__name__)
                logger.warning('Unrecognized data format in stuff.bin. Assuming corrupted file. Removing.')
                UserLogin.logout()
        return login, password

    @staticmethod
    def save_login(login, password):
        marshalled_data = marshal.dumps({'login': login, 'password': password})
        UserLogin.save_data(marshalled_data)

    @staticmethod
    def save_data(data): #login storage with some rational paranoia
        logger = logging.getLogger(__name__)
        new_storage_file = os.path.join(paths.CURRENT_SETTINGS_FOLDER, 'new_stuff.bin')
        try:
            with open(new_storage_file, "wb") as f:
                f.write(data)
        except (IOError, OSError) as e:
            logger.warning('Error writing login to storage file: ' + str(e))
        else:
            try:
                os.rename(new_storage_file, UserLogin.STORAGE_PATH)
                return True
            except (IOError, OSError) as e:
                logger.warning('Error renaming login to storage file: ' + str(e))
                try:
                    os.remove(UserLogin.STORAGE_PATH)
                except OSError:
                    pass
                try:
                    os.rename(new_storage_file, UserLogin.STORAGE_PATH)
                    return True
                except (IOError, OSError) as e:
                    logger.warning('Error renaming login to storage file even after removal of some logs: ' + str(e))

    #TODO this and static method forget_auth_tokens should be separated
    @staticmethod
    def logout():
        logger = logging.getLogger(__name__)
        logger.info("Removing login file")
        try:
            os.remove(UserLogin.STORAGE_PATH)
        except FileNotFoundError:
            logger.info("No login file to remove")
        else:
            logger.info("Login file removed")

    @staticmethod
    def apply_settings_mod(settings_mod):
        current_settings = config.get_settings()
        new_settings = config.merge_dictionaries(current_settings, settings_mod, overwrite=True)
        config.Config.instance().settings = new_settings
        logger = logging.getLogger(__name__)
        logger.info("Setting modifications:\n" + str(new_settings))

    @staticmethod
    def load_stored_profiles():
        for path in (UserLogin.STORED_PROFILES_FILE_PATH, UserLogin.DEFAULT_PROFILES_FILE_PATH):
            try:
                with open(path) as f:
                    return json.load(f)
            except FileNotFoundError:
                pass
            except json.JSONDecodeError as e:
                logger = logging.getLogger(__name__)
                logger.warning("Error loading profile file %s: %s" % (path, str(e.message)))

    @staticmethod
    def save_profiles(profiles):
        try:
            profiles = json.dumps(profiles, sort_keys = True, indent = 4, separators = (',', ': '))
            with open(UserLogin.STORED_PROFILES_FILE_PATH, "w") as f:
                f.write(profiles)
        except (OSError, ValueError) as e:
            logger = logging.getLogger(__name__)
            logger.warning("Error saving profile file %s: %s" % (profiles, str(e.message)))

    @staticmethod
    def load_printer_auth_tokens():
        decoded_data = UserLogin.load_stuffbin()
        if type(decoded_data) == dict:
            auth_tokens = decoded_data.get('auth_tokens')
            if type(auth_tokens) == list:
                return auth_tokens
        return []

    def __init__(self, parent, auto_login=True):
        self.user_token = None
        self.parent = parent
        self.login = None
        self.mac = ''
        self.profiles = {}
        self.auth_tokens = []
        Awaitable.__init__(self, parent, lambda: bool(self.profiles))
        if not config.get_settings()['protocol']['user_login']:
            self.auth_tokens = self.load_printer_auth_tokens()
            self.logger.info("Getting profiles without user login")
            self.http_connection = HTTPClientPrinterAPIV1(self.parent, exit_on_fail=True)
            profiles = self.http_connection.pack_and_send(HTTPClientPrinterAPIV1.PRINTER_PROFILES)
            if profiles:
                self.logger.info("Got profiles for %d printers" % len(profiles))
                self.profiles = profiles
                self.save_profiles(profiles)
            else:
                self.profiles = self.load_stored_profiles()
            self.http_connection.close()
        else:
            if auto_login:
                login, password = UserLogin.load_login()
            self.http_connection = HTTPClient(self.parent)
            if login:
                error = self.login_as_user(login, password, save_password_flag=False)
                if error:
                    self.logger.info(str(error))
            self.http_connection.close()

    def login_as_user(self, login=None, password=None, disposable_token=None, save_password_flag=True):
        if not login:
            return 0, "Empty login"
        if password is None and not disposable_token:
            return 0, "Empty password"
        answer = self.http_connection.pack_and_send(HTTPClient.USER_LOGIN, login, password, disposable_token=disposable_token)
        if answer:
            user_token = answer.get('user_token')
            profiles_str = answer.get('all_profiles')
            settings_mod = answer.get('settings_mod')
            if settings_mod:
                self.apply_settings_mod(settings_mod)
            error = answer.get('error', None)
            if error:
                self.logger.warning("Error processing user_login " + str(error))
                self.logger.error("Login rejected")
                return error['code'], error['message']
            if login and save_password_flag:
                self.save_login(login, password)
            try:
                if not profiles_str:
                    raise RuntimeError("Server returned empty profiles on user login")
                else:
                    profiles = json.loads(profiles_str)
                    UserLogin.save_profiles(profiles)
            except Exception as e:
                self.user_token = user_token
                self.logger.warning("Error while parsing profiles: " + str(e))
                return 42, "Error parsing profiles"
            else:
                self.profiles = profiles
                self.mac = self.http_connection.mac
                self.user_token = user_token
                if login:
                    self.login = login
                else:
                    login_name = answer.get('user_login')
                    if login_name:
                        self.login = login_name
                    else:
                        self.login = "Temporary login"
                self.logger.info("Successful login from user %s" % self.login)

    def save_printer_auth_token(self, usb_info, auth_token):
        self.auth_tokens.append((usb_info,  auth_token))
        marshalled_data = marshal.dumps({'auth_tokens': self.auth_tokens})
        self.save_data(marshalled_data)

    #TODO this and static method logout should be separated
    def forget_auth_tokens(self):
        self.auth_tokens = []
