"""
Exit/sell action decision logic for portfolio execution.

F05/F10 porting: Determines the sell action, ratio, price target, and execution details
based on market signals (RW, timing, profit levels, time stops, stop losses).

Ported from: src/gas_adapter_parts/gdf_01_price_metrics.gs:calcExitSellAction_
            src/gas_adapter_parts/gdf_01_price_metrics.gs:calcCashPreservationPlan_
Parity reference: tests/parity/test_execution_decision_parity_v1.py
"""

import math
import re
from typing import Any, Optional


def is_finite(value: Any) -> bool:
    """Check if value is a finite number (matches JavaScript Number.isFinite())."""
    return isinstance(value, (int, float)) and math.isfinite(value)


def calc_cash_preservation_plan(ctx: dict[str, Any]) -> dict[str, Any]:
    """
    Calculate cash preservation adjustment to sell action.

    Factors: core/leader status, rebound holdback score, cash floor, regime, liquidity,
    account type (tax), RW signals.

    Args:
        ctx: Dict with keys:
          - cashFloorStatus: "TRIM_REQUIRED", "HARD_BLOCK", etc.
          - regime: Market regime (e.g., "RISK_OFF")
          - sellAction: Sell action (e.g., "TRIM_50")
          - isCoreLeader: bool
          - isEtf: bool
          - liquidityStatus: "LOW", "OK", etc.
          - spreadStatus: "WIDE", "OK", "BLOCK", etc.
          - accountType: "일반계좌", "연금계좌", etc.
          - profitPct: Profit percentage
          - rwPartial: Relative weakness signal count (0-5)
          - reboundHoldbackScore: Rebound preservation score

    Returns:
        Dict: {
          "style": "CORE_LAST" | "STEP_25" | "STEP_33" | "STEP_50",
          "recommended_ratio": 0-50 (sell ratio override),
          "protection_bonus": integer (risk bonus points),
          "reasons": "reason1 | reason2 | ..."
        }
    """
    cash_floor_status = str(ctx.get("cashFloorStatus", ""))
    regime = str(ctx.get("regime", ""))
    sell_action = str(ctx.get("sellAction", ctx.get("action", "")))
    is_sell_like = re.search(r"(SELL|TRIM|EXIT)", sell_action) is not None
    is_core_leader = bool(ctx.get("isCoreLeader"))
    is_etf = bool(ctx.get("isEtf"))
    liquidity_status = str(ctx.get("liquidityStatus", ""))
    spread_status = str(ctx.get("spreadStatus", ""))
    account_type = str(ctx.get("accountType", ""))
    profit_pct = float(ctx.get("profitPct", float("nan")))
    rw_partial = int(ctx.get("rwPartial", 0))
    rebound_holdback = float(ctx.get("reboundHoldbackScore", float("nan")))
    holdback_score = rebound_holdback if is_finite(rebound_holdback) else 0

    recommended_ratio = 50 if is_sell_like else 0
    style = "STEP_50"
    protection_bonus = 0
    reasons = []

    if is_core_leader and holdback_score >= 12:
        style = "CORE_LAST"
        recommended_ratio = 25 if cash_floor_status == "TRIM_REQUIRED" else 0
        protection_bonus += 12
        reasons.append("core_last")
    elif holdback_score >= 18:
        style = "STEP_25"
        recommended_ratio = 25
        protection_bonus += 10
        reasons.append("strong_rebound")
    elif holdback_score >= 10:
        style = "STEP_33"
        recommended_ratio = 33
        protection_bonus += 6
        reasons.append("rebound_preserve")

    if is_etf and holdback_score < 10:
        protection_bonus -= 2
        reasons.append("etf_cash_raise")

    if cash_floor_status == "TRIM_REQUIRED" or re.search(r"RISK_OFF", regime):
        protection_bonus += 2
        reasons.append("cash_preserve")

    if liquidity_status == "LOW" or spread_status in ("WIDE", "BLOCK"):
        protection_bonus += 4
        reasons.append("impact_avoid")

    if account_type == "일반계좌" and is_finite(profit_pct) and profit_pct > 0:
        protection_bonus += 3 if profit_pct >= 20 else 2
        reasons.append("tax_drag")
    elif account_type == "일반계좌" and is_finite(profit_pct) and profit_pct < 0:
        protection_bonus -= 2
        reasons.append("tax_loss_harvest")

    if rw_partial >= 3 and not is_core_leader:
        recommended_ratio = max(recommended_ratio, 50)
        protection_bonus -= 4
        reasons.append("rw_force")

    if cash_floor_status == "HARD_BLOCK":
        recommended_ratio = max(recommended_ratio, 50)
        reasons.append("cash_hard_block")

    if not is_sell_like:
        recommended_ratio = 0
    recommended_ratio = max(0, min(50, recommended_ratio))

    return {
        "style": style,
        "recommended_ratio": recommended_ratio,
        "protection_bonus": max(0, round(protection_bonus)),
        "reasons": " | ".join(reasons),
    }


def calc_exit_sell_action(ctx: dict[str, Any]) -> dict[str, Any]:
    """
    Determine exit/sell action based on priority matrix of signals.

    Priority hierarchy (spec/exit/stop_loss.yaml):
      1. Hard stop / strong RW (EXIT_100, rwPartial >= 4)
      2. REGIME_TRIM_50 (RISK_OFF — portfolio-level, skipped here)
      3. RW strong + timing (TRIM_70)
      4. Trailing stop breach
      5. RW medium / timing-based trims (TRIM_50, TRIM_33, TRIM_25)
      6. Profit-taking ladder (TP1/TP2 tiers)
      7. Time stop (TIME_EXIT_100, TIME_TRIM_*)

    Args:
        ctx: Dict with keys from data_feed row + macro context:
          - close, stopPrice, trailingStop, tp1Price, tp2Price, profitPct
          - rwPartial, timingExitScore, daysToTimeStop, timingAction
          - exitSignalDetail, acGate, regime, atr20
          - cashFloorStatus, isCoreLeader, isEtf, liquidityStatus, spreadStatus
          - accountType, reboundHoldbackScore

    Returns:
        Dict: {
          "action": "HOLD" | "EXIT_100" | "TRIM_70" | ... | "TIME_TRIM_25",
          "ratio_pct": 0-100,
          "limit_price": price (KRW integer) or "",
          "price_source": "TP2_PRICE" | "TRAILING_STOP" | ... | "ATR_PROTECT_LIMIT",
          "price_basis": "TAKE_PROFIT_TIER2_PRICE" | ... | "ATR_PROTECT_LIMIT",
          "execution_window": "INTRADAY_ON_TRIGGER" | "INTRADAY_LIMIT_OR_CLOSE_REVIEW" | ...,
          "order_type": "LIMIT_SELL" | "PROTECTIVE_LIMIT_SELL",
          "reason": "RW_EXIT_STRONG" | ... | "TIME_STOP_APPROACHING",
          "validation": "SIGNAL_CONFIRMED" | "NO_SELL_PRICE" | "NO_SELL_ACTION",
          "cash_preserve_style": "STEP_50" | ...,
          "cash_preserve_ratio": 0-50,
          "cash_preserve_reason": "..."
        }
    """
    def safe_float(v, default=float("nan")):
        """Safely convert to float, handling None/invalid values."""
        if v is None or v == "":
            return default
        try:
            return float(v)
        except (ValueError, TypeError):
            return default

    close = safe_float(ctx.get("close"))
    stop_price = safe_float(ctx.get("stopPrice"))
    trailing_stop = safe_float(ctx.get("trailingStop"))
    tp1_price = safe_float(ctx.get("tp1Price"))
    tp2_price = safe_float(ctx.get("tp2Price"))
    profit_pct = safe_float(ctx.get("profitPct"))
    rw_partial = int(ctx.get("rwPartial", 0))
    timing_exit_score = safe_float(ctx.get("timingExitScore"))
    days_to_time_stop = int(ctx.get("daysToTimeStop", 999))
    timing_action = str(ctx.get("timingAction", ""))
    regime = str(ctx.get("regime", ""))
    atr20 = safe_float(ctx.get("atr20"))

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

    # Calculate protective limits
    stop_candidate = (
        trailing_stop if is_finite(trailing_stop) and trailing_stop > 0
        else stop_price if is_finite(stop_price) and stop_price > 0
        else close * 0.995 if is_finite(close) and close > 0
        else None
    )
    protective_limit = (
        round(min(close * 0.995, stop_candidate if stop_candidate else close * 0.995))
        if is_finite(close) and close > 0
        else ""
    )
    atr_buffer = (
        atr20 * 0.3 if is_finite(atr20) and atr20 > 0
        else close * 0.005 if is_finite(close)
        else 0
    )
    close_protect_limit = (
        round(close - atr_buffer)
        if is_finite(close) and close > 0
        else ""
    )

    # Priority 1: Hard stop / strong RW
    if timing_action == "STOP_OR_TIME_EXIT_READY" or rw_partial >= 4:
        action = "EXIT_100"
        ratio = 100
        reason = "RW_EXIT_STRONG" if rw_partial >= 4 else "STOP_OR_TIME_EXIT_READY"
        price = protective_limit
        price_source = "TRAILING_STOP" if is_finite(trailing_stop) else "STOP_OR_CLOSE"
        price_basis = "TRAILING_STOP_TRIGGER" if is_finite(trailing_stop) else "STOP_OR_CLOSE_PROTECT"
        execution_window = "INTRADAY_ON_TRIGGER"
        order_type = "PROTECTIVE_LIMIT_SELL"
    # Priority 3: RW strong + timing
    elif rw_partial >= 3 or timing_exit_score >= 75:
        action = "TRIM_70"
        ratio = 70
        reason = "RW_EXIT" if rw_partial >= 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"
    # Priority 4: Trailing stop breach
    elif is_finite(trailing_stop) and trailing_stop > 0 and is_finite(close) and close <= trailing_stop:
        action = "TRAILING_STOP_BREACH"
        ratio = 70
        reason = "TRAILING_STOP_PRICE_BREACH"
        price = round(trailing_stop)
        price_source = "TRAILING_STOP_PRICE"
        price_basis = "TRAILING_STOP_TRIGGER"
        execution_window = "INTRADAY_ON_TRIGGER"
        order_type = "PROTECTIVE_LIMIT_SELL"
    # Priority 4 (cont): RW medium
    elif rw_partial >= 2 or (rw_partial >= 1 and timing_exit_score >= 50):
        action = "TRIM_50"
        ratio = 50
        reason = "RW_REVIEW" if rw_partial >= 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"
    # Priority 4b: RW early warning
    elif rw_partial >= 1 and timing_exit_score >= 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"
    # Priority 4c: RW signal only
    elif rw_partial >= 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"
    # Priority 5: Profit-taking ladder
    elif is_finite(profit_pct) and profit_pct >= 50:
        action = "PROFIT_TRIM_50"
        ratio = 50
        reason = "PROFIT_PROTECT_50"
        price = round(tp2_price) if is_finite(tp2_price) and tp2_price > 0 else close_protect_limit
        price_source = "TP2_PRICE" if is_finite(tp2_price) else "CLOSE_PROFIT_PROTECT"
        price_basis = "TAKE_PROFIT_TIER2_PRICE" if is_finite(tp2_price) else "PRIOR_CLOSE_X_0.998"
        execution_window = "INTRADAY_LIMIT_OR_CLOSE_REVIEW"
        order_type = "LIMIT_SELL"
    elif is_finite(profit_pct) and profit_pct >= 30:
        action = "PROFIT_TRIM_35"
        ratio = 35
        reason = "PROFIT_PROTECT_30"
        price = round(tp2_price) if is_finite(tp2_price) and tp2_price > 0 else close_protect_limit
        price_source = "TP2_PRICE" if is_finite(tp2_price) else "CLOSE_PROFIT_PROTECT"
        price_basis = "TAKE_PROFIT_TIER2_PRICE" if is_finite(tp2_price) else "PRIOR_CLOSE_X_0.998"
        execution_window = "INTRADAY_LIMIT_OR_CLOSE_REVIEW"
        order_type = "LIMIT_SELL"
    elif is_finite(profit_pct) and profit_pct >= 20:
        action = "PROFIT_TRIM_25"
        ratio = 25
        reason = "PROFIT_PROTECT_20"
        price = round(tp1_price) if is_finite(tp1_price) and tp1_price > 0 else close_protect_limit
        price_source = "TP1_PRICE" if is_finite(tp1_price) else "CLOSE_PROFIT_PROTECT"
        price_basis = "TAKE_PROFIT_TIER1_PRICE" if is_finite(tp1_price) else "PRIOR_CLOSE_X_0.998"
        execution_window = "INTRADAY_LIMIT_OR_CLOSE_REVIEW"
        order_type = "LIMIT_SELL"
    elif is_finite(profit_pct) and profit_pct >= 10:
        action = "TAKE_PROFIT_TIER1"
        ratio = 25
        reason = "TP1_PROFIT_10PCT"
        price = round(tp1_price) if is_finite(tp1_price) and tp1_price > 0 else close_protect_limit
        price_source = "TP1_PRICE" if is_finite(tp1_price) else "CLOSE_PROFIT_PROTECT"
        price_basis = "TAKE_PROFIT_TIER1_PRICE" if is_finite(tp1_price) else "PRIOR_CLOSE_X_0.998"
        execution_window = "INTRADAY_LIMIT_OR_CLOSE_REVIEW"
        order_type = "LIMIT_SELL"
    # Priority 6: Time stop
    elif is_finite(days_to_time_stop) and days_to_time_stop <= 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 is_finite(days_to_time_stop) and days_to_time_stop <= 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 is_finite(days_to_time_stop) and days_to_time_stop <= 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"

    # Apply cash preservation plan adjustments
    cash_preserve_plan = calc_cash_preservation_plan({
        "cashFloorStatus": ctx.get("cashFloorStatus", ""),
        "regime": regime,
        "sellAction": action,
        "isCoreLeader": ctx.get("isCoreLeader"),
        "isEtf": ctx.get("isEtf"),
        "liquidityStatus": ctx.get("liquidityStatus", ""),
        "spreadStatus": ctx.get("spreadStatus", ""),
        "accountType": ctx.get("accountType", ""),
        "profitPct": profit_pct,
        "rwPartial": rw_partial,
        "reboundHoldbackScore": float(ctx.get("reboundHoldbackScore", float("nan"))),
    })

    if action not in ("EXIT_100", "TRAILING_STOP_BREACH", "HOLD"):
        target_ratio = cash_preserve_plan.get("recommended_ratio", 0)
        if is_finite(target_ratio) and target_ratio > 0 and target_ratio < ratio:
            ratio = target_ratio
            if ratio <= 25:
                action = "TRIM_25"
            elif ratio <= 33:
                action = "TRIM_33"
            else:
                action = "TRIM_50"
            reason = (
                f"{reason}|CASH_PRESERVE:{cash_preserve_plan['style']}"
                if reason
                else f"CASH_PRESERVE:{cash_preserve_plan['style']}"
            )

    # SL003 Priority Matrix: when multiple stop conditions trigger, use max price
    is_stop_type_action = re.match(
        r"^(EXIT_100|TRIM_70|TRAILING_STOP_BREACH|TRIM_50|TRIM_33|TRIM_25|TIME_EXIT_100|TIME_TRIM_50|TIME_TRIM_25)$",
        action
    ) is not None

    if is_stop_type_action and is_finite(close) and close > 0:
        slp_candidates = []

        if timing_action == "STOP_OR_TIME_EXIT_READY" or rw_partial >= 4:
            if is_finite(protective_limit) and protective_limit > 0:
                slp_candidates.append({"src": "HARD_STOP", "p": protective_limit})

        if rw_partial >= 3 or timing_exit_score >= 75:
            if is_finite(protective_limit) and protective_limit > 0:
                slp_candidates.append({"src": "RW_TRIM70", "p": protective_limit})

        if is_finite(trailing_stop) and trailing_stop > 0 and is_finite(close) and close <= trailing_stop:
            slp_candidates.append({"src": "TRAILING", "p": round(trailing_stop)})

        if rw_partial >= 2 or (rw_partial >= 1 and timing_exit_score >= 50):
            if is_finite(close_protect_limit) and close_protect_limit > 0:
                slp_candidates.append({"src": "RW_TRIM50", "p": close_protect_limit})

        if is_finite(days_to_time_stop) and days_to_time_stop <= 7:
            if is_finite(close_protect_limit) and close_protect_limit > 0:
                slp_candidates.append({"src": "TIME_STOP", "p": close_protect_limit})

        if len(slp_candidates) >= 2:
            max_slp = max(slp_candidates, key=lambda x: x["p"])
            cur_price = float(price) if price else 0
            if max_slp["p"] > cur_price:
                price = max_slp["p"]
                price_source = "PRIORITY_MATRIX_MAX"
                candidates_str = "|".join([f"{c['src']}:{c['p']}" for c in slp_candidates])
                price_basis = f"SL003_MAX({candidates_str})"

    # Validation
    validation = "NO_SELL_ACTION"
    if action != "HOLD":
        try:
            price_val = float(price) if price else 0
            validation = "SIGNAL_CONFIRMED" if is_finite(price_val) and price_val > 0 else "NO_SELL_PRICE"
        except (ValueError, TypeError):
            validation = "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_plan["style"],
        "cash_preserve_ratio": cash_preserve_plan["recommended_ratio"],
        "cash_preserve_reason": cash_preserve_plan["reasons"],
    }
