from __future__ import annotations

import math
from typing import Any


KRX_TICK_TABLE: tuple[tuple[float, int], ...] = (
    (2_000, 1),
    (5_000, 5),
    (20_000, 10),
    (50_000, 50),
    (200_000, 100),
    (500_000, 500),
    (math.inf, 1000),
)


def krx_tick_unit(price: float) -> int:
    for threshold, tick in KRX_TICK_TABLE:
        if price < threshold:
            return tick
    return 1000


def normalize_tick(price: float) -> int:
    tick = krx_tick_unit(price)
    return int(math.floor(price / tick) * tick)


def compute_stop_price_core(
    entry_price: float | None,
    atr20: float | None,
    current_price: float | None = None,
    atr_multiplier: float | None = None,
) -> dict[str, Any]:
    if entry_price is None:
        return {
            "stop_price": None,
            "stop_price_status": "NO_STOP_PRICE",
            "data_missing": ["entry_price"],
        }

    if atr20 is None and atr_multiplier is None:
        return {
            "stop_price": entry_price * 0.92,
            "stop_price_status": "DATA_MISSING — 하네스 업데이트 필요",
            "data_missing": ["atr20"],
        }

    if atr_multiplier is None and current_price in (None, 0):
        return {
            "stop_price": entry_price * 0.92,
            "stop_price_status": "DATA_MISSING — 하네스 업데이트 필요",
            "data_missing": [name for name, value in (("atr20", atr20), ("current_price", current_price)) if value in (None, 0)],
        }

    if atr_multiplier is None:
        atr20_pct = (atr20 / current_price) * 100
        atr_multiplier = 2.0 if atr20_pct >= 8 else 1.5
    else:
        atr20_pct = (atr20 / current_price) * 100 if current_price not in (None, 0) else None
    return {
        "stop_price": max(entry_price * 0.92, entry_price - atr20 * atr_multiplier),
        "stop_price_status": "PASS",
        "atr20_pct": atr20_pct,
        "atr_multiplier": atr_multiplier,
    }


def compute_stop_action_ladder(context: dict[str, Any]) -> dict[str, Any]:
    timing_action = str(context.get("timingAction") or context.get("timing_action") or "").upper()
    rw_partial = int(context.get("rw_partial") or 0)
    rw_partial_excluding_rw2b = int(context.get("rw_partial_excluding_rw2b") or 0)
    regime_prelim = str(context.get("REGIME_PRELIM") or context.get("regime_prelim") or "").upper()
    timing_exit_score = float(context.get("timingExitScore") or context.get("timing_exit_score") or 0.0)
    profit_pct = float(context.get("profitPct") or context.get("profit_pct") or 0.0)
    days_to_time_stop = int(context.get("daysToTimeStop") or context.get("days_to_time_stop") or 9999)
    trailing_stop_breach = bool(context.get("trailingStopBreach") or context.get("trailing_stop_breach") or False)
    rw2b_fast_track = bool(context.get("RW2b_5d_rapid_weakness") or context.get("rw2b_5d_rapid_weakness") or False)

    if timing_action == "STOP_OR_TIME_EXIT_READY" or rw_partial >= 4:
        return {
            "action": "EXIT_100",
            "reason": "STOP_OR_TIME_EXIT_READY" if timing_action == "STOP_OR_TIME_EXIT_READY" else "RW_EXIT_STRONG",
            "quantity_pct": 100,
            "priority": 1,
        }
    if regime_prelim in {"RISK_OFF", "RISK_OFF_CANDIDATE"}:
        return {
            "action": "REGIME_TRIM_50",
            "reason": "REGIME_RISK_OFF" if regime_prelim == "RISK_OFF" else "REGIME_RISK_OFF_CANDIDATE",
            "quantity_pct": 50,
            "priority": 2,
        }
    if rw2b_fast_track and rw_partial_excluding_rw2b >= 1:
        return {
            "action": "TRIM_50",
            "reason": "RW2B_FAST_TRACK",
            "quantity_pct": 50,
            "priority": 2.5,
        }
    if rw_partial >= 3 or timing_exit_score >= 75:
        return {
            "action": "TRIM_70",
            "reason": "RW_EXIT" if rw_partial >= 3 else "TIMING_EXIT_SCORE",
            "quantity_pct": 70,
            "priority": 3,
        }
    if trailing_stop_breach or rw_partial >= 2 or (rw_partial >= 1 and timing_exit_score >= 50):
        return {
            "action": "TRIM_50",
            "reason": "TRAILING_STOP_BREACH" if trailing_stop_breach else "RW_OR_TIMING_EXIT",
            "quantity_pct": 50,
            "priority": 4,
        }
    if profit_pct >= 10:
        return {
            "action": "TAKE_PROFIT_TIER1",
            "reason": "PROFIT_PCT_THRESHOLD",
            "quantity_pct": 25,
            "priority": 5,
        }
    if days_to_time_stop <= 0:
        return {
            "action": "TIME_EXIT_100",
            "reason": "TIME_STOP_EXPIRED",
            "quantity_pct": 100,
            "priority": 6,
        }
    return {
        "action": "REVIEW_HUMAN",
        "reason": "NO_FORCED_EXIT",
        "quantity_pct": 0,
        "priority": 7,
    }


def compute_dynamic_heat_thresholds(regime: str | None) -> dict[str, float]:
    r = str(regime or "").upper()
    if "EVENT_SHOCK" in r:
        return {"hardBlock": 5.0, "halve": 3.5}
    if "RISK_OFF" in r:
        return {"hardBlock": 7.0, "halve": 5.0}
    if "SECULAR_LEADER" in r and "RISK_ON" in r:
        return {"hardBlock": 13.0, "halve": 9.0}
    if "RISK_ON" in r:
        return {"hardBlock": 12.0, "halve": 8.5}
    return {"hardBlock": 10.0, "halve": 7.0}


def compute_cash_shortfall_harness(
    as_result: dict[str, Any],
    total_asset: float,
    cash_floor_info: dict[str, Any],
    mrs_score: float,
) -> dict[str, Any]:
    asset = total_asset if math.isfinite(total_asset) and total_asset > 0 else 0.0
    d2_krw = float(as_result.get("settlementCashD2Krw") or 0.0)
    min_pct = float(cash_floor_info.get("minPct") or 0.0)
    target_cash_pct = max(5 + (mrs_score / 10) * 15, min_pct)
    return {
        "cash_current_pct_d2": round((d2_krw / asset * 100), 2) if asset > 0 else 0,
        "cash_target_pct": target_cash_pct,
        "cash_shortfall_min_krw": max(0, round(asset * min_pct / 100 - d2_krw)),
        "cash_shortfall_target_krw": max(0, round(asset * target_cash_pct / 100 - d2_krw)),
    }


def compute_timing_decision(ctx: dict[str, Any]) -> dict[str, Any]:
    reasons: list[str] = []
    entry_score = 0
    exit_score = 0

    entry_gate = str(ctx.get("entryModeGate") or "")
    entry_mode = str(ctx.get("entryMode") or "")
    leader_gate = str(ctx.get("leaderGate") or "")
    ac_gate = str(ctx.get("acGate") or "")
    exit_signal = str(ctx.get("exitSignalDetail") or "")
    flow_credit = ctx.get("flowCredit")
    leader_total = ctx.get("leaderTotal")
    rw_partial = ctx.get("rwPartial")
    rsi14 = ctx.get("rsi14")
    disparity = ctx.get("disparity")
    ma20_slope = ctx.get("ma20Slope")
    spread_pct = ctx.get("spreadPct")
    avg_trade_value_5d = ctx.get("avgTradeValue5D")
    profit_pct = ctx.get("profitPct")
    days_to_time_stop = ctx.get("daysToTimeStop")

    if entry_gate == "PASS":
        entry_score += 25
        reasons.append(f"entry_{entry_mode}")
    elif entry_gate == "BLOCK":
        entry_score -= 25
        reasons.append("entry_block")

    if isinstance(leader_total, (int, float)) and math.isfinite(float(leader_total)):
        if leader_total >= 4:
            entry_score += 20
            reasons.append("leader_scan>=4")
        elif leader_total >= 3:
            entry_score += 10
            reasons.append("leader_watch")
    if leader_gate in {"PASS", "EXPLORE_CANDIDATE"}:
        entry_score += 10

    if isinstance(flow_credit, (int, float)) and math.isfinite(float(flow_credit)):
        if flow_credit >= 0.7:
            entry_score += 20
            reasons.append("flow_strong")
        elif flow_credit >= 0.4:
            entry_score += 10
            reasons.append("flow_partial")

    if ac_gate == "CLEAR":
        entry_score += 15
        reasons.append("anti_climax_clear")
    elif ac_gate == "CAUTION":
        entry_score += 5
        reasons.append("anti_climax_caution")
    elif ac_gate == "BLOCK":
        entry_score -= 35
        exit_score += 15
        reasons.append("anti_climax_block")

    if isinstance(ma20_slope, (int, float)) and math.isfinite(float(ma20_slope)):
        if ma20_slope > 0:
            entry_score += 8
        else:
            entry_score -= 8
            exit_score += 8
            reasons.append("ma20_down")
    if isinstance(disparity, (int, float)) and math.isfinite(float(disparity)):
        if -5 <= disparity <= 4:
            entry_score += 10
        elif 4 < disparity <= 8:
            entry_score += 5
        elif disparity > 12:
            entry_score -= 25
            exit_score += 20
            reasons.append("overextended")
        elif disparity < -10:
            entry_score -= 10
            exit_score += 10
            reasons.append("trend_damage")
    if isinstance(rsi14, (int, float)) and math.isfinite(float(rsi14)):
        if 40 <= rsi14 <= 65:
            entry_score += 10
        elif 65 < rsi14 <= 72:
            entry_score += 4
        elif rsi14 > 75:
            entry_score -= 25
            exit_score += 20
            reasons.append("rsi_overbought")
        elif rsi14 < 35:
            entry_score -= 5
            exit_score += 8
            reasons.append("weak_rsi")
    if (
        isinstance(avg_trade_value_5d, (int, float))
        and math.isfinite(float(avg_trade_value_5d))
        and avg_trade_value_5d >= 50
        and (not isinstance(spread_pct, (int, float)) or not math.isfinite(float(spread_pct)) or spread_pct <= 0.8)
    ):
        entry_score += 10
    else:
        entry_score -= 15
        reasons.append("liquidity_or_spread_fail")

    if isinstance(rw_partial, (int, float)) and math.isfinite(float(rw_partial)):
        exit_score += min(100, max(0, int(rw_partial) * 25))
    if exit_signal:
        exit_score += len([part for part in exit_signal.split("|") if part]) * 10
    if isinstance(days_to_time_stop, (int, float)) and 0 <= float(days_to_time_stop) <= 7:
        exit_score += 20
        reasons.append("time_stop_near")
    if isinstance(profit_pct, (int, float)) and profit_pct >= 10:
        exit_score += 15
        reasons.append("profit_protect_zone")

    entry_score = max(0, min(100, round(entry_score)))
    exit_score = max(0, min(100, round(exit_score)))

    action = "HOLD_NO_TIMING_EDGE"
    atr20 = ctx.get("atr20")
    price_status = str(ctx.get("priceStatus") or "")
    if price_status != "PRICE_OK" or not isinstance(atr20, (int, float)) or not math.isfinite(float(atr20)):
        action = "OBSERVE_DATA_MISSING"
    elif exit_score >= 75 or (isinstance(rw_partial, (int, float)) and rw_partial >= 4):
        action = "STOP_OR_TIME_EXIT_READY"
    elif exit_score >= 50 or (isinstance(rw_partial, (int, float)) and rw_partial >= 3):
        action = "EXIT_REVIEW"
    elif entry_gate == "BLOCK" or ac_gate == "BLOCK" or entry_mode == "OVERBOUGHT":
        action = "NO_BUY_OVERHEATED"
    elif entry_score >= 75 and entry_gate == "PASS" and isinstance(leader_total, (int, float)) and leader_total >= 4:
        action = "BUY_BREAKOUT_PILOT_ONLY" if entry_mode == "BREAKOUT" else "BUY_STAGE1_READY"
    elif entry_score >= 60 and entry_gate == "PASS":
        action = "BUY_BREAKOUT_PILOT_ONLY" if entry_mode == "BREAKOUT" else "BUY_PULLBACK_WAIT"
    elif (
        (isinstance(leader_total, (int, float)) and leader_total >= 3)
        or (isinstance(flow_credit, (int, float)) and flow_credit >= 0.4)
    ):
        action = "WATCH_TIMING_SETUP"

    return {
        "entry_score": entry_score,
        "exit_score": exit_score,
        "action": action,
        "reason": "|".join(reasons[:6]),
    }


def compute_sell_decision(ctx: dict[str, Any]) -> dict[str, Any]:
    close = ctx.get("close")
    stop_price = ctx.get("stopPrice")
    trailing_stop = ctx.get("trailingStop")
    tp1_price = ctx.get("tp1Price")
    tp2_price = ctx.get("tp2Price")
    profit_pct = ctx.get("profitPct")
    rw_partial = ctx.get("rwPartial")
    timing_exit_score = ctx.get("timingExitScore")
    days_to_time_stop = ctx.get("daysToTimeStop")
    timing_action = str(ctx.get("timingAction") or "")
    regime = str(ctx.get("regime") or "")
    atr20 = ctx.get("atr20")

    close_f = float(close) if isinstance(close, (int, float)) else float("nan")
    stop_f = float(stop_price) if isinstance(stop_price, (int, float)) else float("nan")
    trailing_f = float(trailing_stop) if isinstance(trailing_stop, (int, float)) else float("nan")
    tp1_f = float(tp1_price) if isinstance(tp1_price, (int, float)) else float("nan")
    tp2_f = float(tp2_price) if isinstance(tp2_price, (int, float)) else float("nan")
    profit_f = float(profit_pct) if isinstance(profit_pct, (int, float)) else float("nan")
    rw_f = float(rw_partial) if isinstance(rw_partial, (int, float)) else float("nan")
    timing_exit_f = float(timing_exit_score) if isinstance(timing_exit_score, (int, float)) else float("nan")
    days_f = float(days_to_time_stop) if isinstance(days_to_time_stop, (int, float)) else float("nan")
    atr_f = float(atr20) if isinstance(atr20, (int, float)) else float("nan")

    action = "HOLD"
    ratio = 0
    reason = ""
    price: Any = ""
    price_source = ""
    price_basis = ""
    execution_window = ""
    order_type = ""

    stop_candidate = trailing_f if math.isfinite(trailing_f) and trailing_f > 0 else stop_f
    if not (math.isfinite(stop_candidate) and stop_candidate > 0) and math.isfinite(close_f) and close_f > 0:
        stop_candidate = close_f * 0.995
    protective_limit = round(min(close_f * 0.995, stop_candidate if math.isfinite(stop_candidate) else close_f * 0.995)) if math.isfinite(close_f) and close_f > 0 else ""
    atr_buffer = atr_f * 0.3 if math.isfinite(atr_f) and atr_f > 0 else (close_f * 0.005 if math.isfinite(close_f) else 0)
    close_protect_limit = round(close_f - atr_buffer) if math.isfinite(close_f) and close_f > 0 else ""

    if timing_action == "STOP_OR_TIME_EXIT_READY" or (math.isfinite(rw_f) and rw_f >= 4):
        action = "EXIT_100"
        ratio = 100
        reason = "RW_EXIT_STRONG" if math.isfinite(rw_f) and rw_f >= 4 else "STOP_OR_TIME_EXIT_READY"
        price = protective_limit
        price_source = "TRAILING_STOP" if math.isfinite(trailing_f) else "STOP_OR_CLOSE"
        price_basis = "TRAILING_STOP_TRIGGER" if math.isfinite(trailing_f) else "STOP_OR_CLOSE_PROTECT"
        execution_window = "INTRADAY_ON_TRIGGER"
        order_type = "PROTECTIVE_LIMIT_SELL"
    elif math.isfinite(rw_f) and rw_f >= 3 or (math.isfinite(timing_exit_f) and timing_exit_f >= 75):
        action = "TRIM_70"
        ratio = 70
        reason = "RW_EXIT" if math.isfinite(rw_f) and rw_f >= 3 else "TIMING_EXIT_SCORE"
        price = protective_limit
        price_source = "RISK_REDUCTION"
        price_basis = "RISK_REDUCTION_CLOSE_PROTECT"
        execution_window = "INTRADAY_AFTER_09_30"
        order_type = "PROTECTIVE_LIMIT_SELL"
    elif math.isfinite(trailing_f) and trailing_f > 0 and math.isfinite(close_f) and close_f <= trailing_f:
        action = "TRAILING_STOP_BREACH"
        ratio = 70
        reason = "TRAILING_STOP_PRICE_BREACH"
        price = round(trailing_f)
        price_source = "TRAILING_STOP_PRICE"
        price_basis = "TRAILING_STOP_TRIGGER"
        execution_window = "INTRADAY_ON_TRIGGER"
        order_type = "PROTECTIVE_LIMIT_SELL"
    elif (math.isfinite(rw_f) and rw_f >= 2) or (math.isfinite(rw_f) and rw_f >= 1 and math.isfinite(timing_exit_f) and timing_exit_f >= 50):
        action = "TRIM_50"
        ratio = 50
        reason = "RW_REVIEW" if math.isfinite(rw_f) and rw_f >= 2 else "TIMING_EXIT_REVIEW"
        price = close_protect_limit
        price_source = "RELATIVE_WEAKNESS_CLOSE"
        price_basis = "PRIOR_CLOSE_X_0.998"
        execution_window = "INTRADAY_AFTER_09_30"
        order_type = "LIMIT_SELL"
    elif math.isfinite(rw_f) and rw_f >= 1 and math.isfinite(timing_exit_f) and timing_exit_f >= 30:
        action = "TRIM_33"
        ratio = 33
        reason = "RW_EARLY_WARNING"
        price = close_protect_limit
        price_source = "EARLY_WARNING_CLOSE"
        price_basis = "PRIOR_CLOSE_X_0.998"
        execution_window = "INTRADAY_AFTER_09_30"
        order_type = "LIMIT_SELL"
    elif math.isfinite(rw_f) and rw_f >= 1:
        action = "TRIM_25"
        ratio = 25
        reason = "RW_SIGNAL_ONLY"
        price = close_protect_limit
        price_source = "SIGNAL_ONLY_CLOSE"
        price_basis = "PRIOR_CLOSE_X_0.998"
        execution_window = "CLOSE_REVIEW_OR_NEXT_OPEN"
        order_type = "LIMIT_SELL"
    elif math.isfinite(profit_f) and profit_f >= 50:
        action = "PROFIT_TRIM_50"
        ratio = 50
        reason = "PROFIT_PROTECT_50"
        price = round(tp2_f) if math.isfinite(tp2_f) and tp2_f > 0 else close_protect_limit
        price_source = "TP2_PRICE" if math.isfinite(tp2_f) else "CLOSE_PROFIT_PROTECT"
        price_basis = "TAKE_PROFIT_TIER2_PRICE" if math.isfinite(tp2_f) else "PRIOR_CLOSE_X_0.998"
        execution_window = "INTRADAY_LIMIT_OR_CLOSE_REVIEW"
        order_type = "LIMIT_SELL"
    elif math.isfinite(profit_f) and profit_f >= 30:
        action = "PROFIT_TRIM_35"
        ratio = 35
        reason = "PROFIT_PROTECT_30"
        price = round(tp2_f) if math.isfinite(tp2_f) and tp2_f > 0 else close_protect_limit
        price_source = "TP2_PRICE" if math.isfinite(tp2_f) else "CLOSE_PROFIT_PROTECT"
        price_basis = "TAKE_PROFIT_TIER2_PRICE" if math.isfinite(tp2_f) else "PRIOR_CLOSE_X_0.998"
        execution_window = "INTRADAY_LIMIT_OR_CLOSE_REVIEW"
        order_type = "LIMIT_SELL"
    elif math.isfinite(profit_f) and profit_f >= 20:
        action = "PROFIT_TRIM_25"
        ratio = 25
        reason = "PROFIT_PROTECT_20"
        price = round(tp1_f) if math.isfinite(tp1_f) and tp1_f > 0 else close_protect_limit
        price_source = "TP1_PRICE" if math.isfinite(tp1_f) else "CLOSE_PROFIT_PROTECT"
        price_basis = "TAKE_PROFIT_TIER1_PRICE" if math.isfinite(tp1_f) else "PRIOR_CLOSE_X_0.998"
        execution_window = "INTRADAY_LIMIT_OR_CLOSE_REVIEW"
        order_type = "LIMIT_SELL"
    elif math.isfinite(profit_f) and profit_f >= 10:
        action = "TAKE_PROFIT_TIER1"
        ratio = 25
        reason = "TP1_PROFIT_10PCT"
        price = round(tp1_f) if math.isfinite(tp1_f) and tp1_f > 0 else close_protect_limit
        price_source = "TP1_PRICE" if math.isfinite(tp1_f) else "CLOSE_PROFIT_PROTECT"
        price_basis = "TAKE_PROFIT_TIER1_PRICE" if math.isfinite(tp1_f) else "PRIOR_CLOSE_X_0.998"
        execution_window = "INTRADAY_LIMIT_OR_CLOSE_REVIEW"
        order_type = "LIMIT_SELL"
    elif math.isfinite(days_f) and days_f <= 0:
        action = "TIME_EXIT_100"
        ratio = 100
        reason = "TIME_STOP_EXPIRED"
        price = protective_limit
        price_source = "TIME_STOP_CLOSE"
        price_basis = "TIME_STOP_CLOSE_PROTECT"
        execution_window = "CLOSE_REVIEW_OR_NEXT_OPEN"
        order_type = "PROTECTIVE_LIMIT_SELL"
    elif math.isfinite(days_f) and days_f <= 7:
        action = "TIME_TRIM_50"
        ratio = 50
        reason = "TIME_STOP_NEAR"
        price = close_protect_limit
        price_source = "TIME_STOP_NEAR_CLOSE"
        price_basis = "ATR_PROTECT_LIMIT"
        execution_window = "CLOSE_REVIEW_OR_NEXT_OPEN"
        order_type = "LIMIT_SELL"
    elif math.isfinite(days_f) and days_f <= 14:
        action = "TIME_TRIM_25"
        ratio = 25
        reason = "TIME_STOP_APPROACHING"
        price = close_protect_limit
        price_source = "TIME_STOP_APPROACHING_CLOSE"
        price_basis = "ATR_PROTECT_LIMIT"
        execution_window = "CLOSE_REVIEW_OR_NEXT_OPEN"
        order_type = "LIMIT_SELL"

    validation = "NO_SELL_ACTION"
    if action != "HOLD":
        validation = "SIGNAL_CONFIRMED" if isinstance(price, (int, float)) and float(price) > 0 else "NO_SELL_PRICE"

    return {
        "action": action,
        "ratio_pct": ratio,
        "limit_price": price,
        "price_source": price_source,
        "price_basis": price_basis,
        "execution_window": execution_window,
        "order_type": order_type,
        "reason": reason,
        "validation": validation,
        "cash_preserve_style": "",
        "cash_preserve_ratio": 0,
        "cash_preserve_reason": [],
    }


def compute_final_decision(ctx: dict[str, Any]) -> dict[str, Any]:
    sell_action = str(ctx.get("sellAction") or "HOLD")
    sell_validation = str(ctx.get("sellValidation") or "")
    allowed_action = str(ctx.get("allowedAction") or "")
    timing_action = str(ctx.get("timingAction") or "")
    timing_entry = ctx.get("timingScoreEntry")
    timing_exit = ctx.get("timingScoreExit")
    ss001_total = ctx.get("ss001Total")
    flow_credit = ctx.get("flowCredit")
    leader_total = ctx.get("leaderTotal")
    rw_partial = ctx.get("rwPartial")
    profit_pct = ctx.get("profitPct")
    days_to_time_stop = ctx.get("daysToTimeStop")
    weight_pct = ctx.get("weightPct")
    ac_gate = str(ctx.get("acGate") or "")
    liquidity_status = str(ctx.get("liquidityStatus") or "")
    spread_status = str(ctx.get("spreadStatus") or "")
    dart_risk = bool(ctx.get("dartRisk"))
    missing_fields = str(ctx.get("missingFields") or "")

    final_action = "HOLD"
    action_priority = 99
    decision_source = "RULE_ENGINE"

    if sell_action != "HOLD" and sell_validation == "SIGNAL_CONFIRMED":
        final_action = "SELL_READY"
        action_priority = 10
    elif allowed_action == "EXIT_SIGNAL" or timing_action == "STOP_OR_TIME_EXIT_READY":
        final_action = "EXIT_SIGNAL"
        action_priority = 28
    elif allowed_action == "REVIEW_EXIT" or timing_action == "EXIT_REVIEW":
        final_action = "EXIT_REVIEW"
        action_priority = 32
    elif timing_action == "NO_BUY_OVERHEATED" and not dart_risk:
        final_action = "NO_BUY_OVERHEATED"
        action_priority = 50
    elif allowed_action == "BUY_STAGE1_READY" or timing_action == "BUY_STAGE1_READY":
        final_action = "BUY_STAGE1_READY"
        action_priority = 60
    elif allowed_action == "BUY_BREAKOUT_PILOT_ONLY" or timing_action == "BUY_BREAKOUT_PILOT_ONLY":
        final_action = "BUY_BREAKOUT_PILOT_ONLY"
        action_priority = 70
    elif allowed_action == "BUY_PULLBACK_WAIT" or timing_action == "BUY_PULLBACK_WAIT":
        final_action = "BUY_PULLBACK_WAIT"
        action_priority = 80
    elif allowed_action == "WATCH_CANDIDATE":
        final_action = "WATCH_TIMING_SETUP"
        action_priority = 90

    if missing_fields:
        decision_source = "RULE_ENGINE_WITH_MISSING_DATA"

    def _finite(value: Any) -> bool:
        return isinstance(value, (int, float)) and math.isfinite(float(value))

    time_stop_urgency = max(0, 20 - min(20, float(days_to_time_stop) * 3)) if _finite(days_to_time_stop) and days_to_time_stop >= 0 else 0
    overweight_penalty = 15 if _finite(weight_pct) and weight_pct > 7 else 0
    overheat_penalty = 30 if ac_gate == "BLOCK" else 10 if ac_gate == "CAUTION" else 0
    liquidity_penalty = 15 if liquidity_status in {"LOW", "DATA_MISSING"} or spread_status in {"BLOCK", "WIDE", "QUOTE_NO_MATCH"} else 0

    if action_priority <= 40:
        priority_score = (float(timing_exit) if _finite(timing_exit) else 0) * 0.35 + (float(rw_partial) if _finite(rw_partial) else 0) * 15 + max(0, float(profit_pct) if _finite(profit_pct) else 0) * 0.30 + time_stop_urgency + overweight_penalty
    elif 50 <= action_priority <= 80:
        priority_score = (float(timing_entry) if _finite(timing_entry) else 0) * 0.35 + (float(ss001_total) if _finite(ss001_total) else 0) * 0.30 + (float(flow_credit) if _finite(flow_credit) else 0) * 20 + (float(leader_total) if _finite(leader_total) else 0) * 5 - overheat_penalty - liquidity_penalty
    else:
        priority_score = (float(timing_entry) if _finite(timing_entry) else 0) * 0.20 + (float(timing_exit) if _finite(timing_exit) else 0) * 0.20 + (float(flow_credit) if _finite(flow_credit) else 0) * 10

    return {
        "final_action": final_action,
        "action_priority": action_priority,
        "priority_score": float(max(0, priority_score)),
        "decision_source": decision_source,
    }
