#!/usr/bin/env python3
"""从「今日扫描候选」产出紧凑摘要 + 供 cron 推送（不依赖日志尾部）。

**首选**读取 `backtest_zt_full/scan_today_candidates.json`（scan 进程内 dump，schema=scan_candidates_v1，
字段 code/name/ind/pool/tb/buy_date/buy_pending/strength/raw_score + 全特征）；
**回退**（JSON 缺失或非当日）才解析 stdout 日志的 scan 段 —— 回退路径对日志格式敏感、且 code 被截成 8 字符，
故仅在异常时使用并显式告警。

用法: python extract_today_candidates.py [--log /tmp/zt_pipeline_last.log] [--json ...] [--quiet]
"""
import argparse, glob, json, os, re, time

APP = '/Users/xpresso/zt_app'
BASE = f'{APP}/backtest_zt_full'
OUT = f'{BASE}/scan_today_candidates.json'
FALLBACK = f'{BASE}/scan_today_candidates_fromlog.json'
SIG = re.compile(r'^([🟢🟡🔴🔵⚪])(强|中|弱|观察|警示)\s+(\S+?)\(([0-9]{6}\.[A-Z]{0,2})\)·(\S+)\s+强度(\d+)')


def code_index():
    idx = {}
    for d in glob.glob(f'{BASE}/daily_*'):
        for p in glob.glob(d + '/*.csv'):
            c = os.path.basename(p)[:-4]
            idx[c[:8]] = c
    return idx


def parse_log(path):
    idx = code_index()
    lines = open(path, errors='ignore').read().splitlines()
    i = next((i for i, l in enumerate(lines) if l.strip() == '=== 2. scan ==='), None)
    if i is None:
        return []
    j = next((j for j, l in enumerate(lines) if j > i and l.startswith('=== 3')), len(lines))
    out = []
    for l in lines[i:j]:
        m = SIG.match(l.strip())
        if not m:
            continue
        tag, tier, name, code, ind, st = m.groups()
        out.append({'tier': tier, 'name': name, 'code': idx.get(code, code), 'code_as_logged': code,
                    'industry': ind, 'strength': int(st), 'source': 'log_parse'})
    return out


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('--log', default='/tmp/zt_pipeline_last.log')
    ap.add_argument('--json', default=OUT)
    ap.add_argument('--quiet', action='store_true')
    a = ap.parse_args()
    src, cand, extra = 'process_dump', [], {}
    if os.path.exists(a.json):
        try:
            d = json.load(open(a.json))
            if d.get('schema') == 'scan_candidates_v1':
                cand = [{'tier': ('强' if c['strength'] >= 70 else '中' if c['strength'] >= 40 else '弱'),
                         'name': c['name'], 'code': c['code'], 'industry': c['ind'], 'pool_cn': c['pool_cn'],
                         'tb': c.get('tb'), 'buy_date': c.get('buy_date'), 'buy_pending': c.get('buy_pending'),
                         'strength': c['strength'], 'raw_score': c.get('raw_score'), 'source': 'process_dump'}
                        for c in d['candidates']]
                extra = {'target': d.get('target'), 'generated_at': d.get('generated_at')}
        except Exception as e:
            print(f"⚠️ {a.json} 读取失败({type(e).__name__}), 回退日志解析")
    if not cand:
        cand = parse_log(a.log)
        src = 'log_parse'
        if cand and not a.quiet:
            print(f"⚠️ 未取到进程内 dump, 已回退日志解析（{len(cand)} 条；此路径对日志格式敏感, 建议排查 scan 第 5 步）")
        if cand:
            json.dump({'schema': 'scan_candidates_fromlog_v1', 'n': len(cand), 'candidates': cand},
                      open(FALLBACK, 'w'), ensure_ascii=False, indent=1)
    strong = [c for c in cand if c['strength'] >= 85]
    if a.quiet:
        return
    tag = '进程内 dump' if src == 'process_dump' else '⚠️ 日志解析(回退)'
    print(f"🔍 今日扫描候选 {len(cand)} 条 / {len({c['code'] for c in cand})} 唯一代码（强度≥85 共 {len(strong)} 只）[{tag}]")
    for c in sorted(cand, key=lambda x: -x['strength']):
        print(f"  {'⭐' if c['strength'] >= 85 else '  '}{c['tier']} {c['name']}({c['code']}) {c['industry']} 强度{c['strength']:.2f}")


if __name__ == '__main__':
    main()
