import { hmacSha256Hex, buildBybitAuthHeaders } from '../lib/bybit/signing.js'

export interface DemoOrderResult {
  orderId: string
  fillPrice: number | null
  filled: boolean
  cancelled: boolean
  side: 'Buy' | 'Sell'
  qty: string
  avgPrice: string
}

const DEMO_BASE = 'https://api-demo.bybit.com'

async function demoRequest(method: 'GET' | 'POST', path: string, apiKey: string, apiSecret: string, recvWindow: number, params: Record<string, unknown> = {}): Promise<any> {
  let url  = DEMO_BASE + path
  let body: string | undefined
  let payloadStr: string

  if (method === 'GET') {
    const qs = new URLSearchParams(params as Record<string, string>).toString()
    if (qs) url += '?' + qs
    payloadStr = qs
  } else {
    body = JSON.stringify(params)
    payloadStr = body
  }

  const headers = buildBybitAuthHeaders(apiKey, apiSecret, recvWindow, payloadStr)
  if (method === 'POST') headers['Content-Type'] = 'application/json'

  const res = await fetch(url, { method, headers, body })
  return res.json()
}

export class DemoExecutor {
  constructor(
    private readonly apiKey: string,
    private readonly apiSecret: string,
    private readonly recvWindow = 5000
  ) {}

  private post(path: string, body: Record<string, unknown>) {
    return demoRequest('POST', path, this.apiKey, this.apiSecret, this.recvWindow, body)
  }

  private get(path: string, query: Record<string, string>) {
    return demoRequest('GET', path, this.apiKey, this.apiSecret, this.recvWindow, query)
  }

  async setLeverage(leverage: number): Promise<boolean> {
    const r = await this.post('/v5/position/set-leverage', {
      category: 'linear', symbol: 'BTCUSDT',
      buyLeverage: String(leverage), sellLeverage: String(leverage)
    })
    return r.retCode === 0 || r.retMsg?.includes('not modified')
  }

  async getBalance(): Promise<number> {
    const r = await this.get('/v5/account/wallet-balance', { accountType: 'UNIFIED' })
    if (r.retCode !== 0) {
      throw new Error(`Bybit balance error ${r.retCode}: ${r.retMsg ?? 'unknown'}`)
    }
    const usdt = r.result?.list?.[0]?.coin?.find((c: any) => c.coin === 'USDT')
    if (!usdt || usdt.walletBalance === undefined || usdt.walletBalance === null || usdt.walletBalance === '') {
      throw new Error('Bybit balance response missing USDT walletBalance')
    }
    const bal = parseFloat(usdt.walletBalance)
    if (!Number.isFinite(bal) || bal < 0) {
      throw new Error(`Bybit balance response invalid walletBalance=${usdt.walletBalance}`)
    }
    return bal
  }

  async getPosition(): Promise<{ side: string; size: number; entry: number; uPnL: number } | null> {
    const r = await this.get('/v5/position/list', { category: 'linear', symbol: 'BTCUSDT' })
    const p = r.result?.list?.[0]
    if (!p || parseFloat(p.size) === 0) return null
    return { side: p.side, size: parseFloat(p.size), entry: parseFloat(p.avgPrice), uPnL: parseFloat(p.unrealisedPnl || '0') }
  }

  async placeLimitOrder(side: 'Buy' | 'Sell', qty: number, price: number, reduceOnly = false): Promise<{ orderId: string } | null> {
    const r = await this.post('/v5/order/create', {
      category: 'linear', symbol: 'BTCUSDT',
      side, orderType: 'Limit',
      qty: qty.toFixed(3), price: price.toFixed(1),
      timeInForce: 'PostOnly',
      reduceOnly
    })
    if (r.retCode !== 0) return null
    return { orderId: r.result.orderId }
  }

  async placeMarketOrder(side: 'Buy' | 'Sell', qty: number, reduceOnly = false): Promise<{ orderId: string } | null> {
    const r = await this.post('/v5/order/create', {
      category: 'linear', symbol: 'BTCUSDT',
      side, orderType: 'Market',
      qty: qty.toFixed(3), timeInForce: 'IOC',
      reduceOnly
    })
    if (r.retCode !== 0) return null
    return { orderId: r.result.orderId }
  }

  async cancelOrder(orderId: string): Promise<boolean> {
    const r = await this.post('/v5/order/cancel', { category: 'linear', symbol: 'BTCUSDT', orderId })
    return r.retCode === 0
  }

  async getOrderStatus(orderId: string): Promise<{ status: string; avgPrice: number; cumExecQty: number } | null> {
    let r = await this.get('/v5/order/realtime', { category: 'linear', symbol: 'BTCUSDT', orderId })
    let o = r.result?.list?.[0]
    // Filled/cancelled orders can disappear from realtime and move to history.
    // Fall back so runner logic can distinguish a verified maker fill from an
    // exchange-side position disappearance.
    if (!o) {
      r = await this.get('/v5/order/history', { category: 'linear', symbol: 'BTCUSDT', orderId })
      o = r.result?.list?.[0]
    }
    if (!o) return null
    return { status: o.orderStatus, avgPrice: parseFloat(o.avgPrice || '0'), cumExecQty: parseFloat(o.cumExecQty || '0') }
  }

  async chaseLimitOrder(
    side: 'Buy' | 'Sell',
    qty: number,
    getBestPrice: () => number | null,
    maxAttempts = 3,
    waitMs = 3000,
    reduceOnly = false
  ): Promise<{ filled: boolean; avgPrice: number; orderId: string | null; attempts: number; elapsedMs: number }> {
    const startedAt = Date.now()
    const normPrice = (p: number): number => Math.round(p * 10) / 10
    let prevDesired: number | null = null
    let activeOrderId: string | null = null
    let activePrice: number | null = null
    let placements = 0

    for (let attempt = 0; attempt < maxAttempts; attempt++) {
      const rawPrice = getBestPrice()
      if (!rawPrice) { await this.sleep(waitMs); continue }
      const desiredPrice = normPrice(rawPrice)

      // Signal-decay gate: if price moves away from signal direction during the
      // chase, abort and let the signal re-evaluate on the next bar. Compute the
      // new desired price BEFORE touching the existing order; if it is unchanged,
      // leave the order alone and keep queue priority.
      if (attempt > 0 && prevDesired !== null && !reduceOnly) {
        const decaying = side === 'Buy' ? desiredPrice < prevDesired : desiredPrice > prevDesired
        if (decaying) {
          if (activeOrderId) await this.cancelOrder(activeOrderId).catch(() => {})
          return { filled: false, avgPrice: 0, orderId: null, attempts: placements, elapsedMs: Date.now() - startedAt }
        }
      }
      prevDesired = desiredPrice

      // Only cancel/replace if the desired price actually changed. If the next
      // price is identical, do not touch the live PostOnly order.
      if (!activeOrderId) {
        const order = await this.placeLimitOrder(side, qty, desiredPrice, reduceOnly)
        if (!order) { await this.sleep(waitMs); continue }
        activeOrderId = order.orderId
        activePrice = desiredPrice
        placements++
      } else if (activePrice !== desiredPrice) {
        await this.cancelOrder(activeOrderId).catch(() => {})
        activeOrderId = null
        activePrice = null
        const order = await this.placeLimitOrder(side, qty, desiredPrice, reduceOnly)
        if (!order) { await this.sleep(waitMs); continue }
        activeOrderId = order.orderId
        activePrice = desiredPrice
        placements++
      }

      await this.sleep(waitMs)
      if (!activeOrderId) continue

      const status = await this.getOrderStatus(activeOrderId)
      if (!status) {
        // Unknown/disappeared without historical fill. Cancel defensively and let
        // the next loop iteration decide whether to recreate at a new price.
        await this.cancelOrder(activeOrderId).catch(() => {})
        activeOrderId = null
        activePrice = null
        continue
      }

      if (status.status === 'Filled') {
        return { filled: true, avgPrice: status.avgPrice, orderId: activeOrderId, attempts: placements, elapsedMs: Date.now() - startedAt }
      }

      if (status.cumExecQty > 0) {
        // Partial fill — accept it and cancel any remainder.
        await this.cancelOrder(activeOrderId).catch(() => {})
        return { filled: true, avgPrice: status.avgPrice, orderId: activeOrderId, attempts: placements, elapsedMs: Date.now() - startedAt }
      }

      if (status.status === 'Cancelled' || status.status === 'Rejected') {
        activeOrderId = null
        activePrice = null
      }
      // Otherwise order remains live. Next loop computes desiredPrice first; if
      // unchanged, the order is left untouched.
    }

    if (activeOrderId) await this.cancelOrder(activeOrderId).catch(() => {})
    return { filled: false, avgPrice: 0, orderId: null, attempts: placements, elapsedMs: Date.now() - startedAt }
  }

  private sleep(ms: number) { return new Promise(r => setTimeout(r, ms)) }
}
