import collections.abc
from functools import partial, wraps

from django.core.exceptions import PermissionDenied
from django.db import OperationalError
from django.http import Http404
from django.http.request import UnreadablePostError
from django.utils.translation import ugettext_lazy as _

from rest_framework import exceptions
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import set_rollback

from intranet.femida.src.api.core.views import ResponseError
from intranet.femida.src.core.workflow import WorkflowError
from intranet.femida.src.monitoring.checkpoints import irreversible_checkpoints


NO_FILES_GIVEN_IN_REQUEST = 'errors.no_files_given_in_request'
UPLOAD_TO_MDS_FAILED = 'errors.upload_to_mds_failed'


def format_message(field, message):
    return {
        'errors': {
            field: [
                {
                    'code': message,
                },
            ],
        },
    }


def format_non_field_message(message):
    return {
        'errors': {
            '': [
                {
                    'code': message,
                }
            ],
            '__all__': [
                {
                    'code': message,
                }
            ]
        }
    }


def format_non_form_message(error):
    if not isinstance(error, collections.abc.Iterable):
        error = [error]

    return {
        'error': [
            {
                'code': e.message,
                'params': getattr(e, 'params', {}) or {},
            }
            for e in error
        ]
    }


def exception_handler(exc, context):
    """
    За основу взят rest_framework.views.exception_handler

    Отличается тем, что мы расширяем формат ошибок:
    {
        "errors": {},  // формат sform
        "detail": "<string>",  // для обратной совместимости
        "error": [
            {
                "code": "<tanker_code>",
                "message": "<человеко_читаемая_ошибка_для_нас>",
                "params": {}  // необязательный
            },
            ...
        ]
    }
    Все три поля необязательны.
    Если ошибка из sform, то придет errors.
    Если ошибка не из sform, то придет error.
    Если на фронте sform, а errors отсутствует, то сфолбечится на error.
    Все detail, которые есть сейчас останутся для обратной совместимости.
    Ошибки старых форм будут приходить пока в старом формате "поле: список ошибок".
    """
    handled = False
    headers = {}
    status_code = status.HTTP_400_BAD_REQUEST
    data = {}
    response = None

    if isinstance(exc, exceptions.Throttled):
        exc.detail = 'too_many_requests'

    if isinstance(exc, exceptions.APIException):
        headers = {}
        if getattr(exc, 'auth_header', None):
            headers['WWW-Authenticate'] = exc.auth_header
        if getattr(exc, 'wait', None):
            headers['Retry-After'] = '%d' % exc.wait

        if isinstance(exc.detail, (list, dict)):
            data = exc.detail
        else:
            data = {
                'detail': exc.detail,
                'error': [{
                    'code': exc.detail,
                    'message': exc.detail,
                }]
            }
        handled = True
        status_code = exc.status_code

    elif isinstance(exc, WorkflowError):
        data = format_non_field_message(exc.message)
        data.update(detail=exc.message)
        handled = True
        status_code = status.HTTP_400_BAD_REQUEST

    elif isinstance(exc, PermissionDenied):
        msg = str(_('Permission denied.'))
        data = {
            'detail': msg,
            'error': [{
                'code': 'permission_denied',
                'message': msg,
            }]
        }
        handled = True
        status_code = status.HTTP_403_FORBIDDEN

    elif isinstance(exc, Http404):
        msg = str(_('Not found.'))
        data = {
            'detail': msg,
            'error': [{
                'code': 'not_found',
                'message': msg,
            }]
        }
        handled = True
        status_code = status.HTTP_404_NOT_FOUND

    elif isinstance(exc, ResponseError):
        response = exc.response

    # Эта ошибка может возникнуть, если порвется соединение,
    # пока приложение читает тело POST-запроса.
    # В основном, это происходит из-за таймаута на балансере,
    # вызванного плохим соединением у клиента
    elif isinstance(exc, UnreadablePostError):
        handled = True
        status_code = status.HTTP_408_REQUEST_TIMEOUT

    if handled:
        set_rollback()
        response = Response(data, status=status_code, headers=headers)

    irreversible_checkpoints.close()
    return response


def _locked_custom_exc(func, return_exc_cls):
    @wraps(func)
    def inner(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except OperationalError as exc:
            if _is_lock_error(exc):
                raise return_exc_cls
            raise
    return inner


class HttpLocked(exceptions.APIException):
    status_code = status.HTTP_423_LOCKED
    default_detail = "Object is locked"


locked_is_forbidden = partial(_locked_custom_exc, return_exc_cls=PermissionDenied)
locked = partial(_locked_custom_exc, return_exc_cls=HttpLocked)


def _is_lock_error(e):
    if not isinstance(e, OperationalError) or not e.args:
        return False
    message = e.args[0]
    lock_errors = ('could not obtain lock', 'lock timeout')
    return any(i in message for i in lock_errors)
