"""
估值游戏 - 股票估值猜测游戏
基于 Tushare 数据接口，猜测股票隐藏年份的最小PB
"""

import subprocess
import sys
import time
import random

# ==================== 前置准备：检查并安装依赖 ====================
REQUIRED_PACKAGES = {"tushare": "tushare", "pandas": "pandas", "numpy": "numpy"}

for _mod, _pkg in REQUIRED_PACKAGES.items():
    try:
        __import__(_mod)
    except ImportError:
        print(f"[安装] 正在安装 {_pkg} ...")
        subprocess.check_call(
            [sys.executable, "-m", "pip", "install", _pkg],
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
        )
        print(f"[安装] {_pkg} 安装完成")

import tushare as ts
import pandas as pd
import numpy as np

# ==================== Tushare 初始化 ====================
TOKEN = "d4ade4dc2337bb63733c3ec90deb49bfe013ac6b81452bdba73641e7"
ts.set_token(TOKEN)
pro = ts.pro_api()

# API 调用间隔（秒），防止触发限频
API_SLEEP = 0.35


# ==================== 工具函数 ====================
def api_call(func, retries=3, **kwargs):
    """带重试与限频的 Tushare API 调用"""
    for i in range(retries):
        try:
            df = func(**kwargs)
            time.sleep(API_SLEEP)
            return df
        except Exception as e:
            if i < retries - 1:
                time.sleep(1)
            else:
                print(f"  [警告] API 调用失败: {e}")
                return None


def print_sep(char="─", width=62):
    print(char * width)


def print_title(title, width=62):
    print()
    print_sep("═", width)
    print(f"  {title}")
    print_sep("═", width)


# ==================== 板块一：条件随机筛选 ====================
def get_custom_params():
    """交互式获取自定义游戏参数，回车使用默认值"""
    print()
    print_sep()
    print("  自定义参数（直接回车使用默认值）")
    print_sep()

    def ask(prompt, default, cast=float):
        raw = input(f"  {prompt} [{default}]: ").strip()
        return cast(raw) if raw else default

    p = {
        "list_year":  ask("上市年份（之前）", 2015, int),
        "max_pb":     ask("历史最大 PB 上限", 7.2),
        "min_bps":    ask("历史最小每股净资产下限", 2.0),
        "min_beta":   ask("贝塔系数下限", 0.5),
        "hide_start": ask("隐藏年份 - 起始", 2021, int),
        "hide_end":   ask("隐藏年份 - 结束", 2025, int),
        "show_start": ask("展示年份 - 起始", 2015, int),
        "show_end":   ask("展示年份 - 结束", 2025, int),
    }
    return p


def fetch_stock_list(list_year):
    """获取在 list_year 之前上市、目前仍上市的全部 A 股"""
    df = api_call(pro.stock_basic, exchange="", list_status="L",
                  fields="ts_code,name,list_date")
    if df is None or df.empty:
        return pd.DataFrame()
    return df[df["list_date"] <= f"{list_year}0101"].reset_index(drop=True)


def check_stock(ts_code, params):
    """
    依次检查：最大PB、最小每股净资产、贝塔系数
    返回 (是否通过, 简要原因)
    """
    s_start = f'{params["show_start"]}0101'
    s_end = f'{params["show_end"]}1231'

    # ① 历史最大 PB
    df = api_call(pro.daily_basic, ts_code=ts_code,
                  start_date=s_start, end_date=s_end,
                  fields="trade_date,pb")
    if df is None or df.empty:
        return False, "无PB数据"
    df = df[df["pb"] > 0]
    if df.empty:
        return False, "PB全为负"
    max_pb = df["pb"].max()
    if max_pb >= params["max_pb"]:
        return False, f"maxPB={max_pb:.2f}"

    # ② 历史最小每股净资产
    df_fin = api_call(pro.fina_indicator, ts_code=ts_code, fields="end_date,bps")
    if df_fin is None or df_fin.empty:
        return False, "无财务数据"
    min_bps = df_fin["bps"].min()
    if min_bps < params["min_bps"]:
        return False, f"minBPS={min_bps:.2f}"

    # ③ 贝塔系数（近 3 年日收益 vs 沪深300）
    beta_s = f'{max(params["show_start"], params["show_end"] - 3)}0101'
    stk = api_call(pro.daily, ts_code=ts_code,
                   start_date=beta_s, end_date=s_end,
                   fields="trade_date,pct_chg")
    idx = api_call(pro.index_daily, ts_code="000300.SH",
                   start_date=beta_s, end_date=s_end,
                   fields="trade_date,pct_chg")
    if stk is None or idx is None or stk.empty or idx.empty:
        return False, "无行情数据"

    merged = pd.merge(stk, idx, on="trade_date", suffixes=("_s", "_m")).dropna()
    if len(merged) < 60:
        return False, "交易日不足"

    cov = np.cov(merged["pct_chg_s"].values, merged["pct_chg_m"].values)
    beta = cov[0][1] / cov[1][1] if cov[1][1] != 0 else 0
    if beta < params["min_beta"]:
        return False, f"Beta={beta:.2f}"

    return True, f"Beta={beta:.2f}"


def select_stock(params, max_tries=30):
    """随机选取一只符合全部条件的股票"""
    print_title("[板块一] 条件随机筛选")
    stocks = fetch_stock_list(params["list_year"])
    if stocks.empty:
        print("  股票列表为空，请检查网络或参数。")
        return None, None
    print(f"  候选股票: {len(stocks)} 只（{params['list_year']}年前上市）")

    order = stocks.index.tolist()
    random.shuffle(order)

    for i, idx in enumerate(order[:max_tries]):
        row = stocks.loc[idx]
        code, name = row["ts_code"], row["name"]
        print(f"  [{i + 1}/{max_tries}] {name}({code}) ... ", end="", flush=True)
        ok, reason = check_stock(code, params)
        tag = "√" if ok else "×"
        print(f"{tag}  {reason}")
        if ok:
            return code, name

    print(f"  尝试 {max_tries} 只均不符合条件，请放宽筛选参数。")
    return None, None


# ==================== 板块二：披露股票信息 ====================
def fetch_yearly_data(ts_code, show_start, show_end):
    """获取指定范围内的年度 PB（最小/平均/最大）和 ROE"""
    # ---- PB ----
    df_pb = api_call(pro.daily_basic, ts_code=ts_code,
                     start_date=f"{show_start}0101",
                     end_date=f"{show_end}1231",
                     fields="trade_date,pb")
    if df_pb is None or df_pb.empty:
        return None

    df_pb = df_pb[df_pb["pb"] > 0].copy()
    df_pb["year"] = df_pb["trade_date"].str[:4].astype(int)
    yearly = df_pb.groupby("year").agg(
        min_pb=("pb", "min"),
        avg_pb=("pb", "mean"),
        max_pb=("pb", "max"),
    ).round(2)

    # ---- ROE（年报） ----
    df_fin = api_call(pro.fina_indicator, ts_code=ts_code, fields="end_date,roe")
    if df_fin is not None and not df_fin.empty:
        annual = df_fin[df_fin["end_date"].str.endswith("1231")].copy()
        annual["year"] = annual["end_date"].str[:4].astype(int)
        annual = annual.drop_duplicates("year", keep="first").set_index("year")
        yearly["roe"] = annual["roe"].round(2)
    else:
        yearly["roe"] = np.nan

    return yearly


def display_table(data, hide_start=None, hide_end=None, title=""):
    """
    以表格形式展示年度数据
    hide_start/hide_end 不为 None 时，对应年份 PB 显示为 '?'
    """
    COL = 10
    if title:
        print_title(title)

    header = (f"{'年份':<8}"
              f"{'最小PB':>{COL}}"
              f"{'平均PB':>{COL}}"
              f"{'最大PB':>{COL}}"
              f"{'ROE(%)':>{COL}}")
    print(header)
    print_sep()

    for year in sorted(data.index):
        hide = (hide_start is not None and hide_start <= year <= hide_end)
        mp = "?" if hide else f"{data.loc[year, 'min_pb']:.2f}"
        ap = "?" if hide else f"{data.loc[year, 'avg_pb']:.2f}"
        xp = "?" if hide else f"{data.loc[year, 'max_pb']:.2f}"
        roe_v = data.loc[year, "roe"]
        roe = f"{roe_v:.2f}" if pd.notna(roe_v) else "-"
        print(f"{year:<8}{mp:>{COL}}{ap:>{COL}}{xp:>{COL}}{roe:>{COL}}")

    print_sep()


# ==================== 板块三：玩家输入猜选信息 ====================
def get_player_guess(hide_start, hide_end):
    """获取玩家对每个隐藏年份最小PB的猜测值"""
    n = hide_end - hide_start + 1
    years = list(range(hide_start, hide_end + 1))
    fmt = "/".join(["X"] * n)

    print_title("[板块三] 输入猜选信息")
    print(f"  请猜测 {years[0]}-{years[-1]} 年的最小 PB")
    print(f"  格式: {fmt}（对应 {'/'.join(map(str, years))}）")

    while True:
        raw = input("  >>> ").strip().replace(" ", "")
        parts = raw.split("/")
        if len(parts) != n:
            print(f"  需要 {n} 个数值，用 / 分隔，请重新输入")
            continue
        try:
            return [float(x) for x in parts]
        except ValueError:
            print("  包含非数字内容，请重新输入")


# ==================== 板块四：验证猜选并展示结果 ====================
def judge_round(guesses, data, hide_start, hide_end):
    """
    逐年判定猜选结果，返回 (胜, 平, 败) 局数
    规则:
      猜 < 实际            → 平局
      实际 ≤ 猜 ≤ 实际×1.37 → 胜局
      猜 > 实际×1.37        → 败局
    """
    years = [y for y in range(hide_start, hide_end + 1) if y in data.index]
    wins = ties = losses = 0
    COL = 10

    print_title("[板块四] 判定结果")
    header = (f"{'年份':<8}"
              f"{'猜选PB':>{COL}}"
              f"{'实际PB':>{COL}}"
              f"{'×1.37':>{COL}}"
              f"{'判定':>{COL}}")
    print(header)
    print_sep()

    for i, year in enumerate(years):
        g = guesses[i]
        actual = data.loc[year, "min_pb"]
        threshold = round(actual * 1.37, 2)

        if g < actual:
            verdict = "平局"
            ties += 1
        elif g <= threshold:
            verdict = "胜局"
            wins += 1
        else:
            verdict = "败局"
            losses += 1

        print(f"{year:<8}{g:>{COL}.2f}{actual:>{COL}.2f}{threshold:>{COL}.2f}{verdict:>{COL}}")

    print_sep()
    return wins, ties, losses


# ==================== 板块五：统计对局结果 ====================
def display_stats(w, t, l):
    """展示累计胜/平/败统计"""
    total = w + t + l
    print()
    print_sep()
    print(f"  累计统计（共 {total} 局）")
    print(f"  胜局: {w}    平局: {t}    败局: {l}")
    print_sep()


# ==================== 主循环 ====================
def main():
    print()
    print("╔════════════════════════════════════════╗")
    print("║            估  值  游  戏              ║")
    print("║    猜测股票 PB 估值，挑战你的能力！    ║")
    print("╚════════════════════════════════════════╝")

    params = get_custom_params()
    total_w = total_t = total_l = 0

    while True:
        # ---- 板块一：筛选股票 ----
        ts_code, name = select_stock(params)
        if ts_code is None:
            break

        # ---- 加载年度数据 ----
        print(f"\n  正在加载 {name} 的年度数据 ...")
        data = fetch_yearly_data(ts_code, params["show_start"], params["show_end"])
        if data is None or data.empty:
            print("  数据加载失败，重新筛选 ...")
            continue

        hide_years = [y for y in range(params["hide_start"], params["hide_end"] + 1)
                      if y in data.index]
        if not hide_years:
            print("  隐藏年份无数据，重新筛选 ...")
            continue

        # ---- 板块二：展示信息（隐藏部分年份 PB） ----
        display_table(data, params["hide_start"], params["hide_end"],
                      f"[板块二] {name}（{ts_code}）  隐藏: {params['hide_start']}-{params['hide_end']}")

        # ---- 板块三：玩家输入猜选 ----
        guesses = get_player_guess(params["hide_start"], params["hide_end"])

        # ---- 板块四：展示完整数据 + 判定 ----
        display_table(data, title=f"完整数据: {name}（{ts_code}）")
        w, t, l = judge_round(guesses, data, params["hide_start"], params["hide_end"])

        total_w += w
        total_t += t
        total_l += l

        # ---- 板块五：统计 & 下一局 ----
        display_stats(total_w, total_t, total_l)

        if input("\n  输入 Y 开始下一局，其他键退出: ").strip().upper() != "Y":
            print("\n  感谢游玩，再见！")
            break


if __name__ == "__main__":
    main()
