import { EventEmitter } from 'node:events'

import type { SignalComponent, SignalDirection, TapeSignalSnapshot, TapeTrade, TapeWindowStats, TapeExchangeBreakdown } from '../core/types.js'

interface SecondBucket {
  second: number
  tradeCount: number
  buyCount: number
  sellCount: number
  buyNotionalUsd: number
  sellNotionalUsd: number
  largeTradeCount: number
  largestTradeUsd: number
  firstPrice: number
  lastPrice: number
  highPrice: number
  lowPrice: number
  priceSum: number
  priceCount: number
  cexNotionalUsd: number
  dexNotionalUsd: number
  exchangeTradeCount: Record<string, number>
  exchangeNotionalUsd: Record<string, number>
}

const DEFAULT_EXCHANGE_WEIGHTS: Record<string, number> = {
  BYBIT: 1.0,
  BINANCE: 0.95,
  OKX: 0.9,
  COINBASE: 0.85,
  KRAKEN: 0.8,
  HYPERLIQUID: 0.6,
  DYDX: 0.5
}

function clamp(value: number, min: number, max: number): number {
  return Math.min(max, Math.max(min, value))
}

function directionFromScore(score: number, threshold = 0.2): SignalDirection {
  if (score >= threshold) {
    return 'long'
  }

  if (score <= -threshold) {
    return 'short'
  }

  return 'flat'
}

function round(value: number, decimals = 4): number {
  const factor = 10 ** decimals
  return Math.round(value * factor) / factor
}

function mergeRecord(target: Record<string, number>, source: Record<string, number>): void {
  for (const [key, value] of Object.entries(source)) {
    target[key] = (target[key] ?? 0) + value
  }
}

export class TapeSignalEngine extends EventEmitter {
  private readonly buckets = new Map<number, SecondBucket>()
  private readonly fastWindowSeconds: number
  private readonly slowWindowSeconds: number
  private emitTimer?: NodeJS.Timeout
  private latestTimestamp = Date.now()

  constructor(
    private readonly options: {
      fastWindowMs: number
      slowWindowMs: number
      emitIntervalMs: number
      largeTradeUsd: number
    }
  ) {
    super()
    this.fastWindowSeconds = Math.max(1, Math.ceil(options.fastWindowMs / 1000))
    this.slowWindowSeconds = Math.max(this.fastWindowSeconds, Math.ceil(options.slowWindowMs / 1000))
  }

  start(): void {
    this.emitTimer = setInterval(() => {
      const snapshot = this.buildSnapshot()
      if (snapshot) {
        this.emit('snapshot', snapshot)
      }
    }, this.options.emitIntervalMs)
  }

  stop(): void {
    if (this.emitTimer) {
      clearInterval(this.emitTimer)
      this.emitTimer = undefined
    }
  }

  ingest(trade: TapeTrade): void {
    const second = Math.floor(trade.timestamp / 1000)
    const exchangeWeight = DEFAULT_EXCHANGE_WEIGHTS[trade.exchange] ?? 1
    const weightedNotionalUsd = trade.notionalUsd * exchangeWeight
    const bucket = this.buckets.get(second) ?? {
      second,
      tradeCount: 0,
      buyCount: 0,
      sellCount: 0,
      buyNotionalUsd: 0,
      sellNotionalUsd: 0,
      largeTradeCount: 0,
      largestTradeUsd: 0,
      firstPrice: trade.price,
      lastPrice: trade.price,
      highPrice: trade.price,
      lowPrice: trade.price,
      priceSum: 0,
      priceCount: 0,
      cexNotionalUsd: 0,
      dexNotionalUsd: 0,
      exchangeTradeCount: {},
      exchangeNotionalUsd: {}
    }

    bucket.tradeCount += 1
    bucket.lastPrice = trade.price
    bucket.highPrice = Math.max(bucket.highPrice, trade.price)
    bucket.lowPrice = Math.min(bucket.lowPrice, trade.price)
    bucket.priceSum += trade.price
    bucket.priceCount += 1
    bucket.largestTradeUsd = Math.max(bucket.largestTradeUsd, trade.notionalUsd)
    bucket.exchangeTradeCount[trade.exchange] = (bucket.exchangeTradeCount[trade.exchange] ?? 0) + 1
    bucket.exchangeNotionalUsd[trade.exchange] = (bucket.exchangeNotionalUsd[trade.exchange] ?? 0) + weightedNotionalUsd

    if (trade.exchangeClass === 'cex') {
      bucket.cexNotionalUsd += weightedNotionalUsd
    } else {
      bucket.dexNotionalUsd += weightedNotionalUsd
    }

    if (trade.side === 'buy') {
      bucket.buyCount += 1
      bucket.buyNotionalUsd += weightedNotionalUsd
    } else {
      bucket.sellCount += 1
      bucket.sellNotionalUsd += weightedNotionalUsd
    }

    if (trade.notionalUsd >= this.options.largeTradeUsd) {
      bucket.largeTradeCount += 1
    }

    this.buckets.set(second, bucket)
    this.latestTimestamp = trade.timestamp
    this.pruneBuckets(second)
  }

  private pruneBuckets(currentSecond: number): void {
    const oldestToKeep = currentSecond - this.slowWindowSeconds - 5

    for (const second of this.buckets.keys()) {
      if (second < oldestToKeep) {
        this.buckets.delete(second)
      }
    }
  }

  private buildSnapshot(): TapeSignalSnapshot | null {
    const nowSecond = Math.floor(this.latestTimestamp / 1000)
    const fast = this.collectWindow(nowSecond, this.fastWindowSeconds)
    const slow = this.collectWindow(nowSecond, this.slowWindowSeconds)

    if (fast.tradeCount === 0 || slow.tradeCount === 0) {
      return null
    }

    const deltaScore = clamp(fast.deltaRatio / 0.35, -1, 1)
    const priceScore = clamp(fast.priceMoveBps / 12, -1, 1)
    const signedDriver = Math.sign(Math.abs(deltaScore) >= Math.abs(priceScore) ? deltaScore : priceScore) || 0
    const burstFactor = slow.notionalPerSecond > 0 ? fast.notionalPerSecond / slow.notionalPerSecond : 1
    const activityScore = clamp((burstFactor - 1) / 2, 0, 1) * signedDriver
    const largePrintScore = clamp(fast.largeTradeCount / 5, 0, 1) * Math.sign(fast.deltaRatio)
    const compositeScore = round(deltaScore * 0.45 + priceScore * 0.35 + activityScore * 0.1 + largePrintScore * 0.1)

    const components: SignalComponent[] = [
      {
        name: 'delta-pressure',
        score: round(deltaScore),
        direction: directionFromScore(deltaScore, 0.15),
        note: `delta ${(fast.deltaRatio * 100).toFixed(1)}%`
      },
      {
        name: 'price-impulse',
        score: round(priceScore),
        direction: directionFromScore(priceScore, 0.15),
        note: `${fast.priceMoveBps >= 0 ? '+' : ''}${fast.priceMoveBps.toFixed(2)} bps`
      },
      {
        name: 'activity-spike',
        score: round(activityScore),
        direction: directionFromScore(activityScore, 0.1),
        note: `${burstFactor.toFixed(2)}x baseline`
      },
      {
        name: 'large-print-pressure',
        score: round(largePrintScore),
        direction: directionFromScore(largePrintScore, 0.1),
        note: `${fast.largeTradeCount} large prints`
      }
    ]

    const reasons: string[] = []
    const topExchange = fast.exchangeBreakdown[0]

    if (Math.abs(fast.deltaRatio) >= 0.12) {
      reasons.push(`${fast.deltaRatio > 0 ? 'buy' : 'sell'} aggressor dominance ${(Math.abs(fast.deltaRatio) * 100).toFixed(1)}%`)
    }

    if (Math.abs(fast.priceMoveBps) >= 3) {
      reasons.push(`price move ${fast.priceMoveBps >= 0 ? '+' : ''}${fast.priceMoveBps.toFixed(2)}bps over ${(fast.windowMs / 1000).toFixed(0)}s`)
    }

    if (burstFactor >= 1.5) {
      reasons.push(`activity ${burstFactor.toFixed(2)}x slower-window baseline`)
    }

    if (fast.largeTradeCount > 0) {
      reasons.push(`${fast.largeTradeCount} large prints, max $${fast.largestTradeUsd.toFixed(0)}`)
    }

    if (topExchange && topExchange.share >= 0.25) {
      reasons.push(`${topExchange.exchange} ${(topExchange.share * 100).toFixed(0)}% of fast-window flow`)
    }

    if (fast.dexNotionalUsd > 0 && fast.totalNotionalUsd > 0) {
      reasons.push(`dex share ${((fast.dexNotionalUsd / fast.totalNotionalUsd) * 100).toFixed(0)}%`)
    }

    return {
      asOf: this.latestTimestamp,
      fast,
      slow,
      components,
      composite: {
        name: 'composite',
        score: compositeScore,
        direction: directionFromScore(compositeScore),
        note: `confidence ${(Math.abs(compositeScore) * 100).toFixed(0)}%`
      },
      reasons
    }
  }

  private collectWindow(nowSecond: number, windowSeconds: number): TapeWindowStats {
    let tradeCount = 0
    let buyCount = 0
    let sellCount = 0
    let buyNotionalUsd = 0
    let sellNotionalUsd = 0
    let largeTradeCount = 0
    let largestTradeUsd = 0
    let firstPrice: number | null = null
    let lastPrice: number | null = null
    let highPrice: number | null = null
    let lowPrice: number | null = null
    let priceSum = 0
    let priceCount = 0
    let cexNotionalUsd = 0
    let dexNotionalUsd = 0

    const exchangeTradeCount: Record<string, number> = {}
    const exchangeNotionalUsd: Record<string, number> = {}

    for (let second = nowSecond - windowSeconds + 1; second <= nowSecond; second += 1) {
      const bucket = this.buckets.get(second)
      if (!bucket) {
        continue
      }

      tradeCount += bucket.tradeCount
      buyCount += bucket.buyCount
      sellCount += bucket.sellCount
      buyNotionalUsd += bucket.buyNotionalUsd
      sellNotionalUsd += bucket.sellNotionalUsd
      largeTradeCount += bucket.largeTradeCount
      largestTradeUsd = Math.max(largestTradeUsd, bucket.largestTradeUsd)
      cexNotionalUsd += bucket.cexNotionalUsd
      dexNotionalUsd += bucket.dexNotionalUsd

      mergeRecord(exchangeTradeCount, bucket.exchangeTradeCount)
      mergeRecord(exchangeNotionalUsd, bucket.exchangeNotionalUsd)

      if (firstPrice == null) {
        firstPrice = bucket.firstPrice
      }

      lastPrice = bucket.lastPrice
      highPrice = highPrice == null ? bucket.highPrice : Math.max(highPrice, bucket.highPrice)
      lowPrice = lowPrice == null ? bucket.lowPrice : Math.min(lowPrice, bucket.lowPrice)
      priceSum += bucket.priceSum
      priceCount += bucket.priceCount
    }

    const totalNotionalUsd = buyNotionalUsd + sellNotionalUsd
    const deltaRatio = totalNotionalUsd > 0 ? (buyNotionalUsd - sellNotionalUsd) / totalNotionalUsd : 0
    const priceMoveBps = firstPrice && lastPrice ? ((lastPrice - firstPrice) / firstPrice) * 10_000 : 0
    const vwap = priceCount > 0 ? round(priceSum / priceCount, 2) : null
    const spreadBps = highPrice && lowPrice && vwap ? round(((highPrice - lowPrice) / vwap) * 10_000, 2) : 0
    const exchangeBreakdown: TapeExchangeBreakdown[] = Object.entries(exchangeNotionalUsd)
      .map(([exchange, notionalUsd]) => ({
        exchange,
        tradeCount: exchangeTradeCount[exchange] ?? 0,
        notionalUsd: round(notionalUsd, 2),
        share: totalNotionalUsd > 0 ? round(notionalUsd / totalNotionalUsd) : 0
      }))
      .sort((left, right) => right.notionalUsd - left.notionalUsd)

    return {
      windowMs: windowSeconds * 1000,
      tradeCount,
      buyCount,
      sellCount,
      totalNotionalUsd: round(totalNotionalUsd, 2),
      buyNotionalUsd: round(buyNotionalUsd, 2),
      sellNotionalUsd: round(sellNotionalUsd, 2),
      deltaRatio: round(deltaRatio),
      largeTradeCount,
      largestTradeUsd: round(largestTradeUsd, 2),
      firstPrice,
      lastPrice,
      highPrice,
      lowPrice,
      vwap,
      spreadBps,
      priceMoveBps: round(priceMoveBps, 2),
      notionalPerSecond: round(totalNotionalUsd / windowSeconds, 2),
      cexNotionalUsd: round(cexNotionalUsd, 2),
      dexNotionalUsd: round(dexNotionalUsd, 2),
      exchangeBreakdown
    }
  }
}
