"""
APEX V16 — Position sizing and budget pre-check.

Three layers, low to high:

1. PRIMITIVES (internal — `budget_pre_check`, `compute_contracts`):
   pure ticks-based math, ported from V15. Kept intact since Day 1.

2. CONVERSION HELPERS (public — `points_to_ticks`, `points_to_dollars`):
   wrap ASSETS_MAP lookups so the rest of the codebase never deals
   with `tick_size`/`tick_value` directly. Fail-loud on unknown
   symbols (no silent default-tick-size bugs).

3. PUBLIC ENTRY POINT (`size_for_entry`):
   the orchestrator-facing API. Takes a brain-produced EntryDecision +
   account_balance + risk config, returns a SizingDecision (act-on-it
   payload) with a nested SizingAudit (look-at-it diagnostics).

V15 sizing formula (verified at APEX_PREDATOR_V15.py:775-826):
    risk_usd          = balance * risk_per_trade * risk_multiplier
    sl_usd_per_contract = sl_ticks * tick_value
    target_contracts  = risk_usd / sl_usd_per_contract
    contracts         = clamp(round(target), 1, MAX_CONTRACTS_PER_TRADE[sym])
    real_risk         = contracts * sl_usd_per_contract

`inverted_quote` (6J/6C) is intentionally NOT read by sizing math: V15
uses it only as an info log post-sizing (APEX_PREDATOR_V15.py:1665) for
direction-flip awareness. The dollar formula is invariant. V16 propagates
the flag in SizingAudit for audit, never reads it in math.

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

from __future__ import annotations

from dataclasses import asdict, dataclass
from typing import Optional

from core import config_futures as cfg_fut
from core.contracts import EntryDecision


# ============================================================
# RESULT TYPES
# ============================================================

@dataclass(frozen=True)
class BudgetCheckResult:
    """Result of the pre-Brain budget check."""
    skip: bool
    reason: str
    min_risk_usd: float
    typical_risk_usd: float
    max_allowed_usd: float


@dataclass(frozen=True)
class SizingResult:
    """Result of compute_contracts."""
    contracts: int                  # 0 if skip=True
    real_risk_usd: float            # 0.0 if skip=True
    target_float: float             # uncapped float target
    skip: bool
    reason: str
    sl_ticks_original: int = 0      # SL ticks as proposed (pre-floor)
    sl_ticks_floored: bool = False  # True if floor/cap altered sl_ticks


# ============================================================
# BUDGET PRE-CHECK
# ============================================================

def budget_pre_check(
    *,
    symbol: str,
    tick_value: float,
    min_sl_ticks: int,
    max_sl_ticks: int,
    daily_pnl: float,
    daily_loss_hard_stop: float,
    max_risk_vs_daily_budget: float,
) -> BudgetCheckResult:
    """
    Skip an asset BEFORE calling Brain if the daily budget can't
    accommodate even the smallest plausible trade.

    Logic:
      daily_remaining = abs(daily_loss_hard_stop - daily_pnl)
      max_allowed     = daily_remaining * max_risk_vs_daily_budget
      min_risk        = 1ct × min_sl_ticks × tick_value
      typical_risk    = 1ct × ((min+max)/2) × tick_value

      - If min_risk > max_allowed         -> skip HARD (impossible)
      - If typical_risk > max_allowed*1.2 -> skip SOFT (probable waste)

    Args:
        symbol: just for logging in reason string
        tick_value: $ per tick per contract (from config_futures)
        min_sl_ticks: smallest SL we'd ever accept for this asset
        max_sl_ticks: largest SL we'd ever accept for this asset
        daily_pnl: current daily P&L (negative for losses)
        daily_loss_hard_stop: e.g. -1500.0 (negative)
        max_risk_vs_daily_budget: fraction (0.33 = 33%)

    Raises:
        ValueError if tick_value <= 0 or min_sl_ticks <= 0 or max < min.
    """
    # Input guards (no silent fallbacks)
    if tick_value <= 0:
        raise ValueError(f"{symbol}: tick_value must be > 0, got {tick_value}")
    if min_sl_ticks <= 0:
        raise ValueError(f"{symbol}: min_sl_ticks must be > 0, got {min_sl_ticks}")
    if max_sl_ticks < min_sl_ticks:
        raise ValueError(
            f"{symbol}: max_sl_ticks ({max_sl_ticks}) < min_sl_ticks ({min_sl_ticks})"
        )

    typical_sl_ticks = (min_sl_ticks + max_sl_ticks) // 2

    min_risk_usd = 1 * min_sl_ticks * tick_value
    typical_risk_usd = 1 * typical_sl_ticks * tick_value

    daily_remaining = abs(daily_loss_hard_stop - daily_pnl)
    max_allowed_usd = daily_remaining * max_risk_vs_daily_budget

    # Hard skip: even smallest plausible trade exceeds budget
    if min_risk_usd > max_allowed_usd:
        return BudgetCheckResult(
            skip=True,
            reason=f"budget_hard (min ${min_risk_usd:.0f} > allowed ${max_allowed_usd:.0f})",
            min_risk_usd=round(min_risk_usd, 2),
            typical_risk_usd=round(typical_risk_usd, 2),
            max_allowed_usd=round(max_allowed_usd, 2),
        )

    # Soft skip: typical SL with 20% margin still exceeds budget
    # (Brain might still propose a tight SL, but probability low)
    if typical_risk_usd > max_allowed_usd * 1.2:
        return BudgetCheckResult(
            skip=True,
            reason=f"budget_soft (typical ${typical_risk_usd:.0f} > allowed ${max_allowed_usd:.0f} x 1.2)",
            min_risk_usd=round(min_risk_usd, 2),
            typical_risk_usd=round(typical_risk_usd, 2),
            max_allowed_usd=round(max_allowed_usd, 2),
        )

    return BudgetCheckResult(
        skip=False,
        reason="budget_ok",
        min_risk_usd=round(min_risk_usd, 2),
        typical_risk_usd=round(typical_risk_usd, 2),
        max_allowed_usd=round(max_allowed_usd, 2),
    )


# ============================================================
# CONTRACT SIZING
# ============================================================

# Worst-case sizing: per i micro indici e MGC il Brain può proporre SL
# molto stretti (es. MNQ 6 ticks). Senza floor il target_contracts
# diventa enorme e viene clampato dal cap di MAX_CONTRACTS_PER_TRADE,
# producendo posizioni sproporzionate rispetto al rischio reale.
# Per questi asset il sizing usa sempre MAX_SL_TICKS (caso peggiore)
# come distanza per la stima del rischio per contratto. L'SL effettivo
# inviato al broker resta quello proposto dal Brain — la modifica è
# solo nella matematica del sizing.
WORST_CASE_SIZING_ASSETS = {"MNQ", "MES", "MYM", "MGC"}


def compute_contracts(
    *,
    symbol: str,
    sl_ticks: int,
    account_balance: float,
    tick_value: float,
    risk_per_trade: float,
    max_contracts: int,
    min_contract_fraction: float,
    daily_pnl: float,
    daily_loss_hard_stop: float,
    max_risk_vs_daily_budget: float,
    risk_multiplier: float = 1.0,
) -> SizingResult:
    """
    Compute integer contracts to trade based on risk targeting.

    Algorithm:
      sl_usd_per_contract = sl_ticks * tick_value
      risk_usd            = account_balance * risk_per_trade * risk_multiplier
      target_float        = risk_usd / sl_usd_per_contract

      if target_float < min_contract_fraction -> skip (size too small)
      else contracts = clamp(round(target_float), 1, max_contracts)
           real_risk = contracts * sl_usd_per_contract
           if real_risk > daily_budget_cap -> skip (exceeds budget)

    Args:
        symbol: for logging
        sl_ticks: SL distance in ticks (Brain-proposed, must be > 0)
        account_balance: account equity in USD
        tick_value: $ per tick per contract
        risk_per_trade: e.g. 0.003 (0.30%)
        max_contracts: per-asset cap (from MAX_CONTRACTS_PER_TRADE)
        min_contract_fraction: e.g. 0.70 (skip if target_float < this)
        daily_pnl: current daily P&L
        daily_loss_hard_stop: e.g. -1500.0
        max_risk_vs_daily_budget: e.g. 0.33
        risk_multiplier: confidence-based scaling (1.0 default; can be
                         e.g. 0.5 for low confidence, 1.5 for high)
    """
    # Input guards
    if tick_value <= 0:
        raise ValueError(f"{symbol}: tick_value must be > 0, got {tick_value}")
    if sl_ticks <= 0:
        raise ValueError(f"{symbol}: sl_ticks must be > 0, got {sl_ticks}")
    if account_balance <= 0:
        raise ValueError(f"{symbol}: account_balance must be > 0, got {account_balance}")
    if max_contracts < 1:
        raise ValueError(f"{symbol}: max_contracts must be >= 1, got {max_contracts}")
    if risk_multiplier <= 0:
        raise ValueError(f"{symbol}: risk_multiplier must be > 0, got {risk_multiplier}")

    sl_ticks_original = sl_ticks
    if symbol in WORST_CASE_SIZING_ASSETS:
        sl_ticks = cfg_fut.MAX_SL_TICKS.get(symbol, sl_ticks)
    else:
        sl_ticks = max(sl_ticks, cfg_fut.MIN_SL_TICKS.get(symbol, sl_ticks))
    sl_ticks_floored = sl_ticks != sl_ticks_original

    sl_usd_per_contract = sl_ticks * tick_value
    risk_usd = account_balance * risk_per_trade * risk_multiplier
    target_float = risk_usd / sl_usd_per_contract

    # Skip if target sizing is below the minimum fraction (would round to 0
    # or to 1 with too-small risk allocation)
    if target_float < min_contract_fraction:
        return SizingResult(
            contracts=0,
            real_risk_usd=0.0,
            target_float=round(target_float, 2),
            skip=True,
            reason=f"size_too_small ({target_float:.2f} < {min_contract_fraction})",
            sl_ticks_original=sl_ticks_original,
            sl_ticks_floored=sl_ticks_floored,
        )

    contracts = max(1, round(target_float))
    contracts = min(contracts, max_contracts)
    # V18 12-mag — MIN_CONTRACTS_PER_TRADE floor per asset.
    # Applicato DOPO il clamp a max_contracts: per gli FX il sizing
    # naturale può collassare a 1 ct, ma 1 ct disabilita la partial
    # exit (N/2 → demote a full EXIT in _handle_partial). Forzando un
    # minimo si conserva la possibilità di scaricare metà posizione
    # quando i Brain emettono PARTIAL_50.
    min_contracts = int(cfg_fut.MIN_CONTRACTS_PER_TRADE.get(symbol, 1))
    if min_contracts > 1:
        contracts = max(contracts, min(min_contracts, max_contracts))
    real_risk_usd = contracts * sl_usd_per_contract

    # Final daily budget guard
    daily_remaining = abs(daily_loss_hard_stop - daily_pnl)
    max_allowed_usd = daily_remaining * max_risk_vs_daily_budget

    if real_risk_usd > max_allowed_usd:
        return SizingResult(
            contracts=0,
            real_risk_usd=round(real_risk_usd, 2),
            target_float=round(target_float, 2),
            skip=True,
            reason=f"risk_exceeds_daily_budget (${real_risk_usd:.0f} > ${max_allowed_usd:.0f})",
            sl_ticks_original=sl_ticks_original,
            sl_ticks_floored=sl_ticks_floored,
        )

    return SizingResult(
        contracts=contracts,
        real_risk_usd=round(real_risk_usd, 2),
        target_float=round(target_float, 2),
        skip=False,
        reason="OK",
        sl_ticks_original=sl_ticks_original,
        sl_ticks_floored=sl_ticks_floored,
    )


# ============================================================
# CONVERSION HELPERS (public)
#
# These are the only functions outside this module that should touch
# tick_size / tick_value directly. Brain code, orchestrator code, and
# tests should call points_to_ticks / points_to_dollars and never
# do `distance / tick_size` inline.
# ============================================================

def _spec(symbol: str) -> dict:
    """Lookup ASSETS_MAP[symbol] or fail loud (no silent default tick math)."""
    spec = cfg_fut.ASSETS_MAP.get(symbol)
    if spec is None:
        raise ValueError(
            f"unknown symbol {symbol!r}: not in config_futures.ASSETS_MAP"
        )
    return spec


def points_to_ticks(symbol: str, distance_points: float) -> int:
    """
    Convert a price-points distance into ticks for `symbol`.

    Raises:
        ValueError on unknown symbol or non-positive distance.
    """
    if distance_points <= 0:
        raise ValueError(
            f"{symbol}: distance_points must be > 0, got {distance_points}"
        )
    spec = _spec(symbol)
    return int(round(distance_points / spec["tick_size"]))


def points_to_dollars(symbol: str, distance_points: float) -> float:
    """
    Convert a price-points distance into USD risk per contract.

    Equivalent to: ticks(symbol, distance) * tick_value(symbol).
    """
    spec = _spec(symbol)
    sl_ticks = points_to_ticks(symbol, distance_points)
    return float(sl_ticks * spec["tick_value"])


# ============================================================
# SizingDecision + SizingAudit (split: act-on-it vs look-at-it)
# ============================================================

@dataclass(frozen=True)
class SizingAudit:
    """
    Diagnostics for sizing_log / brain_log JSONL streams.

    Never read by orchestrator decision logic — only logged. Lets the
    calibration round (5-9 mag) histogram clamp_active rate, real vs
    target risk, etc. across days.
    """
    symbol: str
    risk_multiplier: float
    target_float: float
    sl_distance_points: float
    tick_size: float
    tick_value: float
    sl_usd_per_contract: float
    risk_usd_target: float
    daily_budget_cap_usd: float
    max_contracts_used: int
    clamp_active: bool
    inverted_quote: bool
    sl_ticks_floored: bool = False
    sl_ticks_original: int = 0


@dataclass(frozen=True)
class SizingDecision:
    """
    Core sizing output the orchestrator acts on. Audit nested for logging.

    Orchestrator reads contracts/skip/reason/sl_ticks/real_risk_usd to
    place the order. `audit` is forwarded to the JSONL logger.
    """
    contracts: int
    skip: bool
    reason: str
    sl_ticks: int
    real_risk_usd: float
    audit: SizingAudit

    def audit_dict(self) -> dict:
        """Convenience for JSONL emission: flat dict of audit fields."""
        return asdict(self.audit)


# ============================================================
# PUBLIC ENTRY POINT
# ============================================================

def size_for_entry(
    *,
    entry: EntryDecision,
    symbol: str,
    account_balance: float,
    risk_per_trade: float,
    daily_pnl: float,
    daily_loss_hard_stop: float,
    max_risk_vs_daily_budget: float,
    min_contract_fraction: float = cfg_fut.MIN_CONTRACT_FRACTION,
) -> SizingDecision:
    """
    Convert a brain-produced EntryDecision into an executable contract count.

    Flow:
      1. sl_distance_points = abs(entry.entry_price - entry.sl_price).
      2. Lookup ASSETS_MAP[symbol] (raise on missing).
      3. Convert points -> ticks -> $/contract via the helpers.
      4. Read risk_multiplier from entry.metadata (Brain may set 0.5..1.2).
      5. Read MAX_CONTRACTS_PER_TRADE[symbol] (default 1).
      6. Delegate the integer-clamp math to compute_contracts (existing primitive).
      7. Wrap result + diagnostics in SizingDecision/SizingAudit.

    Raises:
        ValueError on unknown symbol, sl_distance == 0, or invalid inputs
        bubbled from compute_contracts.
    """
    spec = _spec(symbol)
    tick_size = float(spec["tick_size"])
    tick_value = float(spec["tick_value"])
    inverted_quote = bool(spec.get("inverted_quote", False))

    sl_distance_points = abs(entry.entry_price - entry.sl_price)
    if sl_distance_points <= 0:
        raise ValueError(
            f"{symbol}: sl_distance_points must be > 0 "
            f"(entry={entry.entry_price}, sl={entry.sl_price})"
        )

    sl_ticks = points_to_ticks(symbol, sl_distance_points)

    risk_multiplier = float(entry.metadata.get("risk_multiplier", 1.0))
    risk_usd_target = account_balance * risk_per_trade * risk_multiplier

    max_contracts = int(
        cfg_fut.MAX_CONTRACTS_PER_TRADE.get(
            symbol, cfg_fut.MAX_CONTRACTS_PER_TRADE.get("default", 1)
        )
    )

    daily_remaining = abs(daily_loss_hard_stop - daily_pnl)
    daily_budget_cap_usd = daily_remaining * max_risk_vs_daily_budget

    primitive = compute_contracts(
        symbol=symbol,
        sl_ticks=sl_ticks,
        account_balance=account_balance,
        tick_value=tick_value,
        risk_per_trade=risk_per_trade,
        max_contracts=max_contracts,
        min_contract_fraction=min_contract_fraction,
        daily_pnl=daily_pnl,
        daily_loss_hard_stop=daily_loss_hard_stop,
        max_risk_vs_daily_budget=max_risk_vs_daily_budget,
        risk_multiplier=risk_multiplier,
    )

    # Clamp diagnostics: did the round(target_float) hit max_contracts?
    rounded_target = max(1, round(primitive.target_float))
    clamp_active = (rounded_target > max_contracts) and (
        primitive.contracts == max_contracts
    )

    # sl_usd_per_contract reflects the value used by the sizing math
    # (post-floor), so target_float, risk_usd_target and real_risk_usd
    # stay internally consistent in the audit. The broker-side SL
    # remains the Brain-proposed sl_ticks below.
    if symbol in WORST_CASE_SIZING_ASSETS:
        sl_ticks_for_sizing = cfg_fut.MAX_SL_TICKS.get(symbol, sl_ticks)
    else:
        sl_ticks_for_sizing = max(sl_ticks, cfg_fut.MIN_SL_TICKS.get(symbol, sl_ticks))
    sl_usd_per_contract = sl_ticks_for_sizing * tick_value

    audit = SizingAudit(
        symbol=symbol,
        risk_multiplier=risk_multiplier,
        target_float=primitive.target_float,
        sl_distance_points=round(sl_distance_points, 6),
        tick_size=tick_size,
        tick_value=tick_value,
        sl_usd_per_contract=round(sl_usd_per_contract, 2),
        risk_usd_target=round(risk_usd_target, 2),
        daily_budget_cap_usd=round(daily_budget_cap_usd, 2),
        max_contracts_used=max_contracts,
        clamp_active=clamp_active,
        inverted_quote=inverted_quote,
        sl_ticks_floored=primitive.sl_ticks_floored,
        sl_ticks_original=primitive.sl_ticks_original,
    )

    return SizingDecision(
        contracts=primitive.contracts,
        skip=primitive.skip,
        reason=primitive.reason,
        sl_ticks=sl_ticks,
        real_risk_usd=primitive.real_risk_usd,
        audit=audit,
    )
