"""
V2 交易计划生成器 - 5维度综合评分 + 多周期共振分析
"""
import json
import logging
from collections import Counter
from typing import Dict, List, Optional, Tuple
from datetime import datetime

from sqlalchemy.orm import Session

from app.services.cache import get_cached_data
from app.analysis_models import (
    ReviewPlanV2, SymbolScoreV2, TradingPlanV2, SectorHeat,
)

logger = logging.getLogger(__name__)

# 板块分类
SECTOR_MAP = {
    "贵金属": ["沪银", "沪金"],
    "有色金属": ["沪铜", "沪锌", "沪锡", "沪铅", "沪铝", "铝合金", "沪镍"],
    "黑色系": ["焦炭", "焦煤", "螺纹钢", "热卷", "铁矿石"],
    "能源": ["原油", "低硫燃油", "燃油"],
    "化工": ["PTA", "甲醇", "尿素", "PVC", "乙二醇", "合成橡胶", "烧碱", "纯碱", "玻璃", "橡胶"],
    "农产品": ["白糖", "棉花", "棕榈油", "豆粕"],
    "新能源": ["氧化铝", "多晶硅", "碳酸锂", "工业硅"],
    "股指": ["中证1000"],
    "航运": ["集运欧线"],
    "橡胶": ["20号胶"],
}

# 权重
WEIGHTS = {
    "amplitude": 0.20,
    "volume": 0.15,
    "change": 0.15,
    "trend": 0.35,
    "activity": 0.15,
}


def load_symbols_config() -> Dict[str, str]:
    """加载品种配置 {中文名: 合约代码}"""
    from app.config_store import get_config_store
    data = get_config_store().get_config("symbols", {"futures": {}, "stock": {}})
    return data.get("futures", {})


def _get_candle_date(candle: dict) -> Optional[str]:
    """从K线数据中提取日期字符串 (YYYY-MM-DD)"""
    dt_str = candle.get("datetime") or candle.get("time")
    if not dt_str:
        return None
    try:
        if isinstance(dt_str, str):
            dt = datetime.fromisoformat(dt_str.replace("Z", "+00:00"))
        else:
            dt = dt_str
        return dt.strftime("%Y-%m-%d")
    except Exception:
        return None


def _get_candle_values(candles: List[dict]) -> Tuple[list, list, list, list, list]:
    """从K线列表提取 OHLCV"""
    opens, highs, lows, closes, volumes = [], [], [], [], []
    for c in candles:
        opens.append(float(c.get("open", 0)))
        highs.append(float(c.get("high", 0)))
        lows.append(float(c.get("low", 0)))
        closes.append(float(c.get("close", 0)))
        volumes.append(float(c.get("volume", 0)))
    return opens, highs, lows, closes, volumes


def _calc_single_period_trend(candles: List[dict]) -> float:
    """
    计算单周期趋势分 (-50 ~ +50)
    MA排列: 多头+50 / 空头-50
    MACD: DIF>DEA +30 / DIF<DEA -30
    综合: -50 ~ +50 (归一化后映射)
    """
    if len(candles) < 5:
        return 0.0

    last = candles[-1]
    ma10 = last.get("ma10")
    ma20 = last.get("ma20")
    macd_dif = last.get("macd_dif", 0) or 0
    macd_dea = last.get("macd_dea", 0) or 0

    score = 0.0
    # MA排列
    if ma10 and ma20:
        if ma10 > ma20:
            score += 50
        else:
            score -= 50
    # MACD
    if macd_dif > macd_dea:
        score += 30
    else:
        score -= 30

    # 归一化到 -50 ~ +50
    return max(-50, min(50, score * 50 / 80))


def _calc_trend_score(t60: float, t15: float, t5: float) -> Tuple[float, str, str]:
    """
    三周期共振判定
    Returns: (趋势总分 0-100, 方向标签, 方向简写)
    """
    # 共振判定
    if t60 > 20 and t15 > 20 and t5 > 20:
        direction = "多头共振"
        tag = "强多"
        raw = 80 + min(20, (t60 + t15 + t5) / 3 * 20 / 50)
    elif t60 < -20 and t15 < -20 and t5 < -20:
        direction = "空头共振"
        tag = "强空"
        raw = 80 + min(20, abs(t60 + t15 + t5) / 3 * 20 / 50)
    elif t60 > 0 and t15 > 0:
        direction = "偏多震荡"
        tag = "偏多"
        raw = 50 + min(20, (t60 + t15) / 2 * 20 / 50)
    elif t60 < 0 and t15 < 0:
        direction = "偏空震荡"
        tag = "偏空"
        raw = 50 + min(20, abs(t60 + t15) / 2 * 20 / 50)
    else:
        direction = "多空交织"
        tag = "震荡"
        raw = max(0, 40 - abs(t60 + t15 + t5) / 3)

    return round(raw, 1), direction, tag


def _get_price_precision(price: float) -> int:
    """根据价格大小确定合适的小数位数"""
    if price >= 10000:
        return 0  # 如白银8250、股指5000+
    elif price >= 1000:
        return 1  # 如原油528、黄金685(实际2位)
    elif price >= 100:
        return 2  # 如大部分化工品
    else:
        return 2  # 小数值的品种


def _round_by_price(value: float, price: float) -> float:
    """根据价格精度四舍五入"""
    precision = _get_price_precision(price)
    return round(value, precision)


def _calc_pivot_points(high: float, low: float, close: float) -> dict:
    """计算枢轴点和支撑/阻力位（根据价格精度自动调整小数位）"""
    pivot = (high + low + close) / 3
    r1 = 2 * pivot - low
    s1 = 2 * pivot - high
    r2 = pivot + (high - low)
    s2 = pivot - (high - low)
    # 根据价格精度四舍五入
    p = _get_price_precision(close)
    return {
        "pivot": round(pivot, p),
        "r1": round(r1, p),
        "r2": round(r2, p),
        "s1": round(s1, p),
        "s2": round(s2, p),
    }


def _normalize_scores(values: List[float], reverse: bool = False) -> List[float]:
    """将一组值归一化到 0-100"""
    if not values:
        return []
    min_v = min(values)
    max_v = max(values)
    if max_v == min_v:
        return [50.0] * len(values)
    result = []
    for v in values:
        if reverse:
            score = (max_v - v) / (max_v - min_v) * 100
        else:
            score = (v - min_v) / (max_v - min_v) * 100
        result.append(round(score, 1))
    return result


def _normalize_abs_scores(values: List[float]) -> List[float]:
    """将绝对值归一化到 0-100（涨跌幅用绝对值排名）"""
    abs_values = [abs(v) for v in values]
    return _normalize_scores(abs_values)


def _generate_trigger(score_data: dict, direction: str) -> str:
    """
    根据分析结果动态生成触发条件

    Args:
        score_data: 品种评分数据
        direction: 方向 (long/short)

    Returns:
        触发条件文本
    """
    conditions = []
    direction_tag = score_data.get("direction_tag", "震荡")
    volume_ratio = score_data.get("volume_ratio", 1.0)
    amplitude_pct = score_data.get("amplitude_pct", 0)
    change_pct = score_data.get("change_pct", 0)

    # 根据方向标签生成基础条件
    if direction == "long":
        if direction_tag == "强多":
            conditions.append("价格站稳支撑位上方")
        elif direction_tag == "偏多":
            conditions.append("回踩支撑企稳")
        else:
            conditions.append("突破枢轴点P位")
    else:
        if direction_tag == "强空":
            conditions.append("价格跌破阻力位下方")
        elif direction_tag == "偏空":
            conditions.append("反弹阻力受阻")
        else:
            conditions.append("跌破枢轴点P位")

    # 根据量比添加量能条件
    if volume_ratio > 1.5:
        conditions.append("量能持续放大")
    elif volume_ratio > 1.2:
        conditions.append("5分钟K线放量确认")
    else:
        conditions.append("等待量能配合")

    # 根据振幅添加波动条件
    if amplitude_pct > 2:
        conditions.append("注意波动风险")
    elif amplitude_pct > 1:
        conditions.append("关注区间突破")

    # 根据涨跌幅添加趋势条件
    if abs(change_pct) > 2:
        if change_pct > 0:
            conditions.append("强势延续确认")
        else:
            conditions.append("弱势延续确认")

    return " + ".join(conditions[:3])  # 最多返回3个条件


def generate_plan(db: Session, review_date_str: str, week_day: str) -> dict:
    """
    生成V2复盘与交易计划

    Returns:
        {
            "review_date_id": int,
            "summary": {...},
            "scores": [...],
            "plans": [...],
            "sectors": [...],
        }
    """
    symbols_config = load_symbols_config()
    if not symbols_config:
        raise ValueError("品种配置文件为空")

    # 反转映射: 合约代码 -> 中文名
    code_to_name = {v: k for k, v in symbols_config.items()}
    all_symbols = list(symbols_config.values())

    logger.info(f"开始生成交易计划: {review_date_str} ({week_day}), 共 {len(all_symbols)} 个品种")

    # 将复盘日期作为数据截止时间（当天 23:59:59）
    try:
        end_time = datetime.strptime(review_date_str, "%Y-%m-%d").replace(hour=23, minute=59, second=59)
    except ValueError:
        end_time = datetime.now()

    # ==================== 第一步: 数据收集 ====================
    symbol_data = {}
    periods_needed = ["daily", "60min", "15min", "5min"]

    for symbol in all_symbols:
        try:
            data = get_cached_data(
                db, symbol, "futures",
                periods=periods_needed,
                end_time=end_time,
                max_candles=100,
            )
            if data and data.get("timeframes"):
                symbol_data[symbol] = data
            else:
                logger.warning(f"{symbol} 无数据")
        except Exception as e:
            logger.error(f"获取 {symbol} 数据失败: {e}")

    if not symbol_data:
        raise ValueError("无法从数据库获取任何品种数据，请先进行数据更新")

    logger.info(f"成功获取 {len(symbol_data)}/{len(all_symbols)} 个品种数据")

    # ==================== 第二步: 多维度打分 ====================
    raw_scores = []

    for symbol, data in symbol_data.items():
        name = code_to_name.get(symbol, symbol)
        tf = data.get("timeframes", {})

        daily = tf.get("daily", [])
        h1 = tf.get("60min", [])
        m15 = tf.get("15min", [])
        m5 = tf.get("5min", [])

        # 日线数据
        if not daily or len(daily) < 2:
            continue

        _, d_highs, d_lows, d_closes, d_volumes = _get_candle_values(daily)
        today_high = d_highs[-1]
        today_low = d_lows[-1]
        today_close = d_closes[-1]
        prev_close = d_closes[-2] if len(d_closes) >= 2 else today_close
        today_volume = d_volumes[-1]

        # 提取实际数据日期（日线最后一根K线的日期）
        data_date = _get_candle_date(daily[-1])

        # 近5日均量
        recent_vols = d_volumes[-6:-1] if len(d_volumes) >= 6 else d_volumes[:-1]
        avg_vol_5 = sum(recent_vols) / len(recent_vols) if recent_vols else today_volume

        # A. 振幅
        amplitude_pct = (today_high - today_low) / today_close * 100 if today_close else 0

        # B. 量比
        volume_ratio = today_volume / avg_vol_5 if avg_vol_5 > 0 else 1.0

        # C. 涨跌幅
        change_pct = (today_close - prev_close) / prev_close * 100 if prev_close else 0

        # D. 多周期趋势
        t60 = _calc_single_period_trend(h1) if h1 else 0
        t15 = _calc_single_period_trend(m15) if m15 else 0
        t5 = _calc_single_period_trend(m5) if m5 else 0
        trend_total, direction, direction_tag = _calc_trend_score(t60, t15, t5)

        # E. 活跃度 (5min K线近10根)
        if m5 and len(m5) >= 5:
            recent_m5 = m5[-10:] if len(m5) >= 10 else m5
            _, _, _, m5_closes, m5_vols = _get_candle_values(recent_m5)
            # 价格变动率
            if m5_closes and m5_closes[0] > 0:
                price_change_rate = abs(m5_closes[-1] - m5_closes[0]) / m5_closes[0] * 100
            else:
                price_change_rate = 0
            # 量能活跃度
            avg_m5_vol = sum(m5_vols) / len(m5_vols) if m5_vols else 0
            vol_activity = min(100, avg_m5_vol / 100) if avg_m5_vol > 0 else 0
            price_activity = min(100, price_change_rate * 50)
            activity_score = (vol_activity + price_activity) / 2
        else:
            activity_score = 0

        # 关键点位
        pivots = _calc_pivot_points(today_high, today_low, today_close)

        raw_scores.append({
            "symbol": symbol,
            "name": name,
            "data_date": data_date,
            "close_price": today_close,
            "prev_close": prev_close,
            "high_price": today_high,
            "low_price": today_low,
            "volume": today_volume,
            "avg_volume_5": avg_vol_5,
            "amplitude_pct": round(amplitude_pct, 2),
            "volume_ratio": round(volume_ratio, 2),
            "change_pct": round(change_pct, 2),
            "amplitude_raw": amplitude_pct,
            "volume_raw": volume_ratio,
            "change_raw": abs(change_pct),
            "trend_score": trend_total,
            "trend_60m": round(t60, 1),
            "trend_15m": round(t15, 1),
            "trend_5m": round(t5, 1),
            "direction": direction,
            "direction_tag": direction_tag,
            "activity_raw": activity_score,
            "pivots": pivots,
        })

    if not raw_scores:
        raise ValueError("没有足够的数据进行评分计算")

    # ==================== 第三步: 归一化评分 ====================
    amp_scores = _normalize_scores([s["amplitude_raw"] for s in raw_scores])
    vol_scores = _normalize_scores([s["volume_raw"] for s in raw_scores])
    chg_scores = _normalize_abs_scores([s["change_raw"] for s in raw_scores])
    act_scores = _normalize_scores([s["activity_raw"] for s in raw_scores])

    for i, s in enumerate(raw_scores):
        s["amplitude_score"] = amp_scores[i]
        s["volume_score"] = vol_scores[i]
        s["change_score"] = chg_scores[i]
        s["activity_score"] = act_scores[i]

        # 综合评分 = 振幅×0.20 + 量能×0.15 + 涨跌幅×0.15 + 趋势×0.35 + 活跃度×0.15
        # 趋势分映射: trend_score 0-100 直接使用
        s["composite_score"] = round(
            amp_scores[i] * WEIGHTS["amplitude"]
            + vol_scores[i] * WEIGHTS["volume"]
            + chg_scores[i] * WEIGHTS["change"]
            + s["trend_score"] * WEIGHTS["trend"]
            + act_scores[i] * WEIGHTS["activity"],
            1
        )

    # 排序
    raw_scores.sort(key=lambda x: x["composite_score"], reverse=True)

    # ==================== 第四步: 分类 ====================
    for rank, s in enumerate(raw_scores, 1):
        s["rank"] = rank
        score = s["composite_score"]
        trend = s["trend_score"]

        if score >= 55 and trend >= 50:
            s["category"] = "green"
        elif score >= 45 or (score >= 40 and 30 <= trend < 50):
            s["category"] = "yellow"
        else:
            s["category"] = "red"

    # ==================== 第五步: 生成交易计划 ====================
    plans = []
    green_items = [s for s in raw_scores if s["category"] == "green"]

    for s in green_items:
        # 根据方向标签判断实际方向（强多/偏多→做多，强空/偏空→做空）
        direction_tag = s.get("direction_tag", "震荡")
        if direction_tag in ["强空", "偏空"]:
            direction = "short"
        elif direction_tag in ["强多", "偏多"]:
            direction = "long"
        else:
            # 震荡情况下根据涨跌幅判断
            direction = "long" if s["change_pct"] > 0 else "short"

        pivots = s["pivots"]
        price = s["close_price"]

        if direction == "long":
            entry_low = _round_by_price(pivots["s1"], price)
            entry_high = _round_by_price(pivots["pivot"], price)
            stop_loss = _round_by_price(pivots["s2"], price)
            target1 = _round_by_price(pivots["r1"], price)
            target2 = _round_by_price(pivots["r2"], price)
        else:
            entry_low = _round_by_price(pivots["pivot"], price)
            entry_high = _round_by_price(pivots["r1"], price)
            stop_loss = _round_by_price(pivots["r2"], price)
            target1 = _round_by_price(pivots["s1"], price)
            target2 = _round_by_price(pivots["s2"], price)

        # 根据分析结果动态生成触发条件
        trigger = _generate_trigger(s, direction)

        plans.append({
            "symbol": s["symbol"],
            "name": s["name"],
            "direction": direction,
            "composite_score": s["composite_score"],
            "entry_low": entry_low,
            "entry_high": entry_high,
            "stop_loss": stop_loss,
            "target1": target1,
            "target2": target2,
            "trigger": trigger,
            "amplitude_score": s["amplitude_score"],
            "volume_score": s["volume_score"],
            "trend_score": s["trend_score"],
            "activity_score": s["activity_score"],
            "category": s["category"],
        })

    # ==================== 第六步: 板块热度 ====================
    sectors = _calc_sector_heat(raw_scores, code_to_name)

    # ==================== 第七步: 汇总 ====================
    bull_count = sum(1 for s in raw_scores if s["trend_score"] >= 50)
    bear_count = sum(1 for s in raw_scores if s["trend_score"] < 30)
    neutral_count = len(raw_scores) - bull_count - bear_count

    # 统计实际数据日期（取出现最多的日期作为基准数据日期）
    date_counts = Counter(s.get("data_date") for s in raw_scores if s.get("data_date"))
    actual_data_date = date_counts.most_common(1)[0][0] if date_counts else review_date_str
    # 判断数据日期是否与复盘日期一致
    data_date_matches = (actual_data_date == review_date_str)

    # 核心结论
    top3 = raw_scores[:3]
    top_names = [f"{s['symbol']} {s['name']}" for s in top3]
    main_direction = "多头" if bull_count > bear_count else "空头" if bear_count > bull_count else "震荡"
    core_conclusion = f"{main_direction}格局{'延续' if bull_count > 15 else '分化'}，{'贵金属+黑色系共振做多' if bull_count > 20 else '板块轮动明显'}"

    # 风险提示
    risk_warnings = []
    if top3:
        top_sym = top3[0]
        risk_warnings.append(
            f"极值品种严禁追{'多' if top_sym['trend_score'] >= 50 else '空'}："
            f"{top_sym['symbol']}({top_sym['composite_score']}分) "
            f"{'周一大概率高开' if top_sym['change_pct'] > 2 else '注意风险控制'}"
        )
    if bear_count > 0:
        bear_syms = [s for s in raw_scores if s["category"] == "red" and s["trend_score"] < 30][:2]
        if bear_syms:
            names = "/".join(s["symbol"] for s in bear_syms)
            risk_warnings.append(f"空头品种谨慎：{names} 趋势偏空，需严格止损")
    risk_warnings.append("关注宏观数据和消息面变化，可能影响开盘方向")
    if any(s["volume_ratio"] > 2.5 for s in raw_scores):
        high_vol = [s for s in raw_scores if s["volume_ratio"] > 2.5]
        risk_warnings.append(
            f"量能异常品种：{', '.join(s['symbol'] for s in high_vol[:3])} "
            f"量比超2.5倍，需确认量能可持续性"
        )

    # 构建数据基准描述
    if data_date_matches:
        data_basis = f"{actual_data_date} 收盘 ({len(symbol_data)} 品种) | 多周期共振分析"
    else:
        data_basis = f"复盘日期: {review_date_str} | 数据截至: {actual_data_date} 收盘 ({len(symbol_data)} 品种) | 多周期共振分析"

    return {
        "review_date": review_date_str,
        "week_day": week_day,
        "actual_data_date": actual_data_date,
        "data_date_matches": data_date_matches,
        "data_basis": data_basis,
        "core_conclusion": core_conclusion,
        "bull_count": bull_count,
        "bear_count": bear_count,
        "neutral_count": neutral_count,
        "opportunity_count": len(green_items),
        "risk_warnings": risk_warnings,
        "scores": raw_scores,
        "plans": plans,
        "sectors": sectors,
        "symbol_count": len(symbol_data),
    }


def _calc_sector_heat(scores: List[dict], code_to_name: dict) -> List[dict]:
    """计算板块热度"""
    # 构建 symbol -> score 映射
    name_to_score = {}
    for s in scores:
        name_to_score[s["name"]] = s

    sectors = []
    for sector_name, members_names in SECTOR_MAP.items():
        members = []
        for mn in members_names:
            if mn in name_to_score:
                sc = name_to_score[mn]
                members.append({
                    "symbol": sc["symbol"],
                    "name": sc["name"],
                    "score": sc["composite_score"],
                    "trend": sc["trend_score"],
                    "change_pct": sc["change_pct"],
                })

        if not members:
            continue

        avg_score = round(sum(m["score"] for m in members) / len(members), 1)
        avg_trend = round(sum(m["trend"] for m in members) / len(members), 0)

        if avg_trend > 20:
            direction = "多头"
        elif avg_trend < -20:
            direction = "空头"
        else:
            direction = "震荡"

        # 热度等级
        if avg_score >= 75:
            heat = 3
        elif avg_score >= 60:
            heat = 2
        elif avg_score >= 45:
            heat = 1
        else:
            heat = 0

        # 龙头
        leader = max(members, key=lambda m: m["score"])

        sectors.append({
            "sector_name": sector_name,
            "avg_score": avg_score,
            "avg_trend": int(avg_trend),
            "direction": direction,
            "heat_level": heat,
            "leader_symbol": leader["symbol"],
            "leader_score": leader["score"],
            "members": sorted(members, key=lambda m: m["score"], reverse=True),
        })

    sectors.sort(key=lambda s: s["avg_score"], reverse=True)
    return sectors


def save_plan_to_db(db: Session, plan_data: dict) -> int:
    """将计划数据保存到数据库"""
    review_date_str = plan_data["review_date"]
    week_day = plan_data["week_day"]

    # 清理旧数据
    existing = db.query(ReviewPlanV2).filter_by(review_date=review_date_str).first()
    if existing:
        review_plan_id = existing.id
        # 清理关联数据
        db.query(SymbolScoreV2).filter_by(review_date_id=review_plan_id).delete()
        db.query(TradingPlanV2).filter_by(review_date_id=review_plan_id).delete()
        db.query(SectorHeat).filter_by(review_date_id=review_plan_id).delete()
        # 更新元数据
        existing.week_day = week_day
        existing.data_basis = plan_data.get("data_basis")
        existing.core_conclusion = plan_data.get("core_conclusion")
        existing.bull_count = plan_data.get("bull_count")
        existing.bear_count = plan_data.get("bear_count")
        existing.neutral_count = plan_data.get("neutral_count")
        existing.opportunity_count = plan_data.get("opportunity_count")
        existing.risk_warnings = plan_data.get("risk_warnings")
        existing.actual_data_date = plan_data.get("actual_data_date")
        existing.data_date_matches = 1 if plan_data.get("data_date_matches") else 0
    else:
        review_plan = ReviewPlanV2(
            review_date=review_date_str,
            week_day=week_day,
            data_basis=plan_data.get("data_basis"),
            core_conclusion=plan_data.get("core_conclusion"),
            bull_count=plan_data.get("bull_count"),
            bear_count=plan_data.get("bear_count"),
            neutral_count=plan_data.get("neutral_count"),
            opportunity_count=plan_data.get("opportunity_count"),
            risk_warnings=plan_data.get("risk_warnings"),
            actual_data_date=plan_data.get("actual_data_date"),
            data_date_matches=1 if plan_data.get("data_date_matches") else 0,
        )
        db.add(review_plan)
        db.flush()
        review_plan_id = review_plan.id

    # 保存评分数据
    for s in plan_data.get("scores", []):
        pivots = s.get("pivots", {})
        score_record = SymbolScoreV2(
            review_date_id=review_plan_id,
            symbol=s["symbol"],
            name=s["name"],
            close_price=s.get("close_price"),
            prev_close=s.get("prev_close"),
            high_price=s.get("high_price"),
            low_price=s.get("low_price"),
            volume=s.get("volume"),
            avg_volume_5=s.get("avg_volume_5"),
            amplitude_score=s.get("amplitude_score"),
            volume_score=s.get("volume_score"),
            change_score=s.get("change_score"),
            trend_score=s.get("trend_score"),
            activity_score=s.get("activity_score"),
            composite_score=s.get("composite_score"),
            amplitude_pct=s.get("amplitude_pct"),
            change_pct=s.get("change_pct"),
            volume_ratio=s.get("volume_ratio"),
            trend_60m=s.get("trend_60m"),
            trend_15m=s.get("trend_15m"),
            trend_5m=s.get("trend_5m"),
            direction=s.get("direction"),
            direction_tag=s.get("direction_tag"),
            category=s.get("category"),
            pivot=pivots.get("pivot"),
            r1=pivots.get("r1"),
            r2=pivots.get("r2"),
            s1=pivots.get("s1"),
            s2=pivots.get("s2"),
            rank=s.get("rank"),
            data_date=s.get("data_date"),
        )
        db.add(score_record)

    # 保存交易计划
    for p in plan_data.get("plans", []):
        plan_record = TradingPlanV2(
            review_date_id=review_plan_id,
            symbol=p["symbol"],
            name=p["name"],
            direction=p["direction"],
            composite_score=p.get("composite_score"),
            entry_low=p.get("entry_low"),
            entry_high=p.get("entry_high"),
            stop_loss=p.get("stop_loss"),
            target1=p.get("target1"),
            target2=p.get("target2"),
            trigger=p.get("trigger"),
            amplitude_score=p.get("amplitude_score"),
            volume_score=p.get("volume_score"),
            trend_score=p.get("trend_score"),
            activity_score=p.get("activity_score"),
            category=p.get("category"),
        )
        db.add(plan_record)

    # 保存板块热度
    for sec in plan_data.get("sectors", []):
        sector_record = SectorHeat(
            review_date_id=review_plan_id,
            sector_name=sec["sector_name"],
            avg_score=sec.get("avg_score"),
            avg_trend=sec.get("avg_trend"),
            direction=sec.get("direction"),
            heat_level=sec.get("heat_level"),
            leader_symbol=sec.get("leader_symbol"),
            leader_score=sec.get("leader_score"),
            members=sec.get("members"),
        )
        db.add(sector_record)

    db.commit()
    logger.info(f"交易计划已保存: {review_date_str}, ID={review_plan_id}")
    return review_plan_id


def get_plan_data(db: Session, review_plan_id: int) -> Optional[dict]:
    """从数据库读取完整的计划数据"""
    plan_meta = db.query(ReviewPlanV2).filter_by(id=review_plan_id).first()
    if not plan_meta:
        return None

    scores = db.query(SymbolScoreV2).filter_by(
        review_date_id=review_plan_id
    ).order_by(SymbolScoreV2.composite_score.desc()).all()

    plans = db.query(TradingPlanV2).filter_by(
        review_date_id=review_plan_id
    ).order_by(TradingPlanV2.composite_score.desc()).all()

    sectors = db.query(SectorHeat).filter_by(
        review_date_id=review_plan_id
    ).order_by(SectorHeat.avg_score.desc()).all()

    return {
        "meta": {
            "id": plan_meta.id,
            "review_date": plan_meta.review_date,
            "week_day": plan_meta.week_day,
            "data_basis": plan_meta.data_basis,
            "actual_data_date": plan_meta.actual_data_date,
            "data_date_matches": bool(plan_meta.data_date_matches),
            "core_conclusion": plan_meta.core_conclusion,
            "bull_count": plan_meta.bull_count,
            "bear_count": plan_meta.bear_count,
            "neutral_count": plan_meta.neutral_count,
            "opportunity_count": plan_meta.opportunity_count,
            "risk_warnings": plan_meta.risk_warnings,
            "created_at": plan_meta.created_at.isoformat() if plan_meta.created_at else None,
        },
        "scores": [_score_to_dict(s) for s in scores],
        "plans": [_plan_to_dict(p) for p in plans],
        "sectors": [_sector_to_dict(s) for s in sectors],
    }


def _score_to_dict(s: SymbolScoreV2) -> dict:
    return {
        "id": s.id,
        "symbol": s.symbol,
        "name": s.name,
        "data_date": s.data_date,
        "close_price": s.close_price,
        "prev_close": s.prev_close,
        "high_price": s.high_price,
        "low_price": s.low_price,
        "volume": s.volume,
        "avg_volume_5": s.avg_volume_5,
        "amplitude_score": s.amplitude_score,
        "volume_score": s.volume_score,
        "change_score": s.change_score,
        "trend_score": s.trend_score,
        "activity_score": s.activity_score,
        "composite_score": s.composite_score,
        "amplitude_pct": s.amplitude_pct,
        "change_pct": s.change_pct,
        "volume_ratio": s.volume_ratio,
        "trend_60m": s.trend_60m,
        "trend_15m": s.trend_15m,
        "trend_5m": s.trend_5m,
        "direction": s.direction,
        "direction_tag": s.direction_tag,
        "category": s.category,
        "pivot": s.pivot,
        "r1": s.r1,
        "r2": s.r2,
        "s1": s.s1,
        "s2": s.s2,
        "rank": s.rank,
    }


def _plan_to_dict(p: TradingPlanV2) -> dict:
    return {
        "id": p.id,
        "symbol": p.symbol,
        "name": p.name,
        "direction": p.direction,
        "composite_score": p.composite_score,
        "entry_low": p.entry_low,
        "entry_high": p.entry_high,
        "stop_loss": p.stop_loss,
        "target1": p.target1,
        "target2": p.target2,
        "trigger": p.trigger,
        "amplitude_score": p.amplitude_score,
        "volume_score": p.volume_score,
        "trend_score": p.trend_score,
        "activity_score": p.activity_score,
        "category": p.category,
    }


def _sector_to_dict(s: SectorHeat) -> dict:
    return {
        "id": s.id,
        "sector_name": s.sector_name,
        "avg_score": s.avg_score,
        "avg_trend": s.avg_trend,
        "direction": s.direction,
        "heat_level": s.heat_level,
        "leader_symbol": s.leader_symbol,
        "leader_score": s.leader_score,
        "members": s.members,
    }
