import fs from 'node:fs'
import readline from 'node:readline'
import {
  STARTING_CAPITAL,
  PROFILES,
  StrategyEngine,
  emulateChaseEntry,
  formatUsd,
  round
} from '../core/strategy.js'
import type { Profile, Bar10s } from '../core/strategy.js'

const PROFILE: Profile = PROFILES.MAKER_OPT

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

function printReport(st: ReturnType<typeof StrategyEngine.prototype.getState>): void {
  const wins = st.closed.filter(t => t.net > 0)
  const losses = st.closed.filter(t => t.net <= 0)
  const lines = [
    '',
    `=== REPLAY REPORT: ${PROFILE.name} ===`,
    `$${STARTING_CAPITAL} → $${st.capital.toFixed(2)} (${formatUsd(st.capital - STARTING_CAPITAL)}) | DD=${(st.maxDdPct * 100).toFixed(2)}% | fees=$${st.fees.toFixed(2)}`,
    `trades=${st.closed.length} wins=${wins.length} losses=${losses.length} WR=${st.closed.length ? (wins.length / st.closed.length * 100).toFixed(1) : '0'}%`,
    ...st.closed.map(t => `  ${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`),
    '=== END ===',
    ''
  ]
  process.stderr.write(lines.join('\n'))
}

export async function loadSession(dir: string) {
  const bars10s = new Map<number, Bar10s>()
  // Track last trade price per 500ms bucket for synthetic OB generation
  const OB_BUCKET_MS = 500
  const priceBuckets = new Map<number, number>()
  const rl = readline.createInterface({ input: fs.createReadStream(dir + '/trades.jsonl') })
  for await (const line of rl) {
    const d = JSON.parse(line)
    const k = Math.floor(d.ts / 10000)
    const p = d.data.price
    const e = bars10s.get(k)
    if (!e) {
      bars10s.set(k, {
        o: p, h: p, l: p, c: p,
        buyVol: d.data.side === 'buy' ? d.data.notionalUsd : 0,
        sellVol: d.data.side === 'sell' ? d.data.notionalUsd : 0,
        n: 1,
        exVol: { [d.data.exchange]: d.data.notionalUsd }
      })
    } else {
      e.h = Math.max(e.h, p)
      e.l = Math.min(e.l, p)
      e.c = p
      if (d.data.side === 'buy') e.buyVol += d.data.notionalUsd
      else e.sellVol += d.data.notionalUsd
      e.n++
      e.exVol[d.data.exchange] = (e.exVol[d.data.exchange] || 0) + d.data.notionalUsd
    }
    // Synthetic OB: last trade price per 500ms
    priceBuckets.set(Math.floor(d.ts / OB_BUCKET_MS) * OB_BUCKET_MS, p)
  }

  let obs: { ts: number; mid: number; bid: number; ask: number }[] = []
  const obFile = dir + '/orderbook.jsonl'
  if (fs.existsSync(obFile)) {
    const rl2 = readline.createInterface({ input: fs.createReadStream(obFile) })
    for await (const line of rl2) {
      const d = JSON.parse(line)
      if (d.data.midPrice) obs.push({ ts: d.ts, mid: d.data.midPrice, bid: d.data.bestBid, ask: d.data.bestAsk })
    }
    obs.sort((a, b) => a.ts - b.ts)
  }

  // If OB is sparse (< 1 snapshot per 2 seconds), generate from trade prices.
  // Uses a minimal spread ($0.10 = 1 tick for BTCUSDT) so chase fill emulation
  // can actually trigger — the kline-based 15s OBs with $1 spread were structurally
  // unfillable (ask never drops to bid within a 3s chase window).
  const barsArr = [...bars10s.entries()].sort((a, b) => a[0] - b[0]) as [number, Bar10s][]
  const durationSec = barsArr.length * 10
  if (obs.length < durationSec / 2 && priceBuckets.size > 1000) {
    obs = [...priceBuckets.entries()]
      .sort(([a], [b]) => a - b)
      .map(([ts, mid]) => ({ ts, mid, bid: mid - 0.05, ask: mid + 0.05 }))
  }

  return { bars: barsArr, obs }
}

export function replay(
  bars: [number, Bar10s][],
  obs: { ts: number; mid: number; bid: number; ask: number }[],
  profile: Profile
) {
  const engine = new StrategyEngine(profile, STARTING_CAPITAL)
  const barsData = bars.map(([, b]) => b)
  engine.setBars(barsData)

  let obIdx = 0
  for (let i = 60; i < bars.length; i++) {
    const ts = bars[i][0] * 10000
    while (obIdx < obs.length - 1 && obs[obIdx + 1].ts <= ts) obIdx++
    const ob = obs[obIdx]
    if (!ob || Math.abs(ob.ts - ts) > 30000) continue

    engine.setBarIndex(i)
    engine.updateRange(ob.mid, ts)
    const events = engine.tick(ob.mid, ob.bid || ob.mid, ob.ask || ob.mid, ts)

    for (const evt of events) {
      if (evt.type === 'entry') {
        const result = emulateChaseEntry(evt.side, evt.refPrice, obs, obIdx, profile.maxChase, 3000)
        if (result.filled) engine.confirmEntry(evt, result.fillPrice, evt.sizeBtc, 0, ts)
        else engine.rejectEntry(evt, ts)
      } else {
        engine.confirmExit(evt, evt.targetPrice, evt.reason === 'sl' || evt.reason === 'force_close', ts)
      }
    }
  }

  const st = engine.getState()
  if (st.positions.length > 0 && obs.length > 0) {
    const last = obs.at(-1)!
    for (const pos of st.positions) {
      engine.confirmExit({ type: 'exit', positionId: pos.id, side: pos.side, sizeBtc: pos.sizeBtc, reason: 'time', targetPrice: last.mid }, last.mid, false, last.ts)
    }
  }

  return engine.getState()
}

async function runReplay(sessionDir: string): Promise<void> {
  log('REPLAY', `loading ${sessionDir}`)
  const { bars, obs } = await loadSession(sessionDir)
  log('REPLAY', `bars=${bars.length} ob=${obs.length}`)
  const st = replay(bars, obs, PROFILE)
  printReport(st)
}

async function main(): Promise<void> {
  const replayIdx = process.argv.indexOf('--replay')
  if (replayIdx === -1 || !process.argv[replayIdx + 1]) {
    throw new Error('This runner is replay-only. Use: txocap-replay <session-dir> or --replay <session-dir>. For real execution use the demo/live runners.')
  }
  await runReplay(process.argv[replayIdx + 1])
}

const isMain = process.argv[1]?.endsWith('replay.js') || process.argv[1]?.endsWith('replay.ts')
if (isMain) {
  main().catch(e => {
    process.stderr.write(`[FATAL] ${e instanceof Error ? e.stack ?? e.message : String(e)}\n`)
    process.exit(1)
  })
}
