/**
 * Bookmap WebSocket Service
 * Receives real-time order book and trade data from Bookmap add-on
 * Aggregates trades into 1-minute OHLCV bars and stores in prices_intraday_1m table
 */

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

// In-memory storage for aggregating 1-minute bars
// Structure: { symbol: { minuteKey: { open, high, low, close, volume, count } } }
const minuteBars = new Map();

// Track last price per symbol for OHLC calculation
const lastPrices = new Map();

/**
 * Get minute key from timestamp (YYYY-MM-DD HH:MM format)
 */
function getMinuteKey(timestamp) {
  const date = new Date(timestamp);
  const year = date.getUTCFullYear();
  const month = String(date.getUTCMonth() + 1).padStart(2, '0');
  const day = String(date.getUTCDate()).padStart(2, '0');
  const hour = String(date.getUTCHours()).padStart(2, '0');
  const minute = String(date.getUTCMinutes()).padStart(2, '0');
  return `${year}-${month}-${day} ${hour}:${minute}`;
}

/**
 * Initialize minute bar for a symbol and minute
 */
function initMinuteBar(symbol, minuteKey) {
  if (!minuteBars.has(symbol)) {
    minuteBars.set(symbol, new Map());
  }
  
  const symbolBars = minuteBars.get(symbol);
  if (!symbolBars.has(minuteKey)) {
    symbolBars.set(minuteKey, {
      open: null,
      high: null,
      low: null,
      close: null,
      volume: 0,
      count: 0,
      firstTimestamp: null,
      lastTimestamp: null
    });
  }
  
  return symbolBars.get(minuteKey);
}

/**
 * Update minute bar with trade data
 * @param {string} symbol - Stock symbol
 * @param {number} price - Trade price
 * @param {number} volume - Trade volume
 * @param {number|string} timestamp - Timestamp (milliseconds since epoch or ISO string)
 */
function updateMinuteBar(symbol, price, volume, timestamp) {
  // Convert timestamp to Date object (handle both milliseconds and ISO strings)
  const ts = typeof timestamp === 'number' ? new Date(timestamp) : new Date(timestamp);
  
  if (isNaN(ts.getTime())) {
    console.warn(`⚠️  Bookmap: Invalid timestamp for ${symbol}: ${timestamp}`);
    return;
  }
  
  const minuteKey = getMinuteKey(ts);
  const bar = initMinuteBar(symbol, minuteKey);
  
  // Set first timestamp if this is the first trade in this minute
  if (bar.firstTimestamp === null) {
    bar.firstTimestamp = ts;
    bar.open = price;
  }
  
  // Update high/low
  if (bar.high === null || price > bar.high) {
    bar.high = price;
  }
  if (bar.low === null || price < bar.low) {
    bar.low = price;
  }
  
  // Update close (always the last price in the minute)
  bar.close = price;
  
  // Accumulate volume
  bar.volume += volume;
  bar.count += 1;
  bar.lastTimestamp = ts;
  
  // Track last price for this symbol
  lastPrices.set(symbol, price);
}

/**
 * Flush completed minute bars to database
 * Called periodically to save bars that are at least 1 minute old
 */
async function flushMinuteBars() {
  const now = Date.now();
  const oneMinuteAgo = now - 60000; // 1 minute in milliseconds
  
  for (const [symbol, symbolBars] of minuteBars.entries()) {
    const barsToFlush = [];
    
    for (const [minuteKey, bar] of symbolBars.entries()) {
      // Only flush bars that are at least 1 minute old (completed)
      if (bar.lastTimestamp && bar.lastTimestamp.getTime() < oneMinuteAgo) {
        // Ensure we have valid OHLC data
        if (bar.open !== null && bar.high !== null && bar.low !== null && bar.close !== null) {
          barsToFlush.push({
            symbol: symbol.toUpperCase(),
            minuteKey,
            bar,
            timestamp: bar.firstTimestamp
          });
        }
      }
    }
    
    if (barsToFlush.length > 0) {
      try {
        await insertMinuteBars(barsToFlush);
        
        // Remove flushed bars from memory
        for (const { minuteKey } of barsToFlush) {
          symbolBars.delete(minuteKey);
        }
        
        console.log(`📊 Bookmap: Flushed ${barsToFlush.length} minute bars for ${symbol}`);
      } catch (error) {
        console.error(`❌ Bookmap: Error flushing bars for ${symbol}:`, error);
      }
    }
  }
}

/**
 * Insert minute bars into database
 */
async function insertMinuteBars(bars) {
  if (bars.length === 0) return;
  
  // Group by symbol for batch insert
  const bySymbol = new Map();
  for (const { symbol, minuteKey, bar, timestamp } of bars) {
    if (!bySymbol.has(symbol)) {
      bySymbol.set(symbol, []);
    }
    bySymbol.get(symbol).push({ minuteKey, bar, timestamp });
  }
  
  // Insert each symbol's bars
  for (const [symbol, symbolBars] of bySymbol.entries()) {
    const values = symbolBars.map(({ minuteKey, bar, timestamp }) => {
      // Parse minute key to get timestamp
      // Format: "YYYY-MM-DD HH:MM"
      const [datePart, timePart] = minuteKey.split(' ');
      const [year, month, day] = datePart.split('-');
      const [hour, minute] = timePart.split(':');
      
      // Create timestamp at the start of the minute (UTC)
      const ts = new Date(Date.UTC(
        parseInt(year),
        parseInt(month) - 1,
        parseInt(day),
        parseInt(hour),
        parseInt(minute),
        0,
        0
      ));
      
      return {
        symbol: symbol.toUpperCase(),
        ts: ts.toISOString(),
        open: bar.open,
        high: bar.high,
        low: bar.low,
        close: bar.close,
        volume: bar.volume
      };
    });
    
    // Use INSERT ... ON CONFLICT to handle duplicates
    const query = `
      INSERT INTO public.prices_intraday_1m (symbol, ts, open, high, low, close, volume)
      VALUES ${values.map((_, i) => `($${i * 6 + 1}, $${i * 6 + 2}::timestamp with time zone, $${i * 6 + 3}, $${i * 6 + 4}, $${i * 6 + 5}, $${i * 6 + 6}, $${i * 6 + 7})`).join(', ')}
      ON CONFLICT (symbol, ts) DO UPDATE SET
        open = EXCLUDED.open,
        high = EXCLUDED.high,
        low = EXCLUDED.low,
        close = EXCLUDED.close,
        volume = EXCLUDED.volume
    `;
    
    const params = values.flatMap(v => [
      v.symbol,
      v.ts,
      v.open,
      v.high,
      v.low,
      v.close,
      v.volume
    ]);
    
    await rawQuery(query, params);
  }
}

/**
 * Handle incoming Bookmap data
 */
function handleBookmapData(data) {
  try {
    const { symbol, metrics, trade } = data;
    
    if (!symbol) {
      return;
    }
    
    // Update last price from metrics if available
    if (metrics && metrics.last_price) {
      lastPrices.set(symbol, metrics.last_price);
    }
    
    // Process trade data to build OHLCV bars
    if (trade && trade.price && trade.size && trade.timestamp) {
      updateMinuteBar(symbol, trade.price, trade.size, trade.timestamp);
    } else if (metrics && metrics.last_price && metrics.timestamp) {
      // If no trade data but we have last_price, use it as a tick
      // Use a small volume (1) to indicate this is a price update, not a full trade
      updateMinuteBar(symbol, metrics.last_price, 1, metrics.timestamp);
    }
  } catch (error) {
    console.error('❌ Bookmap: Error handling data:', error);
  }
}

/**
 * Start Bookmap WebSocket server
 */
export function startBookmapWebSocketService(port = 3001) {
  const wss = new WebSocketServer({ port });
  
  console.log(`📊 Bookmap WebSocket server listening on port ${port}`);
  
  wss.on('connection', (ws, req) => {
    const clientIp = req.socket.remoteAddress;
    console.log(`📊 Bookmap: Client connected from ${clientIp}`);
    
    ws.on('message', (message) => {
      try {
        const data = JSON.parse(message.toString());
        handleBookmapData(data);
      } catch (error) {
        console.error('❌ Bookmap: Error parsing message:', error);
        console.error('   Message:', message.toString().substring(0, 200));
      }
    });
    
    ws.on('error', (error) => {
      console.error('❌ Bookmap: WebSocket error:', error);
    });
    
    ws.on('close', () => {
      console.log(`📊 Bookmap: Client disconnected from ${clientIp}`);
    });
  });
  
  // Flush completed minute bars every 30 seconds
  setInterval(flushMinuteBars, 30000);
  
  // Also flush on process exit
  process.on('SIGINT', async () => {
    console.log('📊 Bookmap: Flushing remaining bars before shutdown...');
    await flushMinuteBars();
    wss.close();
    process.exit(0);
  });
  
  return wss;
}

/**
 * Stop Bookmap WebSocket service
 */
export function stopBookmapWebSocketService(wss) {
  if (wss) {
    flushMinuteBars().then(() => {
      wss.close();
      console.log('📊 Bookmap WebSocket server stopped');
    });
  }
}

