import datetime
import logging
import secrets

from passlib.hash import pbkdf2_sha512

from nawah.base_module import BaseModule
from nawah.classes import ATTR, EXTN, METHOD, PERM
from nawah.config import Config
from nawah.enums import Event
from nawah.registry import Registry

logger = logging.getLogger('nawah')


class Session(BaseModule):
    '''`Session` module provides data type and controller for sessions in Nawah eco-system. CRUD methods of the module are supposed to used for internal calls only, while methods `auth`, `reauth`, and `signout` are available for use by API as well as internal calls when needed.'''

    collection = 'sessions'
    attrs = {
        'user': ATTR.ID(desc='`_id` of `User` doc the doc belongs to.'),
        'groups': ATTR.LIST(
            desc='List of `_id` for every group the session is authenticated against. This attr is set by `auth` method when called with `groups` Doc Arg for Controller Auth Sequence.',
            list=[
                ATTR.ID(desc='`_id` of Group doc the session is authenticated against.')
            ],
        ),
        'host_add': ATTR.IP(desc='IP of the host the user used to authenticate.'),
        'user_agent': ATTR.STR(
            desc='User-agent of the app the user used to authenticate.'
        ),
        'expiry': ATTR.DATETIME(desc='Python `datetime` ISO format of session expiry.'),
        'token_hash': ATTR.STR(desc='Hashed system-generated session token.'),
        'create_time': ATTR.DATETIME(
            desc='Python `datetime` ISO format of the doc creation.'
        ),
    }
    defaults = {'groups': []}
    extns = {'user': EXTN(module='user', attrs=['*'], force=True)}
    methods = {
        'read': METHOD(
            permissions=[PERM(privilege='read', query_mod={'user': '$__user'})]
        ),
        'create': METHOD(permissions=[PERM(privilege='create')]),
        'update': METHOD(
            permissions=[
                PERM(
                    privilege='update',
                    query_mod={'user': '$__user'},
                    doc_mod={'user': None},
                )
            ],
            query_args={'_id': ATTR.ID()},
        ),
        'delete': METHOD(
            permissions=[PERM(privilege='delete', query_mod={'user': '$__user'})],
            query_args={'_id': ATTR.ID()},
        ),
        'auth': METHOD(permissions=[PERM(privilege='*')], doc_args=[]),
        'reauth': METHOD(
            permissions=[PERM(privilege='*')],
            query_args=[
                {
                    '_id': ATTR.ID(),
                    'token': ATTR.STR(),
                    'groups': ATTR.LIST(list=[ATTR.ID()]),
                },
                {'_id': ATTR.ID(), 'token': ATTR.STR()},
            ],
        ),
        'signout': METHOD(
            permissions=[PERM(privilege='*')],
            query_args={'_id': ATTR.ID()},
        ),
    }

    async def auth(self, skip_events, env, doc):
        for attr in Registry.module('user').unique_attrs:
            if attr in doc.keys():
                key = attr
                break
        user_query = [{key: doc[key], '$limit': 1}]
        if 'groups' in doc.keys():
            user_query.append(
                [{'groups': {'$in': doc['groups']}}, {'privileges': {'*': ['*']}}]
            )
        user_results = await Registry.module('user').read(
            skip_events=[Event.PERM, Event.ON], env=env, query=user_query
        )
        if not user_results['args']['count'] or not pbkdf2_sha512.verify(
            doc['hash'],
            user_results['args']['docs'][0][f'{key}_hash'],
        ):
            raise self.exception(
                status=403,
                msg='Wrong auth credentials.',
                args={'code': 'INVALID_CREDS'},
            )

        user = user_results['args']['docs'][0]

        if Event.ON not in skip_events:
            if user['status'] in ['banned', 'deleted']:
                raise self.exception(
                    status=403,
                    msg=f'User is {user["status"]}.',
                    args={'code': 'INVALID_USER'},
                )

            if user['status'] == 'disabled_password':
                raise self.exception(
                    status=403,
                    msg='User password is disabled.',
                    args={'code': 'INVALID_USER'},
                )

        token = secrets.token_urlsafe(32)
        session = {
            'user': user['_id'],
            'groups': doc['groups'] if 'groups' in doc.keys() else [],
            'host_add': env['REMOTE_ADDR'],
            'user_agent': env['HTTP_USER_AGENT'],
            'expiry': (
                datetime.datetime.utcnow() + datetime.timedelta(days=30)
            ).isoformat(),
            'token_hash': pbkdf2_sha512.using(rounds=100000).hash(token),
        }

        results = await self.create(skip_events=[Event.PERM], env=env, doc=session)
        if results['status'] != 200:
            return results

        session['_id'] = results['args']['docs'][0]['_id']
        session['user'] = user
        del session['token_hash']
        session['token'] = token
        results['args']['docs'][0] = session

        # [DOC] read user privileges and return them
        user_results = await Registry.module('user').read_privileges(
            skip_events=[Event.PERM], env=env, query=[{'_id': user['_id']}]
        )
        if user_results['status'] != 200:
            return user_results
        results['args']['docs'][0]['user'] = user_results['args']['docs'][0]

        # [DOC] Create CONN_AUTH Analytic doc
        if Config.analytics_events['session_conn_auth']:
            analytic_doc = {
                'event': 'CONN_AUTH',
                'subevent': env['client_app'],
                'args': {
                    'user': user_results['args']['docs'][0]['_id'],
                    'session': results['args']['docs'][0]['_id'],
                    'REMOTE_ADDR': env['REMOTE_ADDR'],
                    'HTTP_USER_AGENT': env['HTTP_USER_AGENT'],
                },
            }
            analytic_results = await Registry.module('analytic').create(
                skip_events=[Event.PERM], env=env, doc=analytic_doc
            )
            if analytic_results['status'] != 200:
                logger.error(
                    f'Failed to create \'Analytic\' doc: {analytic_doc}. Results: {analytic_results}'
                )
        # [DOC] Create USER_AUTH Analytic doc
        if Config.analytics_events['session_user_auth']:
            analytic_doc = {
                'event': 'USER_AUTH',
                'subevent': user_results['args']['docs'][0]['_id'],
                'args': {
                    'session': results['args']['docs'][0]['_id'],
                    'REMOTE_ADDR': env['REMOTE_ADDR'],
                    'HTTP_USER_AGENT': env['HTTP_USER_AGENT'],
                    'client_app': env['client_app'],
                },
            }
            analytic_results = await Registry.module('analytic').create(
                skip_events=[Event.PERM], env=env, doc=analytic_doc
            )
            if analytic_results['status'] != 200:
                logger.error(
                    f'Failed to create \'Analytic\' doc: {analytic_doc}. Results: {analytic_results}'
                )

        return self.status(
            status=200,
            msg='You were successfully authed.',
            args={'session': results['args']['docs'][0]},
        )

    async def reauth(self, env, query):
        if str(query['_id'][0]) == 'f00000000000000000000012':
            raise self.exception(
                status=400,
                msg='Reauth is not required for \'__ANON\' user.',
                args={'code': 'ANON_REAUTH'},
            )

        session_query = [{'_id': query['_id'][0]}]
        if 'groups' in query:
            session_query.append({'groups': {'$in': query['groups'][0]}})
        results = await self.read(
            skip_events=[Event.PERM], env=env, query=session_query
        )
        if not results['args']['count']:
            raise self.exception(
                status=403, msg='Session is invalid.', args={'code': 'INVALID_SESSION'}
            )

        if not pbkdf2_sha512.verify(
            query['token'][0], results['args']['docs'][0]['token_hash']
        ):
            raise self.exception(
                status=403,
                msg='Reauth token hash invalid.',
                args={'code': 'INVALID_REAUTH_HASH'},
            )

        del results['args']['docs'][0]['token_hash']
        results['args']['docs'][0]['token'] = query['token'][0]

        if results['args']['docs'][0]['expiry'] < datetime.datetime.utcnow().isoformat():
            results = await self.delete(
                skip_events=[Event.PERM, Event.SOFT],
                env=env,
                query=[{'_id': env['session']['_id']}],
            )
            raise self.exception(
                status=403, msg='Session had expired.', args={'code': 'SESSION_EXPIRED'}
            )

        # [DOC] update user's last_login timestamp
        await Registry.module('user').update(
            skip_events=[Event.PERM],
            env=env,
            query=[{'_id': results['args']['docs'][0]['user']}],
            doc={'login_time': datetime.datetime.utcnow().isoformat()},
        )
        await self.update(
            skip_events=[Event.PERM],
            env=env,
            query=[{'_id': results['args']['docs'][0]['_id']}],
            doc={
                'expiry': (
                    datetime.datetime.utcnow() + datetime.timedelta(days=30)
                ).isoformat()
            },
        )
        # [DOC] read user privileges and return them
        user_results = await Registry.module('user').read_privileges(
            skip_events=[Event.PERM],
            env=env,
            query=[{'_id': results['args']['docs'][0]['user']['_id']}],
        )
        results['args']['docs'][0]['user'] = user_results['args']['docs'][0]

        # [DOC] Create CONN_AUTH Analytic doc
        if Config.analytics_events['session_conn_reauth']:
            analytic_doc = {
                'event': 'CONN_REAUTH',
                'subevent': env['client_app'],
                'args': {
                    'user': user_results['args']['docs'][0]['_id'],
                    'session': results['args']['docs'][0]['_id'],
                    'REMOTE_ADDR': env['REMOTE_ADDR'],
                    'HTTP_USER_AGENT': env['HTTP_USER_AGENT'],
                },
            }
            analytic_results = await Registry.module('analytic').create(
                skip_events=[Event.PERM], env=env, doc=analytic_doc
            )
            if analytic_results['status'] != 200:
                logger.error(
                    f'Failed to create \'Analytic\' doc: {analytic_doc}. Results: {analytic_results}'
                )
        # [DOC] Create USER_AUTH Analytic doc
        if Config.analytics_events['session_user_reauth']:
            analytic_doc = {
                'event': 'USER_REAUTH',
                'subevent': user_results['args']['docs'][0]['_id'],
                'args': {
                    'session': results['args']['docs'][0]['_id'],
                    'REMOTE_ADDR': env['REMOTE_ADDR'],
                    'HTTP_USER_AGENT': env['HTTP_USER_AGENT'],
                    'client_app': env['client_app'],
                },
            }
            analytic_results = await Registry.module('analytic').create(
                skip_events=[Event.PERM], env=env, doc=analytic_doc
            )
            if analytic_results['status'] != 200:
                logger.error(
                    f'Failed to create \'Analytic\' doc: {analytic_doc}. Results: {analytic_results}'
                )

        return self.status(
            status=200,
            msg='You were successfully reauthed.',
            args={'session': results['args']['docs'][0]},
        )

    async def signout(self, env, query):
        if str(query['_id'][0]) == 'f00000000000000000000012':
            raise self.exception(
                status=400,
                msg='Singout is not allowed for \'__ANON\' user.',
                args={'code': 'ANON_SIGNOUT'},
            )

        results = await self.read(
            skip_events=[Event.PERM], env=env, query=[{'_id': query['_id'][0]}]
        )

        if not results['args']['count']:
            raise self.exception(
                status=403, msg='Session is invalid.', args={'code': 'INVALID_SESSION'}
            )

        results = await self.delete(
            skip_events=[Event.PERM], env=env, query=[{'_id': env['session']['_id']}]
        )

        # [DOC] Create CONN_AUTH Analytic doc
        if Config.analytics_events['session_conn_deauth']:
            analytic_doc = {
                'event': 'CONN_DEAUTH',
                'subevent': env['client_app'],
                'args': {
                    'user': env['session']['user']['_id'],
                    'session': env['session']['_id'],
                    'REMOTE_ADDR': env['REMOTE_ADDR'],
                    'HTTP_USER_AGENT': env['HTTP_USER_AGENT'],
                },
            }
            analytic_results = await Registry.module('analytic').create(
                skip_events=[Event.PERM], env=env, doc=analytic_doc
            )
            if analytic_results['status'] != 200:
                logger.error(
                    f'Failed to create \'Analytic\' doc: {analytic_doc}. Results: {analytic_results}'
                )
        # [DOC] Create USER_AUTH Analytic doc
        if Config.analytics_events['session_user_deauth']:
            analytic_doc = {
                'event': 'USER_DEAUTH',
                'subevent': env['session']['user']['_id'],
                'args': {
                    'session': env['session']['_id'],
                    'REMOTE_ADDR': env['REMOTE_ADDR'],
                    'HTTP_USER_AGENT': env['HTTP_USER_AGENT'],
                    'client_app': env['client_app'],
                },
            }
            analytic_results = await Registry.module('analytic').create(
                skip_events=[Event.PERM], env=env, doc=analytic_doc
            )
            if analytic_results['status'] != 200:
                logger.error(
                    f'Failed to create \'Analytic\' doc: {analytic_doc}. Results: {analytic_results}'
                )

        return self.status(
            status=200,
            msg='You are successfully signed-out.',
            args={'session': {'_id': 'f00000000000000000000012'}},
        )
