import json
import decimal
import time
from hashlib import md5
from xml.dom import minidom
import xml.etree.ElementTree

import requests
import aiohttp
# from minipay.config import MiniAppsConfig
# from minipay.exceptions import ModelError, ModeError, TargetError, MethodError
from app_tools_zxw.SDK_微信.SDK_微信支付.minipay.config import MiniAppsConfig
from app_tools_zxw.SDK_微信.SDK_微信支付.minipay.exceptions import ModelError, ModeError, TargetError, MethodError

__all__ = ['BaseMiniPay', 'BaseNotification']


class DecimalEncoder(json.JSONEncoder):
    def default(self, o):
        if isinstance(o, decimal.Decimal):
            return int(float(o) * 100)
        super(DecimalEncoder, self).default(o)


class BaseMiniPay(object):
    def __init__(self, **kwargs):
        self.config = {
            'app_id': None, 'mch_id': None, 'secret': None, 'nonce_str': None,
            'key': None, 'cert': None, 'cert_key': None, 'payment_notify_url': None,
            'refund_notify_url': None, 'default_mode': None, 'default_model': None,
            'default_method': None, 'api_unified_order': None, 'api_order_query': None,
            'api_close_order': None, 'api_refund': None, 'api_refund_query': None,
            'async': None
        }
        self.config_from_object(MiniAppsConfig)
        self.target = None
        self.notify_url = None
        self.method = kwargs.get("method") or self.config['default_method']
        self.request_data = dict()
        self.response_data = dict()
        self.error = dict()
        self.options = dict()
        self.model = kwargs.get('model')
        self.mode = kwargs.get('mode') or self.config['default_mode']
        self.request_data_xml = None

    def config_from_object(self, config_obj):
        for attr, value in config_obj.__dict__.items():
            if attr.startswith('_'):
                continue
            lowed_attr = attr.lower()
            if lowed_attr in self.config.keys():
                self.config[lowed_attr] = value

    def _decision_rules(self):
        pass

    def mini_formatted(self):
        """https://developers.weixin.qq.com/miniprogram/dev/api/wx.requestPayment.html"""
        response = {
            'appId': self.response_data.get('appid'),
            'timeStamp': str(int(time.time())),
            'nonceStr': self.config['nonce_str'],
            'package': 'prepay_id=%s' % self.response_data.get('prepay_id'),
            'signType': 'MD5',
        }
        sign = self.sign(response)
        response['paySign'] = sign
        response.pop('appId')
        return response

    def request(self):
        self._decision_rules()
        self._filter(self.request_data)
        self.sign()
        request_data_xml = self.dict_to_xml(self.request_data)
        self.request_data_xml = request_data_xml.decode()

        if self.target is None:
            raise TargetError("object's target attribute must be a url.")
        if self.method == 'post':
            resp_xml = requests.post(self.target, request_data_xml, **self.options)
        elif self.method == 'get':
            resp_xml = requests.get(self.target, request_data_xml, **self.options)
        else:
            raise MethodError("object's method attribute must be 'post' or 'get'.")

        resp_xml.encoding = 'utf-8'
        self.response_data = self.xml_to_dict(resp_xml.text)

        if self.mode == 'store':
            try:
                self._store()
            except (ModelError, ModeError) as err:
                print(err)
        return self._handle_response()

    async def request_async(self):
        self._decision_rules()
        self._filter(self.request_data)
        self.sign()
        request_data_xml = self.dict_to_xml(self.request_data)
        self.request_data_xml = request_data_xml.decode()

        if self.target is None:
            raise TargetError("object's target attribute must be a url.")

        async with aiohttp.ClientSession() as client:
            if self.method == 'post':
                resp_xml = await client.post(self.target, data=request_data_xml, **self.options)
            elif self.method == 'get':
                resp_xml = await client.get(self.target, params=request_data_xml, **self.options)
            else:
                raise MethodError("object's method attribute must be 'post' or 'get'.")

        resp_xml = await resp_xml.text()
        self.response_data = self.xml_to_dict(resp_xml)

        if self.mode == 'store':
            try:
                self._store()
            except (ModelError, ModeError) as err:
                print(err)
        return self._handle_response()

    def _handle_response(self):
        if self.request_is_successfully:
            if self.business_is_successfully:
                return self._handle_successful_business()
            else:
                return self._handle_failing_business()
        else:
            return self._handle_failing_request()

    def _handle_successful_business(self):
        return self.response_data

    def _handle_failing_business(self):
        self.error['code'] = self.response_data['err_code']
        self.error['desc'] = self.response_data['err_code_des']
        return self.error

    def _handle_failing_request(self):
        self.error['code'] = self.response_data['return_code']
        self.error['desc'] = self.response_data['return_msg']
        return self.error

    @property
    def is_success(self):
        return self.request_is_successfully and self.business_is_successfully

    @property
    def is_fail(self):
        return not self.request_is_successfully or not self.business_is_successfully

    @property
    def request_is_successfully(self):
        return 'SUCCESS' == self.response_data.get('return_code')

    @property
    def business_is_successfully(self):
        return 'SUCCESS' == self.response_data.get('result_code')

    @staticmethod
    def dict_to_xml(data):
        if not isinstance(data, dict):
            raise TypeError("data object must be a dict type.")

        data = json.dumps(data, ensure_ascii=False, cls=DecimalEncoder)
        data = json.loads(data)

        dom = minidom.Document()
        root = dom.createElement('xml')
        dom.appendChild(root)
        for key, value in data.items():
            sub_node = dom.createElement(key)
            root.appendChild(sub_node)
            text = dom.createTextNode(str(value))
            sub_node.appendChild(text)

        return dom.toprettyxml(encoding='utf-8')

    @staticmethod
    def xml_to_dict(data, root='xml'):
        """
        传入一个xml字符串， 返回一个字典类型的对象.
        root可以规定xml的根名称，此方法会对根节点进行忽略
        """
        d = {}
        elements = xml.etree.ElementTree.fromstring(data)
        for element in elements.iter():
            if element.tag == root:
                continue
            d[element.tag] = element.text
        return d

    def _filter(self, data):
        for key, value in data.items():
            if key in self.request_data.keys():
                self.request_data[key] = value

        filtered_data = {}
        for key, value in self.request_data.items():
            if value and value != "":
                if isinstance(value, decimal.Decimal):
                    value = int(float(value) * 100)
                filtered_data[key] = value
        self.request_data = filtered_data

    def sign(self, data=None):
        if data is None:
            data = self.request_data

        if not isinstance(data, dict):
            raise TypeError("")

        formatted = ''
        for key in list(sorted(data.keys())):
            string = key + '=' + str(data.get(key)) + '&'
            formatted += string

        formatted += 'key=%s' % self.config['key']
        sign = md5(formatted.encode())
        sign = sign.hexdigest().upper()
        self.request_data['sign'] = sign
        return sign

    def _store(self):
        if self.mode == 'ignore':
            raise ModeError("Current mode is 'ignore', that can't be store the request data or response data.")
        if self.model is None:
            raise ModelError('%s model attribute does not set.' % self.__class__.__name__)
        if self.mode == 'store':
            new_record = self.model()
            for attr, value in self.response_data.items():
                if hasattr(new_record, attr):
                    setattr(new_record, attr, value)
            for attr, value in self.request_data.items():
                if hasattr(new_record, attr):
                    setattr(new_record, attr, value)
            new_record.save()


class BaseNotification(object):
    def __init__(self, data, **kwargs):
        data = BaseMiniPay.xml_to_dict(data)
        self.response_data = data
        self.config = {
            'key': MiniAppsConfig.KEY,
            'mode': kwargs.get('mode') or MiniAppsConfig.DEFAULT_MODE,
        }
        self.mode = kwargs.get('mode') or self.config['mode']
        self.model = kwargs.get('model')
        self.field = "out_trade_no"

    def sign(self, data=None):
        if data is None:
            data = self.response_data

        if not isinstance(data, dict):
            raise TypeError("")

        formatted = ''
        for key in list(sorted(data.keys())):
            string = key + '=' + str(data.get(key)) + '&'
            formatted += string

        formatted += 'key=%s' % self.config['key']
        sign = md5(formatted.encode())
        sign = sign.hexdigest().upper()
        return sign

    def _verifysign(self):
        serversign = self.response_data.pop('sign')
        localsign = self.sign()
        if serversign == localsign:
            return True
        return False

    def decrypt(self):
        pass

    def _decision_rules(self):
        pass

    def handle(self):
        self._decision_rules()
        if self.is_finish:
            return self._successful_formatted()

        if self._verifysign():
            if self.mode == 'store':
                self._store()
            return self._successful_formatted()
        return self._failing_formatted('sign invaild.')

    @staticmethod
    def _successful_formatted(text=None):
        response = {
            'return_code': 'SUCCESS',
            'return_msg': text or 'success'
        }
        return BaseMiniPay.dict_to_xml(response)

    @staticmethod
    def _failing_formatted(text=None):
        response = {
            'return_code': 'FAIL',
            'return_msg': text or 'FAIL'
        }
        return BaseMiniPay.dict_to_xml(response)

    def _store(self):
        if self.mode == 'ignore':
            raise ModeError("Current mode is 'ignore', that can't be store the request data or response data.")
        if self.model is None:
            raise ModelError('%s model attribute does not set.' % self.__class__.__name__)
        if self.mode == 'store':
            new_record = self.model()
            for attr, value in self.response_data.items():
                if hasattr(new_record, attr):
                    setattr(new_record, attr, value)
            new_record.save()

    @property
    def is_finish(self):
        if self.model is None:
            return False
        if hasattr(self.model, self.field):
            if self.model.objects.filter(out_trade_no=self.field).exists():
                return True
        return False

