/**
 * TradingView Datafeed implementation using WebSocket backend
 *
 * This adapter translates TradingView's Datafeed API calls into
 * WebSocket messages that match the backend protocol defined in
 * backend/src/datasource/websocket_protocol.py
 *
 * Uses the shared WebSocket connection for all communication.
 */

import { wsManager, type MessageHandler } from './useWebSocket'
import { intervalToSeconds } from '../utils'
import type {
  IBasicDataFeed,
  DatafeedConfiguration,
  LibrarySymbolInfo,
  Bar,
  SearchSymbolResultItem,
  HistoryMetadata
} from '../types/tradingview'

interface PendingRequest {
  resolve: (value: any) => void
  reject: (reason: any) => void
  timeout: number
}

interface Subscription {
  symbolInfo: LibrarySymbolInfo
  resolution: string
  onTick: (bar: Bar) => void
  subscriptionId: string
}

interface SymbolDenominators {
  tick: number
  base: number
}

export class WebSocketDatafeed implements IBasicDataFeed {
  private pendingRequests: Map<string, PendingRequest> = new Map()
  private subscriptions: Map<string, Subscription> = new Map()
  private requestTimeout = 10000      // 10 seconds (used for all requests except getBars)
  private barsRequestTimeout = 60000  // 60 seconds (cold-cache pipeline may take a while)
  private configuration: DatafeedConfiguration | null = null
  private messageHandler: MessageHandler
  private symbolDenominators: Map<string, SymbolDenominators> = new Map() // Track denominators per symbol
  // Tracks the last bar time (ms) returned by getBars per "symbolKey_periodSeconds".
  // bar_updates with time < this watermark are stale and already covered by history.
  private lastBarTimes: Map<string, number> = new Map()

  constructor() {
    // Use the shared WebSocket connection (managed by App.vue authentication)
    // Register message handler for datafeed messages
    this.messageHandler = this.handleMessage.bind(this)
    wsManager.addHandler(this.messageHandler)
  }

  private generateRequestId(): string {
    return `tv_${Date.now()}_${Math.random().toString(36).substring(7)}`
  }

  private sendRequest<T>(message: any, timeoutMs?: number): Promise<T> {
    const requestId = message.request_id || this.generateRequestId()
    message.request_id = requestId

    console.log('[TradingView Datafeed] Sending request:', requestId, message.type, message)

    return new Promise((resolve, reject) => {
      const timeout = window.setTimeout(() => {
        console.error('[TradingView Datafeed] Request timeout:', requestId, message.type)
        this.pendingRequests.delete(requestId)
        reject(new Error('Request timeout'))
      }, timeoutMs ?? this.requestTimeout)

      this.pendingRequests.set(requestId, { resolve, reject, timeout })

      const wsState = wsManager.getWebSocket()?.readyState
      console.log('[TradingView Datafeed] WebSocket state before send:', wsState, 'OPEN=' + WebSocket.OPEN)

      wsManager.send(message)
    })
  }

  private handleMessage(message: any): void {
    // On reconnect the server sends a fresh 'connected' message.
    // Any pending requests were sent on the old socket and will never be answered,
    // so reject them immediately so TradingView can retry on the new connection.
    if (message.type === 'connected' && this.pendingRequests.size > 0) {
      console.warn('[TradingView Datafeed] WebSocket reconnected — rejecting', this.pendingRequests.size, 'stale pending request(s)')
      for (const [requestId, pending] of this.pendingRequests) {
        clearTimeout(pending.timeout)
        pending.reject(new Error('WebSocket reconnected'))
        this.pendingRequests.delete(requestId)
      }
      return
    }

    // Handle responses to pending requests
    if (message.request_id && this.pendingRequests.has(message.request_id)) {
      console.log('[TradingView Datafeed] Found pending request for:', message.request_id)
      const pending = this.pendingRequests.get(message.request_id)!
      this.pendingRequests.delete(message.request_id)
      clearTimeout(pending.timeout)

      if (message.type === 'error') {
        console.error('[TradingView Datafeed] Resolving with error:', message.error_message)
        pending.reject(new Error(message.error_message || 'Unknown error'))
      } else {
        console.log('[TradingView Datafeed] Resolving with response:', message.type)
        pending.resolve(message)
      }
    } else if (message.request_id) {
      console.warn('[TradingView Datafeed] No pending request found for:', message.request_id, 'Available:', Array.from(this.pendingRequests.keys()))
    }

    // Handle real-time bar updates
    if (message.type === 'bar_update') {
      const subscription = this.subscriptions.get(message.subscription_id)
      if (subscription && message.bar) {
        const symbolKey = subscription.symbolInfo.ticker || subscription.symbolInfo.name
        const denoms = this.symbolDenominators.get(symbolKey) || { tick: 1, base: 1 }

        // Drop bars already covered by getBars history to prevent time-order violations
        const barTimeMs = message.bar.time * 1000
        const barKey = `${symbolKey}_${message.period_seconds}`
        const watermark = this.lastBarTimes.get(barKey) ?? 0
        if (barTimeMs < watermark) {
          return
        }

        const bar: Bar = {
          time: message.bar.time * 1000, // Convert to milliseconds
          open: parseFloat(message.bar.open) / denoms.tick,
          high: parseFloat(message.bar.high) / denoms.tick,
          low: parseFloat(message.bar.low) / denoms.tick,
          close: parseFloat(message.bar.close) / denoms.tick,
          volume: parseFloat(message.bar.volume) / denoms.base
        }
        subscription.onTick(bar)
      }
    }
  }

  onReady(callback: (configuration: DatafeedConfiguration) => void): void {
    // TradingView requires callback to be called asynchronously
    if (this.configuration) {
      setTimeout(() => callback(this.configuration!), 0)
      return
    }

    this.sendRequest<any>({
      type: 'get_config'
    })
      .then((response) => {
        this.configuration = response.config
        setTimeout(() => callback(this.configuration!), 0)
      })
      .catch((error) => {
        console.error('Failed to get datafeed configuration:', error)
        // Provide default configuration
        const defaultConfig: DatafeedConfiguration = {
          supported_resolutions: ['1', '5', '15', '30', '60', '240', '1D', '1W'],
          supports_search: true,
          supports_group_request: false,
          supports_marks: false,
          supports_timescale_marks: false,
          supports_time: false
        }
        this.configuration = defaultConfig
        setTimeout(() => callback(defaultConfig), 0)
      })
  }

  searchSymbols(
    userInput: string,
    exchange: string,
    symbolType: string,
    onResult: (items: SearchSymbolResultItem[]) => void
  ): void {
    this.sendRequest<any>({
      type: 'search_symbols',
      query: userInput,
      exchange: exchange || undefined,
      symbol_type: symbolType || undefined,
      limit: 30
    })
      .then((response) => {
        onResult(response.results || [])
      })
      .catch((error) => {
        console.error('Symbol search failed:', error)
        onResult([])
      })
  }

  resolveSymbol(
    symbolName: string,
    onResolve: (symbolInfo: LibrarySymbolInfo) => void,
    onError: (reason: string) => void
  ): void {
    console.log('[TradingView Datafeed] Resolving symbol:', symbolName)
    console.log('[TradingView Datafeed] WebSocket state:', wsManager.getWebSocket()?.readyState)

    this.sendRequest<any>({
      type: 'resolve_symbol',
      symbol: symbolName
    })
      .then((response) => {
        console.log('[TradingView Datafeed] Received response:', response)
        if (response.symbol_info) {
          console.log('[TradingView Datafeed] Resolved symbol info:', response.symbol_info)

          // Derive scale denominators from Nautilus precision fields.
          // price_precision=2 → tick divisor=100 (prices stored as integer cents)
          // size_precision=6  → base divisor=1_000_000 (volumes stored as integer micro-units)
          const symbolKey = response.symbol_info.ticker || response.symbol_info.name
          const tickDenom = Math.pow(10, response.symbol_info.price_precision ?? 0)
          const baseDenom = Math.pow(10, response.symbol_info.size_precision ?? 0)

          this.symbolDenominators.set(symbolKey, {
            tick: tickDenom,
            base: baseDenom
          })
          console.log('[TradingView Datafeed] Stored denominators:', symbolKey, { tick: tickDenom, base: baseDenom })

          onResolve(response.symbol_info)
        } else {
          console.error('[TradingView Datafeed] No symbol_info in response')
          onError('Symbol not found')
        }
      })
      .catch((error) => {
        console.error('[TradingView Datafeed] Failed to resolve symbol:', symbolName, error)
        console.error('[TradingView Datafeed] Error details:', {
          message: error.message,
          stack: error.stack,
          pendingRequests: this.pendingRequests.size
        })
        onError(error instanceof Error ? error.message : 'Unknown error')
      })
  }

  getBars(
    symbolInfo: LibrarySymbolInfo,
    resolution: string,
    periodParams: {
      from: number
      to: number
      firstDataRequest: boolean
      countBack?: number
    },
    onResult: (bars: Bar[], meta: HistoryMetadata) => void,
    onError: (reason: string) => void
  ): void {
    const symbolKey = symbolInfo.ticker || symbolInfo.name
    const denoms = this.symbolDenominators.get(symbolKey) || { tick: 1, base: 1 }

    const doRequest = () => this.sendRequest<any>({
      type: 'get_bars',
      symbol: symbolKey,
      period_seconds: intervalToSeconds(resolution),
      from_time: periodParams.from,
      to_time: periodParams.to,
      countback: periodParams.countBack
    }, this.barsRequestTimeout)

    doRequest()
      .catch((err: Error) => {
        if (err.message === 'Request timeout') {
          console.warn('[TradingView Datafeed] getBars timed out, retrying once (data may now be in cache)...')
          return doRequest()
        }
        throw err
      })
      .then((response) => {
        if (response.error) {
          console.error('[TradingView Datafeed] getBars server error:', response.error)
          onError(response.error)
        } else if (response.history) {
          console.log('[TradingView Datafeed] Raw bar sample:', response.history.bars?.[0])
          console.log('[TradingView Datafeed] Denominators:', denoms)

          const rawBars: any[] = response.history.bars || []

          // All bars have non-null prices — ingestor forward-fills interior gaps.
          const bars: Bar[] = rawBars.map((bar: any) => ({
            time:   bar.time * 1000,
            open:   parseFloat(bar.open)   / denoms.tick,
            high:   parseFloat(bar.high)   / denoms.tick,
            low:    parseFloat(bar.low)    / denoms.tick,
            close:  parseFloat(bar.close)  / denoms.tick,
            volume: parseFloat(bar.volume) / denoms.base
          }))

          bars.sort((a, b) => a.time - b.time)

          // Update last-bar watermark so bar_update handler can drop stale replays
          if (bars.length > 0) {
            const barKey = `${symbolKey}_${intervalToSeconds(resolution)}`
            const newLast = bars[bars.length - 1].time
            const prevLast = this.lastBarTimes.get(barKey) ?? 0
            if (newLast > prevLast) {
              this.lastBarTimes.set(barKey, newLast)
            }
          }

          console.log('[TradingView Datafeed] Scaled bar sample:', bars[0])

          const meta: HistoryMetadata = {
            noData: bars.length === 0,
            nextTime: response.history.next_time
          }

          onResult(bars, meta)
        } else {
          onResult([], { noData: true })
        }
      })
      .catch((error) => {
        console.error('Failed to get bars:', error)
        onError(error instanceof Error ? error.message : 'Unknown error')
      })
  }

  subscribeBars(
    symbolInfo: LibrarySymbolInfo,
    resolution: string,
    onTick: (bar: Bar) => void,
    listenerGuid: string,
    onResetCacheNeededCallback: () => void
  ): void {
    this.sendRequest<any>({
      type: 'subscribe_bars',
      symbol: symbolInfo.ticker || symbolInfo.name,
      period_seconds: intervalToSeconds(resolution),
      subscription_id: listenerGuid
    })
      .then((response) => {
        if (response.success) {
          this.subscriptions.set(listenerGuid, {
            symbolInfo,
            resolution,
            onTick,
            subscriptionId: listenerGuid
          })
        }
      })
      .catch((error) => {
        console.error('Failed to subscribe to bars:', error)
      })
  }

  unsubscribeBars(listenerGuid: string): void {
    const sub = this.subscriptions.get(listenerGuid)
    this.sendRequest<any>({
      type: 'unsubscribe_bars',
      period_seconds: sub ? intervalToSeconds(sub.resolution) : 60,
      subscription_id: listenerGuid
    })
      .then(() => {
        this.subscriptions.delete(listenerGuid)
      })
      .catch((error) => {
        console.error('Failed to unsubscribe from bars:', error)
      })
  }

  destroy(): void {
    // Clean up all subscriptions
    for (const [guid] of this.subscriptions) {
      this.unsubscribeBars(guid)
    }
    this.subscriptions.clear()

    // Clear pending requests
    for (const [requestId, pending] of this.pendingRequests) {
      clearTimeout(pending.timeout)
      pending.reject(new Error('Datafeed destroyed'))
    }
    this.pendingRequests.clear()

    // Remove message handler from shared WebSocket
    wsManager.removeHandler(this.messageHandler)
  }
}

export function createTradingViewDatafeed(): IBasicDataFeed {
  return new WebSocketDatafeed()
}
