/**
 * Momentum strategy — Bybit-only demo runner.
 *
 * Price data:  Bybit BTCUSDT linear public trade stream (WebSocket)
 * Execution:   Bybit demo REST via DemoExecutor
 *
 * Signal: 60-min momentum → hold 30 min, time exit, maker limits.
 */
import { loadConfig } from '../core/config.js'
import { BybitRestClient } from '../lib/bybit/rest.js'
import { BybitPublicTradeStreamer } from '../lib/bybit/public-stream.js'
import { DemoExecutor } from '../sim/demo-executor.js'
import { MomentumEngine, DEFAULT_CONFIG } from '../core/momentum.js'
import type { MomentumEntryRequest, MomentumExitRequest } from '../core/momentum.js'
import type { TapeTrade } from '../core/types.js'

const DURATION_MS = Number(process.env.TXOCAP_BENCH_MS ?? 24 * 3_600_000)
const STARTING_CAPITAL = 500

function log(tag: string, msg: string) {
  process.stderr.write(`[${new Date().toISOString().slice(11, 19)}] [${tag}] ${msg}\n`)
}

// ── 10-second bar aggregator ──
class BarAgg {
  private readonly MS = 10_000
  private bars: { o: number; h: number; l: number; c: number }[] = []
  private curKey = -1
  private cur: { o: number; h: number; l: number; c: number } | null = null

  /** Returns true when a new bar is sealed */
  ingest(price: number, ts: number): boolean {
    const key = Math.floor(ts / this.MS)
    if (key !== this.curKey) {
      if (this.cur) this.bars.push({ ...this.cur })
      this.curKey = key
      this.cur = { o: price, h: price, l: price, c: price }
      return this.bars.length > 0
    }
    if (this.cur) {
      this.cur.h = Math.max(this.cur.h, price)
      this.cur.l = Math.min(this.cur.l, price)
      this.cur.c = price
    }
    return false
  }

  get(): { o: number; h: number; l: number; c: number }[] { return this.bars }
}

async function main() {
  const config = loadConfig()
  if (!config.demoApiKey || !config.demoApiSecret) throw new Error('Missing demo API credentials in .env')

  const startedAt = Date.now()
  const rest = new BybitRestClient({
    demoApiKey: config.demoApiKey, demoApiSecret: config.demoApiSecret,
    testnetApiKey: '', testnetApiSecret: '',
    useTestnet: false, recvWindow: config.recvWindow
  })
  const demo = new DemoExecutor(config.demoApiKey, config.demoApiSecret, config.recvWindow)

  // ── Reset balance to $500 ──
  const curBal = await demo.getBalance()
  const diff = Math.round(curBal - STARTING_CAPITAL)
  if (Math.abs(diff) > 1) {
    await rest.applyDemoFunds([{ coin: 'USDT', amountStr: String(Math.abs(diff)) }], diff > 0 ? 1 : 0)
    log('RESET', `${curBal.toFixed(2)} → $${STARTING_CAPITAL}`)
    await new Promise(r => setTimeout(r, 1500))
  }
  await rest.cancelAll({ category: 'linear', symbol: 'BTCUSDT' })
  const pos = await demo.getPosition()
  if (pos) throw new Error(`Open position exists: ${pos.side} ${pos.size} — close it first`)
  await demo.setLeverage(100)

  const startBal = await demo.getBalance()
  log('START', `$${startBal.toFixed(2)} | lb=${DEFAULT_CONFIG.lookbackBars / 6}min hold=${DEFAULT_CONFIG.holdBars / 6}min thresh=${DEFAULT_CONFIG.entryThreshBps}bps fee=${DEFAULT_CONFIG.entryFeeBps}bps | duration=${Math.round(DURATION_MS / 60000)}min`)

  // ── Engine ──
  const barAgg = new BarAgg()
  const engine = new MomentumEngine({ ...DEFAULT_CONFIG, startingCapital: startBal })

  let busy = false
  let stopping = false
  let lastBal = startBal
  let lastBalAt = 0
  const stats = { entries: 0, entryFills: 0, exits: 0, exitFills: 0, exitMarket: 0 }

  async function fetchBal(): Promise<number> {
    const now = Date.now()
    if (now - lastBalAt < 5000) return lastBal
    lastBalAt = now
    try { lastBal = await demo.getBalance() } catch {}
    return lastBal
  }

  async function executeEntry(evt: MomentumEntryRequest): Promise<void> {
    if (busy || stopping) return
    busy = true; stats.entries++
    const side = evt.side === 'long' ? 'Buy' : 'Sell'
    log('SIGNAL', `${evt.side.toUpperCase()} mom=${evt.momentumBps.toFixed(1)}bps qty=${evt.sizeBtc}btc @${evt.refPrice.toFixed(1)}`)
    const result = await demo.chaseLimitOrder(side, evt.sizeBtc, () => evt.refPrice, 3, 3000, false)
    if (result.filled && result.avgPrice) {
      stats.entryFills++
      engine.confirmEntry(evt, result.avgPrice, evt.sizeBtc, Date.now())
      log('FILL', `entry ${evt.side.toUpperCase()} ${evt.sizeBtc}btc @${result.avgPrice.toFixed(1)} (${result.attempts} att, ${(result.elapsedMs / 1000).toFixed(1)}s)`)
    } else {
      engine.rejectEntry(evt, Date.now())
      log('MISS', `entry not filled after ${result.attempts} attempts`)
    }
    busy = false
  }

  async function executeExit(evt: MomentumExitRequest): Promise<void> {
    if (busy || stopping) return
    busy = true; stats.exits++
    const side = evt.side === 'long' ? 'Sell' : 'Buy'
    const result = await demo.chaseLimitOrder(side, evt.sizeBtc, () => evt.targetPrice, evt.reason === 'stop' ? 2 : 6, 3000, true)
    if (result.filled && result.avgPrice) {
      stats.exitFills++
      engine.confirmExit(evt, result.avgPrice, false, Date.now())
    } else {
      stats.exitMarket++
      log('MKTFB', `${evt.reason} exit — maker exhausted, falling back to market`)
      await demo.placeMarketOrder(side, evt.sizeBtc, true)
      await new Promise(r => setTimeout(r, 1500))
      engine.confirmExit(evt, evt.targetPrice, true, Date.now())
    }
    const t = engine.getState().closed.at(-1)
    const bal = await fetchBal()
    if (t) log('EXIT', `${t.exitReason.toUpperCase()} ${t.side.toUpperCase()} ${t.entryPrice.toFixed(1)}→${t.exitPrice.toFixed(1)} net=${t.netBps.toFixed(1)}bps $${t.netUsd.toFixed(2)} bal=$${bal.toFixed(2)}`)
    busy = false
  }

  // ── Bybit trade stream → bars → engine ──
  const stream = new BybitPublicTradeStreamer(rest, {
    categories: ['linear'],
    symbolPrefix: 'BTC',
    subscriptionChunkSize: 1
  })

  stream.on('trade', (trade: TapeTrade) => {
    if (trade.symbol !== 'BTCUSDT') return
    const newBar = barAgg.ingest(trade.price, trade.timestamp)
    if (!newBar || busy) return

    const bars = barAgg.get()
    engine.setBars(bars)
    engine.setBarIndex(bars.length - 1)

    const events = engine.tick(trade.price, trade.timestamp)
    for (const evt of events) {
      if (evt.type === 'entry') void executeEntry(evt)
      else if (evt.type === 'exit') void executeExit(evt)
    }
  })

  stream.on('error', (e: Error) => log('STREAM-ERR', e.message))
  await stream.start()
  log('STREAM', 'Bybit BTCUSDT linear trade stream connected')

  // ── Status every minute ──
  const statusTimer = setInterval(async () => {
    const st = engine.getState()
    const bal = await fetchBal()
    const elapsed = Math.round((Date.now() - startedAt) / 60000)
    const pos = st.position
    const posStr = pos
      ? `${pos.side.toUpperCase()} ${pos.sizeBtc}btc @${pos.entryPrice.toFixed(1)} peak=${pos.peakFavBps.toFixed(1)}bps`
      : 'flat'
    const momStr = st.lastMomentumBps !== 0 ? ` mom=${st.lastMomentumBps.toFixed(1)}bps→${st.lastSignal ?? 'none'}` : ''
    log('STATUS', `${elapsed}min bars=${barAgg.get().length} bal=$${bal.toFixed(2)} pnl=${bal - startBal >= 0 ? '+' : ''}$${(bal - startBal).toFixed(2)} trades=${st.closed.length}${momStr} pos=${posStr}`)
  }, 60_000)

  const endTimer = setTimeout(() => void shutdown('timer'), DURATION_MS)

  async function shutdown(reason: string) {
    if (stopping) return; stopping = true
    log('STOP', reason)
    clearInterval(statusTimer); clearTimeout(endTimer)
    try { await rest.cancelAll({ category: 'linear', symbol: 'BTCUSDT' }) } catch {}
    const openPos = await demo.getPosition().catch(() => null)
    if (openPos && openPos.size > 0) {
      const side = openPos.side === 'Buy' ? 'Sell' : 'Buy'
      await demo.placeMarketOrder(side as 'Buy' | 'Sell', openPos.size, true)
      log('FLAT', `market flatten: ${openPos.side} ${openPos.size}btc`)
    }
    stream.stop()

    const endBal = await demo.getBalance().catch(() => NaN)
    const st = engine.getState()
    const wins = st.closed.filter(t => t.netUsd > 0)
    process.stderr.write('\n' + '═'.repeat(60) + '\n')
    process.stderr.write('MOMENTUM DEMO REPORT\n')
    process.stderr.write('═'.repeat(60) + '\n')
    process.stderr.write(`duration:   ${Math.round((Date.now() - startedAt) / 60000)}min\n`)
    process.stderr.write(`balance:    $${startBal.toFixed(2)} → $${Number.isFinite(endBal) ? endBal.toFixed(2) : 'n/a'}\n`)
    process.stderr.write(`pnl:        ${Number.isFinite(endBal) ? (endBal - startBal >= 0 ? '+' : '') + '$' + (endBal - startBal).toFixed(2) : 'n/a'}\n`)
    process.stderr.write(`trades:     ${st.closed.length} (${wins.length}W/${st.closed.length - wins.length}L WR=${st.closed.length ? (wins.length / st.closed.length * 100).toFixed(0) : 0}%)\n`)
    process.stderr.write(`fees:       $${st.totalFees.toFixed(2)}\n`)
    process.stderr.write(`max DD:     ${st.maxDdPct}%\n`)
    process.stderr.write(`fills:      entry ${stats.entryFills}/${stats.entries}  exit ${stats.exitFills}/${stats.exits}  mkt=${stats.exitMarket}\n`)
    if (st.closed.length) {
      process.stderr.write('\nTrades:\n')
      for (const t of st.closed) {
        process.stderr.write(`  ${t.exitReason.toUpperCase().padEnd(5)} ${t.side.toUpperCase()} ${t.entryPrice.toFixed(1)}→${t.exitPrice.toFixed(1)} ${t.sizeBtc}btc net=${t.netBps.toFixed(1)}bps $${t.netUsd.toFixed(2)} ${(t.holdBars * 10 / 60).toFixed(0)}min\n`)
      }
    }
    process.stderr.write('═'.repeat(60) + '\n')
    process.exit(0)
  }

  process.on('SIGINT', () => void shutdown('sigint'))
  process.on('SIGTERM', () => void shutdown('sigterm'))
}

main().catch(e => {
  process.stderr.write(`[FATAL] ${e instanceof Error ? e.stack ?? e.message : String(e)}\n`)
  process.exit(1)
})
