# encoding=utf-8

import re
import types
import requests
import datetime
import random
import json
import base64
import urllib

from Crypto.PublicKey import RSA
from Crypto.Hash import SHA, SHA256
from Crypto.Signature import PKCS1_v1_5

from hashlib import md5
from xml.etree import ElementTree
from urllib import urlencode, unquote
from django.conf import settings


def _smart_str(s, encoding='utf-8', strings_only=False, errors='strict'):
    """
    Returns a bytestring version of 's', encoded as specified in 'encoding'.
    If strings_only is True, don't convert (some) non-string-like objects.
    """
    if strings_only and isinstance(s, (types.NoneType, int)):
        return s
    if not isinstance(s, basestring):
        try:
            return str(s)
        except UnicodeEncodeError:
            if isinstance(s, Exception):
                # An Exception subclass containing non-ASCII data that doesn't
                # know how to print itself properly. We shouldn't raise a
                # further exception.
                return ' '.join([_smart_str(arg, encoding, strings_only,
                                            errors) for arg in s])
            return unicode(s).encode(encoding, errors)
    elif isinstance(s, unicode):
        return s.encode(encoding, errors)
    elif s and encoding != 'utf-8':
        return s.decode('utf-8', errors).encode(encoding, errors)
    else:
        return s


def _params_filter(params):
    """
    对数组排序并除去字典中的空值和签名参数
    返回数组和链接串
    :param
        params: 待过滤字典
    :return (newparams, prestr)
        newsparams: 过滤后的新字典
        prestr: 待签名字符串
    """
    ks = params.keys()
    ks.sort()
    newparams = {}
    prestr = ''
    for k in ks:
        v = params[k]
        k = _smart_str(k, settings.ALIPAY_INPUT_CHARSET)
        if k not in ('sign', 'sign_type') and v != '':
            newparams[k] = _smart_str(v, settings.ALIPAY_INPUT_CHARSET)
            prestr += '%s=%s&' % (k, newparams[k])
    prestr = prestr[:-1]  # 去掉末尾&
    return newparams, prestr


def _post_filter(data):
    prestr = ''
    for k in ["service", "v", "sec_id", "notify_data"]:
        v = data[k]
        if isinstance(v, list):
            v = v[0]
        prestr += '%s=%s&' % (k, v)
    return prestr[:-1]


def _parse_post_data(d):
    """
        alipay返回的参数为[]，去掉[],直接取第一项
    """
    for k, v in d.items():
        if isinstance(v, list):
            d[k] = v[0]
    return d


def _build_mysign(prestr, private_key, sign_type='MD5'):
    """
        生成签名结果
    """
    if sign_type == 'MD5':
        return md5(prestr + private_key).hexdigest()
    return ''


def _dict_to_xml(d, root):
    """
        转换为符合xml格式的字符串
        按照Alipay规范，去掉开头 xml version 信息

    """
    root = ElementTree.Element(root)
    for key, val in d.items():
        if val != "":
            child = ElementTree.Element(key)
            if not isinstance(val, basestring):
                val = str(val)
            child.text = val
            root.append(child)
    s = ElementTree.tostring(root, encoding='utf8')
    return re.sub("<.*xml version=.*?>\n", "", s)


def _build_create_direct_req_data(subject, out_trade_no, total_fee, callback_url):
    tag = "direct_trade_create_req"
    req_data = {}
    req_data["subject"] = subject
    req_data["out_trade_no"] = out_trade_no
    req_data["total_fee"] = total_fee
    req_data["seller_account_name"] = settings.ALIPAY_SELLER_EMAIL
    req_data["call_back_url"] = callback_url
    req_data["notify_url"] = settings.ALIPAY_NOTIFY_URL
    req_data["out_user"] = ""  # 商户系统用户唯一标识
    req_data["mechant_url"] = ""  # 操作中断返回地址
    req_data["pay_expire"] = ""  # 交易关闭时间
    req_data["agent_id"] = ""  # 代理人ID
    req_data["app_pay"] = "Y"

    return _dict_to_xml(req_data, tag)


def _build_auth_execute_req_data(request_token):
    tag = "auth_and_execute_req"
    req_data = {}
    req_data["request_token"] = request_token
    return _dict_to_xml(req_data, tag)


def _get_request_token(out_trade_no, subject, total_fee, callback_url):
    """
       获取交易授权码 
    """
    params = {}
    params["service"] = settings.ALIPAY_SERVICE_DIRECT_TRADE_CREATE
    params["format"] = settings.ALIPAY_FORMAT
    params["v"] = settings.ALIPAY_VERSION
    params["req_id"] = out_trade_no
    params["partner"] = settings.ALIPAY_PARTNER
    params["req_data"] = _build_create_direct_req_data(subject, out_trade_no, total_fee, callback_url)
    params['sec_id'] = settings.ALIPAY_SIGN_TYPE
    params, prestr = _params_filter(params)
    params['sign'] = _build_mysign(prestr, settings.ALIPAY_MD5_KEY, settings.ALIPAY_SIGN_TYPE)

    r = requests.post(settings.ALIPAY_GATEWAY, params)
    result = unquote(r.content)
    request_token = re.findall('<request_token>(.*?)</request_token>', result)[0]
    return request_token


def _get_auth_execute_url(request_token):
    """
       使用授权码，生成支付地址 
    """
    params = {}
    params["service"] = settings.ALIPAY_SERVICE_AUTH_EXECUTE
    params["format"] = settings.ALIPAY_FORMAT
    params["v"] = settings.ALIPAY_VERSION
    params["partner"] = settings.ALIPAY_PARTNER
    params['sec_id'] = settings.ALIPAY_SIGN_TYPE
    params["req_data"] = _build_auth_execute_req_data(request_token)
    params, prestr = _params_filter(params)
    params['sign'] = _build_mysign(prestr, settings.ALIPAY_MD5_KEY, settings.ALIPAY_SIGN_TYPE)
    url = settings.ALIPAY_GATEWAY + "?" + urlencode(params)
    return url


def alipay_refund_nopwd(trade_no, refund_amount, batch_no, notify_url):
    #  batch_no退款批次号，在商户系统内唯一；规格：退款日期（8位当天日期）+流水号（3~24位，不能接受“000”，但可以接受英文字母）
    assert 11 <= len(batch_no) <= 32

    batch_num = 1  # Refund one order once
    params = {}
    params["service"] = "refund_fastpay_by_platform_nopwd"
    params["partner"] = settings.ALIPAY_PARTNER
    params["_input_charset"] = 'utf-8'
    params['notify_url'] = notify_url

    params['batch_no'] = batch_no
    params['refund_date'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    params['batch_num'] = batch_num
    params['detail_data'] = u'{}^{}^协商退款'.format(trade_no, refund_amount)
    params, prestr = _params_filter(params)
    params['sign'] = _build_mysign(prestr, settings.ALIPAY_MD5_KEY, settings.ALIPAY_SIGN_TYPE)
    params["sign_type"] = 'MD5'
    url = 'https://mapi.alipay.com/gateway.do' + "?" + urlencode({'_input_charset': params['_input_charset']})
    rsp = requests.post(url, data=params)
    return rsp


def check_is_from_alipay(notify_id):
    params = {}
    params["service"] = "notify_verify"
    params["partner"] = settings.ALIPAY_PARTNER
    params["notify_id"] = notify_id
    url = 'https://mapi.alipay.com/gateway.do' + '?' + urlencode(params)
    rsp = requests.get(url)
    check_text = rsp.text
    if check_text == 'true':
        return True
    return False


def get_create_direct_pay_url(out_trade_no, subject, total_fee, callback_url):
    request_token = _get_request_token(out_trade_no, subject, total_fee, callback_url)
    return _get_auth_execute_url(request_token)


def verify_and_get_trade_status(post_data):
    """
    校验是否是来自alipay的请求，并返回alipay的最终支付状态
    :param
        post_data: alipay 异步请求的HttpPost QueryDict 数据
    :return:
        (bool, trade_staus)
    """
    post_data = _parse_post_data(post_data)
    prestr = _post_filter(post_data)
    utf8prestr = prestr.encode("utf8")
    mysign = _build_mysign(utf8prestr, settings.ALIPAY_MD5_KEY, settings.ALIPAY_SIGN_TYPE)
    sign = post_data.get("sign")
    trade_status = re.findall('<trade_status>(.*?)</trade_status>', prestr)[0]
    if mysign == sign:
        return True, trade_status
    return False, trade_status


class AlipayAppPayTools(object):

    def __init__(self, app_id=None,
                 notify_url=None,
                 timeout_express=None,
                 seller_id=None,
                 private_key=None,
                 alipay_public_key=None,
                 partner=None):

        self.app_id = app_id if app_id else settings.ALIPAY_APP_APP_ID
        self.notify_url = notify_url if notify_url else settings.ALIPAY_APP_NOTIFY_URL
        self.timeout_express = timeout_express if timeout_express else settings.ALIPAY_APP_TIMEOUT_EXPRESS
        self.seller_id = seller_id if seller_id else settings.ALIPAY_APP_SELLER_USER_ID
        private_key_str = private_key if private_key else settings.ALIPAY_APP_MY_PRIVATE_KEY
        self.private_key = RSA.importKey(private_key_str)
        alipay_public_key_str = alipay_public_key if alipay_public_key else settings.ALIPAY_APP_ALI_PUBLIC_KEY
        self.alipay_public_key = RSA.importKey(alipay_public_key_str)
        self.partner = partner if partner else settings.ALIPAY_PARTNER

    def use_old_conifg(self):
        self.private_key = RSA.importKey(settings.ALIPAY_APP_OLD_MY_PRIVATE_KEY)
        self.alipay_public_key = RSA.importKey(settings.ALIPAY_APP_OLD_ALI_PUBLIC_KEY)
        self.notify_url = settings.ALIPAY_APP_OLD_NOTIFY_URL

    def verify_seller_id(self, seller_id):
        return seller_id == self.seller_id

    def verify_app_id(self, app_id):
        return app_id == self.app_id

    def build_order_info_str_v1(self, out_trade_no, subject, body, total_amount,
                             notify_url=None,
                             timeout_express=None,
                             seller_id=None,
                             is_huabei=False,
                             huabei_period=0,
                             timestamp=None):

        order_info = {"partner": self.partner,
                      "seller_id": self.seller_id if seller_id is None else seller_id,
                      "out_trade_no": out_trade_no,
                      "subject": subject,
                      "body": body,
                      "total_fee": total_amount,
                      "notify_url": self.notify_url if notify_url is None else notify_url,
                      "service": "mobile.securitypay.pay",
                      "payment_type": "1",
                      "_input_charset": "utf-8",
                      "it_b_pay": self.timeout_express if timeout_express is None else timeout_express,
                      "return_url": "m.alipay.com",
                      }

        # 原始数据准备好
        if is_huabei:
            order_info['hb_fq_param'] = json.dumps({
                "hb_fq_num": huabei_period,
                "hb_fq_seller_percent": "0"
            })

        order_info_str = self._params_to_string(params=order_info, quotes=False, double_quotes=True, sort_keys=False)

        sign = urllib.quote_plus(self._make_sign(order_info_str))

        order_info_str += "&sign=\"" + sign + "\"&sign_type=\"RSA\""
        return order_info_str

    def build_order_info_str_v2(self, out_trade_no, subject, body, total_amount,
                             notify_url=None,
                             timeout_express=None,
                             seller_id=None,
                             timestamp=None):

        timestamp = datetime.datetime.now() if not timestamp else timestamp
        order_info = {"app_id": self.app_id,
                      "method": "alipay.trade.app.pay",
                      "format": "JSON",
                      "charset": "utf-8",
                      "timestamp": timestamp.strftime("%Y-%m-%d %H:%M:%S"),
                      "version": "1.0",
                      "notify_url": self.notify_url if notify_url is None else notify_url,
                      }

        biz_content = {"body": body,
                       "subject": subject,
                       "out_trade_no": out_trade_no,
                       "timeout_express": self.timeout_express if timeout_express is None else timeout_express,
                       "total_amount": total_amount,
                       "seller_id": self.seller_id if seller_id is None else seller_id,
                       "product_code": "QUICK_MSECURITY_PAY",
                       }

        biz_content_json = json.dumps(biz_content)
        order_info["biz_content"] = biz_content_json
        # 原始数据准备好

        order_info_str = self._params_to_string(params=order_info, quotes=True)

        raw_str = self._params_to_string(params=order_info, quotes=False)
        sign = urllib.quote_plus(self._make_sign(raw_str))

        order_info_str += "&sign=" + sign
        return order_info_str

    def _params_to_string(self, params, quotes=False, double_quotes=False, sort_keys=True):

        query = ""
        keys = sorted(params.keys()) if sort_keys else params.keys()
        for key in keys:
            value = params[key]
            query += self._build_key_value(key, value, quotes, double_quotes) + "&"

        query = query[0:-1]
        return query

    def _build_key_value(self, key, value, quotes=False, double_quotes=False):
        tmpl = "%s=\"%s\"" if double_quotes else "%s=%s"
        return tmpl % (key, urllib.quote(value) if quotes else value)

    def _make_sign(self, message):
        pkcs = PKCS1_v1_5.new(self.private_key)
        message_utf8_bytes = message.encode('utf-8')
        hash_obj = SHA.new(message_utf8_bytes)
        sign = pkcs.sign(hash_obj)
        b64sign = base64.b64encode(sign)
        return b64sign

    def _verify_alipay_sign(self, message, sign):
        pkcs = PKCS1_v1_5.new(self.alipay_public_key)
        message_utf8_bytes = message.encode('utf-8')
        hash_obj = SHA.new(message_utf8_bytes)
        v = pkcs.verify(hash_obj, sign)
        return v

    def verify_alipay_notify_sign(self, params_dict):
        try:
            sign_base64 = params_dict['sign']
            sign = base64.b64decode(sign_base64)
        except:
            return False

        params = {}

        for key in sorted(params_dict.keys()):
            if key == "sign" or key == "sign_type":
                continue
            params[key] = params_dict[key]

        raw_str = self._params_to_string(params=params, quotes=False)
        result = self._verify_alipay_sign(raw_str, sign)
        return result


class AlipayWapPayTools(object):
    """
    支付宝pc支付
    """

    def __init__(self, app_notify_url=None):
        """

        :param appid:
        :param app_notify_url: 异步通知接口
        :param return_url: 支付成功跳转地址
        :param alipay_public_key:
        :param app_private_key:
        :param debug:
        """
        self.app_notify_url = app_notify_url if app_notify_url else settings.ALIPAY_NOTIFY_URL
        self.appid = settings.SANDBOX_ALIPAY_APP_APP_ID if settings.DEBUG else settings.ALIPAY_APP_APP_ID
        alipay_public_key_str = settings.SANDBOX_ALIPAY_APP_ALI_PUBLIC_KEY if settings.DEBUG else settings.ALIPAY_APP_ALI_PUBLIC_KEY
        self.alipay_public_key = RSA.importKey(alipay_public_key_str)
        app_private_key_str = settings.SANDBOX_ALIPAY_APP_MY_PRIVATE_KEY if settings.DEBUG else settings.ALIPAY_APP_MY_PRIVATE_KEY
        self.app_private_key = RSA.importKey(app_private_key_str)

        if settings.DEBUG:
            self.__gateway = "https://openapi.alipaydev.com/gateway.do"
        else:
            self.__gateway = "https://openapi.alipay.com/gateway.do"

    def gen_direct_pay_url(self, subject, out_trade_no, total_amount, callback_url, **kwargs):
        biz_content = {
            "subject": subject,
            "out_trade_no": out_trade_no,
            "total_amount": total_amount,
            "product_code": "FAST_INSTANT_TRADE_PAY",
        }

        biz_content.update(kwargs)
        data = self.build_body("alipay.trade.page.pay", biz_content, callback_url)
        return self.__gateway + '?' + self.sign_data(data)

    def build_body(self, method, biz_content, callback_url):
        data = {
            "app_id": self.appid,
            "method": method,
            "charset": "utf-8",
            "sign_type": "RSA",
            "timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
            "version": "1.0",
            "biz_content": biz_content
        }

        if callback_url is not None:
            data["return_url"] = callback_url
            data["notify_url"] = self.app_notify_url

        return data

    def sign_data(self, data):
        data.pop("sign", None)
        # 排序后的字符串
        unsigned_items = self.ordered_data(data)
        unsigned_string = "&".join("{0}={1}".format(k, v) for k, v in unsigned_items)

        sign = self.sign(unsigned_string)
        ordered_items = self.ordered_data(data)
        quoted_string = "&".join("{0}={1}".format(k, urllib.quote_plus(v)) for k, v in ordered_items)

        # 获得最终的订单信息字符串
        signed_string = quoted_string + "&sign=" + urllib.quote_plus(sign)
        return signed_string

    def ordered_data(self, data):
        complex_keys = []
        for key, value in data.items():
            if isinstance(value, dict):
                complex_keys.append(key)

        # 将字典类型的数据dump出来
        for key in complex_keys:
            data[key] = json.dumps(data[key], separators=(',', ':'))

        return sorted([(k, v) for k, v in data.items()])

    def sign(self, unsigned_string):
        # 开始计算签名
        key = self.app_private_key
        signer = PKCS1_v1_5.new(key)
        signature = signer.sign(SHA.new(unsigned_string.encode('utf-8')))
        # base64 编码，转换为unicode表示并移除回车
        sign = base64.b64encode(signature)
        return sign

    def _verify_alipay_sign(self, message, sign):
        pkcs = PKCS1_v1_5.new(self.alipay_public_key)
        message_utf8_bytes = message.encode('utf-8')
        hash_obj = SHA.new(message_utf8_bytes)
        v = pkcs.verify(hash_obj, sign)
        return v

    def verify_alipay_notify_sign(self, params_dict):
        try:
            sign_base64 = params_dict['sign']
            sign = base64.b64decode(sign_base64)
        except:
            return False

        params = {}

        for key in sorted(params_dict.keys()):
            if key == "sign" or key == "sign_type":
                continue
            params[key] = params_dict[key]

        raw_str = self._params_to_string(params=params, quotes=False)
        result = self._verify_alipay_sign(raw_str, sign)
        return result

    def _params_to_string(self, params, quotes=False, double_quotes=False, sort_keys=True):

        query = ""
        keys = sorted(params.keys()) if sort_keys else params.keys()
        for key in keys:
            value = params[key]
            query += self._build_key_value(key, value, quotes, double_quotes) + "&"

        query = query[0:-1]
        return query

    def _build_key_value(self, key, value, quotes=False, double_quotes=False):
        tmpl = "%s=\"%s\"" if double_quotes else "%s=%s"
        return tmpl % (key, urllib.quote(value) if quotes else value)