import datetime
import json
import logging
import re
from typing import TYPE_CHECKING, MutableMapping, MutableSequence, cast

import aiohttp.web
from multidict import MultiDict
from passlib.hash import pbkdf2_sha512
from requests_toolbelt.multipart import decoder

from nawah import data as Data
from nawah.classes import app_encoder
from nawah.config import Config
from nawah.enums import Event
from nawah.exceptions import InvalidAttrException

from .._call import call
from .._config import _compile_anon_session, _compile_anon_user
from .._validate import validate_doc

if TYPE_CHECKING:
    from nawah.classes import Attr
    from nawah.types import NawahDoc, NawahEnv

logger = logging.getLogger('nawah')


async def _http_handler(request: aiohttp.web.Request):
    headers = MultiDict(
        [
            ('Server', 'Nawah'),
            ('Powered-By', 'Nawah, https://nawah.masaar.com'),
            ('Access-Control-Allow-Origin', '*'),
            ('Access-Control-Allow-Methods', 'GET,POST,OPTIONS'),
            (
                'Access-Control-Allow-Headers',
                'Content-Type,X-Auth-Bearer,X-Auth-Token,X-Auth-App',
            ),
            ('Access-Control-Expose-Headers', 'Content-Disposition'),
        ]
    )

    logger.debug('Received new %s request: %s', request.method, request.match_info)

    if request.method == 'OPTIONS':
        return aiohttp.web.Response(
            status=200,
            headers=headers,
            body=app_encoder.encode(
                {
                    'status': 200,
                    'msg': 'OPTIONS request is allowed',
                }
            ),
        )

    # Check for IP quota
    if str(request.remote) not in Config.sys.ip_quota:
        Config.sys.ip_quota[str(request.remote)] = {
            'counter': Config.quota_ip_min,
            'last_check': datetime.datetime.utcnow(),
        }
    else:
        if (
            datetime.datetime.utcnow()
            - Config.sys.ip_quota[str(request.remote)]['last_check']
        ).seconds > 259:
            Config.sys.ip_quota[str(request.remote)][
                'last_check'
            ] = datetime.datetime.utcnow()
            Config.sys.ip_quota[str(request.remote)]['counter'] = Config.quota_ip_min
        else:
            if Config.sys.ip_quota[str(request.remote)]['counter'] - 1 <= 0:
                logger.warning(
                    'Denying \'%s\' request from \'%s\' for hitting IP quota',
                    request.method,
                    request.remote,
                )
                headers['Content-Type'] = 'application/json; charset=utf-8'
                return aiohttp.web.Response(
                    status=429,
                    headers=headers,
                    body=app_encoder.encode(
                        {
                            'status': 429,
                            'msg': 'You have hit calls quota from this IP',
                            'args': {'code': 'CORE_REQ_IP_QUOTA_HIT'},
                        }
                    ),
                )

            Config.sys.ip_quota[str(request.remote)]['counter'] -= 1

    module_name = request.url.parts[1]
    func_name = request.url.parts[2]
    request_args = dict(request.match_info.items())

    # Extract Args Sets based on request.method
    args_sets = Config.modules[module_name].funcs[func_name].query_attrs
    args_sets = cast(MutableSequence[MutableMapping[str, 'Attr']], args_sets)

    # Attempt to validate query as doc
    for args_set in args_sets or []:
        if len(args_set.keys()) == len(args_set.keys()) and sum(
            1 for arg in args_set.keys() if arg in args_set.keys()
        ) == len(args_set.keys()):
            # Check presence and validate all attrs in doc args
            try:
                exception: Exception
                validate_doc(mode='create', doc=request_args, attrs=args_set)  # type: ignore
            except InvalidAttrException as e:
                exception = e
                headers['Content-Type'] = 'application/json; charset=utf-8'
                return aiohttp.web.Response(
                    status=400,
                    headers=headers,
                    body=app_encoder.encode(
                        {
                            'status': 400,
                            'msg': f'{str(e)} for \'{request.method}\' request on module '
                            '\'{module_name}\'',
                            'args': {'code': f'{module_name.upper()}_INVALID_ATTR'},
                        }
                    ).encode('utf-8'),
                )
            break

    conn = Data.create_conn()
    env: 'NawahEnv' = {
        'conn': conn,
        'REMOTE_ADDR': request.remote or 'localhost',
        'client_app': '__public',
        'args': {},
    }

    try:
        env['HTTP_USER_AGENT'] = request.headers['user-agent']
        env['HTTP_ORIGIN'] = request.headers['origin']
    except:
        env['HTTP_USER_AGENT'] = ''
        env['HTTP_ORIGIN'] = ''

    if 'X-Auth-Bearer' in request.headers or 'X-Auth-Token' in request.headers:
        logger.debug('Detected \'X-Auth\' header[s]')
        if (
            'X-Auth-Bearer' not in request.headers
            or 'X-Auth-Token' not in request.headers
            or 'X-Auth-App' not in request.headers
        ):
            logger.debug('Denying request due to missing \'X-Auth\' header')
            headers['Content-Type'] = 'application/json; charset=utf-8'
            return aiohttp.web.Response(
                status=400,
                headers=headers,
                body=app_encoder.encode(
                    {
                        'status': 400,
                        'msg': 'One \'X-Auth\' headers was set but not the other',
                    }
                ).encode('utf-8'),
            )
        # [TODO] Add condition to now allow a connection from a client with client_app = __sys
        if Config.client_apps and (
            request.headers['X-Auth-App'] not in Config.client_apps
            or (
                Config.client_apps[request.headers['X-Auth-App']].type == 'web'
                and env['HTTP_ORIGIN']
                not in Config.client_apps[request.headers['X-Auth-App']].origin
            )
        ):
            logger.debug('Denying request due to unauthorised client_app')
            headers['Content-Type'] = 'application/json; charset=utf-8'
            return aiohttp.web.Response(
                status=403,
                headers=headers,
                body=app_encoder.encode(
                    {
                        'status': 403,
                        'msg': 'X-Auth headers could not be verified',
                        'args': {'code': 'CORE_SESSION_INVALID_XAUTH'},
                    }
                ).encode('utf-8'),
            )
        try:
            session_results = await call(
                'session/read',
                skip_events=[Event.PERM],
                env=env,
                query=[
                    {
                        '_id': request.headers['X-Auth-Bearer'],
                    }
                ],
            )
        except:
            headers['Content-Type'] = 'application/json; charset=utf-8'
            if Config.debug:
                return aiohttp.web.Response(
                    status=500,
                    headers=headers,
                    body=app_encoder.encode(
                        {
                            'status': 500,
                            'msg': f'Unexpected error has occurred [{str(exception)}]',
                            'args': {
                                'code': 'CORE_SERVER_ERROR',
                                'err': str(exception),
                            },
                        }
                    ).encode('utf-8'),
                )

            return aiohttp.web.Response(
                status=500,
                headers=headers,
                body=app_encoder.encode(
                    {
                        'status': 500,
                        'msg': 'Unexpected error has occurred',
                        'args': {'code': 'CORE_SERVER_ERROR'},
                    }
                ).encode('utf-8'),
            )

        if not session_results['args']['count'] or not pbkdf2_sha512.verify(
            request.headers['X-Auth-Token'],
            session_results['args']['docs'][0]['token_hash'],
        ):
            logger.debug('Denying request due to missing failed Call Authorisation')
            headers['Content-Type'] = 'application/json; charset=utf-8'
            return aiohttp.web.Response(
                status=403,
                headers=headers,
                body=app_encoder.encode(
                    {
                        'status': 403,
                        'msg': 'X-Auth headers could not be verified',
                        'args': {'code': 'CORE_SESSION_INVALID_XAUTH'},
                    }
                ).encode('utf-8'),
            )
        session = session_results['args']['docs'][0]
        session_results = await call(
            'session/reauth',
            skip_events=[Event.PERM],
            env=env,
            query=[
                {
                    '_id': request.headers['X-Auth-Bearer'],
                    'token': request.headers['X-Auth-Token'],
                    'groups': [],
                }
            ],
        )
        logger.debug('Denying request due to fail to reauth')
        if session_results['status'] != 200:
            headers['Content-Type'] = 'application/json; charset=utf-8'
            return aiohttp.web.Response(
                status=403,
                headers=headers,
                body=app_encoder.encode(session_results).encode('utf-8'),
            )

        session = session_results['args']['session']
    else:
        anon_user = _compile_anon_user()
        anon_session = _compile_anon_session()
        anon_session['user'] = anon_user
        session = anon_session

    env['session'] = session

    doc: 'NawahDoc' = {}

    if 'Content-Type' in request.headers:
        doc_content = await request.content.read()
        # Check Content-Type to decide how to deserialise content
        if request.headers['Content-Type'].startswith('multipart/form-data;'):
            multipart_content_type = request.headers['Content-Type']
            doc = {}
            for part in decoder.MultipartDecoder(
                doc_content, multipart_content_type
            ).parts:
                content_disposition = part.headers[b'Content-Disposition'].decode(
                    'UTF-8'
                )
                attr_name_match = re.findall('name="([^"]+)"', content_disposition)
                if not attr_name_match:
                    continue
                attr_name = attr_name_match[0]
                doc[attr_name] = part.content
                # For non-file values, decode unto str
                if 'filename' not in content_disposition:
                    doc[attr_name] = doc[attr_name].decode('UTF-8')
        elif request.headers['Content-Type'].startswith('application/json;'):
            doc = json.loads(doc_content)

    results = await call(
        f'{module_name}/{func_name}', env=env, query=[request_args], doc=doc
    )

    logger.debug('Closing connection')
    env['conn'].close()

    if 'return' not in results['args'] or results['args']['return'] == 'json':
        if 'return' in results['args']:
            del results['args']['return']
        headers['Content-Type'] = 'application/json; charset=utf-8'
        if results['status'] == 404:
            return aiohttp.web.Response(
                status=results['status'],
                headers=headers,
                body=app_encoder.encode(
                    {'status': 404, 'msg': 'Requested content not found'}
                ).encode('utf-8'),
            )
        return aiohttp.web.Response(
            status=results['status'],
            headers=headers,
            body=app_encoder.encode(results),
        )

    if results['args']['return'] == 'file':
        del results['args']['return']
        status = results['status']
        expiry_time = datetime.datetime.utcnow() + datetime.timedelta(days=30)
        headers['Last-Modified'] = str(results['args']['docs'][0]['lastModified'])
        headers['Content-Type'] = results['args']['docs'][0]['type']
        headers['Cache-Control'] = 'public, max-age=31536000'
        headers['Expires'] = expiry_time.strftime('%a, %d %b %Y %H:%M:%S GMT')
        content = results['args']['docs'][0]['content']
        if 'If-Modified-Since' in request.headers:
            if request.headers['If-Modified-Since'] == headers['Last-Modified']:
                status = 304
                content = b''
        return aiohttp.web.Response(
            status=status,
            headers=headers,
            body=content,
        )

    if results['args']['return'] == 'msg':
        del results['args']['return']
        headers['Content-Type'] = 'application/json; charset=utf-8'
        return aiohttp.web.Response(
            status=results['status'], headers=headers, body=results['msg']
        )

    headers['Content-Type'] = 'application/json; charset=utf-8'
    return aiohttp.web.Response(
        status=405,
        headers=headers,
        body=app_encoder.encode({'status': 405, 'msg': 'METHOD NOT ALLOWED'}),
    )
