import { hmacSha256Hex, buildBybitAuthHeaders } from './signing.js'
import type { BybitCategory, BybitInstrument } from '../../core/types.js'

interface BybitApiResponse<T> {
  retCode: number
  retMsg: string
  result: T
  retExtInfo: Record<string, unknown>
  time: number
}

interface InstrumentsResult {
  category: string
  list: BybitInstrument[]
  nextPageCursor?: string
}

function buildQuery(params: Record<string, unknown> = {}): string {
  const search = new URLSearchParams()

  for (const [key, value] of Object.entries(params).sort(([a], [b]) => a.localeCompare(b))) {
    if (value == null || value === '') {
      continue
    }

    if (Array.isArray(value)) {
      for (const item of value) {
        search.append(key, String(item))
      }
    } else {
      search.append(key, String(value))
    }
  }

  return search.toString()
}

async function parseJsonResponse<T>(response: Response): Promise<BybitApiResponse<T>> {
  const payload = (await response.json()) as BybitApiResponse<T>

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${JSON.stringify(payload)}`)
  }

  if (payload.retCode !== 0) {
    throw new Error(`Bybit retCode ${payload.retCode}: ${payload.retMsg}`)
  }

  return payload
}

export class BybitRestClient {
  private readonly publicBaseUrl: string
  private readonly privateBaseUrl: string

  constructor(
    private readonly options: {
      demoApiKey?: string
      demoApiSecret?: string
      testnetApiKey?: string
      testnetApiSecret?: string
      useTestnet?: boolean
      recvWindow: number
    }
  ) {
    if (options.useTestnet) {
      this.publicBaseUrl = 'https://api-testnet.bybit.com'
      this.privateBaseUrl = 'https://api-testnet.bybit.com'
    } else if (options.demoApiKey) {
      this.publicBaseUrl = 'https://api.bybit.com'
      this.privateBaseUrl = 'https://api-demo.bybit.com'
    } else {
      this.publicBaseUrl = 'https://api.bybit.com'
      this.privateBaseUrl = 'https://api.bybit.com'
    }
  }

  private get apiKey(): string | undefined {
    return this.options.useTestnet ? this.options.testnetApiKey : this.options.demoApiKey
  }

  private get apiSecret(): string | undefined {
    return this.options.useTestnet ? this.options.testnetApiSecret : this.options.demoApiSecret
  }

  async publicGet<T>(path: string, query: Record<string, unknown> = {}): Promise<T> {
    const queryString = buildQuery(query)
    const url = `${this.publicBaseUrl}${path}${queryString ? `?${queryString}` : ''}`
    const response = await fetch(url)
    const payload = await parseJsonResponse<T>(response)
    return payload.result
  }

  async demoGet<T>(path: string, query: Record<string, unknown> = {}): Promise<T> {
    return this.demoRequest<T>('GET', path, query)
  }

  async demoPost<T>(path: string, body: Record<string, unknown> = {}): Promise<T> {
    return this.demoRequest<T>('POST', path, {}, body)
  }

  async getInstruments(category: BybitCategory): Promise<BybitInstrument[]> {
    const instruments: BybitInstrument[] = []
    let cursor: string | undefined

    do {
      const result = await this.publicGet<InstrumentsResult>('/v5/market/instruments-info', {
        category,
        limit: 1000,
        cursor
      })

      instruments.push(...result.list)
      cursor = result.nextPageCursor || undefined
    } while (cursor)

    return instruments
  }

  async getBtcInstruments(category: BybitCategory, symbolPrefix = 'BTC'): Promise<BybitInstrument[]> {
    const allInstruments = await this.getInstruments(category)
    const USD_QUOTE_COINS = ['USDT', 'USDC', 'USD', 'USDE']

    return allInstruments.filter(instrument => {
      const symbol = instrument.symbol.toUpperCase()
      const symbolMatches = symbol.startsWith(symbolPrefix.toUpperCase())
      const baseMatches = instrument.baseCoin?.toUpperCase() === symbolPrefix.toUpperCase()
      const tradable = !instrument.status || instrument.status === 'Trading'
      if (!tradable || (!symbolMatches && !baseMatches)) return false

      if (category === 'spot' && instrument.quoteCoin) {
        return USD_QUOTE_COINS.includes(instrument.quoteCoin.toUpperCase())
      }

      if (category === 'linear') {
        return symbol === 'BTCUSDT' || symbol === 'BTCPERP'
      }

      if (category === 'inverse') {
        return symbol === 'BTCUSD'
      }

      return true
    })
  }

  async getAccountInfo(): Promise<unknown> {
    return this.demoGet('/v5/account/info')
  }

  async getWalletBalance(accountType = 'UNIFIED'): Promise<unknown> {
    return this.demoGet('/v5/account/wallet-balance', { accountType })
  }

  async getPositions(category: BybitCategory, symbol?: string): Promise<{ list: Array<Record<string, unknown>> }> {
    // NOTE: demoRequest strips the API wrapper and returns parsed.result directly.
    // This method returns { list: [...], category: '...', nextPageCursor: '...' }
    // i.e. r.list  NOT r.result.list.
    return this.demoGet('/v5/position/list', { category, symbol })
  }

  async getOpenOrders(category: BybitCategory, symbol?: string): Promise<unknown> {
    return this.demoGet('/v5/order/realtime', { category, symbol })
  }

  async placeOrder(payload: Record<string, unknown>): Promise<unknown> {
    return this.demoPost('/v5/order/create', payload)
  }

  async cancelAll(payload: Record<string, unknown>): Promise<unknown> {
    return this.demoPost('/v5/order/cancel-all', payload)
  }

  /**
   * Set exchange-managed stop-loss on the open position.
   * Uses Bybit's /v5/position/trading-stop endpoint.
   * stopLossPrice = 0 clears any existing SL.
   */
  async setStopLoss(symbol: string, stopLossPrice: number, category: BybitCategory = 'linear'): Promise<unknown> {
    return this.demoPost('/v5/position/trading-stop', {
      category,
      symbol,
      stopLoss: stopLossPrice === 0 ? '0' : stopLossPrice.toFixed(2),
      slTriggerBy: 'LastPrice',
      tpslMode:    'Full',
      positionIdx: 0,    // one-way mode
    })
  }

  async applyDemoFunds(entries: Array<{ coin: string; amountStr: string }>, adjustType = 0): Promise<unknown> {
    return this.demoPost('/v5/account/demo-apply-money', {
      adjustType,
      utaDemoApplyMoney: entries
    })
  }

  private async demoRequest<T>(
    method: 'GET' | 'POST',
    path: string,
    query: Record<string, unknown> = {},
    body?: Record<string, unknown>
  ): Promise<T> {
    const apiKey = this.apiKey
    const apiSecret = this.apiSecret

    if (!apiKey || !apiSecret) {
      throw new Error('Missing API key or secret (set BYBIT_TESTNET_API_KEY/SECRET or BYBIT_DEMO_API_KEY/SECRET)')
    }

    const queryString = buildQuery(query)
    const bodyString  = body ? JSON.stringify(body) : ''
    const payloadStr  = method === 'GET' ? queryString : bodyString
    const url         = `${this.privateBaseUrl}${path}${queryString ? `?${queryString}` : ''}`
    const headers     = buildBybitAuthHeaders(apiKey, apiSecret, this.options.recvWindow, payloadStr)

    if (method === 'POST') headers['Content-Type'] = 'application/json'

    const response = await fetch(url, {
      method,
      headers,
      body: method === 'POST' ? bodyString : undefined
    })

    const parsed = await parseJsonResponse<T>(response)
    return parsed.result
  }
}
