from config import settings
from models.discipline import DisciplineOutcome, TradeCheckRequest
from models.signal import Signal, SignalSide
from services.discipline_log import discipline_log
from services.runtime_rules import runtime_rules
from services.trade_zones import price_in_trade_zones, trade_zone_status
from services.trading_stats import (
    auto_release_discipline_if_needed,
    daily_loss_pct,
    is_within_session,
    trade_interval_blocked,
    trades_opened_in_session,
)


class RiskDecision:
    def __init__(self, allowed: bool, reason: str | None = None):
        self.allowed = allowed
        self.reason = reason


def _estimate_trade_risk_usd(
    *,
    quantity: float | None,
    price: float | None,
    stop_loss: float | None,
    explicit_risk_usd: float | None,
    point_value: float | None = None,
) -> float | None:
    if explicit_risk_usd is not None:
        return explicit_risk_usd
    if quantity is None or quantity <= 0:
        return None
    if price is not None and stop_loss is not None:
        points = abs(price - stop_loss)
        multiplier = point_value if point_value is not None and point_value > 0 else 1.0
        return points * multiplier * quantity
    return None


def apply_quantity_defaults(signal: Signal) -> None:
    alert = signal.alert
    rules = runtime_rules.state
    if alert.action in (SignalSide.CLOSE, SignalSide.FLAT):
        return

    if alert.quantity is None:
        alert.quantity = settings.default_quantity

    if alert.quantity > rules.max_quantity:
        alert.quantity = rules.max_quantity


def evaluate_trade_intent(
    *,
    action: str,
    source: str = "unknown",
    ticker: str | None = None,
    strategy: str | None = None,
    risk_usd: float | None = None,
    quantity: float | None = None,
    price: float | None = None,
    stop_loss: float | None = None,
    point_value: float | None = None,
    log: bool = True,
) -> RiskDecision:
    auto_release_discipline_if_needed()
    rules = runtime_rules.state
    normalized = action.strip().lower()
    estimated_risk = _estimate_trade_risk_usd(
        quantity=quantity,
        price=price,
        stop_loss=stop_loss,
        explicit_risk_usd=risk_usd,
        point_value=point_value,
    )

    zone_block_reason: str | None = None
    if rules.trade_zones_enabled and normalized not in ("close", "exit", "flat"):
        zone_price = rules.live_price
        if zone_price is None and price is not None and price > 0:
            zone_price = price
        zone_ok, zone_reason = trade_zone_status(
            enabled=True,
            zones=rules.trade_zones,
            price=zone_price,
            ticker=ticker,
            action=normalized,
        )
        if not zone_ok:
            zone_block_reason = zone_reason

    if zone_block_reason:
        decision = RiskDecision(False, zone_block_reason)
    elif not rules.discipline_engaged:
        decision = RiskDecision(True, "Discipline not engaged (monitoring only)")
    elif normalized in ("close", "exit", "flat"):
        decision = RiskDecision(True)
    elif not settings.trading_enabled:
        decision = RiskDecision(False, "Trading disabled (kill switch)")
    elif rules.session_enabled and not is_within_session():
        # Format from the same live rules object used for the decision — never a stale default.
        session_start = rules.session_start
        session_end = rules.session_end
        session_tz = rules.session_timezone
        decision = RiskDecision(
            False,
            f"Outside session ({session_start}-{session_end} {session_tz})",
        )
    elif (
        rules.max_trades_enabled
        and trades_opened_in_session() >= rules.max_trades_per_session
    ):
        decision = RiskDecision(
            False,
            f"Max trades per session reached ({rules.max_trades_per_session})",
        )
    else:
        blocked_interval, interval_reason = trade_interval_blocked()
        if blocked_interval:
            decision = RiskDecision(False, interval_reason)
        elif (
            rules.max_lot_enabled
            and quantity is not None
            and quantity > rules.max_quantity
        ):
            decision = RiskDecision(
                False,
                f"Quantity {quantity} exceeds max lot {rules.max_quantity}",
            )
        else:
            if (
                rules.max_risk_per_trade_enabled
                and estimated_risk is not None
                and estimated_risk > rules.max_risk_usd
            ):
                decision = RiskDecision(
                    False,
                    f"Risk per trade ${estimated_risk:.2f} exceeds max ${rules.max_risk_usd:.2f}",
                )
            else:
                loss_pct = daily_loss_pct()
                if (
                    rules.daily_loss_limit_enabled
                    and loss_pct >= rules.max_daily_loss_pct
                ):
                    decision = RiskDecision(
                        False,
                        f"Daily loss limit reached ({loss_pct:.1f}% / {rules.max_daily_loss_pct}%)",
                    )
                else:
                    decision = RiskDecision(True)

    if log:
        discipline_log.record(
            DisciplineOutcome.ALLOWED if decision.allowed else DisciplineOutcome.BLOCKED,
            reason=decision.reason,
            source=source,
            ticker=ticker,
            action=action,
            pine_strategy=strategy or rules.pine_strategy,
        )

    return decision


def check_trade_request(body: TradeCheckRequest) -> RiskDecision:
    return evaluate_trade_intent(
        action=body.action,
        source=body.source,
        ticker=body.ticker,
        strategy=body.strategy,
        risk_usd=body.risk_usd,
        quantity=body.quantity,
        price=body.price,
        stop_loss=body.stop_loss,
        point_value=body.point_value,
    )


def validate_signal(signal: Signal, open_positions: int) -> RiskDecision:
    alert = signal.alert
    rules = runtime_rules.state

    if alert.action in (SignalSide.CLOSE, SignalSide.FLAT):
        return RiskDecision(True)

    base = evaluate_trade_intent(
        action=alert.action.value,
        source="pine_signal",
        ticker=alert.ticker,
        strategy=alert.strategy,
        quantity=alert.quantity,
        price=alert.price,
        stop_loss=alert.stop_loss,
        log=True,
    )
    if not rules.discipline_engaged or not base.allowed:
        return base

    if open_positions >= rules.max_open_positions:
        decision = RiskDecision(
            False,
            f"Max open positions reached ({rules.max_open_positions})",
        )
        discipline_log.record(
            DisciplineOutcome.BLOCKED,
            reason=decision.reason,
            source="pine_signal",
            ticker=alert.ticker,
            action=alert.action.value,
            pine_strategy=alert.strategy or rules.pine_strategy,
        )
        return decision

    if alert.quantity is not None and alert.quantity <= 0:
        return RiskDecision(False, "Quantity must be positive")

    if (
        rules.max_lot_enabled
        and alert.quantity is not None
        and alert.quantity > rules.max_quantity
    ):
        return RiskDecision(
            False,
            f"Quantity {alert.quantity} exceeds max {rules.max_quantity}",
        )

    if alert.price is not None and alert.stop_loss is not None:
        risk_per_unit = abs(alert.price - alert.stop_loss)
        qty = alert.quantity or settings.default_quantity
        estimated_risk = risk_per_unit * qty
        if (
            rules.max_risk_per_trade_enabled
            and estimated_risk > rules.max_risk_usd
        ):
            return RiskDecision(
                False,
                f"Estimated risk ${estimated_risk:.2f} exceeds max ${rules.max_risk_usd:.2f}",
            )

    return RiskDecision(True)
