# -*- coding: utf-8 -*-
"""交易日盘中/盘后实时抓取并生成市场情绪日报（单页离线 HTML）。

用法:
  python run_live.py [交易日YYYY-MM-DD] [intraday|close] [data_dir]

- 不传日期 -> 取今天
- 不传模式 -> 自动: 16:30 前=盘中(intraday, 仅题材+行业), 之后=盘后(close, 含龙虎榜)
- data_dir 默认 data_live (不覆盖历史 data/)
"""
import json
import re
import subprocess
import sys
import time
from collections import defaultdict
from datetime import datetime
from pathlib import Path

import requests

BASE = Path(__file__).parent
UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
DATACENTER_URL = "https://datacenter-web.eastmoney.com/api/data/v1/get"

EM_SESSION = requests.Session()
EM_SESSION.headers.update({"User-Agent": UA})
EM_MIN_INTERVAL = 0.8
_em_last = [0.0]


def em_get(url, params=None, headers=None, timeout=20, **kw):
    wait = EM_MIN_INTERVAL - (time.time() - _em_last[0])
    if wait > 0:
        time.sleep(wait + 0.2)
    try:
        return EM_SESSION.get(url, params=params, headers=headers, timeout=timeout, **kw)
    finally:
        _em_last[0] = time.time()


def eastmoney_datacenter(report_name, columns="ALL", filter_str="", page_size=50,
                         page_number=1, sort_columns="", sort_types="-1"):
    params = {
        "reportName": report_name, "columns": columns, "filter": filter_str,
        "pageNumber": str(page_number), "pageSize": str(page_size),
        "sortColumns": sort_columns, "sortTypes": sort_types,
        "source": "WEB", "client": "WEB",
    }
    r = em_get(DATACENTER_URL, params=params, headers={"User-Agent": UA, "Referer": "https://datacenter-web.eastmoney.com/"})
    d = r.json()
    res = (d.get("result") or {})
    return res.get("data") or [], res.get("pages", 1)


# ---------- 1. 全市场龙虎榜（盘后） ----------
def daily_dragon_tiger(trade_date):
    stocks, page = [], 1
    while True:
        rows, pages = eastmoney_datacenter(
            "RPT_DAILYBILLBOARD_DETAILSNEW",
            filter_str=f"(TRADE_DATE>='{trade_date}')(TRADE_DATE<='{trade_date}')",
            page_size=200, page_number=page,
            sort_columns="BILLBOARD_NET_AMT", sort_types="-1",
        )
        if not rows:
            break
        for row in rows:
            stocks.append({
                "code": row.get("SECURITY_CODE", ""),
                "name": row.get("SECURITY_NAME_ABBR", ""),
                "reason": row.get("EXPLANATION", ""),
                "close": row.get("CLOSE_PRICE") or 0,
                "change_pct": round(float(row.get("CHANGE_RATE") or 0), 2),
                "net_buy": float(row.get("BILLBOARD_NET_AMT") or 0),
                "buy_amt": float(row.get("BILLBOARD_BUY_AMT") or 0),
                "sell_amt": float(row.get("BILLBOARD_SELL_AMT") or 0),
                "turnover_pct": round(float(row.get("TURNOVERRATE") or 0), 2),
                "accum_amount": float(row.get("ACCUM_AMOUNT") or 0),
                "free_mcap": float(row.get("FREE_MARKET_CAP") or 0),
                "trade_date": str(row.get("TRADE_DATE", ""))[:10],
            })
        if page >= (pages or 1):
            break
        page += 1
    stocks.sort(key=lambda x: x["net_buy"], reverse=True)
    return stocks


# ---------- 2. 同花顺当日强势股题材归因 ----------
def ths_hot_reason(date):
    url = (f"http://zx.10jqka.com.cn/event/api/getharden/date/{date}/"
           f"orderby/date/orderway/desc/charset/GBK/")
    for a in range(3):
        try:
            r = requests.get(url, headers={"User-Agent": UA}, timeout=15)
            d = r.json()
            if d.get("errocode", 0) != 0:
                raise RuntimeError(f"THS error: {d.get('errormsg')}")
            rows = []
            for it in (d.get("data") or []):
                rows.append({
                    "code": it.get("code", ""),
                    "name": it.get("name", ""),
                    "reason": it.get("reason", ""),
                    "close": it.get("close", ""),
                    "change_pct": it.get("zhangfu", ""),
                    "turnover_pct": it.get("huanshou", ""),
                    "amount": it.get("chengjiaoe", ""),
                    "market": it.get("market", ""),
                })
            return rows
        except Exception as e:
            print(f"  [ths] retry {a+1}: {type(e).__name__}", flush=True)
            time.sleep(2)
    return []


# ---------- 2.1 用腾讯接口补齐题材成分股实时涨跌幅 ----------
def to_tx_code(code, market=""):
    """同花顺纯数字代码 + market 字段 → 腾讯前缀代码 sh/sz/bj"""
    code = str(code or "").strip()
    if not code:
        return None
    if code[:2] in ("sh", "sz", "bj"):
        return code
    mk = str(market or "").lower()
    if mk.startswith("sh") or code.startswith("6"):
        return "sh" + code
    if mk.startswith("sz") or code.startswith(("0", "3")):
        return "sz" + code
    if mk.startswith("bj") or code.startswith(("8", "4")):
        return "bj" + code
    return "sh" + code


def fill_realtime_pct(rows):
    """同花顺 zhangfu 常为空，用腾讯 qt.gtimg.cn 批量补齐 change_pct/close（实时）"""
    if not rows:
        return
    mapping = {}
    for r in rows:
        tx = to_tx_code(r.get("code"), r.get("market", ""))
        if tx:
            mapping[tx] = r
    q = ",".join(mapping.keys())
    for a in range(5):
        try:
            r = requests.get("https://qt.gtimg.cn/q=" + q,
                             headers={"User-Agent": UA, "Referer": "https://gu.qq.com/"}, timeout=20)
            r.encoding = "gbk"
            filled = 0
            for line in r.text.strip().split(";"):
                if "v_" not in line or "=" not in line:
                    continue
                code = line.split("v_")[1].split("=")[0]
                f = line.split('"')[1].rstrip('"').split("~")
                if len(f) < 33 or code not in mapping:
                    continue
                rec = mapping[code]
                if f[32] not in (None, ""):
                    rec["change_pct"] = round(float(f[32]), 2)
                    filled += 1
                if f[3] not in (None, ""):
                    rec["close"] = round(float(f[3]), 2)
            print(f"  [pct] 已补 {filled}/{len(mapping)} 只实时涨跌幅", flush=True)
            break
        except Exception as e:
            print(f"  [pct] retry {a+1}: {type(e).__name__}", flush=True)
            time.sleep(1.5 + a)


# ---------- 3. 东财实时行业板块（一级 t:1） ----------
# 东财 push2 主域名在沙箱/树莓派环境会连不通（IPv6 黑洞或限流），
# push2delay 在两地都稳定，放首位优先；其余作兜底轮询
EM_HOSTS = [
    "https://push2delay.eastmoney.com",
    "https://push2.eastmoney.com",
    "https://7.push2.eastmoney.com",
    "https://14.push2.eastmoney.com",
]


def live_industry(trade_date):
    """返回 (industry_raw_list, industry_detail_dict) 实时数据"""
    params = {
        "pn": "1", "pz": "500", "po": "1", "np": "1", "fltt": "2", "invt": "2",
        "fs": "m:90+t:2",
        "fields": "f12,f14,f3,f6,f8,f104,f105,f128,f136,f62",
    }
    diff = None
    for a in range(8):
        host = EM_HOSTS[a % len(EM_HOSTS)]
        try:
            diff = em_get(host + "/api/qt/clist/get", params=params,
                         headers={"User-Agent": UA, "Referer": "https://quote.eastmoney.com/"}).json().get("data", {}).get("diff") or []
            if diff:
                break
        except Exception as e:
            print(f"  [ind] retry {a+1} {host.split('//')[1].split('.')[0]}: {type(e).__name__}", flush=True)
            time.sleep(1.5 + a * 0.5)
    raw, detail = [], {}
    for x in (diff or []):
        code = x.get("f12")
        name = x.get("f14")
        chg = float(x.get("f3") or 0)
        raw.append({
            "code": code, "name": name,
            "kline": {
                "date": trade_date, "change_pct": chg,
                "amount": float(x.get("f6") or 0),
                "turnover": float(x.get("f8") or 0),
            },
        })
        up = int(x.get("f104") or 0)
        down = int(x.get("f105") or 0)
        lead = x.get("f128")
        lead_chg = float(x.get("f136") or 0) if x.get("f136") not in (None, "") else None
        detail[code] = {
            "up": up, "down": down, "flat": None,
            "sampled": up + down,
            "leader": ({"name": lead, "change_pct": lead_chg} if lead else None),
            "laggard": None,
        }
    return raw, detail


# ---------- 4. 腾讯实时指数（qt.gtimg.cn 比东财 push2 稳定性更好）----------
# 字段：f[1]名称 f[3]现价 f[31]涨跌额 f[32]涨跌幅%
INDEX_MAP = {
    "上证指数": "sh000001", "深证成指": "sz399001", "创业板指": "sz399006",
    "沪深300": "sh000300", "科创50": "sh000688", "中证500": "sh000905",
    "北证50": "bj899050", "恒生科技": "r_hkHSTECH",
    "深证100": "sz399330",
}


def live_indices():
    out = {}
    codes = ",".join(INDEX_MAP.values())
    names = {v: k for k, v in INDEX_MAP.items()}
    for a in range(5):
        try:
            r = requests.get("https://qt.gtimg.cn/q=" + codes,
                             headers={"User-Agent": UA, "Referer": "https://gu.qq.com/"}, timeout=15)
            r.encoding = "gbk"
            txt = r.text
            for line in txt.strip().split(";"):
                if "v_" not in line or "=" not in line:
                    continue
                code = line.split("v_")[1].split("=")[0]
                f = line.split('"')[1].rstrip('"').split("~")
                nm = names.get(code)
                if not nm or len(f) < 33 or not f[3]:
                    continue
                out[nm] = {
                    "close": round(float(f[3]), 2),
                    "change_pct": round(float(f[32]), 2),
                    "amount": 0, "turnover": 0,
                }
            if out:
                break
            print(f"  指数抓取空返回，重试 {a+1}", flush=True)
            time.sleep(1.5 + a)
        except Exception as e:
            print(f"  指数抓取重试 {a+1} {type(e).__name__}", flush=True)
            time.sleep(1.5 + a)
    if not out:
        print("  [警告] 指数实时抓取全部失败，本次不含指数卡片", flush=True)
    return out


# ---- A股休市日历（来源：沪深北交易所《2026年部分节假日休市安排通知》）----
# 2026 年已确认无调休补班交易日（所有相邻周末均为正常休市）。
# 数据格式：HOLIDAYS[年份] = 闭市日期集合（含法定节假日及交易所明确标注的周末休市）
HOLIDAYS = {
    2026: {
        "2026-01-01", "2026-01-02", "2026-01-03", "2026-01-04",
        "2026-02-14", "2026-02-15", "2026-02-16", "2026-02-17", "2026-02-18",
        "2026-02-19", "2026-02-20", "2026-02-21", "2026-02-22", "2026-02-23", "2026-02-28",
        "2026-04-04", "2026-04-05", "2026-04-06",
        "2026-05-01", "2026-05-02", "2026-05-03", "2026-05-04", "2026-05-05", "2026-05-09",
        "2026-06-19", "2026-06-20", "2026-06-21",
        "2026-09-20", "2026-09-25", "2026-09-26", "2026-09-27",
        "2026-10-01", "2026-10-02", "2026-10-03", "2026-10-04", "2026-10-05",
        "2026-10-06", "2026-10-07", "2026-10-10",
    },
}
# 调休补班交易日（周末上班）：2026 年为空
MAKEUP = {2026: set()}


def is_trading_day(date_str):
    """返回 (是否交易日, 说明)。date_str 格式 YYYY-MM-DD。"""
    y = int(date_str[:4])
    dt = datetime.strptime(date_str, "%Y-%m-%d")
    if date_str in MAKEUP.get(y, set()):
        return True, "调休补班交易日"
    if dt.weekday() >= 5:
        return False, "周末"
    if date_str in HOLIDAYS.get(y, set()):
        return False, "法定节假日休市"
    if y not in HOLIDAYS:
        return True, "未配置该年休市日历，仅按工作日判断（周末跳过）"
    return True, "交易日"


def main():
    today = datetime.now().strftime("%Y-%m-%d")
    TRADE_DATE = today
    MODE = None
    DATA_DIR = BASE / "data_live"
    for a in sys.argv[1:]:
        if a in ("intraday", "close"):
            MODE = a
        else:
            try:
                datetime.strptime(a, "%Y-%m-%d")  # 当作交易日
                TRADE_DATE = a
            except ValueError:
                DATA_DIR = Path(a)  # 当作数据目录路径
    if MODE is None:
        now = datetime.now()
        MODE = "close" if (now.hour > 16 or (now.hour == 16 and now.minute >= 30)) else "intraday"
    DATA_DIR.mkdir(parents=True, exist_ok=True)

    print(f"=== {TRADE_DATE} 模式={MODE} 数据目录={DATA_DIR} ===", flush=True)
    trading, reason = is_trading_day(TRADE_DATE)
    if not trading:
        print(f"[跳过] {TRADE_DATE} 非交易日（{reason}），不抓取、不生成报告，保留上次交易日数据。", flush=True)
        sys.exit(0)

    # 指数（两种模式都要）
    print("[1/3] 实时指数 ...", flush=True)
    idx = live_indices()
    (DATA_DIR / "index.json").write_text(json.dumps(idx, ensure_ascii=False, indent=1), encoding="utf-8")
    print(f"    指数 {len(idx)} 个", flush=True)

    # 题材（两种模式都要）
    print("[2/3] 同花顺当日强势股题材归因 ...", flush=True)
    hot = ths_hot_reason(TRADE_DATE)
    fill_realtime_pct(hot)  # 同花顺 zhangfu 常空，用腾讯补实时涨跌幅
    (DATA_DIR / "ths_hot.json").write_text(json.dumps(hot, ensure_ascii=False, indent=1), encoding="utf-8")
    print(f"    强势股 {len(hot)} 只", flush=True)

    # 行业实时（两种模式都要）
    print("[3/3] 东财实时行业板块（一级）...", flush=True)
    raw, detail = live_industry(TRADE_DATE)
    (DATA_DIR / "industry_raw.json").write_text(json.dumps(raw, ensure_ascii=False, indent=1), encoding="utf-8")
    (DATA_DIR / "industry_detail.json").write_text(json.dumps(detail, ensure_ascii=False, indent=1), encoding="utf-8")
    print(f"    行业 {len(raw)} 个", flush=True)

    # 龙虎榜（仅盘后）
    if MODE == "close":
        print("[4/4] 东方财富龙虎榜（盘后）...", flush=True)
        lhb = daily_dragon_tiger(TRADE_DATE)
        (DATA_DIR / "lhb.json").write_text(json.dumps(lhb, ensure_ascii=False, indent=1), encoding="utf-8")
        print(f"    龙虎榜 {len(lhb)} 条记录", flush=True)
    else:
        (DATA_DIR / "lhb.json").write_text("[]", encoding="utf-8")

    # 生成 HTML
    out = subprocess.run(
        [sys.executable, str(BASE / "build_report.py"), TRADE_DATE, MODE, str(DATA_DIR)],
        capture_output=True, text=True,
    )
    print(out.stdout)
    if out.returncode != 0:
        print("BUILD ERROR:", out.stderr)
        sys.exit(1)

    # 同步固定名 latest.html，手机收藏一个链接永久有效（每天被最新覆盖）
    import shutil
    out_name = ("A股实时情绪_盘中" if MODE == "intraday" else "A股市场情绪日报") + f"_{TRADE_DATE}.html"
    src = BASE / out_name
    if src.exists():
        shutil.copy(src, BASE / "latest.html")
        print("    已同步 latest.html（手机固定入口：latest.html）", flush=True)
    print("完成。")


if __name__ == "__main__":
    main()
