"""
APEX V16/V17 — TP price resolver.

Two resolution paths (V17):

  1. AI-suggested (preferred when valid, V17 narrative prompt):
     Brain forwards `tp_price_suggested` (assoluto, basato su VWAP / EMA50 /
     swing). Resolver applica un margine conservativo del 15% verso l'entry
     prima di snappare al tick e applicare MIN_TP_TICKS floor. Selezionato
     quando tp_price_suggested > 0 ed è coerente con la direzione
     (BUY: > entry; SELL: < entry).

  2. rr-fallback (V16 originale, variante γ):
     Brain emette `rr_multiplier` ∈ [0.17, 0.67] (advisory, post_val).
     Resolver calcola tp_ticks = round(rr × sl_ticks), poi floor MIN_TP_TICKS.
     Selezionato quando AI path non è disponibile o non è coerente.

In entrambi i path Brain lascia `EntryDecision.tp_price = 0.0` come sentinel
e l'orchestrator chiama `resolve_tp_price(...)` post-sizing per finalizzare
il prezzo TP assoluto.

Math (rr-fallback):
    sl_usd_actual = contracts × sl_ticks × tick_value
    tp_usd_target = rr_multiplier × sl_usd_actual
    tp_distance   = tp_usd_target / (contracts × tick_value)
                  = rr_multiplier × sl_ticks × tick_size      [contracts cancels]
    tp_ticks_raw  = round(rr_multiplier × sl_ticks)
    tp_ticks_final = max(MIN_TP_TICKS[symbol], tp_ticks_raw)
    tp_price       = entry ± tp_ticks_final × tick_size  (BUY: +, SELL: -)

Math (ai_suggested, conservative margin 15% toward entry):
    distance_raw  = |tp_price_suggested − entry|
    distance_cons = (1 − TP_SUGGESTED_CONSERVATIVE_MARGIN) × distance_raw
    tp_ticks_raw  = round(distance_cons / tick_size)
    tp_ticks_final = max(MIN_TP_TICKS[symbol], tp_ticks_raw)
    tp_price       = entry ± tp_ticks_final × tick_size  (BUY: +, SELL: -)

Note: `contracts` algebraically cancels out of the points-distance formula
in the rr-fallback path. We keep it in the signature for narrative coherence
with the AI prompt ("rr × $rischio reale"), and so `sl_usd_actual` /
`tp_usd_target` can be logged for calibration forensics.

Hard clamp on `rr_multiplier` is applied here (RR_MULT_MIN/MAX) on the
rr-fallback path only. The Brain post_validate enforces a *narrower*
prompt-range [0.17, 0.67] as advisory rejection. Hard clamp is
defense-in-depth for marginal AI drift.

Pure function — no broker, no AI, no I/O.
"""

from __future__ import annotations

from dataclasses import dataclass

from core import config_futures as cfg_fut


# ============================================================
# Hard clamp bounds for rr_multiplier (rr-fallback path)
# Prompt-range (advisory, brain post_val): [0.17, 0.67]
# Hard-range  (defense-in-depth, here):    [0.10, 0.80]
# ============================================================
RR_MULT_MIN: float = 0.10
RR_MULT_MAX: float = 0.80

# ============================================================
# Conservative margin applied to AI-suggested TP (V17).
# 0.15 = TP shrunk toward entry by 15% of the raw |tp_suggested − entry|.
# Filosofia APEX "WR > RR": meglio TP raggiungibile con più probabilità
# di hit che non target ottimistico mai centrato.
# ============================================================
TP_SUGGESTED_CONSERVATIVE_MARGIN: float = 0.15

# ============================================================
# Hard cap on the resulting rr_effective (V18 12-mag).
# Incidente live: MYM BUY con sl_ticks=13 e tp_suggested ancorato al
# VWAP (138 ticks) → rr_effective=10.6× → trade ad alto RR ma WR molto
# basso. La filosofia APEX "WR > RR" richiede un cap sul ratio tra TP
# e SL — il target deve essere raggiungibile, non un livello statistico
# lontano.
#
# Valore 2.0:
#   - advisory rr_multiplier max 0.67, hard clamp 0.80 → 2.0 è ampio
#     margine sopra il baseline rr-fallback
#   - per ogni asset config, MIN_SL_TICKS ≥ MIN_TP_TICKS → con cap=2.0
#     non si scontra mai col MIN_TP_TICKS floor in scenari normali
#   - 2× SL è il limite ragionevole per strategie funded high-WR;
#     oltre stiamo ottimizzando per fantasie di payout
#
# Applicato su entrambi i path DOPO il MIN_TP_TICKS floor: il floor
# (copertura costi) ha priorità sul cap (qualità del setup).
# ============================================================
MAX_RR_EFFECTIVE: float = 2.0


# ============================================================
# RESULT TYPE
# ============================================================

@dataclass(frozen=True)
class TPResolution:
    """
    Output of resolve_tp_price.

    tp_price            : finalized absolute price (rounded to asset digits).
    tp_ticks_final      : post-floor TP distance in ticks.
    tp_distance         : tp_ticks_final × tick_size (price points).
    sl_usd_actual       : contracts × sl_ticks × tick_value (real $ at risk).
    tp_usd_target       : target $ on TP hit. rr_fallback: rr_multiplier ×
                          sl_usd_actual; ai_suggested: rr_effective ×
                          sl_usd_actual (de-facto rr derived from the
                          AI-suggested distance after conservative margin
                          and tick snap).
    rr_effective        : tp_ticks_final / sl_ticks. rr-fallback: equals
                          rr_multiplier unless MIN_TP_TICKS floor lifted
                          tp_ticks above the raw round. ai_suggested:
                          de-facto rr produced by the AI suggestion path.
    rr_multiplier_used  : rr-fallback: rr_multiplier post hard-clamp (the
                          value math used). ai_suggested: equals
                          rr_effective (no rr_multiplier consumed).
    min_tp_ticks_applied: True iff MIN_TP_TICKS[symbol] raised tp_ticks above
                          the raw round (in either path).
    tp_source           : "ai_suggested" when AI's tp_price_suggested was
                          used (post 15% conservative margin); "rr_fallback"
                          when the legacy rr_multiplier path was taken.
    """
    tp_price: float
    tp_ticks_final: int
    tp_distance: float
    sl_usd_actual: float
    tp_usd_target: float
    rr_effective: float
    rr_multiplier_used: float
    min_tp_ticks_applied: bool
    tp_source: str = "rr_fallback"
    # V18 12-mag — True iff MAX_RR_EFFECTIVE cap ha tagliato il TP raw
    # (rr_effective sarebbe stato > 2.0 senza il cap). Diagnostica per
    # calibrare: troppi cap → cap stretto / AI prompt troppo ambizioso;
    # zero cap → fix non engagato in produzione.
    rr_capped: bool = False


# ============================================================
# PUBLIC API
# ============================================================

def resolve_tp_price(
    *,
    symbol: str,
    direction: str,
    entry_price: float,
    rr_multiplier: float,
    contracts: int,
    sl_ticks: int,
    tp_price_suggested: float = 0.0,
) -> TPResolution:
    """
    Compute the absolute TP price for an approved entry, post-sizing.

    V17 dual-path:
      - When `tp_price_suggested > 0` AND coherent with `direction`
        (BUY: > entry, SELL: < entry), use the AI-suggested path with
        conservative margin TP_SUGGESTED_CONSERVATIVE_MARGIN (default 15%
        toward entry). Returns TPResolution.tp_source = "ai_suggested".
      - Otherwise fall back to the legacy V16 rr_multiplier math
        (variante γ). Returns TPResolution.tp_source = "rr_fallback".

    Backward compatible: callers that don't pass `tp_price_suggested` get
    the V16 rr-fallback behavior unchanged.

    Args:
        symbol: e.g. "MES", "6B" (must be in ASSETS_MAP).
        direction: "BUY" or "SELL".
        entry_price: float > 0.
        rr_multiplier: AI-chosen multiplier; hard-clamped to
                       [RR_MULT_MIN, RR_MULT_MAX] when used (rr-fallback).
                       Required (>0) only on rr-fallback path; the
                       AI-suggested path ignores it.
        contracts: post-sizing contract count (> 0).
        sl_ticks: post-clamp SL distance in ticks (> 0; from sizing).
        tp_price_suggested: V17 AI-emitted absolute TP price. Default 0.0
                            (= AI suggestion absent, use rr-fallback). When
                            > 0 but NOT direction-coherent, also falls back
                            to rr-multiplier (silent — incoherence is just
                            "no usable suggestion").

    Returns:
        TPResolution with finalized tp_price, tp_source, full diagnostics.

    Raises:
        ValueError on entry_price<=0, sl_ticks<=0, contracts<=0,
                   unknown direction, OR rr_multiplier<=0 when the
                   AI-suggested path is not selected (rr-fallback then
                   has no usable input). KeyError on unknown symbol.
    """
    if entry_price <= 0:
        raise ValueError(f"{symbol}: entry_price must be > 0, got {entry_price}")
    if sl_ticks <= 0:
        raise ValueError(f"{symbol}: sl_ticks must be > 0, got {sl_ticks}")
    if contracts <= 0:
        raise ValueError(f"{symbol}: contracts must be > 0, got {contracts}")
    if direction not in ("BUY", "SELL"):
        raise ValueError(f"{symbol}: direction must be BUY|SELL, got {direction!r}")

    spec = cfg_fut.ASSETS_MAP[symbol]
    tick_size = float(spec["tick_size"])
    tick_value = float(spec["tick_value"])
    digits = int(spec["digits"])
    min_tp_ticks = int(cfg_fut.MIN_TP_TICKS.get(symbol, 1))

    sl_usd_actual = contracts * sl_ticks * tick_value

    # ── PATH SELECTION ────────────────────────────────────────────
    # AI-suggested wins iff value provided AND coherent with direction.
    # Incoherent suggestions (e.g. BUY tp <= entry) silently fall through
    # to rr-fallback rather than raising — incoherence isn't necessarily
    # an AI error; it's just "no usable suggestion."
    use_ai_path = tp_price_suggested > 0 and (
        (direction == "BUY"  and tp_price_suggested > entry_price) or
        (direction == "SELL" and tp_price_suggested < entry_price)
    )

    if use_ai_path:
        # Conservative margin: shrink raw distance toward entry by
        # TP_SUGGESTED_CONSERVATIVE_MARGIN, then snap to tick grid and
        # apply MIN_TP_TICKS floor (defensive lower bound, e.g. against
        # bid-ask-spread-sized targets).
        scale = 1.0 - TP_SUGGESTED_CONSERVATIVE_MARGIN
        if direction == "BUY":
            tp_raw_distance = (tp_price_suggested - entry_price) * scale
        else:
            tp_raw_distance = (entry_price - tp_price_suggested) * scale

        tp_ticks_raw = int(round(tp_raw_distance / tick_size))
        if tp_ticks_raw < min_tp_ticks:
            tp_ticks_after_floor = min_tp_ticks
            min_tp_ticks_applied = True
        else:
            tp_ticks_after_floor = tp_ticks_raw
            min_tp_ticks_applied = False

        # V18 12-mag — MAX_RR_EFFECTIVE cap. MIN_TP_TICKS resta hard floor:
        # se il cap forzerebbe TP sotto i costi round-trip, lasciamo MIN_TP.
        tp_ticks_cap = int(MAX_RR_EFFECTIVE * sl_ticks)
        if tp_ticks_after_floor > tp_ticks_cap:
            tp_ticks_final = max(tp_ticks_cap, min_tp_ticks)
            rr_capped = True
        else:
            tp_ticks_final = tp_ticks_after_floor
            rr_capped = False

        tp_distance = tp_ticks_final * tick_size
        if direction == "BUY":
            tp_price = entry_price + tp_distance
        else:
            tp_price = entry_price - tp_distance

        rr_effective = tp_ticks_final / sl_ticks if sl_ticks > 0 else 0.0
        # AI path: no rr_multiplier consumed; expose the de-facto rr
        # in both rr_effective and rr_multiplier_used so calibration
        # logs can read either field uniformly across paths.
        tp_usd_target = rr_effective * sl_usd_actual

        return TPResolution(
            tp_price=round(tp_price, digits),
            tp_ticks_final=tp_ticks_final,
            tp_distance=round(tp_distance, digits + 2),
            sl_usd_actual=round(sl_usd_actual, 2),
            tp_usd_target=round(tp_usd_target, 2),
            rr_effective=round(rr_effective, 4),
            rr_multiplier_used=round(rr_effective, 4),
            min_tp_ticks_applied=min_tp_ticks_applied,
            tp_source="ai_suggested",
            rr_capped=rr_capped,
        )

    # ── rr-fallback (V16 math, unchanged) ─────────────────────────
    if rr_multiplier <= 0:
        raise ValueError(f"{symbol}: rr_multiplier must be > 0, got {rr_multiplier}")

    # Hard clamp (defense-in-depth; brain post_val rejects on advisory range)
    rr_used = max(RR_MULT_MIN, min(rr_multiplier, RR_MULT_MAX))

    tp_usd_target = rr_used * sl_usd_actual
    tp_ticks_raw = int(round(rr_used * sl_ticks))

    if tp_ticks_raw < min_tp_ticks:
        tp_ticks_after_floor = min_tp_ticks
        min_tp_ticks_applied = True
    else:
        tp_ticks_after_floor = tp_ticks_raw
        min_tp_ticks_applied = False

    # V18 12-mag — MAX_RR_EFFECTIVE cap (parità col path ai_suggested).
    # In rr-fallback il cap è raramente engaged perché rr_used è già
    # hard-clampato a 0.80 < 2.0; può scattare solo se MIN_TP_TICKS
    # alza tp_ticks oltre 2.0 × sl_ticks (sl_ticks molto piccolo).
    tp_ticks_cap = int(MAX_RR_EFFECTIVE * sl_ticks)
    if tp_ticks_after_floor > tp_ticks_cap:
        tp_ticks_final = max(tp_ticks_cap, min_tp_ticks)
        rr_capped = True
    else:
        tp_ticks_final = tp_ticks_after_floor
        rr_capped = False

    tp_distance = tp_ticks_final * tick_size
    if direction == "BUY":
        tp_price = entry_price + tp_distance
    else:
        tp_price = entry_price - tp_distance

    rr_effective = tp_ticks_final / sl_ticks if sl_ticks > 0 else 0.0

    return TPResolution(
        tp_price=round(tp_price, digits),
        tp_ticks_final=tp_ticks_final,
        tp_distance=round(tp_distance, digits + 2),
        sl_usd_actual=round(sl_usd_actual, 2),
        tp_usd_target=round(tp_usd_target, 2),
        rr_effective=round(rr_effective, 4),
        rr_multiplier_used=round(rr_used, 4),
        min_tp_ticks_applied=min_tp_ticks_applied,
        tp_source="rr_fallback",
        rr_capped=rr_capped,
    )
