"""各俱乐部微信支付 V2（MD5）配置与验签。"""
import hashlib
import logging
import os

from django.conf import settings

from jituan.constants import CLUB_ID_DEFAULT
from jituan.models import Club
from jituan.services.club_resolver import get_club_miniapp_appid

logger = logging.getLogger(__name__)

# 星阙(xq) 生产微信支付 — club 表为空时的兜底（禁止回落 settings 旧商户 1746545364）
XQ_WX_PAY = {
    'mch_id': '1747846572',
    'mch_key': 'dujie4637rufjd98ijuyrgcvbvbn4789',
    'api_v3_key': 'dujie4637rufjd98ijuyrgcvbvbn4789',
    'cert_serial_no': '481784CF56E872D9D8122BAC64E6BC7390803582',
    'wx_appid': 'wx0e4be86faac4a8d1',
}

# 与星阙共用同一微信商户号/证书目录的俱乐部（AppID 各读各的 club 表）
SHARED_WX_MERCHANT_CLUBS = frozenset({CLUB_ID_DEFAULT, 'xzj'})

# 兼容旧引用
WX_V3_CERT_SERIAL_NO_OVERRIDE = XQ_WX_PAY['cert_serial_no']


def _pick_club_field(club, cfg_json, *field_names):
    """从 club 列或 config_json 取第一个非空值。"""
    if club:
        for name in field_names:
            if '.' not in name:
                val = (getattr(club, name, None) or '').strip()
                if val:
                    return val
    cfg_json = cfg_json or {}
    for name in field_names:
        key = name.split('.')[-1] if '.' in name else name
        val = (cfg_json.get(key) or '').strip()
        if val:
            return val
    return ''


def _load_pem_private_key(private_key_path):
    from cryptography.hazmat.primitives import serialization
    from cryptography.hazmat.backends import default_backend
    with open(private_key_path, 'rb') as f:
        return serialization.load_pem_private_key(
            f.read(), password=None, backend=default_backend(),
        )


def _public_keys_match(private_key, cert_public_key):
    try:
        return private_key.public_key().public_numbers() == cert_public_key.public_numbers()
    except Exception:
        return False


def resolve_merchant_cert_serial(private_key_path, configured_serial=''):
    """
    扫描私钥同目录下所有 PEM，找到与 apiclient_key.pem 公钥匹配的商户证书，
    返回其序列号。手填/数据库里的序列号不可信，必须以私钥配对证书为准。
    """
    configured = (configured_serial or '').strip().upper()
    if not private_key_path:
        logger.error('[WX_V3_CONFIG] 未配置私钥路径')
        return configured
    if not os.path.isfile(private_key_path):
        logger.error('[WX_V3_CONFIG] 私钥文件不存在 path=%s', private_key_path)
        return configured

    try:
        private_key = _load_pem_private_key(private_key_path)
    except Exception as exc:
        logger.error('[WX_V3_CONFIG] 私钥加载失败 path=%s err=%s', private_key_path, exc)
        return configured

    cert_dir = os.path.dirname(os.path.abspath(private_key_path))
    candidates = [
        private_key_path.replace('apiclient_key.pem', 'apiclient_cert.pem'),
        os.path.join(cert_dir, 'apiclient_cert.pem'),
        os.path.join(cert_dir, 'wechatpay_cert.pem'),
        os.path.join(cert_dir, 'cert.pem'),
    ]
    try:
        for fname in sorted(os.listdir(cert_dir)):
            if not fname.lower().endswith('.pem'):
                continue
            lower = fname.lower()
            # 只跳过私钥文件；pub_key.pem 等仍尝试（有人误命名证书）
            if lower in ('apiclient_key.pem',) or 'private' in lower:
                continue
            candidates.append(os.path.join(cert_dir, fname))
    except OSError as exc:
        logger.warning('[WX_V3_CONFIG] 扫描证书目录失败 dir=%s err=%s', cert_dir, exc)

    seen = set()
    from cryptography import x509
    from cryptography.hazmat.backends import default_backend

    for path in candidates:
        path = os.path.abspath(path)
        if path in seen or not os.path.isfile(path):
            continue
        seen.add(path)
        try:
            if os.path.samefile(path, private_key_path):
                continue
        except OSError:
            pass
        try:
            with open(path, 'rb') as f:
                data = f.read()
            if b'PRIVATE KEY' in data:
                continue
            cert = x509.load_pem_x509_certificate(data, default_backend())
            if not _public_keys_match(private_key, cert.public_key()):
                continue
            serial = format(cert.serial_number, 'X').upper()
            logger.warning(
                '[WX_V3_CONFIG] 私钥与证书已匹配 cert=%s serial=%s (configured=%s)',
                path, serial, configured or '(none)',
            )
            return serial
        except Exception as exc:
            logger.debug('[WX_V3_CONFIG] 跳过证书 path=%s err=%s', path, exc)

    logger.error(
        '[WX_V3_CONFIG] 未找到与私钥匹配的 apiclient_cert.pem dir=%s key=%s configured=%s',
        cert_dir, private_key_path, configured,
    )
    return configured


def _xq_defaults():
    """仅私钥路径/平台证书目录可回落 settings（与商户号无关）。"""
    wx = dict(getattr(settings, 'WECHAT_PAY_V3_CONFIG', {}) or {})
    return {
        'PRIVATE_KEY_PATH': wx.get('PRIVATE_KEY_PATH', ''),
        'PLATFORM_CERT_DIR': wx.get('PLATFORM_CERT_DIR', ''),
        'TRANSFER_SCENE_ID': wx.get('TRANSFER_SCENE_ID', '1005'),
    }


def _xml_text(root, tag, default=''):
    node = root.find(tag)
    if node is None or node.text is None:
        return default
    return node.text


def _resolve_v2_mch_key(club, cfg_json, fallback_key, club_id=None):
    """V2 JSAPI 统一下单用的商户 API 密钥（32 位 MD5 密钥，不是 api_v3_key）。"""
    column_key = (getattr(club, 'mch_key', None) or '').strip() if club else ''
    if column_key:
        return column_key, 'club.mch_key'
    cfg_json = cfg_json or {}
    for field in ('mch_key', 'shanghumiyao', 'WEIXIN_SHANGHUMIYAO'):
        val = (cfg_json.get(field) or '').strip()
        if val:
            return val, f'club.config_json.{field}'
    cid = (club_id or '').strip() or CLUB_ID_DEFAULT
    if cid in SHARED_WX_MERCHANT_CLUBS and XQ_WX_PAY['mch_key']:
        return XQ_WX_PAY['mch_key'], 'XQ_WX_PAY.mch_key'
    fb = (fallback_key or '').strip()
    if fb:
        return fb, 'settings.WEIXIN_SHANGHUMIYAO'
    return '', 'missing'


def get_wechat_v2_config(club_id=None, app_source='mini'):
    """
    微信 JSAPI 支付 V2 配置。
    app_source:
      - mini：小程序 AppID（默认，现网行为）
      - official：服务号 AppID（H5）；商户号/密钥与小程序相同，仅换 AppID
    """
    cid = (club_id or '').strip() or CLUB_ID_DEFAULT
    fallback = {
        'appid': getattr(settings, 'WEIXIN_APPID', ''),
        'mch_id': getattr(settings, 'WEIXIN_MCHID', ''),
        'key': getattr(settings, 'WEIXIN_SHANGHUMIYAO', ''),
        'club_id': cid,
    }

    try:
        club = Club.query.get(club_id=cid)
    except Club.DoesNotExist:
        logger.warning('club %s 不存在，回落全局支付配置', cid)
        return fallback

    cfg_json = club.config_json or {}
    mch_key, key_source = _resolve_v2_mch_key(club, cfg_json, fallback['key'], club_id=cid)
    mini_appid = get_club_miniapp_appid(club)
    official_appid = (club.official_appid or '').strip()
    wx_a = (club.wx_appid or '').strip()
    pay_a = (club.pay_app_id or '').strip()
    if wx_a and pay_a and wx_a != pay_a:
        logger.warning(
            '[WX_V2_CONFIG] club=%s wx_appid(%s) 与 pay_app_id(%s) 不一致；'
            '小程序 JSAPI 以 wx_appid 优先（openid 与登录 AppID 绑定）',
            cid, wx_a, pay_a,
        )

    source = (app_source or 'mini').strip().lower()
    if source in ('official', 'h5', 'oa'):
        appid = official_appid
        if not appid:
            logger.error('[WX_V2_CONFIG] club=%s H5 支付需要 official_appid', cid)
    elif mini_appid:
        appid = mini_appid
    elif cid == CLUB_ID_DEFAULT:
        appid = fallback['appid']
    else:
        logger.error(
            '[WX_V2_CONFIG] club=%s 未配置 wx_appid/pay_app_id，禁止回落星阙全局 AppID',
            cid,
        )
        appid = ''
    pay_fb = XQ_WX_PAY if cid in SHARED_WX_MERCHANT_CLUBS else {}
    mch_id = (
        _pick_club_field(club, cfg_json, 'mch_id')
        or pay_fb.get('mch_id', '')
        or fallback['mch_id']
    )

    if mch_id and key_source == 'settings.WEIXIN_SHANGHUMIYAO':
        logger.warning(
            '[WX_V2_CONFIG] club=%s 已配置 mch_id=%s 但未配置 V2 商户密钥 config_json.mch_key；'
            '当前回落全局 WEIXIN_SHANGHUMIYAO（与 api_v3_key 不是同一个密钥，易导致签名错误）',
            cid, mch_id,
        )

    # 收款商户证书（供退款等对齐收款商户；未配时留空，调用方可回落 settings）
    pay_cert = _pick_club_field(club, cfg_json, 'pay_cert_path')
    pay_key = _pick_club_field(club, cfg_json, 'pay_key_path')

    return {
        'appid': appid,
        'mch_id': mch_id,
        'key': mch_key,
        'club_id': cid,
        '_key_source': key_source,
        '_app_source': source if source in ('official', 'h5', 'oa') else 'mini',
        # 明确标记：这是点单/会员收款商户
        'merchant_role': 'pay',
        'cert_path': pay_cert,
        'key_path': pay_key,
    }


def log_wechat_v2_pay_diag(pay_cfg, *, scene='', openid='', order_hint=''):
    """下单前诊断日志（不打印完整密钥）。"""
    oid = (openid or '')
    oid_preview = (oid[:6] + '...' + oid[-4:]) if len(oid) > 10 else oid or '(empty)'
    key = pay_cfg.get('key') or ''
    logger.warning(
        '[WX_V2_PAY_DIAG] scene=%s club=%s appid=%s mch_id=%s key_len=%d key_source=%s openid=%s hint=%s',
        scene or '-',
        pay_cfg.get('club_id'),
        pay_cfg.get('appid'),
        pay_cfg.get('mch_id'),
        len(key),
        pay_cfg.get('_key_source', '?'),
        oid_preview,
        order_hint or '-',
    )


def parse_wechat_v2_unifiedorder_response(content, *, log_prefix='WX_UNIFIED'):
    """
    解析微信 V2 统一下单 XML；失败时记录完整原始响应并抛出可读异常。
    成功返回 {'prepay_id': '...'}。
    """
    import defusedxml.ElementTree as ET

    raw = (
        content.decode('utf-8', errors='replace')
        if isinstance(content, (bytes, bytearray))
        else str(content or '')
    )
    logger.warning('[%s] raw_response=%s', log_prefix, raw)

    try:
        root = ET.fromstring(content)
    except Exception as exc:
        raise Exception(f'微信支付返回非 XML: {raw[:800]}') from exc

    return_code = _xml_text(root, 'return_code')
    result_code = _xml_text(root, 'result_code')

    if return_code == 'SUCCESS' and result_code == 'SUCCESS':
        prepay_id = _xml_text(root, 'prepay_id')
        if not prepay_id:
            raise Exception(f'微信支付成功但缺少 prepay_id: {raw}')
        return {'prepay_id': prepay_id}

    parts = []
    if return_code:
        parts.append(f'return_code={return_code}')
    if result_code:
        parts.append(f'result_code={result_code}')
    for tag in ('return_msg', 'err_code', 'err_code_des'):
        val = _xml_text(root, tag)
        if val:
            parts.append(f'{tag}={val}')
    if not parts:
        parts.append(f'raw={raw[:800]}')
    raise Exception(f'微信支付下单失败: {", ".join(parts)}')


def verify_wechat_v2_signature(xml_data, club_id=None):
    """验证微信 V2 支付回调 MD5 签名。"""
    try:
        import defusedxml.ElementTree as ET
        root = ET.fromstring(xml_data)
        sign_node = root.find('sign')
        if sign_node is None or not sign_node.text:
            return False
        sign = sign_node.text
        data = {child.tag: child.text for child in root if child.tag != 'sign'}
        string_a = '&'.join([f'{k}={data[k]}' for k in sorted(data.keys()) if data[k]])
        pay_cfg = get_wechat_v2_config(club_id)
        calc = hashlib.md5(f'{string_a}&key={pay_cfg["key"]}'.encode('utf-8')).hexdigest().upper()
        if calc == sign:
            return True
        # 兼容：子 club 未配密钥时尝试全局密钥
        global_key = getattr(settings, 'WEIXIN_SHANGHUMIYAO', '')
        if global_key and global_key != pay_cfg['key']:
            calc2 = hashlib.md5(f'{string_a}&key={global_key}'.encode('utf-8')).hexdigest().upper()
            return calc2 == sign
        return False
    except Exception as e:
        logger.error('微信验签异常: %s', e)
        return False


def club_id_from_czjilu(dingdan_id):
    from products.models import Czjilu
    try:
        row = Czjilu.query.filter(dingdan_id=dingdan_id).first()
        if row and getattr(row, 'club_id', None):
            return row.club_id
    except Exception:
        pass
    return CLUB_ID_DEFAULT


def club_id_from_order(order_id):
    from orders.models import Order
    try:
        row = Order.query.filter(OrderID=order_id).first()
        if row and getattr(row, 'ClubID', None):
            return row.ClubID
    except Exception:
        pass
    return CLUB_ID_DEFAULT


def _log_v3_config(cfg, *, source=''):
    key_path = (cfg.get('PRIVATE_KEY_PATH') or '').strip()
    logger.warning(
        '[WX_V3_CONFIG] source=%s club=%s appid=%s mch=%s serial=%s '
        'api_v3_key_len=%d key_path=%s key_exists=%s',
        source or '-',
        cfg.get('club_id'),
        cfg.get('APPID'),
        cfg.get('MCHID'),
        cfg.get('CERT_SERIAL_NO'),
        len(cfg.get('API_V3_KEY') or ''),
        key_path,
        bool(key_path and os.path.isfile(key_path)),
    )


def get_wechat_v3_config(club_id=None):
    """
    微信 V3（转账/查单）配置。
    优先 club 数据库 → 星阙写死兜底 → 仅私钥路径/平台证书目录回落 settings。
    禁止用 settings 里的旧商户号 1746545364。
    """
    cid = (club_id or '').strip() or CLUB_ID_DEFAULT
    path_defaults = _xq_defaults()

    club = None
    cfg_json = {}
    try:
        club = Club.query.get(club_id=cid)
        cfg_json = club.config_json or {}
    except Club.DoesNotExist:
        logger.warning('club %s 不存在，使用星阙兜底或路径默认', cid)

    xq = XQ_WX_PAY if cid in SHARED_WX_MERCHANT_CLUBS else {}

    # 提现商户号：优先 withdraw_mch_id，空则回落收款 mch_id（兼容旧数据）
    mch_id = (
        _pick_club_field(club, cfg_json, 'withdraw_mch_id')
        or _pick_club_field(club, cfg_json, 'mch_id')
        or xq.get('mch_id', '')
    )
    api_v3_key = _pick_club_field(club, cfg_json, 'api_v3_key') or xq.get('api_v3_key', '')

    # 共用商户的俱乐部：默认用 settings 私钥；仅当库中路径存在且文件在盘上才覆盖
    # （避免旧库里曾填过、但原先被忽略的无效路径突然接管提现）
    # 非共享俱乐部：严禁回落星阙私钥/商户，缺配置直接空值，由上层拒绝打款
    if cid in SHARED_WX_MERCHANT_CLUBS:
        private_key_path = path_defaults['PRIVATE_KEY_PATH']
        if club:
            uploaded_key = (getattr(club, 'private_key_path', None) or '').strip()
            if uploaded_key and os.path.isfile(uploaded_key):
                private_key_path = uploaded_key
        platform_cert_dir = (
            _pick_club_field(club, cfg_json, 'platform_cert_dir')
            or path_defaults['PLATFORM_CERT_DIR']
        )
    else:
        private_key_path = _pick_club_field(club, cfg_json, 'private_key_path')
        platform_cert_dir = _pick_club_field(club, cfg_json, 'platform_cert_dir')
        if not mch_id or not api_v3_key or not private_key_path:
            logger.error(
                '[WX_V3_CONFIG] club=%s 提现商户配置不完整(mch=%s key=%s pem=%s)，'
                '禁止回落星阙商户，避免串号打款',
                cid, bool(mch_id), bool(api_v3_key), bool(private_key_path),
            )

    transfer_scene_id = (
        _pick_club_field(club, cfg_json, 'transfer_scene_id', 'TRANSFER_SCENE_ID')
        or path_defaults['TRANSFER_SCENE_ID']
    )

    appid = get_club_miniapp_appid(club) if club else ''
    if not appid and cid in SHARED_WX_MERCHANT_CLUBS:
        appid = xq.get('wx_appid', '') if cid == CLUB_ID_DEFAULT else ''

    # 序列号必须从与私钥配对的 apiclient_cert.pem 读取，禁止盲用数据库/写死值
    cert_serial = resolve_merchant_cert_serial(private_key_path, '')
    if not cert_serial:
        cert_serial = (
            _pick_club_field(club, cfg_json, 'cert_serial_no')
            or xq.get('cert_serial_no', '')
        )

    cfg = {
        'APPID': appid,
        'MCHID': mch_id,
        'PRIVATE_KEY_PATH': private_key_path,
        'CERT_SERIAL_NO': cert_serial,
        'API_V3_KEY': api_v3_key,
        'PLATFORM_CERT_DIR': platform_cert_dir,
        'TRANSFER_SCENE_ID': transfer_scene_id,
        'club_id': cid,
        'merchant_role': 'withdraw',
    }
    _log_v3_config(cfg, source='club_db' if club else 'xq_fallback')
    return cfg


def club_id_for_tixian_yonghuid(yonghuid):
    from jituan.services.club_user import get_user_club_id
    from users.business_models import User
    user = User.query.filter(UserUID=yonghuid).first()
    return get_user_club_id(user)
