import os
import re
import base64
import hashlib
import json
import redis
import requests

from datetime import datetime
from collections import OrderedDict
from os.path import expandvars

from getpass import getpass
from string import ascii_lowercase as alpha

from bottle import template, request
#from cork import AAAException

from webrecorder.webreccork import ValidationException, AuthException

from webrecorder.models.base import BaseAccess, DupeNameException
from webrecorder.models.user import User, UserTable

from webrecorder.utils import load_wr_config, sanitize_title, get_bool
from webrecorder.webreccork import WebRecCork
from webrecorder.redisutils import RedisTable


# ============================================================================
class UserManager(object):
    USER_RX = re.compile(r'^[A-Za-z0-9][\w-]{2,30}$')

    RESTRICTED_NAMES = ['login', 'logout', 'user', 'admin', 'manager', 'coll', 'collection',
                        'guest', 'settings', 'profile', 'api', 'anon', 'webrecorder',
                        'anonymous', 'register', 'join', 'download', 'live', 'embed', 'docs']

    PASS_RX = re.compile(r'^(?=.*[\d\W])(?=.*[a-z])(?=.*[A-Z]).{8,}$')

    EMAIL_RX = re.compile(r'[\w./+-]+@[\w.-]+')

    LC_USERNAMES_KEY = 'h:lc_users'

    def __init__(self, redis, cork, config):
        self.redis = redis
        self.cork = cork
        self.config = config

        self.default_coll = config['default_coll']

        self.temp_prefix = config['temp_prefix']

        mailing_list = os.environ.get('MAILING_LIST', '').lower()
        self.mailing_list = mailing_list in ('true', '1', 'yes')
        self.default_list_endpoint = os.environ.get('MAILING_LIST_ENDPOINT', '')
        self.list_key = os.environ.get('MAILING_LIST_KEY', '')
        self.list_removal_endpoint = os.path.expandvars(
                                        os.environ.get('MAILING_LIST_REMOVAL', ''))
        self.payload = os.environ.get('MAILING_LIST_PAYLOAD', '')
        self.remove_on_delete = (os.environ.get('REMOVE_ON_DELETE', '')
                                 in ('true', '1', 'yes'))

        self.announce_list = os.environ.get('ANNOUNCE_MAILING_LIST_ENDPOINT', False)
        invites = expandvars(config.get('invites_enabled', 'true')).lower()
        self.invites_enabled = invites in ('true', '1', 'yes')

        try:
            self.redis.hsetnx('h:defaults', 'max_size', int(config['default_max_size']))
            self.redis.hsetnx('h:defaults', 'max_anon_size', int(config['default_max_anon_size']))
        except Exception as e:
            print('WARNING: Unable to init defaults: ' + str(e))

        self.all_users = UserTable(self.redis, self._get_access)

        self.invites = RedisTable(self.redis, 'h:invites')

    def register_user(self, input_data, host):
        msg = OrderedDict()
        redir_extra = ''

        username = input_data.get('username', '')
        full_name = input_data.get('full_name', '')
        email = input_data.get('email', '')

        if 'username' not in input_data:
            msg['username'] = 'Missing Username'

        elif username.startswith(self.temp_prefix):
            msg['username'] = 'Sorry, this is not a valid username'

        if 'email' not in input_data:
            msg['email'] = 'Missing Email'

        if self.invites_enabled:
            try:
                val_email = self.is_valid_invite(input_data['invite'])
                if val_email != email:
                    raise ValidationException('Sorry, this invite can only be used with email: {0}'.format(val_email))
            except ValidationException as ve:
                msg['invite'] = str(ve)

            else:
                redir_extra = '?invite=' + input_data.get('invite', '')

        try:
            self.validate_user(username, email)
            self.validate_password(input_data['password'], input_data['confirmpassword'])

        except ValidationException as ve:
            msg['validation'] = str(ve)

        try:
            move_info = self.get_move_temp_info(input_data)
        except ValidationException as ve:
            msg['move_info'] = str(ve)

        if msg:
            return msg, redir_extra


        try:
            desc = {'name': full_name}

            if move_info:
                desc['move_info'] = move_info

            desc = json.dumps(desc)

            self.cork.register(username, input_data['password'], email, role='archivist',
                          max_level=50,
                          subject='webrecorder.io Account Creation',
                          email_template='webrecorder/templates/emailconfirm.html',
                          description=desc,
                          host=host)

            # add to announce list if user opted in
            if input_data.get('announce_mailer') and self.announce_list:
                self.add_to_mailing_list(username, email, full_name,
                                         list_endpoint=self.announce_list)

            if self.invites_enabled:
                self.delete_invite(email)

            # extend session for upto 90 mins to store data to be migrated
            # to allow time for user to validate registration
            if move_info:
                self.get_session().save()

        except ValidationException as ve:
            msg['validation'] = str(ve)

        except Exception as ex:
            import traceback
            traceback.print_exc()
            msg['other_error'] = 'Registration failed: ' + str(ex)

        if not msg:
            msg['success'] = ('A confirmation e-mail has been sent to <b>{0}</b>. ' +
                              'Please check your e-mail to complete the registration!').format(username)

        return msg, redir_extra

    def get_move_temp_info(self, input_data):
        move_temp = input_data.get('moveTemp')

        if not move_temp:
            return None

        to_coll_title = input_data.get('toColl', '')
        to_coll = sanitize_title(to_coll_title)

        if not to_coll:
            raise ValidationException('invalid_coll_name')


        if not self.access.session_user.is_anon():
            raise ValidationException('invalid_user_import')

        return {'from_user': self.access.session_user.name,
                'to_coll': to_coll,
                'to_title': to_coll_title,
               }

    def validate_registration(self, reg_code, cookie, username):
        cookie_validate = 'valreg=' + reg_code

        if cookie_validate not in cookie:
            return {'error': 'invalid_code'}

        try:
            user, first_coll = self.create_user_from_reg(reg_code, username)

            return {'registered': user.name,
                    'first_coll_name': first_coll.name}

        except ValidationException as ve:
            return {'error': ve.msg}

        except Exception as e:
            import traceback
            traceback.print_exc()
            return {'error': 'invalid_code'}

    def find_case_insensitive_username(self, username):
        lower_username = username.lower()

        new_username = self.redis.hget(self.LC_USERNAMES_KEY,  lower_username)
        if new_username == '-' or new_username == username or new_username is None:
            return None

        if new_username == '':
            return lower_username

        return new_username

    def get_authenticated_user(self, username, password):
        """Returns the user matching the supplied username and password otherwise
        returns None

        :param str username: The username of the user
        :param str password: The users password
        :return: The authenticated user
        :rtype: User|None
        """
        # first, authenticate the user
        # if failing, see if case-insensitive username and try that
        if not self.cork.is_authenticate(username, password):
            username = self.find_case_insensitive_username(username)
            if not username or not self.cork.is_authenticate(username, password):
                return None
        return self.all_users[username]

    def login_user_no_cookie(self, username, password):
        try:
            authed_user = self.get_authenticated_user(username, password)
        except Exception:
            return None

        if not authed_user:
            return None

        self.access.log_in(username, False)
        sesh = self.get_session()
        sesh.should_save = False
        sesh.should_renew = False
        return authed_user

    def login_user(self, input_data):
        """Authenticate users"""
        username = input_data.get('username', '')
        password = input_data.get('password', '')

        try:
            move_info = self.get_move_temp_info(input_data)
        except ValidationException as ve:
            return {'error': str(ve)}

        user = self.get_authenticated_user(username, password)
        # first, authenticate the user
        # if failing, see if case-insensitive username and try that
        if not user:
            return {'error': 'invalid_login'}

        # if not enough space, don't continue with login
        if move_info:
            if not self.has_space_for_new_collection(user.my_id,
                                                     move_info['from_user'],
                                                    'temp'):
                #return {'error': 'Sorry, not enough space to import this Temporary Collection into your account.'}
                return {'error': 'out_of_space'}

        new_collection = None

        try:
            if move_info:
                new_collection = self.move_temp_coll(user, move_info)
        except DupeNameException as de:
            return {'error': 'duplicate_name'}
            #return {'error': 'Collection "{0}" already exists'.format(move_info['to_title'])}

        remember_me = get_bool(input_data.get('remember_me'))

        # login session and access system
        self.access.log_in(user.my_id, remember_me)

        user.update_last_login()

        return {'success': '1',
                'new_coll_name': new_collection.name if new_collection else None,
                'user': user}

    def logout(self):
        sesh = self.get_session()
        sesh.delete()
        return

    def has_user_email(self, email):
        #TODO: implement a email table, if needed?

        for n, user_data in self.all_users.items():
            if user_data['email_addr'] == email:
                return True

        return False

    def get_user_email(self, user):
        if not user:
            return ''

        try:
            user_data = self.all_users[user]
        except:
            user_data = None

        if user_data:
            return user_data.get('email_addr', '')
        else:
            return ''

    def is_username_available(self, username):
        username_lc = username.lower()

        # username matches of the restricted names
        if username_lc in self.RESTRICTED_NAMES:
            return False

        # username doesn't match the allowed regex
        if not self.USER_RX.match(username):
            return False

        # lowercase username already exists
        if self.redis.hexists(self.LC_USERNAMES_KEY, username_lc):
            return False

        # username already exists! (shouldn't match if lowercase exists, but just in case)
        if username in self.all_users:
            return False

        return True

    def validate_user(self, user, email):
        if not self.is_username_available(user):
            raise ValidationException('username_not_available')

        if self.has_user_email(email):
            raise ValidationException('email_not_available')

        return True

    def validate_password(self, password, confirm):
        if password != confirm:
            raise ValidationException('password_mismatch')

        if not self.PASS_RX.match(password):
            raise ValidationException('password_invalid')

        return True

    def _get_access(self):
        return request['webrec.access']

    @property
    def access(self):
        return self._get_access()

    def get_roles(self):
        return [x for x in self.cork._store.roles]

    def get_user(self, username):
        try:
            return self.all_users[username]
        except:
            return None

    def get_user_coll(self, username, coll_name):
        user = self.get_user(username)
        if not user:
            return None, None

        collection = user.get_collection_by_name(coll_name)
        return user, collection

    def get_user_coll_rec(self, username, coll_name, rec):
        user, collection = self.get_user_coll(username, coll_name)
        if collection:
            recording = collection.get_recording(rec)
        else:
            recording = None

        return user, collection, recording

    def update_password(self, curr_password, password, confirm):
        username = self.access.session_user.name

        if not self.cork.verify_password(username, curr_password):
            raise ValidationException('invalid_password')

        self.validate_password(password, confirm)

        self.cork.update_password(username, password)

    def reset_password(self, password, confirm, resetcode):
        self.validate_password(password, confirm)

        try:
            self.cork.reset_password(resetcode, password)
        except AuthException:
            raise ValidationException('invalid_reset_code')

    def is_valid_invite(self, invitekey):
        try:
            if not invitekey:
                return False

            key = base64.b64decode(invitekey.encode('utf-8')).decode('utf-8')
            key.split(':', 1)
            email, hash_ = key.split(':', 1)

            entry = self.invites[email]

            if entry and entry.get('hash_') == hash_:
                return email
        except Exception as e:
            print(e)
            pass

        msg = 'Sorry, that is not a valid invite code. Please try again or request another invite'
        raise ValidationException(msg)

    def delete_invite(self, email):
        try:
            archive_invites = RedisTable(self.redis, 'h:arc_invites')
            archive_invites[email] = self.invites[email]
        except:
            pass

        del self.invites[email]

    def save_invite(self, email, name, desc=''):
        if not email or not name:
            return False

        self.invites[email] = {'name': name, 'email': email, 'reg_data': desc}
        return True

    def send_invite(self, email, email_template, host):
        entry = self.invites[email]
        if not entry:
            print('No Such Email In Invite List')
            return False

        hash_ = base64.b64encode(os.urandom(21)).decode('utf-8')
        entry['hash_'] = hash_

        full_hash = email + ':' + hash_
        invitekey = base64.b64encode(full_hash.encode('utf-8')).decode('utf-8')

        email_text = template(
            email_template,
            host=host,
            email_addr=email,
            name=entry.get('name', email),
            invite=invitekey,
        )
        self.cork.mailer.send_email(email, 'You are invited to join webrecorder.io beta!', email_text)
        entry['sent'] = str(datetime.utcnow())
        return True

    def add_to_mailing_list(self, username, email, name, list_endpoint=None):
        """3rd party mailing list subscription"""
        if not (list_endpoint or self.default_list_endpoint) or not self.list_key:
            print('MAILING_LIST is turned on, but required fields are '
                  'missing.')
            return

        # if no endpoint provided, use default
        if list_endpoint is None:
            list_endpoint = self.default_list_endpoint

        try:
            res = requests.post(list_endpoint,
                                auth=('nop', self.list_key),
                                data=self.payload.format(
                                    email=email,
                                    name=name,
                                    username=username),
                                timeout=1.5)

            if res.status_code != 200:
                print('Unexpected mailing list API response.. '
                      'status code: {0.status_code}\n'
                      'content: {0.content}'.format(res))

        except Exception as e:
            if e is requests.exceptions.Timeout:
                print('Mailing list API timed out..')
            else:
                print('Adding to mailing list failed:', e)

    def remove_from_mailing_list(self, email):
        """3rd party mailing list removal"""
        if not self.list_removal_endpoint or not self.list_key:
            # fail silently, log info
            print('REMOVE_ON_DELETE is turned on, but required '
                  'fields are missing.')
            return

        try:
            email = email.encode('utf-8').lower()
            email_hash = hashlib.md5(email).hexdigest()
            res = requests.delete(self.list_removal_endpoint.format(email_hash),
                                  auth=('nop', self.list_key),
                                  timeout=1.5)

            if res.status_code != 204:
                print('Unexpected mailing list API response.. '
                      'status code: {0.status_code}\n'
                      'content: {0.content}'.format(res))

        except Exception as e:
            if e is requests.exceptions.Timeout:
                print('Mailing list API timed out..')
            else:
                print('Removing from mailing list failed:', e)

    def get_session(self):
        return request.environ['webrec.session']

    def create_new_user(self, username, init_info=None):
        init_info = init_info or {}

        user = self.all_users.make_user(username)
        user.create_new()

        # track lowercase username
        lower_username = username.lower()
        self.redis.hset(self.LC_USERNAMES_KEY, lower_username,
                        username if lower_username != username else '')

        first_coll = None

        move_info = init_info.get('move_info')
        if move_info:
            first_coll = self.move_temp_coll(user, move_info)

        elif self.default_coll:
            first_coll = user.create_collection(self.default_coll['id'],
                                   title=self.default_coll['title'],
                                   desc=self.default_coll['desc'].format(username),
                                   public=False)

        # email subscription set up?
        if self.mailing_list:
            name = init_info.get('name', '')
            self.add_to_mailing_list(username, user['email_addr'], name)

        return user, first_coll

    def create_user_as_admin(self, email, username, passwd, passwd2, role, name):
        """Create a new user with command line arguments or series of prompts,
           preforming basic validation
        """
        self.access.assert_is_superuser()

        errs = []

        # EMAIL
        # validate email
        if not re.match(self.EMAIL_RX, email):
            errs.append('valid email required!')

        if email in [data['email_addr'] for u, data in self.all_users.items()]:
            errs.append('A user already exists with {0} email!'.format(email))

        # USERNAME
        # validate username
        if not username:
            errs.append('please specify a username!')

        if not self.is_username_available(username):
            errs.append('Invalid username.')

        # ROLE
        if role not in self.get_roles():
            errs.append('Not a valid role.')

        # PASSWD
        if passwd != passwd2 or not self.PASS_RX.match(passwd):
            errs.append('Passwords must match and be at least 8 characters long '
                        'with lowercase, uppercase, and either digits or symbols.')

        if errs:
            return errs, None

        # add user to cork
        #self.cork._store.users[username] = {
        self.all_users[username] = {
            'role': role,
            'hash': self.cork._hash(username, passwd).decode('ascii'),
            'email_addr': email,
            'full_name': name,
            'creation_date': str(datetime.utcnow()),
            'last_login': str(datetime.utcnow()),
        }
        #self.cork._store.save_users()

        return None, self.create_new_user(username, {'email': email,
                                                     'name': name})

    def create_user_from_reg(self, reg, username):
        user, init_info = self.cork.validate_registration(reg, username)

        if init_info:
            init_info = json.loads(init_info)

        user, first_coll = self.create_new_user(user, init_info)

        # login here
        self.access.log_in(user.name, remember_me=False)

        return user, first_coll

    def update_user_as_admin(self, user, data):
        """ Update any property on specified user
        For admin-only
        """
        self.access.assert_is_curr_user(user)

        errs = []

        if not data:
            errs.append('Nothing To Update')

        if 'role' in data and data['role'] not in self.get_roles():
            errs.append('Not a valid role.')

        if 'max_size' in data and not isinstance(data['max_size'], int):
            errs.append('max_size must be an int')

        if errs:
            return errs

        if 'name' in data:
            #user['desc'] = '{{"name":"{name}"}}'.format(name=data.get('name', ''))
            user['name'] = data.get('name', '')

        if 'desc' in data:
            user['desc'] = data['desc']

        if 'max_size' in data:
            user['max_size'] = data['max_size']

        if 'role' in data:
            user['role'] = data['role']

        if 'customer_id' in data:
            user['customer_id'] = data['customer_id']

        if 'customer_max_size' in data:
            user['customer_max_size'] = data['customer_max_size']

        return None

    def delete_user(self, username):
        try:
            user = self.all_users[username]
            self.access.assert_is_curr_user(user)
        except Exception:
            return False

        if self.mailing_list and self.remove_on_delete:
            self.remove_from_mailing_list(user['email_addr'])

        # remove user and from all users table
        del self.all_users[username]

        try:
            self.get_session().delete()
        except Exception:
            pass

        return True

    def has_space_for_new_collection(self, to_username, from_username, coll_name):
        try:
            to_user = self.all_users[to_username]
        except:
            return False

        from_user = self.all_users[from_username]
        collection = from_user.get_collection_by_name(coll_name)
        if not collection:
            return False

        return (collection.size <= to_user.get_size_remaining())

    def move_temp_coll(self, user, move_info):
        from_user = self.all_users[move_info['from_user']]
        temp_coll = from_user.get_collection_by_name('temp')
        if not from_user.move(temp_coll, move_info['to_coll'], user):
            return None

        temp_coll.set_prop('title', move_info['to_title'])

        # don't delete data in temp user dir as its waiting to be committed!
        self.get_session().set_anon_commit_wait()

        for recording in temp_coll.get_recordings():
            # will be marked for commit
            recording.set_closed()

        return temp_coll


# ============================================================================
class CLIUserManager(UserManager):
    def __init__(self, redis_url=None):
        config = load_wr_config()

        self.base_access = BaseAccess()

        # Init Redis
        if not redis_url:
            redis_url = os.environ['REDIS_BASE_URL']

        r = redis.StrictRedis.from_url(redis_url, decode_responses=True)

        # Init Cork
        cork = WebRecCork.create_cork(r, config)

        super(CLIUserManager, self).__init__(
            redis=r,
            cork=cork,
            config=config)

    def create_user(self, email=None, username=None, passwd=None, role=None, name=None):
        """Create a new user with command line arguments or series of prompts,
           preforming basic validation
        """

        # EMAIL
        if not email:
            print('let\'s create a new user..')
            email = input('email: ').strip()

        # USERNAME
        if not username:
            username = input('username: ').strip()

        # NAME
        if not name:
            name = input('name (optional): ').strip()

        # ROLE
        if role not in self.get_roles():
            role = self.choose_role()

        # PASSWD
        if not passwd:
            passwd = getpass('password: ')
            passwd2 = getpass('repeat password: ')
        else:
            passwd2 = passwd

        errs, res = self.create_user_as_admin(email, username, passwd, passwd2, role, name)

        if errs:
            for err in errs:
                print(err)
            return

        print('Created user {username} with the email {email} and the role: '
          '\'{role}\''.format(username=username,
                              email=email,
                              role=role))

        return res

    def choose_role(self):
        """Flexible choice prompt for as many roles as the system has"""
        roles = [r for r in self.cork.list_roles()]
        formatted = ['{0} (level {1})'.format(*r) for r in roles]
        condensed = '\n'.join(['{0}.) {1}'.format(*t) for t in zip(alpha, formatted)])
        new_role = input('choose: \n{0}\n\n'.format(condensed))

        if new_role not in alpha[:len(roles)]:
            raise Exception('invalid role choice')

        return roles[alpha.index(new_role)][0]

    def modify_user(self):
        """Modify an existing users. available modifications: role, email"""
        username = input('username to modify: ')
        has_modified = False

        if username not in self.all_users:
            print('{0} doesn\'t exist'.format(username))
            return

        user = self.all_users[username]

        mod_role = input('change role? currently {0} (y/n) '.format(user['role']))
        if mod_role.strip().lower() == 'y':
            new_role = self.choose_role()
            user['role'] = new_role
            has_modified = True
            print('assigned {0} with the new role: {1}'.format(username, new_role))

        mod_email = input('update email? currently {0} (y/n) '.format(user['email_addr']))
        if mod_email.strip().lower() == 'y':
            new_email = input('new email: ')

            if not re.match(r'[\w.-/+]+@[\w.-]+.\w+', new_email):
                print('valid email required!')
                return

            if new_email in [data['email_addr'] for u, data in self.all_users.items()]:
                print('A user already exists with {0} email!'.format(new_email))
                return

            # assume the 3rd party mailing list doesn't support updating addresses
            # so if add & remove are turned on, remove the old and add the
            # new address.
            if self.mailing_list and self.remove_on_delete:
                self.remove_from_mailing_list(user['email_addr'])
                #name = json.loads(self.get_users()[username].get('desc', '{}')).get('name', '')
                name = user['name']
                self.add_to_mailing_list(username, new_email, name)

            user['email_addr'] = new_email
            print('assigned {0} with the new email: {1}'.format(username, new_email))
            has_modified = True

        #
        # additional modifications can be added here
        #

        #if has_modified:
        #    self.cork._store.save_users()

        print('All done!')

    def list_users(self):
        """List all existing users."""
        input_ = input(
            "{number} users, do you want to list users? (y/n)".format(
                number=len(self.all_users)
            )
        )
        if input_ == "Y" or input_ == "y":
            print(
                "\n".join(
                    "User {username}".format(username=user)
                    for user in self.all_users
                )
            )

    def check_user(self, username):
        """Check if username exists.

        :param str username: username
        """
        if username not in self.all_users:
            print("User {username} does not exist".format(username=username))
            return False
        else:
            print("User {username} exists".format(username=username))
            return True

    def delete_user(self):
        """Remove a user from the system"""
        username = input('username to delete: ')
        confirmation = input('** all data for the username `{0}` will be wiped! **\n'
                             'please type the username again to confirm: '.format(username))

        if username != confirmation:
            print('Username confirmation didn\'t match! Aborting..')
            return

        if username not in self.all_users:
            print('The username {0} doesn\'t exist..'.format(username))
            return

        print('removing {0}..'.format(username))

        super(CLIUserManager, self).delete_user(username)

    def _get_access(self):
        return self.base_access