"""
APEX V16 — P&L calculator for futures.

Pure functions. No I/O, no broker calls, no AI.
Inputs are explicit; outputs are typed.

Migrated from V15's _compute_paper_pnl() with the following
intentional differences:

  - V16 raises ValueError if asset specs are missing.
    V15 silently fell back to MES tick specs (0.25 / 1.25),
    which produced wrong P&L for any asset whose specs failed
    to load. Better to crash loud than to compute silently wrong.

  - V16 inputs are typed dataclass-friendly (entry_price, sl_price,
    tp_price as floats; direction as "BUY"/"SELL" string).

Inverted quote handling (6J, 6C, etc.) is NOT done here. It is
handled upstream in sizing / direction translation. The tick_value
from config_futures.py already encodes the dollar value of one tick
in the trader's account currency (USD), regardless of quote
direction. So this module is symmetric BUY/SELL.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Optional


@dataclass(frozen=True)
class PnLResult:
    """Result of a P&L calculation. All values rounded for display."""
    net_profit_usd: float          # sub-tick precision (display)
    net_profit_usd_int: float      # tick-aligned (accounting/close)
    profit_ticks: int              # integer ticks, positive = profit
    profit_ticks_float: float      # fractional ticks
    progress_pct: float            # 0-100% toward TP (can be negative)
    hit_sl: bool
    hit_tp: bool


def compute_pnl(
    *,
    entry_price: float,
    sl_price: float,
    tp_price: float,
    current_price: float,
    direction: str,
    contracts: int,
    tick_size: float,
    tick_value: float,
) -> PnLResult:
    """
    Compute P&L for an open futures trade.

    Args:
        entry_price: trade entry
        sl_price:    stop loss
        tp_price:    take profit
        current_price: current market price
        direction:   "BUY" or "SELL" (case-insensitive)
        contracts:   contract count (positive integer)
        tick_size:   minimum price increment for the asset
                     (e.g., 0.25 for MES, 0.0001 for 6E)
        tick_value:  dollar value of one tick for one contract
                     (e.g., 1.25 for MES, 6.25 for 6E)

    Returns:
        PnLResult.

    Raises:
        ValueError: if tick_size <= 0, contracts <= 0, or direction
                    not in BUY/SELL.
    """
    # --- Input validation: refuse silent degradation ---
    if tick_size <= 0:
        raise ValueError(f"tick_size must be > 0, got {tick_size}")
    if tick_value <= 0:
        raise ValueError(f"tick_value must be > 0, got {tick_value}")
    if contracts <= 0:
        raise ValueError(f"contracts must be > 0, got {contracts}")

    direction = direction.upper().strip()
    if direction not in ("BUY", "SELL"):
        raise ValueError(f"direction must be BUY or SELL, got {direction!r}")

    # --- Direction-aware price math ---
    if direction == "BUY":
        price_diff = current_price - entry_price
        hit_sl = current_price <= sl_price
        hit_tp = current_price >= tp_price
        tp_diff = tp_price - entry_price
    else:  # SELL
        price_diff = entry_price - current_price
        hit_sl = current_price >= sl_price
        hit_tp = current_price <= tp_price
        tp_diff = entry_price - tp_price

    # --- Sub-tick precision (display) ---
    profit_ticks_float = price_diff / tick_size
    net_profit_usd = profit_ticks_float * tick_value * contracts

    # --- Tick-aligned precision (accounting/close) ---
    profit_ticks = int(round(profit_ticks_float))
    net_profit_usd_int = profit_ticks * tick_value * contracts

    # --- Progress toward TP (0-100% nominally; can exceed if past TP, can go negative) ---
    progress_pct = 0.0
    if tp_diff and abs(tp_diff) > 0:
        progress_pct = (price_diff / tp_diff) * 100.0

    return PnLResult(
        net_profit_usd=round(net_profit_usd, 2),
        net_profit_usd_int=round(net_profit_usd_int, 2),
        profit_ticks=profit_ticks,
        profit_ticks_float=round(profit_ticks_float, 2),
        progress_pct=round(progress_pct, 1),
        hit_sl=hit_sl,
        hit_tp=hit_tp,
    )


# ============================================================
# CONVENIENCE: extract specs from config_futures
# ============================================================

def get_specs_or_raise(symbol: str, asset_specs: dict) -> tuple[float, float]:
    """
    Look up tick_size and tick_value for symbol from asset_specs dict
    (typically core.config_futures.ASSETS_MAP).

    Raises:
        KeyError if symbol not in asset_specs
        KeyError if tick_size or tick_value missing for that symbol
    """
    if symbol not in asset_specs:
        raise KeyError(
            f"No specs for symbol {symbol!r}. "
            f"Add it to config_futures.ASSETS_MAP or check the symbol name."
        )
    spec = asset_specs[symbol]
    if "tick_size" not in spec:
        raise KeyError(f"tick_size missing in specs for {symbol!r}")
    if "tick_value" not in spec:
        raise KeyError(f"tick_value missing in specs for {symbol!r}")
    return float(spec["tick_size"]), float(spec["tick_value"])
