import readline from 'node:readline'

import { IpcClient } from '../core/ipc.js'
import { formatUsd } from '../core/strategy.js'
import type { EngineStateBroadcast } from '../core/strategy.js'
import type { TapeSignalSnapshot, TapeTrade } from '../core/types.js'

function truncate(value: string, width: number): string {
  if (width <= 0) return ''
  return value.length > width ? `${value.slice(0, Math.max(0, width - 1))}…` : value
}

function formatCompactUsd(value: number): string {
  const abs = Math.abs(value)
  if (abs >= 1_000_000_000) return `$${(value / 1_000_000_000).toFixed(2)}b`
  if (abs >= 1_000_000) return `$${(value / 1_000_000).toFixed(2)}m`
  if (abs >= 1_000) return `$${(value / 1_000).toFixed(1)}k`
  return `$${value.toFixed(0)}`
}

function formatUptime(ms: number): string {
  const total = Math.max(0, Math.floor(ms / 1000))
  const h = Math.floor(total / 3600)
  const m = Math.floor((total % 3600) / 60)
  const s = total % 60
  return [h, m, s].map(v => String(v).padStart(2, '0')).join(':')
}

function formatAge(ms: number): string {
  const s = Math.max(0, Math.floor(ms / 1000))
  if (s < 60) return `${s}s`
  const m = Math.floor(s / 60)
  return `${m}m${s % 60}s`
}

function padSection(lines: string[], count: number): string[] {
  const out = lines.slice(0, count)
  while (out.length < count) out.push('')
  return out
}

function sideLabel(side: 'long' | 'short'): string {
  return side === 'long' ? 'LONG ' : 'SHORT'
}

function modeLabel(mode?: EngineStateBroadcast['mode']): string {
  if (!mode) return 'WAITING'
  if (mode === 'demo') return 'DEMO'
  if (mode === 'live') return 'LIVE'
  return 'REPLAY'
}

interface ExchangeTotals {
  trades: number
  notionalUsd: number
  buyNotionalUsd: number
  lastSeen: number
}

class Dashboard {
  private readonly startedAt = Date.now()
  private readonly recentTrades: TapeTrade[] = []
  private readonly totalsByExchange = new Map<string, ExchangeTotals>()

  private latestTrade?: TapeTrade
  private latestTradeAt = 0
  private latestSignal?: TapeSignalSnapshot
  private latestSignalAt = 0
  private latestEngineState?: EngineStateBroadcast
  private latestEngineStateAt = 0

  private signalCount = 0
  private tradesTotal = 0
  private connected = false
  private disconnectedAt = 0
  private stopped = false
  private renderTimer?: NodeJS.Timeout
  private keypressHandler?: (str: string, key: { name?: string; ctrl?: boolean }) => void

  start(): void {
    if (!process.stdout.isTTY) throw new Error('dashboard requires a TTY')
    process.stdout.write('\x1b[?1049h\x1b[?25l')
    readline.emitKeypressEvents(process.stdin)
    if (process.stdin.isTTY) process.stdin.setRawMode(true)
    this.keypressHandler = (_s, key) => {
      if ((key.ctrl && key.name === 'c') || key.name === 'q') process.kill(process.pid, 'SIGINT')
      if (key.name === 'c') {
        this.recentTrades.length = 0
        this.totalsByExchange.clear()
        this.tradesTotal = 0
        this.signalCount = 0
      }
    }
    process.stdin.on('keypress', this.keypressHandler)
    this.renderTimer = setInterval(() => this.render(), 500)
    this.render()
  }

  stop(): void {
    if (this.stopped) return
    this.stopped = true
    if (this.renderTimer) clearInterval(this.renderTimer)
    if (this.keypressHandler) {
      process.stdin.off('keypress', this.keypressHandler)
      if (process.stdin.isTTY) process.stdin.setRawMode(false)
    }
    process.stdout.write('\x1b[?25h\x1b[?1049l')
  }

  onConnect(): void { this.connected = true }
  onDisconnect(): void { this.connected = false; this.disconnectedAt = Date.now() }

  onTrade(trade: TapeTrade): void {
    this.latestTrade = trade
    this.latestTradeAt = Date.now()
    this.tradesTotal++

    // Hide tiny zero-ish prints from cluttering the tape
    if (trade.notionalUsd >= 1) {
      this.recentTrades.push(trade)
      if (this.recentTrades.length > 60) this.recentTrades.shift()
    }

    const totals = this.totalsByExchange.get(trade.exchange) ?? {
      trades: 0,
      notionalUsd: 0,
      buyNotionalUsd: 0,
      lastSeen: 0
    }
    totals.trades++
    totals.notionalUsd += trade.notionalUsd
    if (trade.side === 'buy') totals.buyNotionalUsd += trade.notionalUsd
    totals.lastSeen = trade.timestamp
    this.totalsByExchange.set(trade.exchange, totals)
  }

  onSignal(snapshot: TapeSignalSnapshot): void {
    this.latestSignal = snapshot
    this.latestSignalAt = Date.now()
    this.signalCount++
  }

  onEngineState(state: EngineStateBroadcast): void {
    this.latestEngineState = state
    this.latestEngineStateAt = Date.now()
  }

  private strategyStatus(): string {
    const es = this.latestEngineState
    if (!es) return 'waiting for strategy state...'
    if (es.barCount < 60) return `WARMING UP: ${es.barCount}/60 bars collected before trading starts`
    if (es.positions.length > 0) {
      const p = es.positions[0]
      return `OPEN ${sideLabel(p.side)} ${p.sizeBtc}btc @${p.entryPrice.toFixed(1)} | uPnL ${p.unrealizedBps >= 0 ? '+' : ''}${p.unrealizedBps.toFixed(1)}bps | stop ${p.stopPrice.toFixed(1)} | tp1 ${p.tp1Price.toFixed(1)}`
    }
    if (es.realizedVol < 5) return `WAITING: volatility too low (${es.realizedVol.toFixed(2)} < 5.00 bps)`
    if (es.realizedVol > 7) return `WAITING: volatility too high (${es.realizedVol.toFixed(2)} > 7.00 bps)`
    if (es.rangeBps < 10) return `WAITING: range too tight (${es.rangeBps.toFixed(1)} < 10.0 bps)`
    return 'ARMED: tradeable regime, waiting for a valid signal'
  }

  private marketSummary(width: number): string[] {
    const sig = this.latestSignal
    const es = this.latestEngineState
    const lines: string[] = []

    if (!sig) {
      lines.push('  waiting for market data...')
      return lines
    }

    const slowRange = sig.slow.highPrice && sig.slow.lowPrice && sig.slow.vwap
      ? ((sig.slow.highPrice - sig.slow.lowPrice) / sig.slow.vwap * 10000)
      : null
    const absorption = sig.fast.deltaRatio * sig.fast.priceMoveBps < 0 && Math.abs(sig.fast.deltaRatio) > 0.25 && Math.abs(sig.fast.priceMoveBps) > 0.5
    const topEx = sig.fast.exchangeBreakdown.slice(0, 4).map(e => `${e.exchange}:${(e.share * 100).toFixed(0)}%`).join(' ')
    const lastTradeAge = this.latestTrade ? formatAge(Date.now() - this.latestTrade.timestamp) : 'n/a'

    lines.push(truncate(
      `  price ${sig.fast.lastPrice?.toFixed(1) ?? '?'} | move ${sig.fast.priceMoveBps >= 0 ? '+' : ''}${sig.fast.priceMoveBps.toFixed(2)}bps | delta ${(sig.fast.deltaRatio * 100).toFixed(1)}% | spread ${sig.fast.spreadBps?.toFixed(2) ?? '?'}bps`,
      width
    ))
    lines.push(truncate(
      `  fast flow ${formatCompactUsd(sig.fast.totalNotionalUsd)}/${(sig.fast.windowMs / 1000).toFixed(0)}s | slow range ${slowRange != null ? slowRange.toFixed(1) : '?'}bps | rv ${es ? es.realizedVol.toFixed(2) : '?'}bps | tape ${lastTradeAge} ago${absorption ? ' | ABSORPTION' : ''}`,
      width
    ))
    lines.push(truncate(`  venues ${topEx}`, width))
    return lines
  }

  private accountSummary(width: number): string[] {
    const es = this.latestEngineState
    if (!es) return ['  waiting for account / engine state...']

    const bal = es.demoBalanceUsd
    const actualPnl = bal != null && es.startingEquity > 0 ? bal - es.startingEquity : null
    const retPct = actualPnl != null && es.startingEquity > 0 ? (actualPnl / es.startingEquity * 100).toFixed(2) : '0.00'
    const rows = [
      truncate(`  balance $${bal != null ? bal.toFixed(2) : 'n/a'} (${retPct}%) | start $${es.startingEquity.toFixed(0)} | pnl ${actualPnl != null ? formatUsd(Math.round(actualPnl * 100) / 100) : 'n/a'} | max DD ${(es.maxDrawdownPct * 100).toFixed(2)}%`, width),
      truncate(`  positions ${es.openCount} | closed ${es.closedCount} | bars ${es.barCount} | range ${es.rangeBps.toFixed(1)}bps | rv ${es.realizedVol.toFixed(2)}bps`, width)
    ]

    if (es.positions.length > 0) {
      for (const p of es.positions.slice(0, 2)) {
        rows.push(truncate(`  ${sideLabel(p.side)} ${p.sizeBtc}btc @${p.entryPrice.toFixed(1)} | uPnL ${p.unrealizedBps >= 0 ? '+' : ''}${p.unrealizedBps.toFixed(1)}bps | hold ${Math.round(p.holdMs / 1000)}s | stop ${p.stopPrice.toFixed(1)} | tp1 ${p.tp1Price.toFixed(1)}`, width))
      }
    } else {
      rows.push('  flat')
    }

    return rows
  }

  private recentExecution(width: number): string[] {
    const es = this.latestEngineState
    if (!es || es.closedTrades.length === 0) return ['  no completed trades yet']

    return es.closedTrades.slice(-3).reverse().map(t => {
      const pnl = t.netPnlUsd >= 0 ? `+${Math.abs(t.netPnlUsd).toFixed(2)}` : `-${Math.abs(t.netPnlUsd).toFixed(2)}`
      return truncate(`  ${pnl.padStart(7)} | ${t.exitReason.toUpperCase().padEnd(4)} | ${sideLabel(t.side)} ${t.sizeBtc}btc | ${t.entryPrice.toFixed(1)}→${t.exitPrice.toFixed(1)} | ${Math.round(t.durationMs / 1000)}s`, width)
    })
  }

  private exchangeSummary(width: number, now: number): string[] {
    const rows = [...this.totalsByExchange.entries()]
      .sort((a, b) => b[1].notionalUsd - a[1].notionalUsd)
      .slice(0, 5)
      .map(([exchange, totals]) => {
        const buyShare = totals.notionalUsd > 0 ? (totals.buyNotionalUsd / totals.notionalUsd) * 100 : 0
        return truncate(`  ${exchange.padEnd(12)} ${formatCompactUsd(totals.notionalUsd).padStart(8)} | buy ${buyShare.toFixed(0).padStart(3)}% | ${formatAge(now - totals.lastSeen).padStart(5)} ago`, width)
      })
    return rows.length > 0 ? rows : ['  waiting for venue data...']
  }

  private tapeRows(width: number, height: number): string[] {
    const trades = this.recentTrades.slice(-height).reverse()
    if (trades.length === 0) return ['  waiting for trades...']
    return trades.map(t => {
      const time = new Date(t.timestamp).toISOString().slice(11, 19)
      return truncate(`  ${time} ${t.exchange.padEnd(11)} ${t.side.toUpperCase().padEnd(4)} ${t.size.toFixed(4).padStart(8)} @ ${t.price.toFixed(1).padStart(9)} ${formatCompactUsd(t.notionalUsd).padStart(9)}`, width)
    })
  }

  private render(): void {
    if (!process.stdout.isTTY) return
    const width = process.stdout.columns || 120
    const height = process.stdout.rows || 40
    const now = Date.now()
    const lines: string[] = []
    const rule = '─'.repeat(Math.max(20, Math.min(width, 140)))

    const es = this.latestEngineState
    const mode = modeLabel(es?.mode)
    const serverUp = es?.serverUptimeMs != null ? formatUptime(es.serverUptimeMs) : 'n/a'
    const feedAge = this.latestTradeAt > 0 ? formatAge(now - this.latestTradeAt) : 'n/a'
    const stateAge = this.latestEngineStateAt > 0 ? formatAge(now - this.latestEngineStateAt) : 'n/a'
    const stale = !this.connected || (this.latestEngineStateAt > 0 && now - this.latestEngineStateAt > 10_000)
    const connLabel = !this.connected
      ? (this.disconnectedAt > 0 ? `DISCONNECTED ${formatAge(now - this.disconnectedAt)} ago` : 'CONNECTING')
      : stale ? 'STALE' : 'OK'

    lines.push(truncate(`txocap | ${mode} | server ${serverUp} | ipc ${connLabel} | trades ${this.tradesTotal} | q quit | c clear`, width))
    lines.push(truncate(`feed: trade ${feedAge} ago | engine ${stateAge} ago`, width))

    lines.push(rule)
    lines.push('Market')
    lines.push(...padSection(this.marketSummary(width), 3))

    lines.push(rule)
    lines.push('Strategy')
    lines.push(...padSection([
      truncate(`  ${this.strategyStatus()}`, width),
      truncate(`  ${this.latestSignal ? `latest signal: ${this.latestSignal.composite.direction.toUpperCase()} score ${this.latestSignal.composite.score.toFixed(2)}` : 'latest signal: waiting...'}`, width)
    ], 2))

    lines.push(rule)
    lines.push('Account')
    lines.push(...padSection(this.accountSummary(width), 4))

    lines.push(rule)
    lines.push('Recent execution')
    lines.push(...padSection(this.recentExecution(width), 4))

    lines.push(rule)
    lines.push('Venue flow')
    lines.push(...padSection(this.exchangeSummary(width, now), 5))

    lines.push(rule)
    lines.push('Recent tape')
    const reserved = lines.length + 1
    const tapeHeight = Math.max(4, height - reserved)
    lines.push(...this.tapeRows(width, tapeHeight))

    process.stdout.write('\x1b[H\x1b[2J')
    process.stdout.write(lines.slice(0, height - 1).join('\n'))
    process.stdout.write('\n')
  }
}

async function main(): Promise<void> {
  const dashboard = new Dashboard()
  dashboard.start()

  const tape = new IpcClient('tape')
  tape.on('connect', () => dashboard.onConnect())
  tape.on('disconnect', () => dashboard.onDisconnect())
  tape.on('trade', (data: TapeTrade) => dashboard.onTrade(data))
  tape.on('signal', (data: TapeSignalSnapshot) => dashboard.onSignal(data))
  tape.on('engine_state', (data: EngineStateBroadcast) => dashboard.onEngineState(data))
  tape.connect()

  const shutdown = (): void => {
    tape.disconnect()
    dashboard.stop()
    process.exit(0)
  }
  process.on('SIGINT', shutdown)
  process.on('SIGTERM', shutdown)
}

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