import type { ActivationConfig, TradeSetup, TradeTarget } from './types';

const HIGH_CONVICTION_ACTIONS = new Set(['LONG_HIGH', 'SHORT_HIGH']);

/**
 * Floor for the primary target's reach probability — mirrors
 * MIN_TARGET_PROBABILITY in app/services/qualification.py. A primary below
 * this is a lottery target whose distance inflates R:R past the min_rr gate.
 */
export const MIN_TARGET_PROBABILITY = 20;

function actionDirection(action: TradeSetup['recommended_action']): 'long' | 'short' | 'neutral' {
  if (!action || action === 'NEUTRAL') return 'neutral';
  if (action.startsWith('LONG')) return 'long';
  if (action.startsWith('SHORT')) return 'short';
  return 'neutral';
}

/** The starred primary target (the one the headline R:R refers to), falling
 * back to the most likely target when no star is stored. */
export function primaryTarget(setup: TradeSetup): TradeTarget | null {
  const starred = setup.targets?.find((t) => t.is_primary);
  if (starred) return starred;
  if (!setup.targets?.length) return null;
  return [...setup.targets].sort((a, b) => b.probability - a.probability)[0];
}

/** Probability of the starred primary target (the one the headline R:R refers to). */
export function primaryTargetProbability(setup: TradeSetup): number | null {
  return primaryTarget(setup)?.probability ?? null;
}

/** R:R recomputed from the current price (0 if no reward/risk left). */
export function liveRiskReward(setup: TradeSetup, currentPrice: number): number {
  const reward = setup.direction === 'long' ? setup.target - currentPrice : currentPrice - setup.target;
  const risk = setup.direction === 'long' ? currentPrice - setup.stop_loss : setup.stop_loss - currentPrice;
  if (reward <= 0 || risk <= 0) return 0;
  return reward / risk;
}

/**
 * Whether a setup clears the activation gate. Mirrors the backend predicate in
 * app/services/qualification.py — keep the two in sync.
 */
export function qualifiesSetup(setup: TradeSetup, config: ActivationConfig): boolean {
  if (setup.rr_ratio < config.min_rr) return false;
  // Live R:R from current price — drops setups whose price has already run
  // toward target (reward consumed) or through the stop.
  if (setup.current_price != null && liveRiskReward(setup, setup.current_price) < config.min_rr) {
    return false;
  }
  const targetProbability = primaryTargetProbability(setup);
  if (targetProbability == null || targetProbability < MIN_TARGET_PROBABILITY) return false;
  if ((setup.confidence_score ?? 0) < config.min_confidence) return false;
  // Residual cross-sectional momentum is the core selection (long-only). While
  // the gate is active, shorts never qualify; missing ranks do not qualify
  // because the production edge depends on this cross-sectional selection.
  if (config.min_momentum_percentile > 0) {
    if (setup.direction === 'short') return false;
    if (setup.momentum_percentile == null || setup.momentum_percentile < config.min_momentum_percentile) {
      return false;
    }
  }
  // NEUTRAL = "no clear setup"; an opposite action means this setup is counter-bias.
  if (config.exclude_neutral) {
    const actionDir = actionDirection(setup.recommended_action);
    if (actionDir === 'neutral' || actionDir !== setup.direction) return false;
  }
  if (config.require_high_conviction && !HIGH_CONVICTION_ACTIONS.has(setup.recommended_action ?? '')) {
    return false;
  }
  if (config.exclude_conflicts && (setup.risk_level ?? '') !== 'Low') return false;
  return true;
}

/**
 * Why a setup does NOT clear the gate — the first failing rule, phrased for the
 * dashboard's radar list. Returns null when the setup qualifies. Mirrors
 * qualifiesSetup rule-for-rule (keep the order in sync).
 */
export function disqualifyReason(setup: TradeSetup, config: ActivationConfig): string | null {
  if (setup.rr_ratio < config.min_rr) {
    return `R:R ${setup.rr_ratio.toFixed(1)} below gate ${config.min_rr.toFixed(1)}`;
  }
  if (setup.current_price != null && liveRiskReward(setup, setup.current_price) < config.min_rr) {
    return 'price has run — live R:R below gate';
  }
  const targetProbability = primaryTargetProbability(setup);
  if (targetProbability == null || targetProbability <= 0) return 'no target probability';
  if (targetProbability < MIN_TARGET_PROBABILITY) {
    return `target probability below ${MIN_TARGET_PROBABILITY}%`;
  }
  if ((setup.confidence_score ?? 0) < config.min_confidence) {
    return `confidence below ${config.min_confidence.toFixed(0)}%`;
  }
  if (config.min_momentum_percentile > 0) {
    if (setup.direction === 'short') return 'short — momentum gate is long-only';
    if (setup.momentum_percentile == null) return 'no residual momentum rank';
    if (setup.momentum_percentile < config.min_momentum_percentile) {
      return `momentum below ${config.min_momentum_percentile.toFixed(0)}th %ile`;
    }
  }
  if (config.exclude_neutral) {
    const actionDir = actionDirection(setup.recommended_action);
    if (actionDir === 'neutral') return 'model action neutral';
    if (actionDir !== setup.direction) return 'counter to model bias';
  }
  if (config.require_high_conviction && !HIGH_CONVICTION_ACTIONS.has(setup.recommended_action ?? '')) {
    return 'conviction below High';
  }
  if (config.exclude_conflicts && (setup.risk_level ?? '') !== 'Low') return 'risk flags not clean';
  return null;
}

/**
 * Symbol of the current single 'top pick' — the #1 row the dashboard highlights:
 * the highest residual 12-1 momentum percentile among qualified setups. Returns
 * null when there are no actionable setups. Keep in step with the Top Setups
 * ranking in DashboardPage.
 */
export function topPickSymbol(
  trades: TradeSetup[] | undefined,
  activation: ActivationConfig | undefined,
): string | null {
  const all = trades ?? [];
  if (all.length === 0) return null;
  const qualified = activation ? all.filter((t) => qualifiesSetup(t, activation)) : [];
  const top = [...qualified].sort(
    (a, b) =>
      (b.strategy_rank ?? b.momentum_percentile ?? -Infinity) -
      (a.strategy_rank ?? a.momentum_percentile ?? -Infinity),
  )[0];
  return top?.symbol ?? null;
}

/** Short human summary of the active gate, e.g. for tooltips/labels. */
export function activationSummary(config: ActivationConfig): string {
  const parts = [];
  if (config.min_momentum_percentile > 0) parts.push(`top ${(100 - config.min_momentum_percentile).toFixed(0)}% residual momentum`);
  parts.push(`R:R ≥ ${config.min_rr.toFixed(1)}`, `conf ≥ ${config.min_confidence.toFixed(0)}%`);
  if (config.exclude_neutral) parts.push('directional');
  if (config.require_high_conviction) parts.push('high-conviction');
  if (config.exclude_conflicts) parts.push('clean');
  return parts.join(' · ');
}
