/**
 * Backtests signal patterns against historical data
 * Returns win rate, avg win/loss, expectancy, best conditions
 */

import { rawQuery } from '../db.js';
import {
  calculateSharpeRatio,
  calculateMaxDrawdown,
  calculateStreaks,
  calculateSessionPerformance,
  findBestWorstSymbols,
  buildEquityCurve
} from './performanceMetrics.js';

/**
 * Backtest a signal pattern
 * @param {Object} signalPattern - Pattern definition with rocketScoreMin, badges, session, tapeAligned
 * @param {number} lookbackDays - Number of days to look back (default 90)
 * @returns {Promise<Object>} Backtest results
 */
export async function backtestSignal(signalPattern, lookbackDays = 90) {
  const results = {
    pattern: signalPattern,
    lookbackDays,
    totalOccurrences: 0,
    wins: 0,
    losses: 0,
    winRate: 0,
    avgWin: 0,
    avgLoss: 0,
    expectancy: 0,
    bestConditions: [],
    worstConditions: [],
    trades: []
  };
  
  try {
    // 1. Find historical matches
    const historicalMatches = await findHistoricalMatches(signalPattern, lookbackDays);
    results.totalOccurrences = historicalMatches.length;
    
    if (historicalMatches.length === 0) {
      return results;
    }
    
    // 2. For each match, calculate outcome
    for (const match of historicalMatches) {
      const outcome = await calculateOutcome(match);
      if (outcome) {
        results.trades.push(outcome);
        
        if (outcome.isWin) {
          results.wins++;
          results.avgWin += parseFloat(outcome.profitPct);
        } else {
          results.losses++;
          results.avgLoss += Math.abs(parseFloat(outcome.profitPct));
        }
      }
    }
    
    // 3. Calculate basic metrics
    if (results.totalOccurrences > 0) {
      results.winRate = parseFloat(((results.wins / results.totalOccurrences) * 100).toFixed(2));
      if (results.wins > 0) {
        results.avgWin = parseFloat((results.avgWin / results.wins).toFixed(2));
      }
      if (results.losses > 0) {
        results.avgLoss = parseFloat((results.avgLoss / results.losses).toFixed(2));
      }
      results.expectancy = parseFloat((
        (results.wins / results.totalOccurrences * results.avgWin) -
        (results.losses / results.totalOccurrences * results.avgLoss)
      ).toFixed(2));
    }
    
    // 4. Calculate advanced metrics
    if (results.trades.length > 0) {
      const returns = results.trades.map(t => parseFloat(t.profitPct) || 0);
      const equityCurve = buildEquityCurve(results.trades);
      
      results.sharpeRatio = parseFloat(calculateSharpeRatio(returns).toFixed(2));
      results.maxDrawdown = parseFloat((calculateMaxDrawdown(equityCurve) * 100).toFixed(2));
      results.streaks = calculateStreaks(results.trades);
      results.sessionPerformance = calculateSessionPerformance(results.trades);
      results.symbolPerformance = findBestWorstSymbols(results.trades, 5);
      results.equityCurve = equityCurve;
      results.totalReturn = parseFloat(returns.reduce((a, b) => a + b, 0).toFixed(2));
    }
    
    // 5. Find best/worst conditions
    results.bestConditions = findBestConditions(results.trades);
    results.worstConditions = findWorstConditions(results.trades);
    
    return results;
  } catch (error) {
    console.error('Backtest error:', error);
    return {
      ...results,
      error: error.message
    };
  }
}

/**
 * Find historical matches for a pattern
 * Uses the options flow query structure but with pattern-specific filters
 */
async function findHistoricalMatches(pattern, lookbackDays) {
  const { rocketScoreMin, badges, session, tapeAligned } = pattern;
  
  // Build date range
  const endDate = new Date();
  const startDate = new Date();
  startDate.setDate(startDate.getDate() - lookbackDays);
  
  const startDay = startDate.toISOString().split('T')[0];
  const endDay = endDate.toISOString().split('T')[0];
  
  // Use the existing options flow query but we'll need to call it and then filter
  // For now, let's use a simpler direct query approach
  // In production, you might want to use the full optionsFlowQuery and filter results
  const query = `
    WITH base AS (
      SELECT
        ofm.ctid AS rid,
        UPPER(TRIM(ofm."Symbol")) AS symbol_norm,
        NULLIF(REGEXP_REPLACE(ofm."Spot"::text, '[\\s$,]', '', 'g'),'')::numeric AS spot_num,
        NULLIF(REGEXP_REPLACE(ofm."Premium"::text, '[\\s$,]', '', 'g'),'')::numeric AS premium_num,
        NULLIF(REGEXP_REPLACE(ofm."Volume"::text, '[\\s$,]', '', 'g'),'')::numeric AS vol_num,
        NULLIF(REGEXP_REPLACE(ofm."OI"::text, '[\\s$,]', '', 'g'),'')::numeric AS oi_num,
        CASE WHEN UPPER(TRIM(ofm."CallPut")) IN ('C','CALL','CALLS','CE') THEN 'CALL'
             WHEN UPPER(TRIM(ofm."CallPut")) IN ('P','PUT','PUTS','PE')   THEN 'PUT'
        END AS cp_norm,
        CASE
          WHEN UPPER(TRIM(ofm."Side")) IN ('A','AA','ASK','BUY','BOT','BTO','AT_ASK')  THEN 'BUY'
          WHEN UPPER(TRIM(ofm."Side")) IN ('B','BB','BID','SELL','SLD','STO','AT_BID') THEN 'SELL'
          ELSE NULL
        END AS side_norm,
        CASE
          WHEN ofm."CreatedDate"::text ~ '^\\d{4}-\\d{2}-\\d{2}$' THEN 
            to_timestamp(ofm."CreatedDate"::text || ' ' || COALESCE(btrim(ofm."CreatedTime"::text), '00:00:00'), 'YYYY-MM-DD HH24:MI:SS')
          ELSE to_timestamp(ofm."CreatedDate"::text || ' ' || COALESCE(btrim(ofm."CreatedTime"::text), '00:00:00'), 'MM/DD/YYYY HH24:MI:SS')
        END AS flow_ts_local,
        CASE
          WHEN UPPER(TRIM(ofm."ExpirationDate"))::text ~ '^\\d{4}-\\d{2}-\\d{2}$' THEN ofm."ExpirationDate"::date
          WHEN UPPER(TRIM(ofm."ExpirationDate"))::text ~ '^\\d{1,2}/\\d{1,2}/\\d{2,4}$' THEN to_date(ofm."ExpirationDate"::text, 'MM/DD/YYYY')
          ELSE NULL
        END AS exp_date,
        NULLIF(REGEXP_REPLACE(ofm."Strike"::text, '[\\s$,]', '', 'g'),'')::numeric AS strike_num
      FROM public."OptionsFlow_monthly" ofm
      WHERE ofm."Premium" IS NOT NULL
        AND btrim(ofm."Premium"::text) <> ''
        AND ofm."StockEtf" = 'STOCK'
        AND (ofm."CreatedDate")::date >= $1::date
        AND (ofm."CreatedDate")::date <= $2::date
    ),
    flow AS (
      SELECT
        b.*,
        (b.flow_ts_local AT TIME ZONE 'America/Chicago') AS flow_ts_utc,
        CASE
          WHEN (cp_norm='CALL' AND side_norm='BUY') OR (cp_norm='PUT' AND side_norm='SELL') THEN 'BULL'
          WHEN (cp_norm='PUT'  AND side_norm='BUY') OR (cp_norm='CALL' AND side_norm='SELL') THEN 'BEAR'
          ELSE NULL
        END AS direction,
        CASE
          WHEN EXTRACT(HOUR FROM flow_ts_local)::int BETWEEN 4 AND 9
               AND NOT (EXTRACT(HOUR FROM flow_ts_local)::int = 9 AND EXTRACT(MINUTE FROM flow_ts_local)::int >= 30) THEN 'PRE'
          WHEN (EXTRACT(HOUR FROM flow_ts_local)::int = 9 AND EXTRACT(MINUTE FROM flow_ts_local)::int >= 30)
            OR (EXTRACT(HOUR FROM flow_ts_local)::int > 9 AND EXTRACT(HOUR FROM flow_ts_local)::int < 16) THEN 'RTH'
          WHEN EXTRACT(HOUR FROM flow_ts_local)::int BETWEEN 16 AND 20 THEN 'POST'
          ELSE 'OFF'
        END AS session_bucket
      FROM base b
    ),
    scored AS (
      SELECT
        f.*,
        (CASE
           WHEN f.premium_num >= 2000000 THEN 3.0
           WHEN f.premium_num >=  800000 THEN 2.0
           WHEN f.premium_num >=  200000 THEN 1.0
           ELSE 0.0
         END)
        + 1.2 * CASE WHEN COALESCE(f.vol_num,0) > COALESCE(f.oi_num,0) THEN 1 ELSE 0 END
        + 1.0 * CASE f.session_bucket WHEN 'RTH' THEN 1 WHEN 'POST' THEN 0.5 WHEN 'PRE' THEN 0.3 ELSE 0 END
        AS rocket_score
      FROM flow f
      WHERE f.premium_num > 80000
        AND f.direction IS NOT NULL
        AND f.spot_num IS NOT NULL
    ),
    filtered AS (
      SELECT 
        s.symbol_norm,
        s.flow_ts_utc,
        s.rocket_score,
        s.session_bucket,
        0 AS tape_alignment,  -- Simplified: would need price context to calculate properly
        s.spot_num,
        s.direction,
        '' AS badges,
        s.premium_num
      FROM scored s
      WHERE 1=1
        ${rocketScoreMin !== undefined ? `AND s.rocket_score >= ${rocketScoreMin}` : ''}
        ${session ? `AND s.session_bucket = '${session}'` : ''}
    )
    SELECT * FROM filtered
    ORDER BY flow_ts_utc DESC
    LIMIT 500
  `;
  
  try {
    const rows = await rawQuery(query, [startDay, endDay]);
    
    // Map to expected format
    return rows.map(row => ({
      symbol_norm: row.symbol_norm,
      flow_ts_utc: row.flow_ts_utc,
      rocketScore: parseFloat(row.rocket_score || 0),
      badges: row.badges || '',
      session_bucket: row.session_bucket,
      tape_alignment: row.tape_alignment === 1 || row.tape_alignment === true,
      premium_num: parseFloat(row.premium_num || 0),
      spot_num: parseFloat(row.spot_num || 0),
      direction: row.direction
    }));
  } catch (error) {
    console.error('Error finding historical matches:', error);
    return [];
  }
}

/**
 * Calculate outcome for a single match
 */
async function calculateOutcome(match) {
  const { symbol_norm, flow_ts_utc, spot_num, direction } = match;
  
  if (!spot_num || !direction) {
    return null;
  }
  
  const flowTime = new Date(flow_ts_utc);
  
  // Get price 1 hour later, 2 hours later, EOD
  const priceAfter1h = await getPriceAt(symbol_norm, flowTime, 60); // 60min later
  const priceAfter2h = await getPriceAt(symbol_norm, flowTime, 120);
  const priceEOD = await getPriceAtEOD(symbol_norm, flowTime);
  
  const entry = spot_num;
  const stop = direction === 'BULL' ? entry * 0.985 : entry * 1.015; // 1.5% stop
  const target = direction === 'BULL' ? entry * 1.015 : entry * 0.985; // 1.5% target
  
  // Determine outcome (did it hit target before stop?)
  let isWin = false;
  let exitPrice = entry;
  let exitTime = null;
  
  // Check 1h
  if (priceAfter1h) {
    if (direction === 'BULL') {
      if (priceAfter1h >= target) {
        isWin = true;
        exitPrice = target;
        exitTime = 60;
      } else if (priceAfter1h <= stop) {
        isWin = false;
        exitPrice = stop;
        exitTime = 60;
      }
    } else {
      if (priceAfter1h <= target) {
        isWin = true;
        exitPrice = target;
        exitTime = 60;
      } else if (priceAfter1h >= stop) {
        isWin = false;
        exitPrice = stop;
        exitTime = 60;
      }
    }
  }
  
  // If still no exit, check 2h
  if (!exitTime && priceAfter2h) {
    if (direction === 'BULL') {
      if (priceAfter2h >= target) {
        isWin = true;
        exitPrice = target;
        exitTime = 120;
      } else if (priceAfter2h <= stop) {
        isWin = false;
        exitPrice = stop;
        exitTime = 120;
      }
    } else {
      if (priceAfter2h <= target) {
        isWin = true;
        exitPrice = target;
        exitTime = 120;
      } else if (priceAfter2h >= stop) {
        isWin = false;
        exitPrice = stop;
        exitTime = 120;
      }
    }
  }
  
  // If still no exit, use EOD
  if (!exitTime && priceEOD) {
    exitPrice = priceEOD;
    exitTime = 'EOD';
    isWin = direction === 'BULL' 
      ? priceEOD > entry 
      : priceEOD < entry;
  }
  
  // If we still don't have an exit, skip this trade
  if (!exitTime) {
    return null;
  }
  
  const profitPct = ((exitPrice - entry) / entry * 100) * (direction === 'BULL' ? 1 : -1);
  
  return {
    ...match,
    entry,
    exit: exitPrice,
    exitTime,
    profitPct: profitPct.toFixed(2),
    isWin
  };
}

/**
 * Get price at a specific time (minutes after flow time)
 * Handles both 1-minute and 5-minute data, with fallback to daily data
 */
async function getPriceAt(symbol, fromTime, minutesLater) {
  const targetTime = new Date(fromTime.getTime() + minutesLater * 60 * 1000);
  
  // Try intraday 1m first, fallback to daily if not available
  const query = `
    SELECT close 
    FROM public.prices_intraday_1m
    WHERE UPPER(symbol) = UPPER($1)
      AND ts <= $2
    ORDER BY ts DESC
    LIMIT 1
  `;
  
  try {
    const result = await rawQuery(query, [symbol, targetTime.toISOString()]);
    if (result[0]?.close) {
      return parseFloat(result[0].close);
    }
    
    // Fallback to daily data if intraday not available
    const date = targetTime.toISOString().split('T')[0];
    const dailyQuery = `
      SELECT close 
      FROM public.prices_daily
      WHERE UPPER(symbol) = UPPER($1)
        AND "Date" <= $2
      ORDER BY "Date" DESC
      LIMIT 1
    `;
    const dailyResult = await rawQuery(dailyQuery, [symbol, date]);
    return dailyResult[0]?.close ? parseFloat(dailyResult[0].close) : null;
  } catch (error) {
    console.error(`Error getting price for ${symbol} at ${targetTime}:`, error);
    return null;
  }
}

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

/**
 * Find best conditions
 */
function findBestConditions(trades) {
  if (!trades || trades.length === 0) {
    return [];
  }
  
  // Group by session
  const bySession = {};
  
  trades.forEach(trade => {
    const session = trade.session_bucket || 'UNKNOWN';
    if (!bySession[session]) {
      bySession[session] = { wins: 0, total: 0 };
    }
    bySession[session].total++;
    if (trade.isWin) bySession[session].wins++;
  });
  
  const conditions = Object.entries(bySession).map(([session, stats]) => ({
    condition: `${session} session`,
    winRate: ((stats.wins / stats.total) * 100).toFixed(1) + '%',
    occurrences: stats.total
  }));
  
  return conditions.sort((a, b) => parseFloat(b.winRate) - parseFloat(a.winRate)).slice(0, 3);
}

/**
 * Find worst conditions
 */
function findWorstConditions(trades) {
  if (!trades || trades.length === 0) {
    return [];
  }
  
  // Group by session
  const bySession = {};
  
  trades.forEach(trade => {
    const session = trade.session_bucket || 'UNKNOWN';
    if (!bySession[session]) {
      bySession[session] = { wins: 0, total: 0 };
    }
    bySession[session].total++;
    if (trade.isWin) bySession[session].wins++;
  });
  
  const conditions = Object.entries(bySession).map(([session, stats]) => ({
    condition: `${session} session`,
    winRate: ((stats.wins / stats.total) * 100).toFixed(1) + '%',
    occurrences: stats.total
  }));
  
  return conditions.sort((a, b) => parseFloat(a.winRate) - parseFloat(b.winRate)).slice(0, 3);
}

