/**
 * What actually closes a trade.
 *
 * The setup's `target` is NOT an exit under the production policy: it is a
 * screening artifact — the headline Gate Target Ladder proposal, used to
 * compute the R:R and
 * probability that admit the setup through the activation gate. The live exit
 * (`paper_trade_service.resolve_open_trades`) never reads it; `atr_trailing`
 * closes on the initial stop, a trailing stop, or the max hold.
 *
 * This module derives the real plan so the UI can show it instead of implying
 * a take-profit that will never fire. See docs/research/sr-levels-and-exits.md.
 */
import type { ExitPolicy, TradeSetup } from './types';

/**
 * Stop width used when the scanner builds a setup: stop = entry ∓ 1.5 × ATR
 * (`rr_scanner_service.scan_symbol`, and `backtest_service.ATR_MULTIPLIER`).
 * Lets us recover ATR from a setup without another round trip:
 *     ATR = |entry − stop| / 1.5
 * Guarded by test_prod_strategy_parity.py so a backend change can't silently
 * desync this.
 */
export const SETUP_STOP_ATR_MULTIPLIER = 1.5;

export interface ExitPlan {
  mode: ExitPolicy['mode'];
  /** Does the setup's target actually close the trade? Only when mode === 'target'. */
  honorsTarget: boolean;
  /** Distance from entry to the initial stop, i.e. 1R per share. */
  riskPerShare: number;
  initialStop: number;
  /** Trailing-stop width in price, once the trail is active (atr_trailing only). */
  trailWidth: number | null;
  /**
   * Price the trade must reach before the trailing stop rises above the initial
   * stop and takes over. Below this, the initial stop is what's protecting you.
   */
  trailTakesOverAt: number | null;
  /** Trail width expressed in R — the intuitive "how much give-back". */
  trailWidthR: number | null;
  maxHoldDays: number;
  headline: string;
}

/**
 * Derive the real exit plan for a setup under the live policy.
 * Returns null when the setup has no usable risk distance.
 */
export function deriveExitPlan(setup: TradeSetup, policy?: ExitPolicy): ExitPlan | null {
  const isLong = setup.direction === 'long';
  const riskPerShare = Math.abs(setup.entry_price - setup.stop_loss);
  if (!(riskPerShare > 0)) return null;

  // Fall back to the shipped defaults when the policy hasn't loaded yet, so the
  // card never renders a blank or (worse) a target-based plan.
  const mode = policy?.mode ?? 'atr_trailing';
  const maxHoldDays = policy?.hold_days ?? 30;
  const atrMultiplier = policy?.atr_multiplier ?? 3;

  const atr = riskPerShare / SETUP_STOP_ATR_MULTIPLIER;

  if (mode === 'atr_trailing') {
    const trailWidth = atrMultiplier * atr;
    // The trail only bites once it clears the initial stop:
    //   highestClose − trailWidth > stop  ⇔  highestClose > entry ± (trailWidth − R)
    const takeoverOffset = trailWidth - riskPerShare;
    const trailTakesOverAt = isLong
      ? setup.entry_price + takeoverOffset
      : setup.entry_price - takeoverOffset;
    return {
      mode,
      honorsTarget: false,
      riskPerShare,
      initialStop: setup.stop_loss,
      trailWidth,
      trailTakesOverAt,
      trailWidthR: trailWidth / riskPerShare,
      maxHoldDays,
      headline: `${atrMultiplier}× ATR trailing stop · max ${maxHoldDays} trading days`,
    };
  }

  if (mode === 'trailing') {
    const trailWidth = (setup.entry_price * (policy?.trailing_pct ?? 12)) / 100;
    return {
      mode,
      honorsTarget: false,
      riskPerShare,
      initialStop: setup.stop_loss,
      trailWidth,
      trailTakesOverAt: null,
      trailWidthR: trailWidth / riskPerShare,
      maxHoldDays,
      headline: `${policy?.trailing_pct ?? 12}% trailing stop · max ${maxHoldDays} trading days`,
    };
  }

  if (mode === 'target') {
    return {
      mode,
      honorsTarget: true,
      riskPerShare,
      initialStop: setup.stop_loss,
      trailWidth: null,
      trailTakesOverAt: null,
      trailWidthR: null,
      maxHoldDays,
      headline: 'Take profit at the selected level, or exit at the stop',
    };
  }

  // 'time'
  return {
    mode,
    honorsTarget: false,
    riskPerShare,
    initialStop: setup.stop_loss,
    trailWidth: null,
    trailTakesOverAt: null,
    trailWidthR: null,
    maxHoldDays,
    headline: `Hold to the stop or ${maxHoldDays} trading days — no target, no trail`,
  };
}

/** How far price has run from the scan entry, in R. Sign is direction-aware. */
export function driftInR(setup: TradeSetup, currentPrice: number): number | null {
  const risk = Math.abs(setup.entry_price - setup.stop_loss);
  if (!(risk > 0)) return null;
  const moved = setup.direction === 'long'
    ? currentPrice - setup.entry_price
    : setup.entry_price - currentPrice;
  return moved / risk;
}
