/**
 * Strategy Backtester
 * Validates signals with historical win rates
 */

import { rawQuery } from '../db.js';

/**
 * Backtest a pattern/strategy
 * @param {Object} pattern - Pattern definition
 * @param {Object} options - Backtest options
 * @returns {Promise<Object>} Backtest results
 */
export async function backtestStrategy(pattern, options = {}) {
  const {
    lookbackDays = 90,
    minOccurrences = 10
  } = options;
  
  try {
    // Query historical matches
    const matches = await findHistoricalMatches(pattern, lookbackDays);
    
    if (!matches || matches.length < minOccurrences) {
      return {
        pattern: pattern.name || 'Unknown',
        totalOccurrences: matches?.length || 0,
        error: `Insufficient data: Only ${matches?.length || 0} occurrences found (minimum: ${minOccurrences})`
      };
    }
    
    // Calculate outcomes for each match
    const outcomes = await calculateOutcomes(matches, pattern);
    
    // Aggregate results
    const results = aggregateResults(outcomes, pattern);
    
    return results;
  } catch (error) {
    console.error('Backtest error:', error);
    return {
      pattern: pattern.name || 'Unknown',
      error: error.message
    };
  }
}

/**
 * Find historical matches for a pattern
 */
async function findHistoricalMatches(pattern, lookbackDays) {
  // This is a simplified version - real implementation would match on:
  // - Badge combinations
  // - Rocket score ranges
  // - Session buckets
  // - Premium sizes
  // - Tape alignment
  
  try {
    // Strategy backtester requires calculated fields (rocket_score, tape_alignment, etc.)
    // that are not available in OptionsFlow_monthly. Returning empty for now.
    // TODO: Calculate these fields on-the-fly or use a different approach
    console.warn('Strategy backtester requires calculated fields not available in OptionsFlow_monthly, returning empty matches');
    return [];
  } catch (error) {
    console.error('Error finding historical matches:', error);
    return [];
  }
}

/**
 * Calculate outcomes for each match
 */
async function calculateOutcomes(matches, pattern) {
  const outcomes = [];
  
  for (const match of matches) {
    const symbol = match.symbol;
    const flowTime = new Date(match.flow_ts_utc);
    const direction = match.direction;
    
    // Get price at flow time
    const priceAtFlow = await getPriceAtTime(symbol, flowTime);
    
    if (!priceAtFlow) continue;
    
    // Get price 1 hour later, 2 hours later, EOD
    const price1H = await getPriceAtTime(symbol, new Date(flowTime.getTime() + 60 * 60 * 1000));
    const price2H = await getPriceAtTime(symbol, new Date(flowTime.getTime() + 2 * 60 * 60 * 1000));
    const priceEOD = await getPriceAtEOD(symbol, flowTime);
    
    // Classify outcome
    const outcome = classifyOutcome(
      direction,
      priceAtFlow,
      price1H,
      price2H,
      priceEOD,
      pattern
    );
    
    outcomes.push({
      symbol,
      flowTime: flowTime.toISOString(),
      direction,
      priceAtFlow,
      price1H,
      price2H,
      priceEOD,
      outcome,
      move1H: price1H ? ((price1H - priceAtFlow) / priceAtFlow) * 100 : null,
      move2H: price2H ? ((price2H - priceAtFlow) / priceAtFlow) * 100 : null,
      moveEOD: priceEOD ? ((priceEOD - priceAtFlow) / priceAtFlow) * 100 : null
    });
  }
  
  return outcomes;
}

/**
 * Get price at specific time
 */
async function getPriceAtTime(symbol, timestamp) {
  try {
    const query = `
      SELECT close
      FROM public.prices_intraday_1m
      WHERE UPPER(symbol) = UPPER($1)
        AND ts <= $2
      ORDER BY ts DESC
      LIMIT 1
    `;
    
    const result = await rawQuery(query, [symbol, timestamp.toISOString()]);
    
    if (result && result[0]) {
      return parseFloat(result[0].close);
    }
    
    return null;
  } catch (error) {
    console.error(`Error getting price for ${symbol} at ${timestamp}:`, error);
    return null;
  }
}

/**
 * Get price at end of day
 */
async function getPriceAtEOD(symbol, date) {
  try {
    const tradeDate = date.toISOString().split('T')[0];
    
    const query = `
      SELECT close
      FROM public.prices_daily
      WHERE UPPER(symbol) = UPPER($1)
        AND "Date" = $2
      LIMIT 1
    `;
    
    const result = await rawQuery(query, [symbol, tradeDate]);
    
    if (result && result[0]) {
      return parseFloat(result[0].close);
    }
    
    return null;
  } catch (error) {
    console.error(`Error getting EOD price for ${symbol} on ${date}:`, error);
    return null;
  }
}

/**
 * Classify outcome (WIN, LOSS, or NEUTRAL)
 */
function classifyOutcome(direction, priceAtFlow, price1H, price2H, priceEOD, pattern) {
  const minMove = pattern.minMove || 1.0; // Minimum % move to count as win
  
  // Use EOD if available, otherwise use 2H, otherwise 1H
  const finalPrice = priceEOD || price2H || price1H;
  
  if (!finalPrice) {
    return 'NO_DATA';
  }
  
  const move = ((finalPrice - priceAtFlow) / priceAtFlow) * 100;
  
  if (direction === 'BULL') {
    if (move >= minMove) return 'WIN';
    if (move <= -minMove) return 'LOSS';
    return 'NEUTRAL';
  } else if (direction === 'BEAR') {
    if (move <= -minMove) return 'WIN';
    if (move >= minMove) return 'LOSS';
    return 'NEUTRAL';
  }
  
  return 'NEUTRAL';
}

/**
 * Aggregate backtest results
 */
function aggregateResults(outcomes, pattern) {
  const wins = outcomes.filter(o => o.outcome === 'WIN');
  const losses = outcomes.filter(o => o.outcome === 'LOSS');
  const neutrals = outcomes.filter(o => o.outcome === 'NEUTRAL');
  const noData = outcomes.filter(o => o.outcome === 'NO_DATA');
  
  const total = wins.length + losses.length;
  const winRate = total > 0 ? wins.length / total : 0;
  
  const avgWin = wins.length > 0
    ? wins.reduce((sum, w) => sum + Math.abs(w.moveEOD || w.move2H || w.move1H || 0), 0) / wins.length
    : 0;
  
  const avgLoss = losses.length > 0
    ? losses.reduce((sum, l) => sum + Math.abs(l.moveEOD || l.move2H || l.move1H || 0), 0) / losses.length
    : 0;
  
  const expectancy = (winRate * avgWin) - ((1 - winRate) * avgLoss);
  
  // Find best and worst setups
  const bestSetup = findBestSetup(outcomes, pattern);
  const worstSetup = findWorstSetup(outcomes, pattern);
  
  return {
    pattern: pattern.name || 'Unknown Pattern',
    totalOccurrences: outcomes.length,
    wins: wins.length,
    losses: losses.length,
    neutrals: neutrals.length,
    noData: noData.length,
    winRate: Math.round(winRate * 10000) / 100, // Percentage with 2 decimals
    avgWin: Math.round(avgWin * 100) / 100,
    avgLoss: Math.round(avgLoss * 100) / 100,
    expectancy: Math.round(expectancy * 100) / 100,
    riskReward: avgLoss > 0 ? Math.round((avgWin / avgLoss) * 100) / 100 : null,
    bestSetup,
    worstSetup,
    outcomes: outcomes.slice(0, 20) // Return first 20 for inspection
  };
}

/**
 * Find best setup conditions
 */
function findBestSetup(outcomes, pattern) {
  const wins = outcomes.filter(o => o.outcome === 'WIN');
  
  if (wins.length === 0) return null;
  
  // Group by premium size
  const byPremium = {};
  wins.forEach(w => {
    const premium = w.premium_num || 0;
    const tier = premium > 1000000 ? 'HIGH' : premium > 500000 ? 'MEDIUM' : 'LOW';
    if (!byPremium[tier]) byPremium[tier] = [];
    byPremium[tier].push(w);
  });
  
  // Find tier with highest win rate
  let bestTier = null;
  let bestRate = 0;
  
  for (const [tier, wins] of Object.entries(byPremium)) {
    const total = outcomes.filter(o => {
      const premium = o.premium_num || 0;
      const oTier = premium > 1000000 ? 'HIGH' : premium > 500000 ? 'MEDIUM' : 'LOW';
      return oTier === tier;
    }).length;
    
    if (total > 0) {
      const rate = wins.length / total;
      if (rate > bestRate) {
        bestRate = rate;
        bestTier = tier;
      }
    }
  }
  
  return bestTier ? {
    condition: `Premium > ${bestTier === 'HIGH' ? '$1M' : bestTier === 'MEDIUM' ? '$500K' : '$0'}`,
    winRate: Math.round(bestRate * 100),
    sampleSize: byPremium[bestTier].length
  } : null;
}

/**
 * Find worst setup conditions
 */
function findWorstSetup(outcomes, pattern) {
  // Similar to best setup but find lowest win rate
  return null; // Simplified for now
}

/**
 * Backtest multiple patterns
 */
export async function backtestMultiplePatterns(patterns, options = {}) {
  const results = [];
  
  for (const pattern of patterns) {
    const result = await backtestStrategy(pattern, options);
    results.push(result);
  }
  
  // Sort by win rate and expectancy
  results.sort((a, b) => {
    if (a.error || b.error) return 0;
    const aScore = a.winRate * a.expectancy;
    const bScore = b.winRate * b.expectancy;
    return bScore - aScore;
  });
  
  return results;
}

/**
 * Format backtest results for display
 */
export function formatBacktestResults(results) {
  if (!results || results.error) {
    return {
      display: `❌ ${results.pattern}: ${results.error || 'Backtest failed'}`,
      full: results
    };
  }
  
  const winRatePct = results.winRate.toFixed(1);
  const expectancyPct = results.expectancy > 0 ? `+${results.expectancy.toFixed(2)}` : results.expectancy.toFixed(2);
  const rr = results.riskReward ? `R:R = ${results.riskReward.toFixed(2)}:1` : 'N/A';
  
  return {
    display: `📊 ${results.pattern}: ${winRatePct}% win rate (${results.wins}W/${results.losses}L) | Avg Win: +${results.avgWin.toFixed(2)}% | Avg Loss: -${results.avgLoss.toFixed(2)}% | Expectancy: ${expectancyPct}% | ${rr}`,
    summary: {
      pattern: results.pattern,
      winRate: `${winRatePct}%`,
      wins: results.wins,
      losses: results.losses,
      expectancy: `${expectancyPct}%`,
      riskReward: rr
    },
    full: results
  };
}

