"""付呗开放平台客户端：签名、小程序下单、退款、回调验签。"""
from __future__ import annotations

import hashlib
import json
import logging
import random
import string
import time
from decimal import Decimal, ROUND_HALF_UP
from typing import Any, Dict, Optional, Tuple

import requests

logger = logging.getLogger(__name__)

METHOD_MINA = 'openapi.payment.order.mina'
METHOD_REFUND = 'openapi.payment.order.refund'
METHOD_QUERY = 'openapi.payment.order.query'


def _yuan_str(amount) -> str:
    d = Decimal(str(amount)).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
    return format(d, 'f')


def build_sign(params: Dict[str, Any], app_secret: str) -> str:
    """付呗签名：参数 ASCII 排序后 key=value&... 再无缝拼接 secret，MD5 大写。"""
    items = []
    for k in sorted(params.keys()):
        if k == 'sign':
            continue
        v = params[k]
        if v is None or v == '':
            continue
        items.append(f'{k}={v}')
    raw = '&'.join(items) + (app_secret or '')
    return hashlib.md5(raw.encode('utf-8')).hexdigest().upper()


def verify_callback_sign(params: Dict[str, Any], app_secret: str) -> bool:
    got = (params.get('sign') or '').strip().upper()
    if not got:
        return False
    expect = build_sign(params, app_secret)
    return got == expect


def _nonce(n: int = 24) -> str:
    return ''.join(random.choices(string.ascii_letters + string.digits, k=n))


def call_fubei_api(
    *,
    gateway_url: str,
    app_id: str,
    app_secret: str,
    method: str,
    biz: Dict[str, Any],
    timeout: int = 15,
) -> Dict[str, Any]:
    """调用付呗 gateway，成功返回 data 字典；失败抛 Exception。"""
    biz_content = json.dumps(biz, ensure_ascii=False, separators=(',', ':'))
    payload = {
        'app_id': app_id,
        'method': method,
        'format': 'json',
        'sign_method': 'md5',
        'nonce': _nonce(),
        'version': '1.0',
        'biz_content': biz_content,
    }
    payload['sign'] = build_sign(payload, app_secret)

    url = (gateway_url or '').strip() or 'https://shq-api.51fubei.com/gateway'
    logger.info('[FUBEI] method=%s app_id=%s biz_keys=%s', method, app_id, list(biz.keys()))
    resp = requests.post(url, json=payload, timeout=timeout, headers={'Content-Type': 'application/json; charset=utf-8'})
    try:
        body = resp.json()
    except Exception:
        logger.error('[FUBEI] 非JSON响应 status=%s body=%s', resp.status_code, resp.text[:500])
        raise Exception(f'付呗接口异常: HTTP {resp.status_code}')

    result_code = body.get('result_code')
    if str(result_code) not in ('200', '200.0'):
        msg = body.get('result_message') or body.get('sub_code') or '付呗下单失败'
        logger.warning('[FUBEI] 业务失败 method=%s code=%s msg=%s body=%s', method, result_code, msg, body)
        raise Exception(str(msg))

    data = body.get('data')
    if isinstance(data, str):
        try:
            data = json.loads(data)
        except Exception:
            pass
    if not isinstance(data, dict):
        raise Exception('付呗返回 data 格式异常')
    return data


def mina_prepay(
    *,
    gateway_url: str,
    app_id: str,
    app_secret: str,
    merchant_order_sn: str,
    sub_openid: str,
    total_fee,
    store_id: int,
    call_back_url: str,
    body: str = '',
    attach: str = '',
) -> Dict[str, Any]:
    """小程序支付下单，返回含 sign_params / order_sn 等。"""
    biz = {
        'merchant_order_sn': merchant_order_sn,
        'sub_openid': sub_openid,
        'total_fee': float(_yuan_str(total_fee)),
        'store_id': int(store_id),
    }
    if call_back_url:
        biz['call_back_url'] = call_back_url
    if body:
        biz['body'] = (body or '')[:128]
    if attach:
        biz['attach'] = (attach or '')[:127]

    return call_fubei_api(
        gateway_url=gateway_url,
        app_id=app_id,
        app_secret=app_secret,
        method=METHOD_MINA,
        biz=biz,
    )


def refund_order(
    *,
    gateway_url: str,
    app_id: str,
    app_secret: str,
    merchant_order_sn: str,
    merchant_refund_sn: str,
    refund_money=None,
    order_sn: str = '',
) -> Dict[str, Any]:
    biz: Dict[str, Any] = {
        'merchant_refund_sn': merchant_refund_sn,
    }
    if merchant_order_sn:
        biz['merchant_order_sn'] = merchant_order_sn
    if order_sn:
        biz['order_sn'] = order_sn
    if refund_money is not None:
        biz['refund_money'] = float(_yuan_str(refund_money))

    return call_fubei_api(
        gateway_url=gateway_url,
        app_id=app_id,
        app_secret=app_secret,
        method=METHOD_REFUND,
        biz=biz,
    )


def query_order(
    *,
    gateway_url: str,
    app_id: str,
    app_secret: str,
    merchant_order_sn: str = '',
    order_sn: str = '',
) -> Dict[str, Any]:
    """订单查询。trade_state=SUCCESS 表示已支付。"""
    biz: Dict[str, Any] = {}
    if merchant_order_sn:
        biz['merchant_order_sn'] = merchant_order_sn
    if order_sn:
        biz['order_sn'] = order_sn
    if not biz:
        raise Exception('付呗查单缺少订单号')
    return call_fubei_api(
        gateway_url=gateway_url,
        app_id=app_id,
        app_secret=app_secret,
        method=METHOD_QUERY,
        biz=biz,
    )


def parse_callback_form(post_dict: Dict[str, Any]) -> Tuple[Dict[str, Any], Dict[str, Any]]:
    """解析付呗 form 回调，返回 (顶层参数含 sign, data 业务字典)。"""
    params = {k: (v if not isinstance(v, list) else (v[0] if v else '')) for k, v in post_dict.items()}
    # 兼容 QueryDict.dict()
    data_raw = params.get('data') or ''
    data: Dict[str, Any] = {}
    if isinstance(data_raw, dict):
        data = data_raw
    elif isinstance(data_raw, str) and data_raw.strip():
        try:
            data = json.loads(data_raw)
        except Exception:
            # 偶发双重编码
            try:
                data = json.loads(data_raw.replace("'", '"'))
            except Exception:
                logger.error('[FUBEI_NOTIFY] data 解析失败: %s', data_raw[:300])
                data = {}
    return params, data


def extract_jsapi_pay_params(sign_params: Dict[str, Any]) -> Dict[str, str]:
    """从付呗 sign_params 抽出小程序 wx.requestPayment 所需字段。"""
    if not sign_params:
        raise Exception('付呗未返回支付签名包')
    # 部分字段可能混有 store_id 等，只取调起支付字段
    out = {
        'appId': str(sign_params.get('appId') or sign_params.get('appid') or ''),
        'timeStamp': str(sign_params.get('timeStamp') or sign_params.get('timestamp') or ''),
        'nonceStr': str(sign_params.get('nonceStr') or sign_params.get('noncestr') or ''),
        'package': str(sign_params.get('package') or ''),
        'signType': str(sign_params.get('signType') or sign_params.get('sign_type') or 'MD5'),
        'paySign': str(sign_params.get('paySign') or sign_params.get('pay_sign') or ''),
    }
    if not out['timeStamp'] or not out['package'] or not out['paySign']:
        raise Exception('付呗签名包字段不完整')
    return out
