"""
전략 진화 엔진 — EMA + Softmax 기반 적응형 가중치 관리.
"""
import math
import json
import logging
from typing import Dict, List, Any

logger = logging.getLogger("lotto-backend")

# ── Constants (importable without DB) ─────────────────────────────────────────
ALPHA = 0.3           # EMA 감쇠율
TEMPERATURE = 2.0     # Softmax 온도
MIN_WEIGHT = 0.05     # 최소 가중치
INITIAL_EMA = 0.15    # 콜드스타트 초기값
MIN_DATA_DRAWS = 10   # 학습 최소 회차

STRATEGIES = ["combined", "simulation", "heatmap", "manual", "custom"]
RANK_BONUS = {5: 0.1, 4: 0.3, 3: 0.6, 2: 0.8, 1: 1.0}


# ── Pure functions (no DB dependency) ─────────────────────────────────────────

def calc_draw_score(results: List[Dict]) -> float:
    """구매 결과 리스트 → 평균 성과 점수"""
    if not results:
        return 0.0
    scores = []
    for r in results:
        s = r.get("correct", 0) / 6.0
        s += RANK_BONUS.get(r.get("rank", 0), 0)
        scores.append(s)
    return sum(scores) / len(scores)


def _softmax_weights(ema_scores: Dict[str, float]) -> Dict[str, float]:
    """EMA 점수 → Softmax → 최소 가중치 보장 → 정규화"""
    raw = {s: math.exp(ema / TEMPERATURE) for s, ema in ema_scores.items()}
    total = sum(raw.values())
    weights = {s: v / total for s, v in raw.items()}

    clamped = {}
    surplus = 0.0
    unclamped = []
    for s, w in weights.items():
        if w < MIN_WEIGHT:
            clamped[s] = MIN_WEIGHT
            surplus += MIN_WEIGHT - w
        else:
            unclamped.append(s)
            clamped[s] = w

    if surplus > 0 and unclamped:
        unclamped_total = sum(clamped[s] for s in unclamped)
        for s in unclamped:
            clamped[s] -= surplus * (clamped[s] / unclamped_total)

    final_total = sum(clamped.values())
    return {s: round(v / final_total, 4) for s, v in clamped.items()}


# ── DB-dependent functions (use lazy imports) ─────────────────────────────────

def _db():
    """Lazy import to avoid circular/relative import issues in tests"""
    from . import db as _db_mod
    return _db_mod


def _recommender():
    from . import recommender as _rec_mod
    return _rec_mod


def _analyzer():
    from . import analyzer as _ana_mod
    return _ana_mod


def update_ema_for_strategy(strategy: str, draw_score: float) -> float:
    db = _db()
    weights = db.get_strategy_weights()
    current = next((w for w in weights if w["strategy"] == strategy), None)
    old_ema = current["ema_score"] if current else INITIAL_EMA
    new_ema = ALPHA * draw_score + (1 - ALPHA) * old_ema
    return new_ema


def recalculate_weights() -> Dict[str, float]:
    db = _db()
    weights_rows = db.get_strategy_weights()
    ema_scores = {w["strategy"]: w["ema_score"] for w in weights_rows}

    for s in STRATEGIES:
        if s not in ema_scores:
            ema_scores[s] = INITIAL_EMA

    new_weights = _softmax_weights(ema_scores)

    for s, w in new_weights.items():
        row = next((r for r in weights_rows if r["strategy"] == s), None)
        db.update_strategy_weight(
            strategy=s,
            weight=w,
            ema_score=ema_scores[s],
            total_sets=row["total_sets"] if row else 0,
            total_hits_3plus=row["total_hits_3plus"] if row else 0,
        )

    logger.info(f"[strategy_evolver] 가중치 재계산: {new_weights}")
    return new_weights


def evolve_after_check(strategy: str, draw_no: int, results: List[Dict]) -> None:
    db = _db()
    draw_score = calc_draw_score(results)
    new_ema = update_ema_for_strategy(strategy, draw_score)

    weights_rows = db.get_strategy_weights()
    current = next((w for w in weights_rows if w["strategy"] == strategy), None)
    hits_3plus = sum(1 for r in results if r.get("correct", 0) >= 3)

    db.update_strategy_weight(
        strategy=strategy,
        weight=current["weight"] if current else 0.2,
        ema_score=new_ema,
        total_sets=(current["total_sets"] if current else 0) + len(results),
        total_hits_3plus=(current["total_hits_3plus"] if current else 0) + hits_3plus,
    )

    recalculate_weights()


def get_weights_with_trend() -> Dict[str, Any]:
    db = _db()
    weights = db.get_strategy_weights()
    perfs = db.get_strategy_performance()

    strat_perfs = {}
    for p in perfs:
        s = p["strategy"]
        if s not in strat_perfs:
            strat_perfs[s] = []
        strat_perfs[s].append(p)

    result = []
    for w in weights:
        sp = strat_perfs.get(w["strategy"], [])
        if len(sp) >= 5:
            recent_avg = sum(p["avg_score"] for p in sp[-3:]) / 3
            older_avg = sum(p["avg_score"] for p in sp[-5:-2]) / 3
            delta = recent_avg - older_avg
            trend = "up" if delta > 0.02 else ("down" if delta < -0.02 else "stable")
        else:
            trend = "stable"

        result.append({
            "strategy": w["strategy"],
            "weight": w["weight"],
            "ema_score": w["ema_score"],
            "total_sets": w["total_sets"],
            "hits_3plus": w["total_hits_3plus"],
            "trend": trend,
        })

    all_draws = set()
    for p in perfs:
        all_draws.add(p["draw_no"])

    return {
        "weights": result,
        "last_evolved": weights[0]["updated_at"] if weights else None,
        "min_data_draws": MIN_DATA_DRAWS,
        "current_data_draws": len(all_draws),
        "status": "active" if len(all_draws) >= MIN_DATA_DRAWS else "learning",
    }


def generate_smart_recommendation(sets: int = 5) -> Dict[str, Any]:
    db = _db()
    rec = _recommender()
    ana = _analyzer()

    weights_data = db.get_strategy_weights()
    weight_map = {w["strategy"]: w["weight"] for w in weights_data}
    draws = db.get_all_draw_numbers()
    if not draws:
        return {"error": "No draw data"}

    latest = db.get_latest_draw()
    cache = ana.build_analysis_cache(draws)
    past_recs = db.list_recommendations_ex(limit=100, sort="id_desc")

    candidates = []
    seen_keys = set()

    def _add_candidate(nums: list, strategy: str, raw_score: float = None):
        key = tuple(sorted(nums))
        if key in seen_keys:
            return
        seen_keys.add(key)
        if raw_score is None:
            sc = ana.score_combination(nums, cache)
            raw_score = sc["score_total"]
        meta = raw_score * weight_map.get(strategy, 0.1)
        candidates.append({
            "numbers": sorted(nums),
            "raw_score": round(raw_score, 4),
            "strategy": strategy,
            "meta_score": round(meta, 4),
        })

    # combined: 10세트
    for _ in range(10):
        try:
            r = ana.generate_combined_recommendation(draws)
            if "final_numbers" in r:
                _add_candidate(r["final_numbers"], "combined")
        except Exception:
            pass

    # simulation: best_picks 상위 10개
    best = db.get_best_picks(limit=10)
    for b in best:
        nums = json.loads(b["numbers"]) if isinstance(b["numbers"], str) else b["numbers"]
        _add_candidate(nums, "simulation", b.get("score_total"))

    # heatmap: 10세트
    for _ in range(10):
        try:
            r = rec.recommend_with_heatmap(draws, past_recs)
            _add_candidate(r["numbers"], "heatmap")
        except Exception:
            pass

    # manual: 10세트
    for _ in range(10):
        try:
            r = rec.recommend_numbers(draws)
            _add_candidate(r["numbers"], "manual")
        except Exception:
            pass

    candidates.sort(key=lambda c: -c["meta_score"])
    top = candidates[:sets]

    result_sets = []
    for c in top:
        sc = ana.score_combination(c["numbers"], cache)
        contributions = {}
        for strat in STRATEGIES:
            contributions[strat] = round(weight_map.get(strat, 0) * sc["score_total"], 4)
        contrib_total = sum(contributions.values()) or 1
        contributions = {s: round(v / contrib_total, 3) for s, v in contributions.items()}

        result_sets.append({
            "numbers": c["numbers"],
            "meta_score": c["meta_score"],
            "source_strategy": c["strategy"],
            "contribution": contributions,
            "individual_scores": {k: round(v, 4) for k, v in sc.items()},
        })

    perfs = db.get_strategy_performance()
    data_draws = len(set(p["draw_no"] for p in perfs))
    status = "active" if data_draws >= MIN_DATA_DRAWS else "learning"

    return {
        "sets": result_sets,
        "strategy_weights_used": weight_map,
        "learning_status": {
            "draws_learned": data_draws,
            "status": status,
            "message": "" if status == "active" else f"{MIN_DATA_DRAWS}회차 이상 데이터 필요 (현재 {data_draws}회차)",
        },
        "based_on_latest_draw": latest["drw_no"] if latest else None,
    }
