"""
微信支付网关实现 (Wechat Pay Gateway)
基于 www.lytiao.com 项目提取

作者: 卡若
版本: v4.0
"""

import json
import time
import hashlib
import logging
import xml.etree.ElementTree as ET
from typing import Dict, Any, Optional
from urllib.parse import urlencode
import uuid

from payment_factory import (
    AbstractGateway, CreateTradeData, TradeResult, NotifyResult,
    SignatureError, logger
)


class WechatGateway(AbstractGateway):
    """
    微信支付网关
    
    支持:
    - Native扫码支付 (platform_type='native')
    - JSAPI公众号/小程序支付 (platform_type='jsapi')
    - H5支付 (platform_type='h5')
    - APP支付 (platform_type='app')
    """
    
    UNIFIED_ORDER_URL = 'https://api.mch.weixin.qq.com/pay/unifiedorder'
    ORDER_QUERY_URL = 'https://api.mch.weixin.qq.com/pay/orderquery'
    CLOSE_ORDER_URL = 'https://api.mch.weixin.qq.com/pay/closeorder'
    REFUND_URL = 'https://api.mch.weixin.qq.com/secapi/pay/refund'
    
    def __init__(self, config: Dict[str, Any]):
        super().__init__(config)
        self.appid = config.get('appid', '')
        self.app_secret = config.get('app_secret', '')
        self.mch_id = config.get('mch_id', '')
        self.mch_key = config.get('mch_key', '')
        self.cert_path = config.get('cert_path', '')
        self.key_path = config.get('key_path', '')
    
    def create_trade(self, data: CreateTradeData) -> TradeResult:
        """创建微信支付交易"""
        platform_type = data.platform_type.upper()
        
        # 构建统一下单参数
        params = {
            'appid': self.appid,
            'mch_id': self.mch_id,
            'nonce_str': self._generate_nonce(),
            'body': data.goods_title[:128],
            'out_trade_no': data.trade_sn,
            'total_fee': str(data.amount),  # 微信以分为单位
            'spbill_create_ip': data.create_ip or '127.0.0.1',
            'notify_url': data.notify_url,
            'trade_type': platform_type,
            'attach': json.dumps(data.attach) if data.attach else '',
        }
        
        # JSAPI需要openid
        if platform_type == 'JSAPI':
            if not data.open_id:
                raise ValueError("微信JSAPI支付需要提供 openid")
            params['openid'] = data.open_id
        
        # H5支付需要scene_info
        if platform_type == 'MWEB':
            params['scene_info'] = json.dumps({
                'h5_info': {
                    'type': 'Wap',
                    'wap_url': data.return_url,
                    'wap_name': data.goods_title[:32]
                }
            })
        
        # 生成签名
        params['sign'] = self._generate_sign(params)
        
        # 调用统一下单接口
        import requests
        xml_data = self._dict_to_xml(params)
        response = requests.post(self.UNIFIED_ORDER_URL, data=xml_data.encode('utf-8'))
        result = self._xml_to_dict(response.text)
        
        if result.get('return_code') != 'SUCCESS':
            raise Exception(f"微信支付统一下单失败: {result.get('return_msg')}")
        
        if result.get('result_code') != 'SUCCESS':
            raise Exception(f"微信支付统一下单失败: {result.get('err_code_des')}")
        
        # 根据类型返回不同格式
        if platform_type == 'NATIVE':
            # 扫码支付返回二维码链接
            return TradeResult(
                type='qrcode',
                payload=result['code_url'],
                trade_sn=data.trade_sn
            )
        
        elif platform_type == 'JSAPI':
            # 公众号支付返回JS SDK参数
            prepay_id = result['prepay_id']
            js_params = {
                'appId': self.appid,
                'timeStamp': str(int(time.time())),
                'nonceStr': self._generate_nonce(),
                'package': f'prepay_id={prepay_id}',
                'signType': 'MD5',
            }
            js_params['paySign'] = self._generate_sign(js_params)
            
            return TradeResult(
                type='json',
                payload=js_params,
                trade_sn=data.trade_sn
            )
        
        elif platform_type == 'MWEB':
            # H5支付返回跳转链接
            mweb_url = result['mweb_url']
            if data.return_url:
                mweb_url += f"&redirect_url={urlencode({'': data.return_url})[1:]}"
            
            return TradeResult(
                type='url',
                payload=mweb_url,
                trade_sn=data.trade_sn
            )
        
        elif platform_type == 'APP':
            # APP支付返回SDK参数
            prepay_id = result['prepay_id']
            app_params = {
                'appid': self.appid,
                'partnerid': self.mch_id,
                'prepayid': prepay_id,
                'package': 'Sign=WXPay',
                'noncestr': self._generate_nonce(),
                'timestamp': str(int(time.time())),
            }
            app_params['sign'] = self._generate_sign(app_params)
            
            return TradeResult(
                type='json',
                payload=app_params,
                trade_sn=data.trade_sn
            )
        
        else:
            raise ValueError(f"不支持的微信支付类型: {platform_type}")
    
    def verify_sign(self, data: Dict[str, Any]) -> bool:
        """验证微信签名"""
        sign = data.pop('sign', '')
        if not sign:
            return False
        
        calculated_sign = self._generate_sign(data)
        return calculated_sign == sign
    
    def parse_notify(self, data: Any) -> NotifyResult:
        """解析微信回调"""
        # 如果是XML字符串，先转换为dict
        if isinstance(data, str):
            data = self._xml_to_dict(data)
        
        # 验证签名
        if not self.verify_sign(data.copy()):
            raise SignatureError("微信签名验证失败")
        
        result_code = data.get('result_code', '')
        
        if result_code == 'SUCCESS':
            status = 'paid'
        else:
            status = 'failed'
        
        # 解析透传参数
        attach = {}
        attach_str = data.get('attach', '')
        if attach_str:
            try:
                attach = json.loads(attach_str)
            except:
                pass
        
        # 解析支付时间 (格式: 20240117100530)
        time_end = data.get('time_end', '')
        if time_end:
            pay_time = int(time.mktime(time.strptime(time_end, '%Y%m%d%H%M%S')))
        else:
            pay_time = int(time.time())
        
        return NotifyResult(
            status=status,
            trade_sn=data.get('out_trade_no', ''),
            platform_sn=data.get('transaction_id', ''),
            pay_amount=int(data.get('cash_fee', 0)),
            pay_time=pay_time,
            currency=data.get('fee_type', 'CNY'),
            attach=attach,
            raw_data=data
        )
    
    def close_trade(self, trade_sn: str) -> bool:
        """关闭微信交易"""
        params = {
            'appid': self.appid,
            'mch_id': self.mch_id,
            'out_trade_no': trade_sn,
            'nonce_str': self._generate_nonce(),
        }
        params['sign'] = self._generate_sign(params)
        
        import requests
        xml_data = self._dict_to_xml(params)
        response = requests.post(self.CLOSE_ORDER_URL, data=xml_data.encode('utf-8'))
        result = self._xml_to_dict(response.text)
        
        return result.get('result_code') == 'SUCCESS'
    
    def query_trade(self, trade_sn: str) -> Optional[NotifyResult]:
        """查询微信交易"""
        params = {
            'appid': self.appid,
            'mch_id': self.mch_id,
            'out_trade_no': trade_sn,
            'nonce_str': self._generate_nonce(),
        }
        params['sign'] = self._generate_sign(params)
        
        import requests
        xml_data = self._dict_to_xml(params)
        response = requests.post(self.ORDER_QUERY_URL, data=xml_data.encode('utf-8'))
        result = self._xml_to_dict(response.text)
        
        if result.get('trade_state') == 'SUCCESS':
            return self.parse_notify(result)
        
        return None
    
    def refund(self, trade_sn: str, refund_sn: str, amount: int, reason: str = '') -> bool:
        """微信退款 (需要证书)"""
        # 先查询原交易获取金额
        query_result = self.query_trade(trade_sn)
        if not query_result:
            return False
        
        params = {
            'appid': self.appid,
            'mch_id': self.mch_id,
            'nonce_str': self._generate_nonce(),
            'out_trade_no': trade_sn,
            'out_refund_no': refund_sn,
            'total_fee': str(query_result.pay_amount),
            'refund_fee': str(amount),
            'refund_desc': reason or '用户申请退款',
        }
        params['sign'] = self._generate_sign(params)
        
        import requests
        xml_data = self._dict_to_xml(params)
        
        # 退款需要证书
        response = requests.post(
            self.REFUND_URL,
            data=xml_data.encode('utf-8'),
            cert=(self.cert_path, self.key_path)
        )
        result = self._xml_to_dict(response.text)
        
        return result.get('result_code') == 'SUCCESS'
    
    def success_response(self) -> str:
        """微信回调成功响应"""
        return '<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>'
    
    def fail_response(self) -> str:
        """微信回调失败响应"""
        return '<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[ERROR]]></return_msg></xml>'
    
    def _generate_nonce(self) -> str:
        """生成随机字符串"""
        return uuid.uuid4().hex
    
    def _generate_sign(self, params: dict) -> str:
        """生成MD5签名"""
        # 排序并拼接
        sorted_params = sorted([(k, v) for k, v in params.items() if v and k != 'sign'])
        sign_str = '&'.join([f'{k}={v}' for k, v in sorted_params])
        sign_str += f'&key={self.mch_key}'
        
        # MD5签名
        return hashlib.md5(sign_str.encode('utf-8')).hexdigest().upper()
    
    def _dict_to_xml(self, data: dict) -> str:
        """字典转XML"""
        xml = ['<xml>']
        for k, v in data.items():
            if isinstance(v, str):
                xml.append(f'<{k}><![CDATA[{v}]]></{k}>')
            else:
                xml.append(f'<{k}>{v}</{k}>')
        xml.append('</xml>')
        return ''.join(xml)
    
    def _xml_to_dict(self, xml_str: str) -> dict:
        """XML转字典"""
        root = ET.fromstring(xml_str)
        return {child.tag: child.text for child in root}
