import base64
import functools
import ipaddress
import json
import logging
import socket
import time
import typing as t

import requests
import rsa
from flask import current_app, url_for, g, request
from flask_jwt_extended import get_jwt_claims
from jsonschema import validate, SchemaError

from dimensigon import defaults
from dimensigon.domain.entities import Server, Scope, User, Locker, State, Gate
from dimensigon.network.exceptions import NotValidMessage
from dimensigon.use_cases.helpers import get_servers_from_scope
from dimensigon.use_cases.lock import lock_scope
from dimensigon.utils.helpers import get_now
from dimensigon.web import db, errors
from dimensigon.web.network import unpack_msg, pack_msg, unpack_msg2, pack_msg2

if t.TYPE_CHECKING:
    import flask


def save_if_hidden_ip(remote_addr: str, server: Server):
    ip = ipaddress.ip_address(remote_addr)
    if not ip.is_loopback:
        def get_ip(dns: str):
            if dns is None:
                return
            else:
                try:
                    return ipaddress.ip_address(socket.gethostbyname(dns))
                except:
                    return

        gate = [gate for gate in server.gates if ip in (gate.ip, get_ip(gate.dns))]
        if not gate:
            try:
                with lock_scope(Scope.CATALOG):
                    for port in set([gate.port for gate in server.gates]):
                        gate = server.add_new_gate(dns_or_ip=remote_addr, port=port, hidden=True)
                        db.session.add(gate)
                    db.session.commit()
            except errors.LockerError:
                logger = logging.getLogger('dimensigon')
                logger.warning(f"unable to save {remote_addr} from {server}")


def _proxy_request(request: 'flask.Request', destination: Server, verify=False) -> requests.Response:

    url = destination.url() + request.full_path
    req_data = request.get_json()

    if request.path == '/ping':
        server_data = {'id': str(g.server.id), 'name': g.server.name,
                       'time': get_now().strftime(defaults.DATETIME_FORMAT)}
        if req_data:
            if 'servers' not in req_data:
                req_data['servers'] = {}
            req_data['servers'].update({len(req_data['servers']) + 1: server_data})
        else:
            req_data = dict(servers={1: server_data})
    kwargs = {
        'json': req_data,
        'allow_redirects': False
    }

    headers = {key.lower(): value for key, value in request.headers.items()}

    # Let requests reset the host for us.
    if 'host' in headers:
        del headers['host']

    headers['d-source'] = headers.get('d-source', '') + ':' + str(g.server.id)

    kwargs['headers'] = headers

    cookies = request.cookies

    kwargs['cookies'] = cookies

    return requests.request(request.method, url, verify=verify, **kwargs)


def set_source():
    # not call get_or_raise to allow make requests without D-Source header
    if not hasattr(g, 'source'):
        source_id, proxies = (request.headers.get('D-Source', '') + ':').split(':', 1)
        proxies = proxies.strip(':')
        source = None
        if source_id:
            source = Server.query.get(source_id)
        # check hidden ip on server
        if proxies:
            lp = proxies.split(':')
            neighbour = None
            if lp[-1]:
                neighbour = Server.query.get(lp[-1])
            if neighbour:
                save_if_hidden_ip(request.remote_addr, neighbour)
        if not source:
            source = request.headers.get('D-Source') or request.remote_addr
        elif not proxies:
            save_if_hidden_ip(request.remote_addr, source)

        if not isinstance(source, Server):
            servers = db.session.query(Server).filter_by(deleted=False).join(Gate).filter_by(ip=source).all()
            if len(servers) == 1:
                source = servers[0]

        g.source = source


def forward_or_dispatch(*methods):
    def inner(func):
        from flask import request
        logger = logging.getLogger('dimensigon.forwarder')

        @functools.wraps(func)
        def wrapper_decorator(*args, **kwargs):
            destination_id = None
            if 'D-Destination' in request.headers:
                destination_id = request.headers['D-Destination']
            else:
                # Get information from content
                # Code Compatibility. Use D-Destination header instead
                data = request.get_json()
                if data is not None and 'destination' in data:
                    destination_id = data.get('destination')

            if destination_id and destination_id != str(g.server.id) and (not methods or request.method in methods):
                destination: Server = Server.query.get(destination_id)
                if destination is None:
                    return errors.format_error_response(errors.EntityNotFound('Server', destination_id))
                try:
                    logger.debug(f"Forwarding request {request.method} {request.full_path} to {destination.route}")
                    resp = _proxy_request(request=request, destination=destination)
                except requests.exceptions.RequestException as e:
                    return errors.format_error_response(errors.ProxyForwardingError(destination, e))
                else:
                    return resp.content, resp.status_code, dict(resp.headers)

            else:

                value = func(*args, **kwargs)
                return value

        return wrapper_decorator

    return inner


def securizer(func):
    from flask import request
    @functools.wraps(func)
    def wrapper_decorator(*args, **kwargs):

        # cipher_key = session.get('cipher_key', None)
        cipher_key = None
        securizer_method = None

        if 'D-Securizer' in request.headers:
            securizer_method = request.headers.get('D-Securizer')
            if securizer_method == 'plain' and not current_app.config.get('SECURIZER_PLAIN', False):
                    return {'error': 'plain data is not allowed'}, 406

        if request.method != 'GET':
            if request.is_json:
                if securizer_method == 'plain':
                    pass
                else:
                    g.original_json = request.get_json()
                    cipher_key = base64.b64decode(request.json.get('key')) if 'key' in request.json else cipher_key
                    # session['cipher_key'] = cipher_key
                    try:
                        if request.path in url_for('api_1_0.join'):
                            temp_pub_key = rsa.PublicKey.load_pkcs1(request.json.pop('my_pub_key').encode('ascii'))
                            data = unpack_msg2(data=request.get_json(),
                                               pub_key=temp_pub_key,
                                               priv_key=getattr(getattr(g, 'dimension', None), 'private', None),
                                               cipher_key=cipher_key)
                        else:
                            data = unpack_msg(data=request.get_json())
                    except (rsa.pkcs1.VerificationError, NotValidMessage) as e:
                        return {'error': str(e),
                                'message': request.get_json()}, 400
                    if current_app.config.get('SECURIZER', None):
                        # if data and isinstance(data, dict):
                        #     # fill json request with unpacked data
                        #     for key in list(request.json.keys()):
                        #         request.json.pop(key)
                        #     for key, val in data.items():
                        #         request.json[key] = val
                        # else:
                        request._cached_data = json.dumps(data)
                        request._cached_json = (data, data)


            else:
                if request.data:
                    return {'error': 'Content Type must be application/json'}, 400

        rest = tuple()
        rv = func(*args, **kwargs)
        if isinstance(rv, tuple):
            len_rv = len(rv)

            if len_rv >= 2:
                rest = rv[1:]
                rv = rv[0]

        if rv is None:
            return rv

        if isinstance(rv, dict):

            if request.path == url_for('api_1_0.join'):
                rv = pack_msg2(data=rv, pub_key=temp_pub_key,
                               priv_key=getattr(getattr(g, 'dimension', None), 'private', None),
                               cipher_key=cipher_key)
            else:
                if securizer_method == 'plain' and current_app.config.get('SECURIZER_PLAIN', False):
                    pass
                else:
                    rv = pack_msg(data=rv)

        if isinstance(rv, list):
            if securizer_method == 'plain' and current_app.config.get('SECURIZER_PLAIN', False):
                pass
            else:
                rv = pack_msg(data=rv)

        if rest:
            rv = (rv,) + rest
        return rv

    return wrapper_decorator


def validate_schema(schema_name=None, **methods):
    from flask import request
    def decorator(f):
        @functools.wraps(f)
        def wrapper(*args, **kw):
            schema = methods.get(request.method.upper()) or methods.get(request.method.lower()) or schema_name
            if schema:
                try:
                    validate(request.json, schema)
                except SchemaError as e:
                    current_app.logger.error(f"Error validating: {json.dumps(request.json, indent=2)}")
                    raise e
            return f(*args, **kw)

        return wrapper

    return decorator


def lock_catalog(f):
    @functools.wraps(f)
    def wrapper(*args, **kw):
        # check if applicant in jwt
        claims = get_jwt_claims()
        if claims:
            if claims.get('applicant'):
                locker = Locker.query.get(Scope.CATALOG)
                if locker.state == State.LOCKED and locker.applicant == claims.get('applicant'):
                    try:
                        ret = f(*args, **kw)
                    except Exception as e:
                        db.session.rollback()
                        raise
                    return ret

        servers = get_servers_from_scope(Scope.CATALOG)
        with lock_scope(Scope.CATALOG, servers):
            try:
                ret = f(*args, **kw)
            except Exception as e:
                db.session.rollback()
                raise
        return ret

    return wrapper


def rollback_on_error(f):
    @functools.wraps(f)
    def wrapper(*args, **kw):
        try:
            return f(*args, **kw)
        except Exception:
            db.session.rollback()
            raise

    return wrapper


def run_as(username: str):
    from flask import Flask
    def decorator(f):
        @functools.wraps(f)
        def wrapper(*args, **kwargs):
            # app resolution
            app = None
            if 'app' in kwargs:
                app = kwargs['app']
            else:
                for a in args:
                    if isinstance(a, Flask):
                        app = a
                        break
                else:
                    app = current_app
            if app:
                ctx = app.app_context()
                ctx.push()
            try:
                user = User.get_by_user(username)
                if user is None:
                    raise errors.EntityNotFound("User", username, ['name'])
                from flask_jwt_extended.utils import ctx_stack
                ctx_stack.top.jwt_user = user
                jwt = {'identity': str(user.id)}
                ctx_stack.top.jwt = jwt
                return f(*args, **kwargs)
            finally:
                db.session.close()
                if app:
                    ctx.pop()

        return wrapper

    return decorator


logger = logging.getLogger('dimensigon.time')

def log_time(tag=''):
    def decorator(f):
        @functools.wraps(f)
        def wrapper(*args, **kwargs):
            start = time.time()
            r = f(*args, **kwargs)
            elapsed = time.time() - start
            logger.debug(
                f"{getattr(f, '__name__', '')} {f'({tag})' if tag else ''} took {elapsed} seconds to complete")
            return r
        return wrapper

    return decorator
