/**
 * Crypto.com OTC 2.0 WebSocket Client
 * Based on: https://exchange-docs.crypto.com/exchange/v1/rest-ws/index_OTC2.html
 *
 * Features:
 * - Automatic authentication on connection
 * - Channel subscriptions (user.otc_qr.requests, user.otc_qr.quotes, user.otc.deals)
 * - Request-for-Quote (RFQ) via WebSocket
 * - Deal execution via WebSocket
 * - Heartbeat handling
 * - Automatic reconnection
 */

import WebSocket from 'ws';
import { EventEmitter } from 'events';
import { signRequest } from '../auth/signature';
import {
  CryptoComApiResponse,
  CryptoComWebSocketClientConfig,
  SubscriptionChannel,
  SubscriptionMessage,
  RequestQuoteParams,
  RequestDealParams,
  RequestDealResult,
  QuoteRequestUpdate,
  Quote,
  Deal,
} from '../types';
import {
  API_METHODS,
  WS_CHANNELS,
  ENVIRONMENTS,
  DEFAULT_CONFIG,
  RESPONSE_CODES,
  WEBSOCKET_RATE_LIMITS,
} from '../config';
import { RateLimiter } from '../utils/rate-limiter';
import { getExponentialBackoffDelay } from '../utils/retry';

interface PendingRequest {
  resolve: (value: CryptoComApiResponse) => void;
  reject: (error: Error) => void;
  timeout: NodeJS.Timeout;
}

export interface CryptoComWebSocketClientEvents {
  connected: () => void;
  disconnected: (code: number) => void;
  reconnected: () => void;
  authenticated: () => void;
  error: (error: Error) => void;
  quoteRequest: (data: QuoteRequestUpdate[]) => void;
  quote: (data: Quote[]) => void;
  deal: (data: Deal[]) => void;
  message: (message: unknown) => void;
}

export class CryptoComWebSocketClient extends EventEmitter {
  private ws: WebSocket | null = null;
  private config: Required<CryptoComWebSocketClientConfig>;
  private requestIdCounter: number = 0;
  private authenticated: boolean = false;
  private reconnecting: boolean = false;
  private shouldReconnect: boolean = true;
  private heartbeatTimer: NodeJS.Timeout | null = null;
  private pendingRequests: Map<number, PendingRequest> = new Map();
  private subscribedChannels: Set<SubscriptionChannel> = new Set();
  private reconnectAttempt: number = 0;
  private wsRateLimiter: RateLimiter;

  constructor(config: CryptoComWebSocketClientConfig) {
    super();

    this.config = {
      apiKey: config.apiKey,
      apiSecret: config.apiSecret,
      wsUrl: config.wsUrl || ENVIRONMENTS.production.wsUrl,
      reconnectInterval: config.reconnectInterval || DEFAULT_CONFIG.reconnectInterval,
      heartbeatInterval: config.heartbeatInterval || DEFAULT_CONFIG.heartbeatInterval,
    };
    this.wsRateLimiter = new RateLimiter(WEBSOCKET_RATE_LIMITS['request-quote'] || 2);
  }

  /**
   * Get the next request ID
   */
  private getNextId(): number {
    return ++this.requestIdCounter;
  }

  /**
   * Connect to WebSocket and authenticate
   */
  async connect(): Promise<void> {
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      console.log('[CryptoComWS] Already connected');
      return;
    }

    return new Promise((resolve, reject) => {
      this.shouldReconnect = true;

      console.log(`[CryptoComWS] Connecting to ${this.config.wsUrl}...`);
      this.ws = new WebSocket(this.config.wsUrl);

      this.ws.on('open', async () => {
        console.log('[CryptoComWS] Connected');
        this.emit('connected');

        // IMPORTANT: Add 1-second delay before sending requests
        // This is recommended by Crypto.com to avoid TOO_MANY_REQUESTS errors
        // as websocket rate limits are pro-rated based on connection time
        await this.sleep(DEFAULT_CONFIG.connectionDelay);

        try {
          await this.authenticate();
          this.startHeartbeat();
          resolve();
        } catch (error) {
          reject(error);
        }
      });

      this.ws.on('message', (data: WebSocket.Data) => {
        this.handleMessage(data);
      });

      this.ws.on('close', (code: number) => {
        console.log(`[CryptoComWS] Disconnected with code: ${code}`);
        this.cleanup();
        this.emit('disconnected', code);

        if (this.shouldReconnect && !this.reconnecting) {
          this.scheduleReconnect();
        }
      });

      this.ws.on('error', (error: Error) => {
        console.error('[CryptoComWS] WebSocket error:', error.message);
        this.emit('error', error);

        // Only reject if we haven't connected yet
        if (!this.authenticated) {
          reject(error);
        }
      });

      this.ws.on('pong', () => {
        // Connection is alive
      });
    });
  }

  /**
   * Authenticate with the WebSocket server
   */
  private async authenticate(): Promise<void> {
    const id = this.getNextId();
    const nonce = Date.now();

    const request = {
      id,
      method: API_METHODS.AUTH,
      nonce,
    };

    const { api_key, sig } = signRequest(request, this.config.apiKey, this.config.apiSecret);

    const authRequest = {
      ...request,
      api_key,
      sig,
    };

    const response = await this.sendRequest(authRequest);

    if (response.code !== RESPONSE_CODES.SUCCESS) {
      throw new Error(`Authentication failed: ${response.message || `code ${response.code}`}`);
    }

    this.authenticated = true;
    console.log('[CryptoComWS] Authenticated successfully');
    this.emit('authenticated');
  }

  /**
   * Subscribe to channels
   */
  async subscribe(channels: SubscriptionChannel[]): Promise<void> {
    if (!this.authenticated) {
      throw new Error('Not authenticated. Call connect() first.');
    }

    const id = this.getNextId();
    const request = {
      id,
      method: 'subscribe',
      params: { channels },
      nonce: Date.now(),
    };

    const response = await this.sendRequest(request);

    if (response.code !== RESPONSE_CODES.SUCCESS) {
      throw new Error(`Subscription failed: ${response.message || `code ${response.code}`}`);
    }

    // Track subscribed channels for reconnection
    channels.forEach((channel) => this.subscribedChannels.add(channel));

    console.log(`[CryptoComWS] Subscribed to: ${channels.join(', ')}`);
  }

  /**
   * Unsubscribe from channels
   */
  async unsubscribe(channels: SubscriptionChannel[]): Promise<void> {
    if (!this.authenticated) {
      throw new Error('Not authenticated');
    }

    const id = this.getNextId();
    const request = {
      id,
      method: 'unsubscribe',
      params: { channels },
      nonce: Date.now(),
    };

    const response = await this.sendRequest(request);

    if (response.code !== RESPONSE_CODES.SUCCESS) {
      throw new Error(`Unsubscription failed: ${response.message || `code ${response.code}`}`);
    }

    // Remove from tracked channels
    channels.forEach((channel) => this.subscribedChannels.delete(channel));

    console.log(`[CryptoComWS] Unsubscribed from: ${channels.join(', ')}`);
  }

  /**
   * Subscribe to all OTC channels
   */
  async subscribeToAllOtcChannels(): Promise<void> {
    await this.subscribe([
      WS_CHANNELS.QUOTE_REQUESTS as SubscriptionChannel,
      WS_CHANNELS.QUOTES as SubscriptionChannel,
      WS_CHANNELS.DEALS as SubscriptionChannel,
    ]);
  }

  /**
   * Request a quote (RFQ) - with rate limiting
   */
  async requestQuote(params: RequestQuoteParams): Promise<CryptoComApiResponse> {
    if (!this.authenticated) {
      throw new Error('Not authenticated. Call connect() first.');
    }

    await this.wsRateLimiter.acquire();

    const id = this.getNextId();
    const request = {
      id,
      method: API_METHODS.REQUEST_QUOTE,
      params: params as unknown as Record<string, unknown>,
      nonce: Date.now(),
    };

    return this.sendRequest(request);
  }

  /**
   * Request a deal - with rate limiting
   */
  async requestDeal(params: RequestDealParams): Promise<CryptoComApiResponse<RequestDealResult>> {
    if (!this.authenticated) {
      throw new Error('Not authenticated. Call connect() first.');
    }

    await this.wsRateLimiter.acquire();

    const id = this.getNextId();
    const request = {
      id,
      method: API_METHODS.REQUEST_DEAL,
      params: params as unknown as Record<string, unknown>,
      nonce: Date.now(),
    };

    return this.sendRequest(request) as Promise<CryptoComApiResponse<RequestDealResult>>;
  }

  /**
   * Send a request and wait for response
   */
  private sendRequest(request: {
    id: number;
    method: string;
    params?: Record<string, unknown>;
    nonce: number;
    api_key?: string;
    sig?: string;
  }): Promise<CryptoComApiResponse> {
    return new Promise((resolve, reject) => {
      if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
        reject(new Error('WebSocket not connected'));
        return;
      }

      // Set timeout for request
      const timeout = setTimeout(() => {
        if (this.pendingRequests.has(request.id)) {
          this.pendingRequests.delete(request.id);
          reject(new Error(`Request timeout for ${request.method}`));
        }
      }, DEFAULT_CONFIG.requestTimeout);

      this.pendingRequests.set(request.id, { resolve, reject, timeout });

      const message = JSON.stringify(request);
      this.ws.send(message);
    });
  }

  /**
   * Handle incoming WebSocket messages
   */
  private handleMessage(data: WebSocket.Data): void {
    try {
      const message = JSON.parse(data.toString());

      // Handle response to pending request
      if (message.id !== undefined && message.id !== -1 && this.pendingRequests.has(message.id)) {
        const pending = this.pendingRequests.get(message.id)!;
        clearTimeout(pending.timeout);
        this.pendingRequests.delete(message.id);
        pending.resolve(message);
        return;
      }

      // Handle subscription push data
      if (message.result?.subscription || message.result?.channel) {
        this.handleSubscriptionData(message as SubscriptionMessage);
        return;
      }

      // Handle heartbeat from server
      if (message.method === 'public/heartbeat') {
        this.respondToHeartbeat(message.id);
        return;
      }

      // Emit generic message for unhandled messages
      this.emit('message', message);
    } catch (error) {
      console.error('[CryptoComWS] Error parsing message:', error);
    }
  }

  /**
   * Handle subscription data from server
   */
  private handleSubscriptionData(message: SubscriptionMessage): void {
    const channel = message.result?.channel;
    const data = message.result?.data;

    if (!data || !Array.isArray(data)) {
      return;
    }

    switch (channel) {
      case WS_CHANNELS.QUOTE_REQUESTS:
        this.emit('quoteRequest', data as QuoteRequestUpdate[]);
        break;

      case WS_CHANNELS.QUOTES:
        this.emit('quote', data as Quote[]);
        break;

      case WS_CHANNELS.DEALS:
        this.emit('deal', data as Deal[]);
        break;

      default:
        this.emit('message', message);
    }
  }

  /**
   * Respond to server heartbeat
   */
  private respondToHeartbeat(id: number): void {
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      const response = {
        id,
        method: API_METHODS.HEARTBEAT_RESPONSE,
      };
      this.ws.send(JSON.stringify(response));
    }
  }

  /**
   * Start heartbeat ping to keep connection alive
   */
  private startHeartbeat(): void {
    this.stopHeartbeat();

    this.heartbeatTimer = setInterval(() => {
      if (this.ws && this.ws.readyState === WebSocket.OPEN) {
        this.ws.ping();
      }
    }, this.config.heartbeatInterval);
  }

  /**
   * Stop heartbeat
   */
  private stopHeartbeat(): void {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer);
      this.heartbeatTimer = null;
    }
  }

  /**
   * Schedule reconnection attempt with exponential backoff
   */
  private scheduleReconnect(): void {
    if (this.reconnecting) {
      return;
    }

    this.reconnecting = true;
    this.reconnectAttempt += 1;
    const delay = getExponentialBackoffDelay(
      this.reconnectAttempt,
      this.config.reconnectInterval,
      60000, // max 60s
      true
    );
    console.log(
      `[CryptoComWS] Scheduling reconnect attempt ${this.reconnectAttempt} in ${Math.round(delay)}ms...`
    );

    setTimeout(async () => {
      if (!this.shouldReconnect) {
        this.reconnecting = false;
        return;
      }

      try {
        await this.connect();

        // Re-subscribe to previously subscribed channels
        if (this.subscribedChannels.size > 0) {
          const channels = Array.from(this.subscribedChannels);
          await this.subscribe(channels);
        }

        this.reconnecting = false;
        this.reconnectAttempt = 0;
        this.emit('reconnected');
        console.log('[CryptoComWS] Reconnected successfully');
      } catch (error) {
        console.error('[CryptoComWS] Reconnect failed:', error);
        this.reconnecting = false;
        this.scheduleReconnect();
      }
    }, delay);
  }

  /**
   * Cleanup resources
   */
  private cleanup(): void {
    this.authenticated = false;
    this.stopHeartbeat();

    // Reject all pending requests
    for (const [id, pending] of this.pendingRequests.entries()) {
      clearTimeout(pending.timeout);
      pending.reject(new Error('Connection closed'));
    }
    this.pendingRequests.clear();
  }

  /**
   * Disconnect from WebSocket
   */
  disconnect(): void {
    this.shouldReconnect = false;
    this.reconnectAttempt = 0;
    this.cleanup();

    if (this.ws) {
      this.ws.close();
      this.ws = null;
    }

    console.log('[CryptoComWS] Disconnected');
  }

  /**
   * Check if connected and authenticated
   */
  isConnected(): boolean {
    return (
      this.ws !== null &&
      this.ws.readyState === WebSocket.OPEN &&
      this.authenticated
    );
  }

  /**
   * Helper to sleep for specified milliseconds
   */
  private sleep(ms: number): Promise<void> {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }
}

/**
 * Factory function to create a WebSocket client
 */
export function createWebSocketClient(
  config: CryptoComWebSocketClientConfig
): CryptoComWebSocketClient {
  return new CryptoComWebSocketClient(config);
}
