# coding=utf8

from __future__ import unicode_literals, absolute_import, print_function

import inspect
from decimal import Decimal
from functools import wraps

from django.db import models
from django.utils.html import escape

from gm_types.error import ERROR as CODES

# NOTE: all gaia.rpc related modules/functions should be imported here
from rpc.decorators import list_interface
from rpc.decorators import bind as _gaia_rpc_bind
from rpc.decorators import bind_context as _gaia_rpc_bind_context
from rpc.decorators import cache_page

from rpc.context import get_gaia_local_invoker, get_rpc_remote_invoker
from rpc.exceptions import GaiaRPCFaultException
from rpc.db_opt import add_related

from rpc.cache import page_cache
from rpc.cache import doctor_tags_cache
# from rpc.cache import wechat_cache

from rpc.tool.error_code import gen
from rpc.tool.param_check import assert_uint
from rpc.tool.es_helpers import get_objects_from_queryset_and_pk_list
from rpc.tool.log_tool import logging_exception
from rpc.tool.log_tool import info_logger
from rpc.tool.log_tool import period_task_logger
from rpc.all import get_rpc_remote_invoker

from talos import APP_ROUTE_PREFIX


class RPCMixin(object):

    @staticmethod
    def get_rpc_invoker():
        return get_gaia_local_invoker()

    @classmethod
    def call_rpc(cls, api_endpoint, **kwargs):
        """call rpc.

        get rpc invoker from current ctx, this method only support call rpc sequentially.
        """
        r = cls.get_rpc_invoker()
        result = r[api_endpoint](**kwargs)
        return result.unwrap()

    @staticmethod
    def get_remote_rpc_invoker():
        return get_rpc_remote_invoker()

    @classmethod
    def call_remote_rpc(cls, api_endpoint, **kwargs):
        """call remote rpc.

        get remote rpc invoker from current ctx, this method only support call rpc sequentially.
        """
        r = cls.get_remote_rpc_invoker()
        result = r[api_endpoint](**kwargs)
        return result.unwrap()


def bind(endpoint):
    def decorator(method):
        f = _gaia_rpc_bind(endpoint)(method)

        if endpoint.startswith(APP_ROUTE_PREFIX):
            _endpoint = endpoint.replace(APP_ROUTE_PREFIX, 'api')
            f = _gaia_rpc_bind(_endpoint)(method)

        return f

    return decorator


def bind_context(name, **options):
    def decorator(method):
        f = _gaia_rpc_bind_context(name, **options)(method)

        if name.startswith(APP_ROUTE_PREFIX):
            _endpoint = name.replace(APP_ROUTE_PREFIX, 'api')
            f = _gaia_rpc_bind_context(_endpoint, **options)(method)

        return f

    return decorator


SKIP_TYPES = (int, long, float, Decimal, type(None), bool, list, tuple, dict)


def _get_val(v):
    if isinstance(v, SKIP_TYPES):
        return v
    else:
        try:
            v = unicode(v)
        except:
            v = str(v)
        return escape(v)


def _fetch_related(obj, fields):
    if hasattr(obj, 'to_dict'):
        try:
            return obj.to_dict(fields)
        except:
            return obj.to_dict()
    else:
        if not fields:
            fields = ['id']
        return {f: _get_val(getattr(obj, f)) for f in fields}


def get_non_model_field_value(obj, field):
    fval = getattr(obj, field, None)
    if callable(fval):
        i = inspect.getargspec(fval)
        if len(i.args) == 1:
            fval = fval()
        else:
            fval = None
    return fval


def to_dict(obj, fields=None, excludes=None, expands=None):
    ret = {}
    model_fields = []
    for field in obj._meta.get_fields():
        field_name = field.name
        model_fields.append(field_name)
        if (excludes is not None and field_name in excludes) or \
                (fields is not None and field_name not in fields):
            # skip field that in the excludes or not in ther fields
            continue
        try:
            fval = getattr(obj, field_name, None)
        except:
            fval = None
        if not isinstance(field, models.fields.related.RelatedField) or not fval:
            # Basic Field, get value directly
            ret[field_name] = _get_val(fval)
        else:
            if expands is None or field_name not in expands.keys():
                if isinstance(field, models.ForeignKey):
                    ret[field_name] = fval.id if fval else None
                else:
                    # ManyToManyField
                    ret[field_name] = list(fval.values_list('id', flat=True))
            else:
                # need expand related object
                related_fields = expands[field_name]
                if isinstance(field, models.ForeignKey):
                    ret[field_name] = _fetch_related(fval, related_fields)
                else:
                    # ManyToManyField
                    ret[field_name] = [
                        _fetch_related(o, related_fields) for o in fval.all()
                    ]
    # access the field not a Model's Field
    if fields:
        for field_name in set(fields) - set(model_fields):
            field_list = field_name.split('__')
            if len(field_list) > 1:
                fval = obj
                for i in field_list:
                    if fval is None:
                        break
                    try:
                        fval = getattr(fval, i, None)
                    except:
                        break
            else:
                fval = get_non_model_field_value(obj, field_name)
            ret[field_name] = _get_val(fval)
    return ret
