"""
Portfolio routing decision with multi-gate filtering.

F10 porting: Evaluates holding positions through 5 sequential gates
(stop breach, relative stop, intraday lock, heat, mean reversion) and
returns final routing action per holding.

Ported from: src/gas_adapter_parts/gdf_03_portfolio_gates.gs:runRouteFlow_
Parity reference: tests/parity/test_routing_decision_parity_v1.py
"""

import re
from typing import Any, Optional


def is_finite(value: Any) -> bool:
    """Check if value is a finite number."""
    try:
        import math
        return isinstance(value, (int, float)) and math.isfinite(value)
    except:
        return False


def run_route_flow(
    holdings: list[dict[str, Any]],
    df_map: dict[str, dict[str, Any]],
    h1_context: dict[str, Any]
) -> dict[str, Any]:
    """
    Route holdings through multi-gate decision framework.

    Gates:
    1. Stop_Breach: Direct stop loss trigger → EXIT_100 or TRIM_50
    2. Relative_Stop: Market beta-adjusted stop → TRIM_50
    3. Intraday_Lock: P4 constraints (blocked keywords, allowlist)
    4. Heat_Gate: Portfolio heat control (BLOCK_NEW_BUY, HALVE_QTY)
    5. Mean_Reversion: Mean-reversion gate (MRG001)

    Args:
        holdings: List of holding dicts with keys: ticker, stopPrice, close, profitPct, etc.
        df_map: Dict mapping ticker → data_feed row dict
        h1_context: Market context dict with keys: intradayLock, heatGate, kospiRet20d, etc.

    Returns:
        Dict: {
          "routes": [{"ticker": str, "final_action": str, ...}, ...],
          "traces": [{"ticker": str, "gates": [...]}, ...],
          "lock": bool
        }
    """
    routes = []
    traces = []

    intraday_lock = bool(h1_context.get("intradayLock"))
    heat_gate = str(h1_context.get("heatGate", ""))
    kospi_ret20d = float(h1_context.get("kospiRet20d", 0))

    for h in holdings:
        ticker = str(h.get("ticker", ""))
        df = df_map.get(ticker, {})
        base_final_action = str(df.get("finalAction", "INSUFFICIENT_DATA")).upper()
        final_action = base_final_action
        trace_gates = []

        # Gate 1: Stop_Price Breach
        stop_breach = bool(h.get("stopBreach"))
        if stop_breach:
            if intraday_lock:
                final_action = "TRIM_50"  # P4: EXIT_100 → TRIM_50
                trace_gates.append({
                    "gate": "STOP_BREACH",
                    "result": "DOWNGRADE_P4",
                    "reason": "intraday_lock: stop_breach→TRIM_50"
                })
            else:
                final_action = "EXIT_100"
                trace_gates.append({
                    "gate": "STOP_BREACH",
                    "result": "FORCE_EXIT",
                    "reason": f"breach: close={h.get('close')} ≤ stop={h.get('stopPrice')}"
                })
        else:
            trace_gates.append({
                "gate": "STOP_BREACH",
                "result": "PASS",
                "reason": "no_breach"
            })

        # Gate 2: Relative_Stop (beta-adjusted)
        if final_action != "EXIT_100":
            ret20d = float(df.get("ret20d", float("nan")))
            atr20 = float(df.get("atr20", float("nan")))
            close = float(h.get("close", 0)) or float(df.get("close", 0))
            profit_pct = float(h.get("profitPct", float("nan")))
            holding_days = int(h.get("holdingDays", 0))

            if is_finite(ret20d) and is_finite(atr20) and close > 0:
                # Beta calculation
                if abs(kospi_ret20d) >= 0.5:
                    beta = min(3.0, max(0.3, ret20d / kospi_ret20d))
                else:
                    beta = 1.0

                excess = ret20d - beta * kospi_ret20d
                sigma = (atr20 / close * 100) * (20 ** 0.5)  # sqrt(20)
                thresh = -2.0 * sigma

                # Trigger conditions
                abs_floor = is_finite(profit_pct) and profit_pct < -20.0
                rel_break = excess < thresh
                time_stop = holding_days >= 60 and excess < 0

                if abs_floor or rel_break or time_stop:
                    rs_type = "ABS_FLOOR" if abs_floor else ("REL_EXCESS" if rel_break else "TIME_STOP")
                    trace_gates.append({
                        "gate": "RELATIVE_STOP",
                        "result": "TRIM_50",
                        "reason": f"{rs_type}: excess={excess:.2f} thr={thresh:.2f}"
                    })
                    if final_action == "HOLD" or "BUY" in final_action:
                        final_action = "TRIM_50"
                else:
                    trace_gates.append({
                        "gate": "RELATIVE_STOP",
                        "result": "PASS",
                        "reason": f"excess={excess:.2f} thr={thresh:.2f}"
                    })
            else:
                trace_gates.append({
                    "gate": "RELATIVE_STOP",
                    "result": "SKIP",
                    "reason": "insufficient_data"
                })
        else:
            trace_gates.append({
                "gate": "RELATIVE_STOP",
                "result": "INACTIVE",
                "reason": "stop_breach_exit_100"
            })

        # Gate 3: Intraday_Lock (P4 constraints)
        if intraday_lock:
            # Downgrade blocked keywords
            blocked_keywords = ["BUY", "ADD"]
            allowed_actions = ["HOLD", "WATCH", "TRIM_25", "TRIM_33", "TRIM_50", "EXIT_100"]

            if any(keyword in final_action for keyword in blocked_keywords):
                downgraded = "WATCH" if "BUY" in final_action else "TRIM_50"
                trace_gates.append({
                    "gate": "INTRADAY_LOCK",
                    "result": "DOWNGRADE",
                    "reason": f"P4: {final_action}→{downgraded}"
                })
                final_action = downgraded

            # Force allowlist check
            if final_action not in allowed_actions:
                trace_gates.append({
                    "gate": "INTRADAY_LOCK",
                    "result": "FORCE_WATCH",
                    "reason": f"P4_ALLOWLIST: {final_action}→WATCH"
                })
                final_action = "WATCH"
            else:
                trace_gates.append({
                    "gate": "INTRADAY_LOCK",
                    "result": "PASS",
                    "reason": "action_in_allowlist"
                })
        else:
            trace_gates.append({
                "gate": "INTRADAY_LOCK",
                "result": "INACTIVE",
                "reason": "post_market"
            })

        # Gate 4: Heat_Gate (portfolio heat control)
        if "BUY" in final_action:
            if heat_gate == "BLOCK_NEW_BUY":
                trace_gates.append({
                    "gate": "HEAT_GATE",
                    "result": "BLOCK_BUY",
                    "reason": "total_heat>=10%: BUY→WATCH"
                })
                final_action = "WATCH"
            elif heat_gate == "HALVE_NEW_BUY_QUANTITY":
                trace_gates.append({
                    "gate": "HEAT_GATE",
                    "result": "HALVE_QTY",
                    "reason": "total_heat>=7%: qty 50% reduction"
                })
            else:
                trace_gates.append({
                    "gate": "HEAT_GATE",
                    "result": "PASS",
                    "reason": heat_gate or "ok"
                })
        else:
            trace_gates.append({
                "gate": "HEAT_GATE",
                "result": "PASS",
                "reason": heat_gate or "not_buy"
            })

        # Gate 5: Mean_Reversion (MRG001)
        if "BUY" in final_action:
            mrg_close = float(df.get("close", 0))
            mrg_ma20 = float(df.get("ma20", 0))
            if mrg_close > 0 and mrg_ma20 > 0:
                dev_ratio = mrg_close / mrg_ma20
                mrg_threshold = 1.10  # 10% deviation threshold
                if dev_ratio > mrg_threshold:
                    trace_gates.append({
                        "gate": "MEAN_REVERSION",
                        "result": "BLOCK",
                        "reason": f"MRG001: close/ma20={dev_ratio:.3f} > {mrg_threshold}"
                    })
                    final_action = "WATCH"
                else:
                    trace_gates.append({
                        "gate": "MEAN_REVERSION",
                        "result": "PASS",
                        "reason": f"close/ma20={dev_ratio:.3f}"
                    })
            else:
                trace_gates.append({
                    "gate": "MEAN_REVERSION",
                    "result": "SKIP",
                    "reason": "insufficient_data"
                })
        else:
            trace_gates.append({
                "gate": "MEAN_REVERSION",
                "result": "PASS",
                "reason": "not_buy"
            })

        routes.append({
            "ticker": ticker,
            "final_action": final_action,
            "base_action": base_final_action,
        })
        traces.append({
            "ticker": ticker,
            "gates": trace_gates,
        })

    return {
        "decisions": routes,
        "traces": traces,
        "lock": True
    }
