# -*- coding: utf-8 -*-
from __future__ import unicode_literals

import flask
import logging
from flask_login import current_user
from flask.views import View
from voluptuous import Schema, MultipleInvalid
from jsonschema import Draft4Validator

from datacloud.score_api.server.errors import ApiException


logger = logging.getLogger(__name__)


__all__ = [
    'ApiView',
    'NotAllowed',
    'InvalidArgs',
    'InvalidParameters',
]


class InternalError(ApiException):
    """ A request method is not supported for the requested resource """
    status_code = 500
    code = 'internal_error'


class NotAllowed(ApiException):
    """ A request method is not supported for the requested resource """
    status_code = 405
    code = 'method_not_allowed'


class InvalidArgs(ApiException):
    """ A request arguments is not matched with declared schema """
    status_code = 400
    code = 'invalid_arguments'


class InvalidParameters(ApiException):
    """ A request arguments is not matched with declared schema """
    status_code = 400
    code = 'invalid_parameters'


class Forbidden(ApiException):
    """ A request arguments is not matched with declared schema """
    status_code = 403
    code = 'resource_use_forbidden'


class BodyIsNotJSON(ApiException):
    """ A request body can not be parsed as json """
    status_code = 400
    code = 'json_in_request_body_expected'


class BodySchemaMissmatch(ApiException):
    """ A request body is not matched with declared schema """
    status_code = 400
    code = 'invalid_request_body'


class ApiView(View):
    """ Base class for Impulse API """
    methods = ['GET', 'POST']
    result_schema = None
    request_body_schema = None
    request_body_required = False
    params_schema = {}
    args_schema = {}

    def __init__(self, *args, **kwarsg):
        super(ApiView, self).__init__(*args, **kwarsg)
        self._args_schema = None
        self._params_schema = None

    def dispatch_request(self, **kwargs):
        request = flask.request
        req_id = request.headers.get('X-Req-Id', None)
        req_id = req_id or request.headers.get('X-Request-Id', None)
        flask.g.request_id = req_id

        if not current_user.is_authenticated:
            return flask.current_app.login_manager.unauthorized()

        self.validate_parameters(kwargs)
        flask.g.args = self.validate_args(_multidict_to_dict(request.args))

        # silent and checkt to process get requests without json data
        flask.g.request_body = self.validate_request_body(flask.request.get_json(silent=True))
        if not flask.g.request_body:
            flask.g.request_body = {}

        if request.method == 'GET':
            handler = self.get
        elif request.method == 'POST':
            handler = self.post
        else:
            raise NotAllowed()
        try:
            response_data = handler(**kwargs)
        except ApiException:
            raise
        except Exception as e:
            logger.exception(e)
            raise InternalError()

        if isinstance(response_data, tuple):
            response_data, response_code = response_data
        else:
            response_code = 200
        response_data = self.validate_result(response_data)
        return self.encode_response(response_data, response_code)

    def validate_parameters(self, params):
        """ Validate handler url paramters. """
        if self.params_schema is None:
            return
        if not self._params_schema:
            self._params_schema = Schema(self.params_schema, required=True)
        try:
            self._params_schema(params)
        except MultipleInvalid as e:
            raise _format_errors(e, InvalidParameters)

    def validate_args(self, args):
        """ Validates handler arguments. Returns dict """
        if self.args_schema is None:
            return args
        if not self._args_schema:
            schema = dict()
            schema.update(self.args_schema)
            self._args_schema = Schema(schema, required=True)
        try:
            return self._args_schema(args)
        except MultipleInvalid as e:
            raise _format_errors(e, InvalidArgs)

    def validate_result(self, data):
        """
        Validates response content for matching with json schema.
        Stores warnings to result.
        """
        if not self.result_schema:
            return data
        validator = Draft4Validator(self.result_schema)
        is_valid = validator.is_valid(data)
        if not is_valid:
            logger.warning('Result JSON-schema errors: %s', ', '.join(e.message for e in validator.iter_errors(data)))
        return data

    def validate_request_body(self, data):
        if data is None:
            if self.request_body_required:
                raise BodyIsNotJSON()
            else:
                return None
        if not self.request_body_schema:
            return data
        validator = Draft4Validator(self.request_body_schema)
        is_valid = validator.is_valid(data)
        if not is_valid:
            errors = [
                ('.'.join(map(str, e.absolute_path)), 'invalid', e.message)
                for e in validator.iter_errors(data)
            ]
            raise BodySchemaMissmatch(errors=errors)
        return data

    def encode_response(self, data, response_code):
        """
        Converts python dict to json and pack into to Flask response object.
        Sets 'Content-Type' header.
        """
        response = flask.jsonify(data)
        response.headers['Content-Type'] = 'application/json; charset=utf-8'
        response.status_code = response_code
        return response

    def get(self, *args, **kwargs):
        raise NotAllowed()

    def post(self, *args, **kwargs):
        raise NotAllowed()


def _multidict_to_dict(multidict):
    """ Converts Flask dict with multiple keys to standart python dict """
    return {
        k: v[0] if len(v) == 1 else v
        for k, v in multidict.iterlists()
    }


def _format_errors(validation_error, error_type):
    """ Converts MultipleInvalid to ApiException """
    errors = validation_error.errors
    errors_msg = [(' '.join(e.path), 'invalid', e.msg) for e in errors]
    raise error_type(errors=errors_msg)
