# This module is a modified copy of code of Yasha's Borevich library
# django-jsonresponse (https://github.com/jjay/django-jsonresponse) distributed
# under BSD license


import datetime
import functools
import json
import logging
import re
import time

from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.core.serializers.json import DjangoJSONEncoder
from django.http import Http404, HttpResponse, HttpResponseBadRequest
from django.http.response import HttpResponseBase
from django.utils.encoding import force_str
from django.utils.log import log_response
from requests import HTTPError, RequestException

from passerelle.utils.conversion import exception_to_text

DEFAULT_DEBUG = getattr(settings, 'JSONRESPONSE_DEFAULT_DEBUG', False)
CALLBACK_NAME = getattr(settings, 'JSONRESPONSE_CALLBACK_NAME', 'callback')


class APIError(RuntimeError):
    '''Exception to raise when there is a remote application or business logic error.'''

    def __init__(self, *args, **kwargs):
        self.err = kwargs.pop('err', 1)
        self.log_error = kwargs.pop('log_error', False)
        self.http_status = kwargs.pop('http_status', 200)
        self.__dict__.update(kwargs)
        super().__init__(*args)


class JSONEncoder(DjangoJSONEncoder):
    def default(self, o):
        if isinstance(o, time.struct_time):
            o = datetime.datetime(*tuple(o)[:6])
        return super().default(o)


class to_json:
    def __init__(self, error_code=500, logger=None, **kwargs):
        self.error_code = error_code
        self.kwargs = kwargs
        self.logger = logger
        if 'cls' not in self.kwargs:
            self.kwargs['cls'] = JSONEncoder

    def __call__(self, f):
        @functools.wraps(f)
        def wrapper(*args, **kwargs):
            method = self.api_func
            if args and getattr(getattr(args[0], f.__name__, None), "__self__", False):
                method = self.api_method
            return method(f, *args, **kwargs)

        return wrapper

    def obj_to_response(self, req, obj):
        if isinstance(obj, dict) and 'err' not in obj:
            obj['err'] = 0
        return obj

    def err_to_response(self, err):
        if hasattr(err, "__module__"):
            err_module = err.__module__ + "."
        else:
            err_module = ""

        if hasattr(err, "owner"):
            err_module += err.owner.__name__ + "."

        err_class = err_module + err.__class__.__name__

        err_desc = force_str(err)

        response = {
            "err": 1,
            "err_class": err_class,
            "err_desc": err_desc,
            "data": getattr(err, 'data', None),
        }
        if getattr(err, 'extra_dict', None):
            response.update(err.extra_dict)
        return response

    def render_data(self, req, data, status=200):
        debug = DEFAULT_DEBUG
        debug = debug or req.GET.get('debug', 'false').lower() in ('true', 't', '1', 'on')
        debug = debug or req.GET.get('decode', '0').lower() in ('true', 't', '1', 'on')
        if 'callback' in req.GET or 'jsonpCallback' in req.GET:
            format = req.GET.get('format', 'jsonp')
        else:
            format = req.GET.get('format', 'json')
        jsonp_cb = req.GET.get('callback') or req.GET.get('jsonpCallback') or CALLBACK_NAME
        if not re.match(r'^[$a-zA-Z_][a-zA-Z0-9_]*$', jsonp_cb):
            return HttpResponseBadRequest('invalid JSONP callback name')
        content_type = "application/json"

        kwargs = dict(self.kwargs)
        if debug:
            kwargs["indent"] = 4
            kwargs["ensure_ascii"] = False

        plain = json.dumps(data, **kwargs)
        if format == 'jsonp':
            plain = "%s(%s);" % (jsonp_cb, plain)
            content_type = "application/javascript"

        return HttpResponse(plain, content_type="%s; charset=UTF-8" % content_type, status=status)

    def api_func(self, f, *args, **kwargs):
        return self.api(f, args[0], *args, **kwargs)

    def api_method(self, f, *args, **kwargs):
        return self.api(f, args[1], *args, **kwargs)

    def api(self, f, req, *args, **kwargs):
        from passerelle.utils import log_http_request

        logger = self.logger or logging.getLogger('passerelle.jsonresponse')
        try:
            resp = f(*args, **kwargs)
            if isinstance(resp, HttpResponseBase):
                return resp

            data = self.obj_to_response(req, resp)
            status = 200
            return self.render_data(req, data, status)
        except Exception as e:
            extras = {'method': req.method, 'exception': exception_to_text(e), 'request': req}
            if req.method == 'POST':
                max_size = settings.LOGGED_REQUESTS_MAX_SIZE
                if hasattr(logger, 'connector'):
                    max_size = logger.connector.logging_parameters.requests_max_size or max_size
                extras.update({'body': req.body[:max_size]})

            if isinstance(e, ObjectDoesNotExist):
                logger.warning('object not found: %r', e, extra=extras)
            elif isinstance(e, PermissionDenied):
                logger.warning('Permission denied', extra=extras)
            elif isinstance(e, HTTPError):
                log_http_request(
                    logger,
                    request=e.request,
                    response=e.response,
                    extra=extras,
                    duration=e.response.elapsed.total_seconds(),
                )
            elif isinstance(e, RequestException):
                log_http_request(logger, request=e.request, exception=e, extra=extras)
            elif isinstance(e, Http404):
                # Http404 is for silent object not found exceptions
                pass
            elif getattr(e, 'log_error', True):
                logger.exception("Error occurred while processing request", extra=extras)
            else:
                logger.warning("Error occurred while processing request", extra=extras)

            try:
                really_raise = int(req.GET.get('raise') or 0)
            except ValueError:
                really_raise = False
            if really_raise:
                raise

            data = self.err_to_response(e)
            if getattr(e, 'err_code', None):
                data['err'] = e.err_code
            if getattr(e, 'http_status', None):
                status = e.http_status
            elif isinstance(e, (ObjectDoesNotExist, Http404)):
                status = 404
            elif isinstance(e, PermissionDenied):
                status = 403
            else:
                status = self.error_code

            response = self.render_data(req, data, status)

            # We preempt Django logging of HTTP responses to prevent it from
            # logging our 500 responses as errors.
            if response.status_code >= 500:
                log_response(
                    '%s: %s',
                    response.reason_phrase,
                    e,
                    response=response,
                    request=req,
                    level='warning',
                )

            return response
