import json
import requests
from base64 import b64encode
import hashlib
import socket
import shutil
import os
import binascii
import logging

logger = logging.getLogger('urbackup-server-python-api-wrapper')


"""Backwards compatible imports"""
try:
    import urllib
except ImportError:
    import urllib.parse


class urbackup_server:

    #If you have basic authentication via .htpasswd
    server_basic_username = ''
    server_basic_password = ''

    action_incr_file = 1
    action_full_file = 2
    action_incr_image = 3
    action_full_image = 4
    action_resumed_incr_file = 5
    action_resumed_full_file = 6
    action_file_restore = 8
    action_image_restore = 9
    action_client_update = 10
    action_check_db_integrity = 11
    action_backup_db = 12
    action_recalc_stats = 13

    _session = ''
    _logged_in = False
    _lastlogid = 0

    def __init__(self, url, username, password):
        self._server_url = url
        self._server_username = username
        self._server_password = password

    def _get_response(self, action, params, method='POST'):

        headers = {
            'Accept': 'application/json',
            'Content-Type': 'application/json; charset=UTF-8'
        }

        if('server_basic_username' in globals() and len(self.server_basic_username)>0):
            userAndPass = b64encode(
                str.encode('%s:%s' % (self.server_basic_username, self.server_basic_password))
            ).decode('ascii')
            headers['Authorization'] = 'Basic %s' %  userAndPass

        try:
            action_url = '%s?%s' % (self._server_url, urllib.urlencode({'a': action}))
        except AttributeError:
            action_url = '%s?%s' % (self._server_url, urllib.parse.urlencode({'a': action}))

        if(len(self._session)>0):
            params['ses'] = self._session

        if method == 'POST':
            r = requests.post(
                action_url,
                data=params
            )
        elif method == 'GET':
            r = requests.get(
                action_url,
                params=params
            )
        else:
            raise Exception('Request with method \'%s\' has not been implemented yet.' % method)

        return r

    def _get_json(self, action, params = {}):
        tries = 50

        while tries>0:
            response = self._get_response(action, params)

            if(response.status_code == 200):
                break

            tries -= 1
            if tries <= 0:
                return None
            else:
                logger.error('API call failed. Retrying...')

        return response.json()

    def _download_file(self, action, outputfn, params):

        req = self._get_response(action, params, 'GET');

        if req.status_code != 200:
            return False

        with open(outputfn, 'wb') as stream:
            for chunk in req.iter_content(chunk_size=128):
                stream.write(chunk)

        return True

    def _md5(self, s):
        return hashlib.md5(s.encode()).hexdigest()

    def login(self):

        if not self._logged_in:

            logger.debug('Trying anonymous login...')

            login = self._get_json('login', {});

            if not login or 'success' not in login or not login['success'] :
                logger.debug('Logging in...')

                salt = self._get_json('salt', {'username': self._server_username})
                if not salt or not ('ses' in salt):
                    logger.warning('Username does not exist')
                    return False

                self._session = salt['ses'];

                if 'salt' in salt:
                    password_md5_bin = hashlib.md5((salt['salt']+self._server_password).encode()).digest()
                    password_md5 = binascii.hexlify(password_md5_bin).decode()

                    if 'pbkdf2_rounds' in salt:
                        pbkdf2_rounds = int(salt['pbkdf2_rounds'])
                        if pbkdf2_rounds>0:
                            password_md5 = binascii.hexlify(hashlib.pbkdf2_hmac('sha256', password_md5_bin,
                                                               salt['salt'].encode(), pbkdf2_rounds)).decode()

                    password_md5 = self._md5(salt['rnd']+password_md5)

                    login = self._get_json('login', {
                        'username': self._server_username,
                        'password': password_md5 })

                    if not login or 'success' not in login or not login['success']:
                        logger.warning('Error during login. Password wrong?')
                        return False

                    else:
                        self._logged_in = True
                        return True
                else:
                    return False
            else:
                self._logged_in = True
                self._session = login['session'];
                return True
        else:
            return True

    def get_client_status(self, clientname):

        if not self.login():
            return None

        status = self._get_json('status')

        if not status:
            return None

        if not 'status' in status:
            return None

        for client in status['status']:

            if client['name'] == clientname:

                return client;

        logger.warning('Could not find client status. No permission?')
        return None

    def download_installer(self, installer_fn, new_clientname, os='linux'):
        """Download installer for os, defaults to linux"""
        
        if not os.lower() in ['linux', 'osx', 'mac', 'windows']:
            raise Exception('OS not supported')

        if not self.login():
            return False

        new_client = self._get_json('add_client', {'clientname': new_clientname})

        if 'already_exists' in new_client:

            status = self.get_client_status(new_clientname)

            if status == None:
                return False

            return self._download_file('download_client', installer_fn, {'clientid': status['id'] })


        if not 'new_authkey' in new_client:
            return False

        return self._download_file('download_client', installer_fn,
            {
                'clientid': new_client['new_clientid'],
                'authkey': new_client['new_authkey'],
                'os': os
            })

    def add_client(self, clientname):

        if not self.login():
            return None

        ret = self._get_json('add_client', { 'clientname': clientname})
        if ret==None or 'already_exists' in ret:
            return None

        return ret

    def get_global_settings(self):
        if not self.login():
            return None

        settings = self._get_json('settings', {'sa': 'general'} )

        if not settings or not 'settings' in settings:
            return None

        return settings['settings']

    def set_global_setting(self, key, new_value):
        if not self.login():
            return False

        settings = self._get_json('settings', {'sa': 'general'} )

        if not settings or not 'settings' in settings:
            return False

        settings['settings'][key] = new_value
        settings['settings']['sa'] = 'general_save'

        ret = self._get_json('settings', settings['settings'])

        return ret != None and 'saved_ok' in ret

    def get_client_settings(self, clientname):

        if not self.login():
            return None

        client = self.get_client_status(clientname)

        if client == None:
            return None

        clientid = client['id'];

        settings = self._get_json('settings', {'sa': 'clientsettings',
                                    't_clientid': clientid})

        if not settings or not 'settings' in settings:
            return None

        return settings['settings']

    def change_client_setting(self, clientname, key, new_value):
        if not self.login():
            return False

        client = self.get_client_status(clientname)

        if client == None:
            return False

        clientid = client['id'];

        settings = self._get_json('settings', {'sa': 'clientsettings',
                                    't_clientid': clientid})

        if not settings or not 'settings' in settings:
            return False

        settings['settings'][key] = new_value
        settings['settings']['overwrite'] = 'true'
        settings['settings']['sa'] = 'clientsettings_save'
        settings['settings']['t_clientid'] = clientid

        ret = self._get_json('settings', settings['settings'])

        return ret != None and 'saved_ok' in ret

    def get_client_authkey(self, clientname):

        if not self.login():
            return None

        settings = self.get_client_settings(clientname)

        if settings:
            return settings['internet_authkey']

        return None

    def get_server_identity(self):

        if not self.login():
            return None

        status = self._get_json('status')

        if not status:
            return None

        if not 'server_identity' in status:
            return None

        return status['server_identity']

    def get_status(self):
        if not self.login():
            return None

        status = self._get_json('status')

        if not status:
            return None

        if not 'status' in status:
            return None

        return status['status']

    def get_livelog(self, clientid = 0):
        if not self.login():
            return None

        log = self._get_json('livelog', {'clientid': clientid, 'lastid': self._lastlogid})

        if not log:
            return None

        if not 'logdata' in log:
            return None

        self._lastlogid = log['logdata'][-1]['id']

        return log['logdata']

    def get_usage(self):
        if not self.login():
            return None

        usage = self._get_json('usage')

        if not usage:
            return None

        if not 'usage' in usage:
            return None

        return usage['usage']

    def get_extra_clients(self):
        if not self.login():
            return None

        status = self._get_json('status')

        if not status:
            return None

        if not 'extra_clients' in status:
            return None

        return status['extra_clients']

    def _start_backup(self, clientname, backup_type):

        client_info = self.get_client_status(clientname)

        if not client_info:
            return False

        ret = self._get_json('start_backup', {'start_client': client_info['id'],
                                         'start_type': backup_type } );

        if ( ret == None
            or 'result' not in ret
            or len(ret['result'])!=1
            or 'start_ok' not in ret['result'][0]
            or not ret['result'][0]['start_ok']):
            return False

        return True

    def start_incr_file_backup(self, clientname):
        return self._start_backup(clientname, 'incr_file');

    def start_full_file_backup(self, clientname):
        return self._start_backup(clientname, 'full_file');

    def start_incr_image_backup(self, clientname):
        return self._start_backup(clientname, 'incr_image');

    def start_full_image_backup(self, clientname):
        return self._start_backup(clientname, 'full_image');

    def add_extra_client(self, addr):
        if not self.login():
            return None

        ret = self._get_json('status', {'hostname': addr } )

        if not ret:
            return False

        return True

    def remove_extra_client(self, ecid):
        if not self.login():
            return None

        ret = self._get_json('status', {'hostname': ecid,
            'remove': 'true' })

        if not ret:
            return False

        return True

    def get_actions(self):
        if not self.login():
            return None

        ret = self._get_json('progress')

        if not ret or not 'progress' in ret:
            return None

        return ret['progress']

    def stop_action(self, action):
        if (not 'clientid' in action
            or not 'id' in action):
            return False

        if not self.login():
            return None

        ret = self._get_json('progress',
                             {'stop_clientid': action['clientid'],
                              'stop_id': action['id']})

        if not ret or not 'progress' in ret:
            return False

        return True
