/**
 * Besu Client - Handles HTTP and WebSocket connections to Besu nodes
 */

import axios, { AxiosInstance } from 'axios';
import WebSocket from 'ws';
import { v4 as uuidv4 } from 'uuid';

export interface BesuConfig {
  httpUrls: string[];
  wsUrls: string[];
  chainId: number;
}

export interface UpstreamNode {
  httpUrl: string;
  wsUrl: string;
  healthy: boolean;
  lastHealthCheck: number;
}

export class BesuClient {
  private config: BesuConfig;
  private httpClients: AxiosInstance[];
  private upstreamNodes: UpstreamNode[];
  private currentHttpIndex: number = 0;
  private wsConnections: Map<string, WebSocket> = new Map();
  private healthCheckInterval?: NodeJS.Timeout;

  constructor(config: BesuConfig) {
    this.config = config;
    
    // Initialize upstream nodes
    this.upstreamNodes = config.httpUrls.map((httpUrl, index) => ({
      httpUrl,
      wsUrl: config.wsUrls[index] || httpUrl.replace('http://', 'ws://').replace('https://', 'wss://'),
      healthy: true,
      lastHealthCheck: Date.now(),
    }));

    // Initialize HTTP clients
    this.httpClients = config.httpUrls.map(url => 
      axios.create({
        baseURL: url,
        timeout: 30000,
        headers: {
          'Content-Type': 'application/json',
        },
      })
    );

    // Start health checks
    this.startHealthChecks();
  }

  private startHealthChecks() {
    // Health check every 5-10 seconds
    const interval = 8000; // 8 seconds
    this.healthCheckInterval = setInterval(async () => {
      await this.performHealthChecks();
    }, interval);

    // Initial health check
    this.performHealthChecks();
  }

  private async performHealthChecks() {
    const healthCheckPromises = this.upstreamNodes.map(async (node, index) => {
      try {
        const response = await axios.post(node.httpUrl, {
          jsonrpc: '2.0',
          method: 'eth_blockNumber',
          params: [],
          id: 1,
        }, {
          timeout: 5000,
        });

        if (response.data && response.data.result) {
          node.healthy = true;
          node.lastHealthCheck = Date.now();
        } else {
          node.healthy = false;
        }
      } catch (error) {
        node.healthy = false;
        console.warn(`Health check failed for ${node.httpUrl}:`, error);
      }
    });

    await Promise.all(healthCheckPromises);
  }

  private getHealthyNodes(): UpstreamNode[] {
    return this.upstreamNodes.filter(node => node.healthy);
  }

  private getNextHttpNode(): UpstreamNode | null {
    const healthyNodes = this.getHealthyNodes();
    if (healthyNodes.length === 0) {
      return this.upstreamNodes[0] || null; // Fallback to first node even if unhealthy
    }
    
    const index = this.currentHttpIndex % healthyNodes.length;
    this.currentHttpIndex = (this.currentHttpIndex + 1) % healthyNodes.length;
    return healthyNodes[index];
  }

  async callRpc(method: string, params: any[] = []): Promise<any> {
    const node = this.getNextHttpNode();
    if (!node) {
      throw new Error('No healthy Besu nodes available');
    }

    const clientIndex = this.upstreamNodes.indexOf(node);
    const client = this.httpClients[clientIndex];

    try {
      const response = await client.post('', {
        jsonrpc: '2.0',
        method,
        params,
        id: 1,
      });

      if (response.data.error) {
        throw new Error(`RPC error: ${response.data.error.message || JSON.stringify(response.data.error)}`);
      }

      return response.data.result;
    } catch (error: unknown) {
      if (axios.isAxiosError(error) && error.response) {
        throw new Error(`Besu RPC error: ${error.response.status} ${error.response.statusText}`);
      }
      throw error;
    }
  }

  async sendRawTransaction(signedTx: string): Promise<string> {
    // For sending transactions, use a single healthy node (don't round-robin)
    const node = this.getNextHttpNode();
    if (!node) {
      throw new Error('No healthy Besu nodes available');
    }

    const clientIndex = this.upstreamNodes.indexOf(node);
    const client = this.httpClients[clientIndex];

    try {
      const response = await client.post('', {
        jsonrpc: '2.0',
        method: 'eth_sendRawTransaction',
        params: [signedTx],
        id: 1,
      });

      if (response.data.error) {
        throw new Error(`RPC error: ${response.data.error.message || JSON.stringify(response.data.error)}`);
      }

      return response.data.result;
    } catch (error: any) {
      if (axios.isAxiosError(error) && error.response) {
        throw new Error(`Besu RPC error: ${error.response.status} ${error.response.statusText}`);
      }
      throw error;
    }
  }

  async createWebSocketConnection(connectionId: string): Promise<WebSocket> {
    // Use the first healthy WS node, or first node if none healthy
    const healthyNodes = this.getHealthyNodes();
    const node = healthyNodes.length > 0 ? healthyNodes[0] : this.upstreamNodes[0];
    
    if (!node) {
      throw new Error('No Besu WebSocket nodes available');
    }

    const ws = new WebSocket(node.wsUrl);

    return new Promise((resolve, reject) => {
      ws.on('open', () => {
        this.wsConnections.set(connectionId, ws);
        resolve(ws);
      });

      ws.on('error', (error) => {
        reject(error);
      });

      ws.on('close', () => {
        this.wsConnections.delete(connectionId);
      });
    });
  }

  getWebSocketConnection(connectionId: string): WebSocket | undefined {
    return this.wsConnections.get(connectionId);
  }

  closeWebSocketConnection(connectionId: string) {
    const ws = this.wsConnections.get(connectionId);
    if (ws) {
      ws.close();
      this.wsConnections.delete(connectionId);
    }
  }

  async close() {
    if (this.healthCheckInterval) {
      clearInterval(this.healthCheckInterval);
    }

    // Close all WebSocket connections
    for (const [connectionId, ws] of this.wsConnections.entries()) {
      ws.close();
    }
    this.wsConnections.clear();
  }

  getHealthStatus(): { nodes: UpstreamNode[]; healthyCount: number; totalCount: number } {
    const healthyCount = this.getHealthyNodes().length;
    return {
      nodes: this.upstreamNodes,
      healthyCount,
      totalCount: this.upstreamNodes.length,
    };
  }
}
