"""Exit engine — position-level exit management.

Evaluates stop-loss hits, take-profit targets, and trailing ATR-based stops
for open positions.  Called once per evaluation tick *before* the signal
pipelines run so that exit signals take priority over new entry signals.

Priority order (first match wins per position):
    1. stop_loss hit   → EXIT_FULL, reason ``"stop_hit"``
    2. target_2 hit    → EXIT_FULL, reason ``"target_2_hit"``
    3. trailing stop   → EXIT_FULL, reason ``"trailing_stop_hit"``
    4. target_1 hit    → EXIT_HALF, reason ``"target_1_hit"``

Requirements: 8.1, 8.2, 8.3, 8.4, 8.5, 8.6, 8.7
"""

from __future__ import annotations

import logging

from services.signal_engine.config import ExitConfig
from services.signal_engine.models import (
    ExitSignal,
    ExitType,
    OpenPositionState,
)

logger = logging.getLogger(__name__)


def evaluate_exits(
    positions: list[OpenPositionState],
    current_prices: dict[str, float],
    config: ExitConfig,
) -> list[ExitSignal]:
    """Evaluate exit conditions for all open positions.

    For each position the current price is looked up in *current_prices*
    (keyed by ticker).  If the ticker is absent the position's own
    ``current_price`` field is used as a fallback.

    Checks are applied in priority order — only the **first** matching
    condition per position emits an ``ExitSignal``.

    Parameters
    ----------
    positions:
        Snapshots of open positions to evaluate.
    current_prices:
        Latest prices keyed by ticker symbol.
    config:
        Exit engine configuration (trailing stop ATR multiplier, etc.).

    Returns
    -------
    list[ExitSignal]
        One signal per position that triggered an exit condition.
        Positions with no exit condition produce no signal.
    """
    signals: list[ExitSignal] = []

    for pos in positions:
        price = current_prices.get(pos.ticker, pos.current_price)

        signal = _evaluate_single_position(pos, price, config)
        if signal is not None:
            signals.append(signal)

    return signals


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------


def _evaluate_single_position(
    pos: OpenPositionState,
    price: float,
    config: ExitConfig,
) -> ExitSignal | None:
    """Check exit conditions for a single position in priority order.

    Priority: stop_loss > target_2 > trailing_stop > target_1.
    """

    # 1. Stop-loss hit (highest priority)
    if price <= pos.stop_loss:
        return ExitSignal(
            position_id=pos.position_id,
            ticker=pos.ticker,
            exit_type=ExitType.EXIT_FULL,
            reason="stop_hit",
            price=price,
        )

    # 2. Target 2 hit → full exit
    if price >= pos.target_2:
        return ExitSignal(
            position_id=pos.position_id,
            ticker=pos.ticker,
            exit_type=ExitType.EXIT_FULL,
            reason="target_2_hit",
            price=price,
        )

    # 3. Trailing stop (only active after partial exit)
    if pos.partial_exit_done:
        trailing_stop = _compute_trailing_stop(pos, price, config)
        if price <= trailing_stop:
            return ExitSignal(
                position_id=pos.position_id,
                ticker=pos.ticker,
                exit_type=ExitType.EXIT_FULL,
                reason="trailing_stop_hit",
                price=price,
            )

    # 4. Target 1 hit → partial exit (only if not already done)
    if not pos.partial_exit_done and price >= pos.target_1:
        return ExitSignal(
            position_id=pos.position_id,
            ticker=pos.ticker,
            exit_type=ExitType.EXIT_HALF,
            reason="target_1_hit",
            price=price,
        )

    return None


def _compute_trailing_stop(
    pos: OpenPositionState,
    price: float,
    config: ExitConfig,
) -> float:
    """Compute the effective trailing stop level.

    The trailing stop is ``price - ATR * multiplier``, but it only
    ratchets **upward** — if the position already has a higher trailing
    stop recorded, that value is kept.

    When ATR is unavailable (``None``), the existing ``trailing_stop``
    on the position is returned as-is.  If neither is set, returns 0.0
    (effectively no trailing stop).
    """
    existing = pos.trailing_stop if pos.trailing_stop is not None else 0.0

    if pos.atr is None:
        return existing

    new_level = price - pos.atr * config.trailing_stop_atr_multiplier

    # Ratchet upward only
    return max(existing, new_level)
