import { loadConfig } from '../core/config.js'
import { BybitRestClient } from '../lib/bybit/rest.js'
import { createTradeSources, MultiVenueTradeAggregator } from '../lib/venues/index.js'
import { BybitOrderbook } from '../sim/orderbook.js'
import { DemoExecutor } from '../sim/demo-executor.js'
import { IpcServer } from '../core/ipc.js'
import { TapeSignalEngine } from '../signals/tape-engine.js'
import { STARTING_CAPITAL, PROFILES, StrategyEngine, formatUsd, round } from '../core/strategy.js'
import type { Profile, EntryRequest, ExitRequest, EngineStateBroadcast } from '../core/strategy.js'
import type { TapeSignalSnapshot, TapeTrade } from '../core/types.js'

const DURATION_MS = Number(process.env.TXOCAP_BENCH_MS ?? 3_600_000)
const MAX_ELIGIBLE = Number(process.env.TXOCAP_BENCH_MAX_ELIGIBLE ?? 0)

// CLI-overridable guards for testing fill behavior under different regimes
function parseOverrides(): Partial<Profile> {
  const o: Partial<Profile> = {}
  if (process.env.RV_MIN) o.minRealizedVolBps = Number(process.env.RV_MIN)
  if (process.env.RV_MAX) o.maxRealizedVolBps = Number(process.env.RV_MAX)
  if (process.env.ENTRY_CD) o.entryCooldownMs = Number(process.env.ENTRY_CD) * 1000
  if (process.env.LOSS_CD) o.lossCooldownMs = Number(process.env.LOSS_CD) * 1000
  if (process.env.MAX_HOLD) o.maxHoldMs = Number(process.env.MAX_HOLD) * 1000
  if (process.env.TRAIL_ACT) o.trailActivationBps = Number(process.env.TRAIL_ACT)
  if (process.env.FEE_ENTRY) o.entryFeeBps = Number(process.env.FEE_ENTRY)
  if (process.env.FEE_EXIT) o.exitFeeBps = Number(process.env.FEE_EXIT)
  return o
}

const overrides = parseOverrides()
const PROFILE: Profile = { ...PROFILES.MAKER_OPT, ...overrides }

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

async function main(): Promise<void> {
  const config = loadConfig()
  if (!config.demoApiKey || !config.demoApiSecret) throw new Error('Missing demo API credentials')

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

  // Reset demo balance to STARTING_CAPITAL
  const currentBal = await demo.getBalance()
  const diff = Math.round(currentBal - STARTING_CAPITAL)
  if (diff > 1) {
    await restClient.applyDemoFunds([{ coin: 'USDT', amountStr: String(Math.abs(diff)) }], 1) // reduce
    log('RESET', `removed $${diff} demo funds (${currentBal.toFixed(2)} → ~${STARTING_CAPITAL})`)
  } else if (diff < -1) {
    await restClient.applyDemoFunds([{ coin: 'USDT', amountStr: String(Math.abs(diff)) }], 0) // add
    log('RESET', `added $${Math.abs(diff)} demo funds (${currentBal.toFixed(2)} → ~${STARTING_CAPITAL})`)
  }
  await new Promise(r => setTimeout(r, 1000))
  const startBal = await demo.getBalance()

  await restClient.cancelAll({ category: 'linear', symbol: 'BTCUSDT' })
  const existingPos = await demo.getPosition()
  if (existingPos) throw new Error(`Open position exists: ${existingPos.side} ${existingPos.size}`)
  await demo.setLeverage(100)

  // ── Same engine as replay and future live execution ──
  const engine = new StrategyEngine(PROFILE, startBal)
  const orderbook = new BybitOrderbook()
  const publicStream = new MultiVenueTradeAggregator(createTradeSources(config, restClient))

  // IPC server so dashboard can connect
  const ipc = new IpcServer('tape')
  ipc.start()
  log('IPC', `serving on ${ipc.path}`)

  // Signal engine for dashboard display
  const signalEngine = new TapeSignalEngine({
    fastWindowMs: config.signalFastWindowMs, slowWindowMs: config.signalSlowWindowMs,
    emitIntervalMs: config.signalEmitIntervalMs, largeTradeUsd: config.signalLargeTradeUsd
  })
  signalEngine.on('snapshot', (snap: TapeSignalSnapshot) => ipc.broadcast({ type: 'signal', ts: snap.asOf, data: snap }))
  signalEngine.start()

  let lastStateBroadcastAt = 0
  let lastDemoBalance: number | null = null
  let lastDemoBalanceFetchAt = 0

  async function fetchDemoBalance(): Promise<number | null> {
    const now = Date.now()
    if (now - lastDemoBalanceFetchAt < 5000) return lastDemoBalance
    lastDemoBalanceFetchAt = now
    try { lastDemoBalance = await demo.getBalance(); return lastDemoBalance } catch { return lastDemoBalance }
  }

  function broadcastSimState(force = false): void {
    const now = Date.now()
    if (!force && now - lastStateBroadcastAt < 1000) return
    lastStateBroadcastAt = now
    const ob = orderbook.getState()
    const st = engine.getState()
    const state: EngineStateBroadcast = {
      mode: 'demo',
      startingEquity: startBal,
      barCount: engine.getBarCount(),
      serverUptimeMs: now - startedAt,
      demoBalanceUsd: lastDemoBalance,
      equity: st.capital, unrealizedPnl: 0, realizedPnl: st.rpnl,
      totalFees: st.fees, openCount: st.positions.length, closedCount: st.closed.length,
      maxDrawdownPct: st.maxDdPct, midPrice: ob.midPrice,
      rangeBps: engine.getRangeBps(), realizedVol: engine.getRv(),
      positions: st.positions.map(p => ({
        id: String(p.id), side: p.side, sizeBtc: p.sizeBtc, entryPrice: p.entryPrice,
        unrealizedBps: ob.midPrice ? (p.side === 'long'
          ? ((ob.midPrice - p.entryPrice) / p.entryPrice) * 10_000
          : ((p.entryPrice - ob.midPrice) / p.entryPrice) * 10_000) : 0,
        stopPrice: p.stopPrice, tp1Price: p.tp1Price, holdMs: now - p.openedAt
      })),
      closedTrades: st.closed.slice(-20).map(t => ({
        id: String(t.id), side: t.side, exitReason: t.reason, entryPrice: t.entryPrice,
        exitPrice: t.exitPrice, sizeBtc: t.sizeBtc, netPnlUsd: t.net, durationMs: t.dur, style: t.style
      }))
    }
    ipc.broadcast({ type: 'engine_state', ts: now, data: state })
  }

  const ov = Object.keys(overrides).length > 0 ? ` overrides: ${Object.entries(overrides).map(([k,v]) => `${k}=${v}`).join(' ')}` : ''
  log('BENCH', `profile=${PROFILE.name} rv=[${PROFILE.minRealizedVolBps},${PROFILE.maxRealizedVolBps}] entryCd=${PROFILE.entryCooldownMs/1000}s lossCd=${PROFILE.lossCooldownMs/1000}s maxHold=${PROFILE.maxHoldMs/1000}s duration=${Math.round(DURATION_MS / 60000)}m bal=$${startBal.toFixed(2)}${ov}`)

  let busy = false
  let stopRequested = false

  const stats = {
    shouldEnter: 0, eligible: 0,
    entryAttempts: 0, entryFills: 0, entryMisses: 0,
    exitAttempts: 0, exitFills: 0, exitMisses: 0, exitMarketFallbacks: 0,
    entryAttemptMs: [] as number[], exitAttemptMs: [] as number[],
  }

  async function executeEntry(evt: EntryRequest): Promise<void> {
    if (busy) return
    busy = true
    stats.entryAttempts++

    const entrySide = evt.side === 'long' ? 'Buy' : 'Sell'
    const getBestPrice = (): number | null => {
      const s = orderbook.getState()
      return entrySide === 'Buy' ? (s.bestBid ?? null) : (s.bestAsk ?? null)
    }

    log('SIGNAL', `${evt.side.toUpperCase()} ${evt.absorption ? 'ABS' : 'MOM'} score=${evt.score.toFixed(2)} rv=${evt.rv.toFixed(2)} range=${evt.rangeBps.toFixed(1)}bps qty=${evt.sizeBtc.toFixed(3)}btc risk=$${evt.riskUsd.toFixed(2)} conv=${evt.conviction.toFixed(2)}`)
    const result = await demo.chaseLimitOrder(entrySide, evt.sizeBtc, getBestPrice, PROFILE.maxChase, 3000, false)
    stats.entryAttemptMs.push(result.elapsedMs)

    if (result.filled && result.avgPrice) {
      stats.entryFills++
      engine.confirmEntry(evt, result.avgPrice, evt.sizeBtc, 0, Date.now())
      log('FILL', `entry ${evt.side.toUpperCase()} ${evt.sizeBtc}btc @${result.avgPrice.toFixed(1)} in ${result.attempts} attempts / ${(result.elapsedMs / 1000).toFixed(1)}s`)
    } else {
      stats.entryMisses++
      engine.rejectEntry(evt, Date.now())
      log('MISS', `entry not filled after ${result.attempts} attempts / ${(result.elapsedMs / 1000).toFixed(1)}s`)
    }
    busy = false
    if (stopRequested && engine.getState().positions.length === 0) void shutdown('eligible_limit')
  }

  async function executeExit(evt: ExitRequest): Promise<void> {
    if (busy) return
    busy = true
    stats.exitAttempts++

    const exitSide = evt.side === 'long' ? 'Sell' : 'Buy'
    const getBestPrice = (): number | null => {
      const s = orderbook.getState()
      return exitSide === 'Buy' ? (s.bestBid ?? null) : (s.bestAsk ?? null)
    }

    // SL exits: price is running away, few chase attempts then market fallback
    // Trail exits: we're in profit, must protect it — chase fast and hard
    // Time/force exits: we have time, chase medium pace
    const isSl = evt.reason === 'sl'
    const isTrail = evt.reason === 'trail'
    const chaseAttempts = isSl ? PROFILE.maxChase : isTrail ? PROFILE.maxChase * 4 : PROFILE.maxChase * 3
    const chaseWaitMs = isSl ? 3000 : isTrail ? 1500 : 2000  // trail: faster reprice

    const result = await demo.chaseLimitOrder(exitSide, evt.sizeBtc, getBestPrice, chaseAttempts, chaseWaitMs, true)
    stats.exitAttemptMs.push(result.elapsedMs)

    if (result.filled && result.avgPrice) {
      stats.exitFills++
      engine.confirmExit(evt, result.avgPrice, false, Date.now())
      const t = engine.getState().closed.at(-1)!
      const balAfter = await fetchDemoBalance()
      log('EXIT', `${evt.reason.toUpperCase()} ${t.side.toUpperCase()} ${t.sizeBtc}btc ${t.entryPrice.toFixed(1)}→${t.exitPrice.toFixed(1)} engine_net=${formatUsd(t.net)} bal=${balAfter?.toFixed(2) ?? 'n/a'} mode=maker`)
    } else {
      stats.exitMisses++
      if (isSl) {
        // SL: price is adverse, maker won't fill — use market fallback
        stats.exitMarketFallbacks++
        const balBefore = lastDemoBalance
        log('MISS', `exit SL maker failed, using market fallback (TAKER fees apply)`)
        await demo.placeMarketOrder(exitSide, evt.sizeBtc, true)
        await new Promise(r => setTimeout(r, 2000))
        const balAfter = await fetchDemoBalance()
        const actualPnl = balBefore != null && balAfter != null ? balAfter - balBefore : null
        const px = getBestPrice() ?? evt.targetPrice
        engine.confirmExit(evt, px, true, Date.now())
        const t = engine.getState().closed.at(-1)!
        log('EXIT', `${evt.reason.toUpperCase()} ${t.side.toUpperCase()} ${t.sizeBtc}btc engine_net=${formatUsd(t.net)} actual_pnl=${actualPnl != null ? formatUsd(round(actualPnl)) : 'n/a'} bal=${balAfter?.toFixed(2) ?? 'n/a'} mode=market-fallback(TAKER)`)
      } else {
        // Non-SL maker chase exhausted — fall back to market order
        stats.exitMarketFallbacks++
        const balBefore = lastDemoBalance
        log('MISS', `exit ${evt.reason} maker chase exhausted after ${chaseAttempts} attempts, falling back to market`)
        await demo.placeMarketOrder(exitSide, evt.sizeBtc, true)
        await new Promise(r => setTimeout(r, 2000))
        const balAfter = await fetchDemoBalance()
        const actualPnl = balBefore != null && balAfter != null ? balAfter - balBefore : null
        const px = getBestPrice() ?? evt.targetPrice
        engine.confirmExit(evt, px, true, Date.now())
        const t = engine.getState().closed.at(-1)!
        log('EXIT', `${evt.reason.toUpperCase()} ${t.side.toUpperCase()} ${t.sizeBtc}btc engine_net=${formatUsd(t.net)} actual_pnl=${actualPnl != null ? formatUsd(round(actualPnl)) : 'n/a'} bal=${balAfter?.toFixed(2) ?? 'n/a'} mode=maker-exhaust(TAKER)`)
      }
    }
    busy = false
    if (stopRequested && engine.getState().positions.length === 0) void shutdown('eligible_limit')
  }

  // ── OB update: run the engine (same logic as replay/live) ──
  orderbook.on('update', () => {
    if (busy) return
    const ob = orderbook.getState()
    if (!ob.midPrice) return
    engine.updateRange(ob.midPrice, ob.updatedAt)
    broadcastSimState()

    const events = engine.tick(ob.midPrice, ob.bestBid ?? ob.midPrice, ob.bestAsk ?? ob.midPrice, Date.now())

    for (const evt of events) {
      if (evt.type === 'entry') {
        stats.shouldEnter++
        stats.eligible++
        if (MAX_ELIGIBLE > 0 && stats.eligible >= MAX_ELIGIBLE) stopRequested = true
        void executeEntry(evt)
      }
      if (evt.type === 'exit') {
        void executeExit(evt)
      }
    }
  })

  publicStream.on('trade', (trade: TapeTrade) => {
    engine.ingestTrade(trade.price, trade.side, trade.notionalUsd, trade.exchange, trade.timestamp)
    signalEngine.ingest(trade)
    ipc.broadcast({ type: 'trade', ts: trade.timestamp, data: trade })
  })

  orderbook.on('ready', () => { log('OB', 'orderbook connected'); broadcastSimState(true) })
  orderbook.on('error', (e: Error) => log('OB-ERR', e.message))
  publicStream.on('error', () => {})
  await publicStream.start()
  orderbook.start()

  const statusTimer = setInterval(async () => {
    const st = engine.getState()
    await fetchDemoBalance()
    const bal = lastDemoBalance
    const actualPnl = bal != null ? bal - startBal : null
    log('STATUS', `elapsed=${Math.round((Date.now() - startedAt) / 60000)}m bal=${bal != null ? '$' + bal.toFixed(2) : 'n/a'} actual_pnl=${actualPnl != null ? formatUsd(round(actualPnl)) : 'n/a'} engine_pnl=${formatUsd(st.rpnl)} rv=${engine.getRv().toFixed(2)} range=${engine.getRangeBps().toFixed(1)}bps eligible=${stats.eligible} entry=${stats.entryFills}/${stats.entryAttempts} exit=${stats.exitFills}/${stats.exitAttempts} open=${st.positions.length ? st.positions[0].side.toUpperCase() : 'none'}`)
    broadcastSimState()
  }, 30_000)

  const endTimer = setTimeout(() => { log('DONE', 'benchmark complete'); void shutdown('timer') }, DURATION_MS)

  let stopping = false
  async function shutdown(reason: string): Promise<void> {
    if (stopping) return; stopping = true
    clearInterval(statusTimer); clearTimeout(endTimer)
    try { await restClient.cancelAll({ category: 'linear', symbol: 'BTCUSDT' }) } catch {}
    const pos = await demo.getPosition().catch(() => null)
    if (pos && pos.size > 0) {
      const side = pos.side === 'Buy' ? 'Sell' : 'Buy'
      await demo.placeMarketOrder(side as any, pos.size, true)
      log('FLAT', `shutdown: market flattened ${pos.side} ${pos.size}btc`)
    }
    try { publicStream.stop() } catch {}
    try { orderbook.stop() } catch {}
    try { signalEngine.stop() } catch {}
    try { ipc.stop() } catch {}

    const endBal = await demo.getBalance().catch(() => NaN)
    const st = engine.getState()
    const avg = (xs: number[]) => xs.length > 0 ? (xs.reduce((a, b) => a + b, 0) / xs.length).toFixed(1) : 'n/a'
    const w = st.closed.filter(t => t.net > 0)
    process.stderr.write('\n=== DEMO FILL BENCHMARK REPORT ===\n')
    process.stderr.write(`profile: ${PROFILE.name} | duration: ${Math.round((Date.now() - startedAt) / 60000)}m\n`)
    process.stderr.write(`balance: ${startBal.toFixed(2)} -> ${Number.isFinite(endBal) ? endBal.toFixed(2) : 'n/a'}\n`)
    process.stderr.write(`eligible: ${stats.eligible} | entry fills: ${stats.entryFills}/${stats.entryAttempts} | exit maker: ${stats.exitFills}/${stats.exitAttempts} | exit market: ${stats.exitMarketFallbacks}\n`)
    process.stderr.write(`avg entry latency: ${avg(stats.entryAttemptMs)}ms | avg exit latency: ${avg(stats.exitAttemptMs)}ms\n`)
    process.stderr.write(`engine: capital=$${st.capital.toFixed(2)} rpnl=${formatUsd(st.rpnl)} fees=$${st.fees.toFixed(2)} dd=${(st.maxDdPct * 100).toFixed(2)}%\n`)
    process.stderr.write(`trades: ${st.closed.length} wins: ${w.length} WR: ${st.closed.length ? (w.length / st.closed.length * 100).toFixed(1) : '0'}%\n`)
    for (const t of st.closed) {
      process.stderr.write(`  ${t.reason.toUpperCase()} ${t.side.toUpperCase()} ${t.sizeBtc}btc ${t.entryPrice.toFixed(1)}→${t.exitPrice.toFixed(1)} net=${formatUsd(t.net)} ${Math.round(t.dur / 1000)}s\n`)
    }
    process.stderr.write('=== END ===\n\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) })
