/**
 * OHLC Service - High-level API for historical market data
 *
 * Workflow (mirroring sandbox/dexorder/ohlc_client.py):
 * 1. Check Iceberg for existing data
 * 2. Identify missing ranges
 * 3. If complete, return immediately
 * 4. Otherwise, request missing data via relay
 * 5. Wait for completion notification
 * 6. Query Iceberg again for complete dataset
 * 7. Return results
 *
 * This provides transparent caching - clients don't need to know
 * whether data came from cache or was fetched on-demand.
 */

import type { FastifyBaseLogger } from 'fastify';
import type { IcebergClient } from '../clients/iceberg-client.js';
import type { ZMQRelayClient, BarUpdateCallback } from '../clients/zmq-relay-client.js';
export type { BarUpdateCallback } from '../clients/zmq-relay-client.js';
import type {
  HistoryResult,
  SymbolInfo,
  SearchResult,
  DatafeedConfig,
  TradingViewBar,
} from '../types/ohlc.js';
import {
  secondsToNanos,
  backendToTradingView,
  DEFAULT_SUPPORTED_RESOLUTIONS,
} from '../types/ohlc.js';

export interface OHLCServiceConfig {
  icebergClient: IcebergClient;
  relayClient: ZMQRelayClient;
  logger: FastifyBaseLogger;
  requestTimeout?: number;  // Request timeout in ms (default: 30000)
}

/**
 * OHLC Service
 *
 * Provides high-level API for fetching OHLC data with smart caching.
 */
export class OHLCService {
  private icebergClient: IcebergClient;
  private relayClient: ZMQRelayClient;
  private logger: FastifyBaseLogger;

  constructor(config: OHLCServiceConfig) {
    this.icebergClient = config.icebergClient;
    this.relayClient = config.relayClient;
    this.logger = config.logger;
  }

  /**
   * Subscribe to realtime OHLC bar updates for a ticker+period.
   * ZMQ subscribe is issued on the first call for a given topic; subsequent calls
   * for the same topic only add the callback (no extra ZMQ events).
   */
  subscribeToTicker(ticker: string, periodSeconds: number, callback: BarUpdateCallback): void {
    this.relayClient.subscribeToTicker(ticker, periodSeconds, callback);
  }

  /**
   * Unsubscribe a callback from realtime OHLC bar updates.
   * ZMQ unsubscribe is issued when the last callback for a topic is removed.
   */
  unsubscribeFromTicker(ticker: string, periodSeconds: number, callback: BarUpdateCallback): void {
    this.relayClient.unsubscribeFromTicker(ticker, periodSeconds, callback);
  }

  /**
   * Fetch OHLC data with smart caching
   *
   * Steps:
   * 1. Query Iceberg for existing data
   * 2. If complete, return immediately
   * 3. If missing data, request via relay
   * 4. Wait for completion notification
   * 5. Query Iceberg again for complete dataset
   * 6. Return results
   */
  async fetchOHLC(
    ticker: string,
    period_seconds: number,
    from_time: number,      // Unix timestamp in SECONDS
    to_time: number,        // Unix timestamp in SECONDS
    countback?: number
  ): Promise<HistoryResult> {
    this.logger.debug({
      ticker,
      period_seconds,
      from_time,
      to_time,
      countback,
    }, 'Fetching OHLC data');

    // Convert times to nanoseconds, then align to period boundaries using
    // [ceil(start), ceil(end)) semantics:
    //   - start: ceil to next period boundary — excludes any in-progress candle whose
    //     official timestamp is before from_time.
    //   - end: ceil to next period boundary, used as EXCLUSIVE upper bound — includes
    //     the last candle whose timestamp < to_time, excludes one sitting exactly on
    //     to_time (which would be the next candle, not yet started).
    const periodNanos = BigInt(period_seconds) * 1_000_000_000n;
    const raw_start = secondsToNanos(from_time);
    const raw_end = secondsToNanos(to_time);
    // bigint ceiling: ceil(a/b)*b = ((a + b - 1) / b) * b
    const start_time = ((raw_start + periodNanos - 1n) / periodNanos) * periodNanos;
    const end_time   = ((raw_end   + periodNanos - 1n) / periodNanos) * periodNanos;  // exclusive

    // Step 1: Check Iceberg for existing data
    let data = await this.icebergClient.queryOHLC(ticker, period_seconds, start_time, end_time);

    // Step 2: Identify missing ranges
    const missingRanges = await this.icebergClient.findMissingOHLCRanges(
      ticker,
      period_seconds,
      start_time,
      end_time
    );

    this.logger.info({ ticker, period_seconds, dataCount: data.length, missingRangeCount: missingRanges.length, missingRanges }, 'OHLC cache check result');

    if (missingRanges.length === 0 && data.length > 0) {
      // All data exists in Iceberg
      this.logger.info({ ticker, period_seconds, cached: true }, 'OHLC data found in cache, returning immediately');
      return this.formatHistoryResult(data, start_time, end_time, period_seconds, countback);
    }

    // Step 3: Request each missing range from the relay individually so we
    // only fetch what's actually absent, not the whole requested window.
    this.logger.info({ ticker, period_seconds, missingRanges: missingRanges.length, dataCount: data.length }, 'Requesting missing OHLC data from relay');

    try {
      for (const [rangeStart, rangeEnd] of missingRanges) {
        const notification = await this.relayClient.requestHistoricalOHLC(
          ticker,
          period_seconds,
          rangeStart,
          rangeEnd
          // countback is NOT passed as a limit — the ingestor must fetch the full range.
          // Countback is applied below after we have the complete dataset.
        );
        this.logger.info({
          ticker,
          period_seconds,
          rangeStart: rangeStart.toString(),
          rangeEnd: rangeEnd.toString(),
          row_count: notification.row_count,
          status: notification.status,
        }, 'Relay range request completed');
      }

      // Step 4: Query Iceberg again for complete dataset
      data = await this.icebergClient.queryOHLC(ticker, period_seconds, start_time, end_time);
      this.logger.info({ ticker, period_seconds, dataCount: data.length }, 'Final Iceberg query complete, returning result');

      return this.formatHistoryResult(data, start_time, end_time, period_seconds, countback);

    } catch (error: any) {
      this.logger.error({
        error,
        ticker,
        period_seconds,
      }, 'Failed to fetch historical data');

      throw error;
    }
  }

  /**
   * Format OHLC data as TradingView history result.
   *
   * Interior gaps (confirmed trading periods with no trades) arrive as null-OHLC
   * rows from Iceberg. Edge gaps (data not yet ingested, in-progress candles) are
   * simply absent rows. Both are returned as-is; clients fill as appropriate.
   */
  private formatHistoryResult(
    data: any[],
    // @ts-ignore
    start_time: bigint,
    // @ts-ignore
    end_time: bigint,
    // @ts-ignore
    period_seconds: number,
    countback?: number
  ): HistoryResult {
    if (data.length === 0) {
      return {
        bars: [],
        noData: true,
      };
    }

    // Convert to TradingView format without null-filling missing slots.
    let bars: TradingViewBar[] = data.map(backendToTradingView);

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

    if (countback && bars.length > countback) {
      bars = bars.slice(-countback);
    }

    return {
      bars,
      noData: false,
    };
  }

  /**
   * Get datafeed configuration
   */
  async getConfig(): Promise<DatafeedConfig> {
    return {
      supported_resolutions: DEFAULT_SUPPORTED_RESOLUTIONS,
      supports_search: true,
      supports_group_request: false,
      supports_marks: false,
      supports_timescale_marks: false,
      supports_time: false,
    };
  }

  /**
   * Search symbols
   *
   * For now, stub with default symbol
   */
  async searchSymbols(
    query: string,
    type?: string,
    exchange?: string,
    limit: number = 30
  ): Promise<SearchResult[]> {
    this.logger.debug({ query, type, exchange, limit }, 'Searching symbols');

    // TODO: Implement central symbol registry
    // For now, return default symbol if query matches
    if (query.toLowerCase().includes('btc') || query.toLowerCase().includes('binance')) {
      return [{
        symbol: 'BTC/USDT',
        full_name: 'BTC/USDT (BINANCE)',
        description: 'Bitcoin / Tether USD',
        exchange: 'BINANCE',
        ticker: 'BTC/USDT.BINANCE',
        type: 'crypto',
      }];
    }

    return [];
  }

  /**
   * Resolve symbol metadata
   *
   * For now, stub with default symbol
   */
  async resolveSymbol(symbol: string): Promise<SymbolInfo> {
    this.logger.debug({ symbol }, 'Resolving symbol');

    // TODO: Implement central symbol registry
    // For now, return default symbol info for BTC/USDT.BINANCE
    if (symbol === 'BTC/USDT.BINANCE' || symbol === 'BTC/USDT') {
      return {
        symbol: 'BTC/USDT',
        name: 'BTC/USDT',
        ticker: 'BTC/USDT.BINANCE',
        description: 'Bitcoin / Tether USD',
        type: 'crypto',
        session: '24x7',
        timezone: 'Etc/UTC',
        exchange: 'BINANCE',
        minmov: 1,
        pricescale: 100,
        has_intraday: true,
        has_daily: true,
        has_weekly_and_monthly: false,
        supported_resolutions: DEFAULT_SUPPORTED_RESOLUTIONS,
        data_status: 'streaming',
      };
    }

    throw new Error(`Symbol not found: ${symbol}`);
  }
}
