# -*- coding: utf-8 -*-
"""
mo_models.py — MoFin 唯一数据模型（Single Source of Truth）

⚠️ 铁律：MoFin 中所有以下操作必须走这个文件，严禁各自实现：
  1. 判断港股 — 用 is_hk_stock(code)
  2. 计算总资产 — 用 calc_total_assets(pf)
  3. 获取港币汇率 — 用 get_hk_rate()
  4. 币种转换 — 用 to_cny(price, code)

创建日期: 2026-06-29
原因: 之前 total_assets 在 6+ 文件中各自计算，公式不一致（3个漏了 frozen_cash）；
      is_hk_stock 有 3 种不同实现，存在误判风险；
      hk_rate 在多个文件中硬编码不同值（0.866/0.87/0.8664/0.8700）。
"""

import sys
import os

# ── 港股检测 ────────────────────────────────────────────────────────

def is_hk_stock(code):
    """判断是否为港股。
    
    规则：港股代码为5位数字，以0或1开头。
    例：00700(腾讯), 01888(建滔积层板), 00981(中芯国际)
    
    排除：
    - A股6位代码如 000657(中钨高新) — len==6 不会被误判
    - 美股字母代码如 AAPL
    - 带前缀的代码如 hk00700 → 自动去前缀后判断
    """
    code = (str(code or '')).strip().upper()
    # 去常见前缀
    for prefix in ('HK', 'SH', 'SZ', 'BJ'):
        if code.startswith(prefix):
            code = code[len(prefix):]
    # 港股: 5位数字, 0或1开头
    return len(code) == 5 and code.isdigit() and code[0] in ('0', '1')


def is_a_stock(code):
    """判断是否为A股（沪深京）"""
    code = (str(code or '')).strip().upper()
    for prefix in ('SH', 'SZ', 'BJ'):
        if code.startswith(prefix):
            code = code[len(prefix):]
    if len(code) == 6 and code.isdigit():
        if code.startswith(('0', '3', '6')):
            return True
        if code.startswith(('4', '8', '9')):
            return True  # 北交所/科创板
    return False


def normalize_code(code):
    """归一化股票代码：去市场前缀，去后缀，统一大写"""
    code = (str(code or '')).strip().upper()
    for prefix in ('HK', 'SH', 'SZ', 'BJ'):
        if code.startswith(prefix):
            code = code[len(prefix):]
    return code


# ── 港币汇率 ──────────────────────────────────────────────────────────

def get_hk_rate():
    """获取 HKD→CNY 汇率。优先用 hk_rate 模块（支持API+缓存），失败回退 0.87"""
    try:
        from hk_rate import hkd_to_cny
        return hkd_to_cny()
    except Exception:
        pass
    # 最后的兜底
    return 0.87


def to_cny(price, code):
    """如果 code 是港股，把 price 从 HKD 转为 CNY；否则原样返回"""
    if price is None or price == 0:
        return price
    if is_hk_stock(code):
        return round(float(price) * get_hk_rate(), 2)
    return price


# ── 总资产计算（唯一公式） ────────────────────────────────────────────

def calc_total_mv(holdings):
    """计算持仓总市值。港股 price 为 HKD，需 × 汇率转 CNY"""
    total = 0
    rate = get_hk_rate()
    for h in (holdings or []):
        p = (h.get('price', 0) or 0)
        s = (h.get('shares', 0) or 0)
        if h.get('currency') == 'HKD' or (str(h.get('code','')).startswith(('0','1')) and len(str(h.get('code',''))) == 5):
            total += s * p * rate
        else:
            total += s * p
    return round(total, 2)


def calc_total_assets(pf):
    """
    计算总资产 = 持仓市值 + 可用现金 + 冻结资金
    
    这是 MoFin 中 total_assets 的 **唯一正确公式**。
    所有文件必须调用此函数，严禁各自实现。

    Args:
        pf: dict，包含 holdings、cash、frozen_cash 字段
    
    Returns:
        float: 总资产（人民币）
    """
    total_mv = calc_total_mv(pf.get('holdings', []))
    cash = float(pf.get('cash', 0) or 0)
    frozen = float(pf.get('frozen_cash', 0) or 0)
    return round(total_mv + cash + frozen, 2)


def calc_position_pct(pf):
    """计算仓位百分比"""
    total = calc_total_assets(pf)
    if total > 0:
        total_mv = calc_total_mv(pf.get('holdings', []))
        return round(total_mv / total * 100, 2)
    return 0


# ── 数据验证 ──────────────────────────────────────────────────────────

def validate_portfolio(pf):
    """验证 portfolio.json 数据一致性，返回 issues 列表"""
    issues = []
    holdings = pf.get('holdings', [])
    
    # 1. 总资产校验
    stored = pf.get('total_assets', 0)
    calculated = calc_total_assets(pf)
    if stored > 0 and abs(stored - calculated) / max(stored, 1) > 0.01:
        issues.append(
            f"total_assets 不匹配: 存储{stored:.2f} ≠ 计算{calculated:.2f}"
            f" (市值{calc_total_mv(holdings):.2f}+现金{pf.get('cash',0):.2f}+冻结{pf.get('frozen_cash',0):.2f})"
        )
    
    # 2. 币种一致性
    for h in holdings:
        code = str(h.get('code', ''))
        currency = h.get('currency', h.get('_currency', ''))
        if is_hk_stock(code) and currency == 'HKD':
            issues.append(
                f"⚠️ 港股{code}({h.get('name','?')}) currency=HKD，"
                f"portfolio.json 应全部存 CNY"
            )
    
    # 3. 零股检查
    for h in holdings:
        if (h.get('shares', 0) or 0) <= 0 and h.get('code'):
            issues.append(f"持仓{h.get('code')}({h.get('name','?')}) 股数为0或负数")
    
    return issues


# ── 向后兼容别名 ──────────────────────────────────────────────────────

# 让旧代码中散落的 is_hk_stock 引用也能正确工作
__all__ = [
    'is_hk_stock',
    'is_a_stock', 
    'normalize_code',
    'get_hk_rate',
    'to_cny',
    'calc_total_mv',
    'calc_total_assets',
    'calc_position_pct',
    'validate_portfolio',
]

# 模块自检
if __name__ == '__main__':
    # 测试 is_hk_stock
    test_cases = [
        ('00700', True),      # 腾讯
        ('01888', True),      # 建滔积层板
        ('000657', False),    # 中钨高新 A股
        ('600519', False),    # 茅台 A股
        ('AAPL', False),      # 苹果 美股
        ('hk00700', True),    # 带前缀港股
        ('SH600519', False),  # 带前缀A股
    ]
    print("=== is_hk_stock 测试 ===")
    all_ok = True
    for code, expected in test_cases:
        result = is_hk_stock(code)
        status = '✅' if result == expected else '❌'
        if result != expected:
            all_ok = False
        print(f"  {status} is_hk_stock('{code}') = {result} (expected {expected})")
    
    # 测试 calc_total_assets
    print("\n=== calc_total_assets 测试 ===")
    pf = {
        'holdings': [
            {'code': '00700', 'shares': 100, 'price': 365.0},
            {'code': '600519', 'shares': 200, 'price': 1700.0},
        ],
        'cash': 50000.0,
        'frozen_cash': 10000.0,
    }
    expected_mv = 100 * 365.0 + 200 * 1700.0  # 36500 + 340000 = 376500
    expected_ta = expected_mv + 50000 + 10000  # 436500
    calc_ta = calc_total_assets(pf)
    print(f"  total_mv = {calc_total_mv(pf['holdings'])} (expected {expected_mv})")
    print(f"  total_assets = {calc_ta} (expected {expected_ta})")
    print(f"  {'✅' if abs(calc_ta - expected_ta) < 0.01 else '❌'} calc_total_assets")
    
    # 测试 validate
    print("\n=== validate_portfolio 测试 ===")
    issues = validate_portfolio(pf)
    if issues:
        for i in issues:
            print(f"  ⚠️ {i}")
    else:
        print("  ✅ 无问题")
    
    print(f"\n{'全部通过 ✅' if all_ok else '有失败 ❌'}")
