#!/usr/bin/env python
# -*- coding:utf-8 -*-
#
#   Author  :   RobertDing
#   E-mail  :   robertdingx@gmail.com
#   Date    :   16/03/25 13:15:41
#   Desc    :   view 等 基类
#
import json
from functools import wraps
from distutils.version import LooseVersion
from django.views.decorators.csrf import csrf_exempt

from django.conf import settings
from django.views.generic import View
from django.views.decorators.csrf import ensure_csrf_cookie
from django.shortcuts import render
from django.http import JsonResponse
from django.http.response import HttpResponseBase

from helios.rpc.exceptions import RPCFaultException

from gm_types.gaia import PLATFORM_CHOICES
from gm_types.ascle.error import ERROR

from utils.exception import SunException
from utils.user_util import require_login
from utils.logger import log_error



_EMPTY_OBJECT = object()


class LazyAttrDict(object):
    def __init__(self, query_dict, configs, request_version):
        self._configs = configs
        self._query_dict = query_dict
        self.request_version = self.request_version

    def __getattr__(self, attr):
        config = self._configs.get(attr)
        if config is None:  # 若是没有写config直接返回值
            return self._query_dict.get(attr)
        default = config.get('default', _EMPTY_OBJECT)
        raw = self._query_dict.get(attr, default)
        if raw is _EMPTY_OBJECT:
            if self.request_version:  # 目前使用参数version来判断请求来源
                version_add = config.get('version_add', '0.0.1')
                if LooseVersion(self.request_version) >= LooseVersion(version_add):
                    version_deprecated = config.get('version_deprecated')
                    if not version_deprecated or LooseVersion(self.request_version) < LooseVersion(version_deprecated):
                        raise SunException(ERROR.ARG_MISS, '缺少参数: %s' % attr)
            else:
                raise SunException(ERROR.ARG_MISS, '缺少参数: %s' % attr)

        if raw is default:
            return default
        else:
            try:
                access = config.get('access', str)
                if access == bool:  # 处理Ajax传Boolean
                    if raw == 1 or raw == '1' or raw.lower() == 'true':
                        raw = True
                    else:
                        raw = False
                else:
                    raw = access(raw)
            except:
                raise SunException(ERROR.ARG_ERROR, '错误参数: %s' % attr)

            # 因为有'access': lambda s: s.strip()类似的情况，需要在转换之后去判断blank
            if raw == '' and not config.get("blank", True):  # 提取出来的参数不允许为''
                raise SunException(ERROR.ARG_ERROR, '参数不能为空: %s' % attr)
            return raw

    def get(self, k):
        return self.__getattr__(k)

    def to_dict(self, exclude=[]):
        """
            只转化 config 中配置的参数, 方便用于表单提交
            @param exclude: 为排出不做提交的参数
        """
        p = {}
        for k in self._configs.keys():
            if k not in exclude:
                p[k] = getattr(self, k)
        return p

    def __len__(self):
        return len(self._query_dict)

    def __iter__(self):
        return iter(self._query_dict)


def handler_exception(fn):
    @wraps(fn)
    def _wrapper(view, request, *args, **kwargs):
        try:
            response = fn(view, request, *args, **kwargs)
        except Exception as exception:
            if settings.DEBUG and not isinstance(exception, (RPCFaultException, SunException)):
                log_error()
                raise

            if isinstance(exception, RPCFaultException):
                if exception.error == 401:  # gaia返回的需要登录重新处理
                    return require_login(request, origin='RPCFaultException_401')
                elif exception.error == 404:
                    log_error()
                exception = SunException(exception.error, exception.message)
            elif isinstance(exception, SunException):
                pass
            else:
                # 服务器错误
                # 参照 http://git.gengmei.cc/backend/gm-types/blob/master/gm_types/ascle/error.py
                exception = SunException(ERROR.ASCLE_ERROR)
                log_error()

            data = {
                'data': None,
                'error': exception.code,
                'message': exception.message,
            }
            response = JsonResponse(data)
        return response

    return _wrapper


class APIView(View):
    """
    API 基类

    - 如果方法返回的数据是 HttpResponseBase 的子类则不做 JsonResponse 处理

    处理 GET 与 POST 参数
    要求子类显示声明要求的参数, 格式如下

    ```
    args_GET: {
        name: {
            'access': int,          # 处理参数并返回结果
            'default': 0            # 不写default，则表示参数不能为None
            'version_add': '2.8.0'  # client 请求参数添加版本, 默认是APP兼容最低版本
            'version_deprecated': '2.9.0'  # client请求参数废弃版本，默认为None
            'blank' : True,         # 默认为True, 代表允许从request解析到参数允许为''或者'  '
        }
    }
       * version_add 和 version_deprecated 需要根据请求参数version去确认是否必须
    args_POST: ...
    ```
    """
    args_GET = {}
    args_POST = {}

    decorators = []

    template = None  # 返回html页面

    # 兼容起始版本，方法名后缀，低于版本号执行此方法, 从小到大排序
    """
        1) 若compatible_versions = ['2.4.0', '2.8.0', ]
            请求参数version<'2.4.0',  执行 get_240 / render_get_240
            请求参数'2.4.0'<=version<'2.8.0',  执行 get_270 / render_get_270
            请求参数version>='2.8.0', 执行 get / render_get
        2) 其他method(post/delete)方法和get方法同理
        3) 可参考示例：api/client/account.py  ClientLoginView
    """
    compatible_versions = []

    @classmethod
    def as_view(cls, enable_csrf=True, **initkwargs):
        view = super().as_view(**initkwargs)
        if ensure_csrf_cookie not in cls.decorators:
            cls.decorators.insert(0, ensure_csrf_cookie)

        # 客户端需要添加忽略csrftoken的校验
        if not enable_csrf and csrf_exempt not in cls.decorators:
            cls.decorators.insert(0, csrf_exempt)

        for deco in cls.decorators[::-1]:
            view = deco(view)

        return view

    def prepare(self, request, *args, **kwargs):
        self.request = request
        # 请求是否来自客户端, client js请求也会带上version的
        self.request_version = request.GET.get('version')
        self.request_from_client = False
        self.args_default = ClientDefaultArgs(request.GET)
        self.args_get = LazyAttrDict(request.GET, self.args_GET, self.request_version)
        self.args_post = LazyAttrDict(request.POST, self.args_POST, self.request_version)
        self.rpc = request.rpc.origin

    @handler_exception
    def dispatch(self, request, *args, **kwargs):

        self.prepare(request, *args, **kwargs)

        version = None
        method_name = request.method.lower()
        if method_name in self.http_method_names:
            handler = getattr(self, method_name, None)
            if self.request_from_client:
                # 若是客户端请求，做兼容处理，带上版本号
                version = self.args_default.get_min_verion(self.compatible_versions)
                if version:
                    version = version.replace('.', '')
                    client_handler = getattr(self, '{}_{}'.format(method_name, version), None)
                    if callable(client_handler):
                        handler = client_handler

            if callable(handler):
                data = handler(request, *args, **kwargs)
            else:
                raise SunException(ERROR.HTTP_METHOD_NOT_ALLOW)
        else:
            raise SunException(ERROR.HTTP_METHOD_NOT_ALLOW)

        if isinstance(data, HttpResponseBase):
            return data

        # 找是否有组织数据的函数
        render_name = 'render_{}'.format(method_name)
        render_method = getattr(self, render_name, None)
        if version:
            # 若是客户端请求有兼容版本
            client_render = getattr(self, '{}_{}'.format(render_name, version), None)
            if callable(client_render):
                render_method = client_render
        # 执行组织数据函数
        if callable(render_method):
            if data is not None:
                data = render_method(data, *args, **kwargs)
            else:
                data = render_method(*args, **kwargs)

        if isinstance(data, HttpResponseBase):
            return data
        elif self.template is not None:
            if settings.DEBUG and self.args_get.debug:
                return JsonResponse(data, safe=False)
            else:
                return render(request, self.template, data)
        else:
            return JsonResponse(self.write_success(data))

    def write_fail(self, code, message):
        response = {
            'error': code,
            'data': None,
            'message': message
        }
        return response

    def write_success(self, data, message=""):
        response = {
            'error': 0,
            'data': data,
            'message': message,
        }
        return response

    def write_response(self, data, message):
        """ 需要自定义message的使用这个函数, 只允许在render_{method}方法中调用
        """
        return JsonResponse(self.write_success(data, message))

    def write_404(self):
        return render(self.request, 'client/404.jinja.html')

    def start_num(self, page_size=settings.PAGE_SIZE):
        """
            获取 列表的start_num 为了兼容客户端
        """
        # TODO Deprecated 过几个版本之后可以去掉,等app都改过来
        start_num = int(self.request.GET.get('start_num', 0))
        page = int(self.request.GET.get('page', 0))
        if page > 0:
            start_num = (page - 1) * page_size
        return start_num

    def make_pair(self, data):
        val = data.pop('value', '')
        key = data.pop('key', '')
        if key:
            key += '__contains'
        if key:
            data.update({key: val})

        for key in list(data.keys()):
            if data.get(key) == '':
                del data[key]
        return data

    def handle_filter(self, filter):
        if not isinstance(filter, str):
            return {}

        filter_data = json.loads(filter)

        return self.make_pair(filter_data)


class ClientDefaultArgs(LazyAttrDict):
    """
        客户端每个接口默认传递的参数
    """
    args_CLIENT = {
        'app_name': {},  # gengmei_doctor
        'version': {},  # 应用版本
        'channel': {'default': None},  # 去掉，eg: benzhan / AppStore
        'lng': {'access': float, 'default': 0},  # 经度
        'lat': {'access': float, 'default': 0},  # 纬度
        'platform': {},  # choices in [android / iPhone]
        'os_version': {'default': None},  # 系统版本
        'model': {'default': None},  # 移动设备型号
        'screen': {'default': None},  # 屏幕分辨率
        'device_id': {'default': None},  # 设备唯一标识
        'class_name': {'default': None},  # 仅Android：Application类名
        'idfa': {'default': None},  # 仅iOS：广告唯一标识
        'idfv': {'default': None},  # 仅iOS，iOS10之后idfa可以关掉
    }

    def __init__(self, query_dict):
        super(ClientDefaultArgs, self).__init__(query_dict, self.args_CLIENT, None)

    @property
    def is_android(self):
        return self.platform == PLATFORM_CHOICES.ANDROID

    def get_min_verion(self, versions):
        """
            获取兼容到最小版本的版本号
        """
        version = None
        versions.sort()
        for v in versions:
            if LooseVersion(self.version) < LooseVersion(v):
                version = v
                break
        return version

    def get_device_id(self):
        device_id = self.device_id or self.idfv or self.idfa
        return device_id
