#!/usr/bin/env python3
from __future__ import annotations
import json, re, urllib.parse, urllib.request
from datetime import datetime
from pathlib import Path

ROOT = Path('/Users/mibo/.openclaw/workspace')
PORT = ROOT / 'data' / 'portfolio'
HOLD = PORT / 'ths_holdings_latest.json'
QOUT = PORT / 'market_quote_cache.json'
MOUT = PORT / 'portfolio_metrics_latest.json'
SLOG = PORT / 'snapshots.jsonl'


def load_json(p: Path):
    return json.loads(p.read_text('utf-8')) if p.exists() else {}


def market_symbol(code: str) -> str:
    c = str(code)
    if len(c) == 5:
        return f'hk{c}'
    if c.startswith(('5','6','9')):
        return f'sh{c}'
    return f'sz{c}'


def fetch_tencent(symbols: list[str]) -> dict[str, dict]:
    if not symbols:
        return {}
    qs = ','.join(symbols)
    url = f'https://qt.gtimg.cn/q={urllib.parse.quote(qs)}'
    req = urllib.request.Request(url, headers={'User-Agent':'Mozilla/5.0'})
    txt = urllib.request.urlopen(req, timeout=12).read().decode('gbk', errors='ignore')
    out = {}
    for line in txt.split(';'):
        line = line.strip()
        if not line.startswith('v_') or '="' not in line:
            continue
        left, right = line.split('="', 1)
        sym = left.replace('v_', '')
        payload = right.rstrip('"')
        parts = payload.split('~')
        # 通用位置：1 名称 2 代码 3 现价 4 昨收
        name = parts[1] if len(parts) > 1 else sym
        code = parts[2] if len(parts) > 2 else ''
        price = None
        prev = None
        try:
            price = float(parts[3])
            prev = float(parts[4])
        except Exception:
            nums = [x for x in parts if re.match(r'^-?\d+(\.\d+)?$', x)]
            if len(nums) >= 2:
                price = float(nums[0])
                prev = float(nums[1])
        pct = ((price - prev) / prev * 100) if price is not None and prev not in (None, 0) else None
        out[sym] = {'symbol': sym, 'name': name, 'code': code, 'price': price, 'prev_close': prev, 'pct': pct}
    return out


def main():
    hold = load_json(HOLD)
    hs = hold.get('holdings', [])
    symbols = [market_symbol(h.get('code','')) for h in hs if h.get('code')]
    bench_syms = ['sh000300', 'sh000905', 'sz399006']
    quotes = fetch_tencent(sorted(set(symbols + bench_syms)))

    benchmark = {k: quotes.get(k) for k in bench_syms}
    quote_rows = {}
    total = 0.0
    for h in hs:
        amt = float(str(h.get('holding_amount','0')).replace(',','') or 0)
        total += amt
        sym = market_symbol(h.get('code',''))
        q = quotes.get(sym, {})
        pct = q.get('pct')
        contrib = (amt * pct / 100.0) if isinstance(pct, (int,float)) else None
        quote_rows[h.get('code','')] = {
            'symbol': sym,
            'price': q.get('price'),
            'prev_close': q.get('prev_close'),
            'pct': pct,
            'contribution_est': contrib,
        }

    items = []
    for h in hs:
        c = h.get('code','')
        amt = float(str(h.get('holding_amount','0')).replace(',','') or 0)
        q = quote_rows.get(c,{})
        items.append({
            'code': c,
            'name': h.get('name',''),
            'amount': amt,
            'weight': (amt/total*100) if total else 0,
            'pct': q.get('pct'),
            'contribution_est': q.get('contribution_est'),
        })

    gain = sorted([x for x in items if x.get('contribution_est') is not None], key=lambda x:x['contribution_est'], reverse=True)[:5]
    loss = sorted([x for x in items if x.get('contribution_est') is not None], key=lambda x:x['contribution_est'])[:5]

    metrics = {
        'generated_at': datetime.now().astimezone().isoformat(),
        'total_position_value': total,
        'cr3': sum(x['weight'] for x in sorted(items, key=lambda z:z['amount'], reverse=True)[:3]),
        'cr5': sum(x['weight'] for x in sorted(items, key=lambda z:z['amount'], reverse=True)[:5]),
        'max_weight': max([x['weight'] for x in items], default=0),
        'gainers': gain,
        'losers': loss,
    }

    QOUT.write_text(json.dumps({'generated_at': metrics['generated_at'], 'quotes': quote_rows, 'benchmark': benchmark}, ensure_ascii=False, indent=2), 'utf-8')
    MOUT.write_text(json.dumps(metrics, ensure_ascii=False, indent=2), 'utf-8')
    with SLOG.open('a', encoding='utf-8') as f:
        f.write(json.dumps({'ts': metrics['generated_at'], 'summary': hold.get('summary',{}), 'metrics': {'cr5':metrics['cr5'],'max_weight':metrics['max_weight']}}, ensure_ascii=False) + '\n')
    print('ok')


if __name__ == '__main__':
    main()
