/**
 * gRPC-Web Client for Message Router
 * Handles communication with the MPC Message Router service
 */

// Note: This implementation uses fetch-based HTTP/2 for gRPC-Web
// In production, you may want to use @grpc/grpc-js or grpc-web library

export interface MPCMessage {
  messageId: string;
  sessionId: string;
  fromParty: string;
  fromPartyIndex: number;
  isBroadcast: boolean;
  roundNumber: number;
  messageType: string;
  payload: string; // base64 encoded
  createdAt: number;
}

export interface RegisteredParty {
  partyId: string;
  role: string;
  online: boolean;
  registeredAt: number;
  lastSeenAt: number;
}

export type MessageHandler = (message: MPCMessage) => void;

/**
 * gRPC-Web client for Message Router
 */
export class GrpcWebClient {
  private baseUrl: string;
  private partyId: string | null = null;
  private connected: boolean = false;
  private heartbeatInterval: NodeJS.Timeout | null = null;
  private messagePollingInterval: NodeJS.Timeout | null = null;
  private messageHandlers: Map<string, MessageHandler> = new Map();
  private lastMessageTimestamp: number = 0;

  constructor(messageRouterUrl: string) {
    // Convert gRPC address to HTTP endpoint
    // mpc-grpc.szaiai.com:443 -> https://mpc-grpc.szaiai.com
    this.baseUrl = this.normalizeUrl(messageRouterUrl);
  }

  private normalizeUrl(url: string): string {
    // Remove port if present and add https://
    let normalized = url.replace(/:443$/, '');
    if (!normalized.startsWith('http://') && !normalized.startsWith('https://')) {
      normalized = 'https://' + normalized;
    }
    return normalized;
  }

  /**
   * Connect to the Message Router
   */
  async connect(): Promise<void> {
    // For gRPC-Web, connection is implicit
    // We just verify the server is reachable
    try {
      const response = await fetch(`${this.baseUrl}/health`, {
        method: 'GET',
      });

      if (response.ok) {
        this.connected = true;
        console.log('Connected to Message Router');
      } else {
        throw new Error(`Health check failed: ${response.status}`);
      }
    } catch (error) {
      console.error('Failed to connect to Message Router:', error);
      // Allow connection without health check for development
      this.connected = true;
    }
  }

  /**
   * Check if connected
   */
  isConnected(): boolean {
    return this.connected;
  }

  /**
   * Get current party ID
   */
  getPartyId(): string | null {
    return this.partyId;
  }

  /**
   * Register as a party
   */
  async registerParty(partyId: string, role: string = 'temporary'): Promise<void> {
    const response = await this.callGrpc('RegisterParty', {
      party_id: partyId,
      party_role: role,
      version: '1.0.0',
    });

    if (response.success) {
      this.partyId = partyId;
      this.startHeartbeat();
      console.log('Registered as party:', partyId);
    } else {
      throw new Error(String(response.message) || 'Registration failed');
    }
  }

  /**
   * Subscribe to messages for a session
   */
  subscribeMessages(
    sessionId: string,
    partyId: string,
    handler: MessageHandler
  ): () => void {
    const key = `${sessionId}:${partyId}`;
    this.messageHandlers.set(key, handler);

    // Start polling for messages
    if (!this.messagePollingInterval) {
      this.startMessagePolling(sessionId, partyId);
    }

    // Return unsubscribe function
    return () => {
      this.messageHandlers.delete(key);
      if (this.messageHandlers.size === 0) {
        this.stopMessagePolling();
      }
    };
  }

  /**
   * Route a message to other parties
   */
  async routeMessage(
    sessionId: string,
    fromParty: string,
    toParties: string[],
    isBroadcast: boolean,
    payload: string
  ): Promise<void> {
    await this.callGrpc('RouteMessage', {
      session_id: sessionId,
      from_party: fromParty,
      to_parties: isBroadcast ? [] : toParties,
      round_number: 0,
      message_type: 'tss',
      payload: this.base64ToBytes(payload),
    });
  }

  /**
   * Join a session
   */
  async joinSession(
    sessionId: string,
    partyId: string,
    joinToken: string
  ): Promise<{
    success: boolean;
    partyIndex: number;
    sessionInfo: unknown;
    otherParties: unknown[];
  }> {
    const response = await this.callGrpc('JoinSession', {
      session_id: sessionId,
      party_id: partyId,
      join_token: joinToken,
    });

    return {
      success: Boolean(response.success),
      partyIndex: Number(response.party_index) || 0,
      sessionInfo: response.session_info,
      otherParties: Array.isArray(response.other_parties) ? response.other_parties : [],
    };
  }

  /**
   * Mark party as ready
   */
  async markPartyReady(sessionId: string, partyId: string): Promise<boolean> {
    const response = await this.callGrpc('MarkPartyReady', {
      session_id: sessionId,
      party_id: partyId,
    });
    return Boolean(response.all_ready);
  }

  /**
   * Report completion
   */
  async reportCompletion(
    sessionId: string,
    partyId: string,
    publicKey?: string,
    signature?: string
  ): Promise<void> {
    await this.callGrpc('ReportCompletion', {
      session_id: sessionId,
      party_id: partyId,
      public_key: publicKey ? this.base64ToBytes(publicKey) : undefined,
      signature: signature ? this.base64ToBytes(signature) : undefined,
    });
  }

  /**
   * Get registered parties
   */
  async getRegisteredParties(
    roleFilter?: string,
    onlyOnline?: boolean
  ): Promise<RegisteredParty[]> {
    const response = await this.callGrpc('GetRegisteredParties', {
      role_filter: roleFilter || '',
      only_online: onlyOnline || false,
    });
    return Array.isArray(response.parties) ? (response.parties as RegisteredParty[]) : [];
  }

  /**
   * Start heartbeat
   */
  private startHeartbeat(): void {
    if (this.heartbeatInterval) {
      return;
    }

    this.heartbeatInterval = setInterval(async () => {
      if (!this.partyId) return;

      try {
        await this.callGrpc('Heartbeat', {
          party_id: this.partyId,
          timestamp: Date.now(),
        });
      } catch (error) {
        console.warn('Heartbeat failed:', error);
      }
    }, 30000); // 30 seconds
  }

  /**
   * Start polling for messages
   */
  private startMessagePolling(sessionId: string, partyId: string): void {
    this.messagePollingInterval = setInterval(async () => {
      try {
        const response = await this.callGrpc('GetPendingMessages', {
          session_id: sessionId,
          party_id: partyId,
          after_timestamp: this.lastMessageTimestamp,
        });

        const messages = Array.isArray(response.messages) ? response.messages : [];
        for (const msg of messages as Array<Record<string, unknown>>) {
          const key = `${String(msg.session_id)}:${partyId}`;
          const handler = this.messageHandlers.get(key);
          if (handler) {
            handler({
              messageId: String(msg.message_id),
              sessionId: String(msg.session_id),
              fromParty: String(msg.from_party),
              fromPartyIndex: this.getPartyIndexFromId(String(msg.from_party)),
              isBroadcast: Boolean(msg.is_broadcast),
              roundNumber: Number(msg.round_number) || 0,
              messageType: String(msg.message_type),
              payload: this.bytesToBase64(msg.payload as Uint8Array | number[]),
              createdAt: Number(msg.created_at) || 0,
            });
          }
          this.lastMessageTimestamp = Math.max(this.lastMessageTimestamp, Number(msg.created_at) || 0);
        }
      } catch (error) {
        console.warn('Message polling failed:', error);
      }
    }, 500); // Poll every 500ms
  }

  /**
   * Stop message polling
   */
  private stopMessagePolling(): void {
    if (this.messagePollingInterval) {
      clearInterval(this.messagePollingInterval);
      this.messagePollingInterval = null;
    }
  }

  /**
   * Make a gRPC-Web call
   */
  private async callGrpc(method: string, request: unknown): Promise<Record<string, unknown>> {
    // Using gRPC-Web JSON format
    const response = await fetch(`${this.baseUrl}/mpc.router.v1.MessageRouter/${method}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
      },
      body: JSON.stringify(request),
    });

    if (!response.ok) {
      throw new Error(`gRPC call failed: ${response.status} ${response.statusText}`);
    }

    return response.json();
  }

  /**
   * Convert base64 to bytes
   */
  private base64ToBytes(base64: string): Uint8Array {
    const binaryString = atob(base64);
    const bytes = new Uint8Array(binaryString.length);
    for (let i = 0; i < binaryString.length; i++) {
      bytes[i] = binaryString.charCodeAt(i);
    }
    return bytes;
  }

  /**
   * Convert bytes to base64
   */
  private bytesToBase64(bytes: Uint8Array | number[]): string {
    const uint8Array = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
    let binary = '';
    for (let i = 0; i < uint8Array.length; i++) {
      binary += String.fromCharCode(uint8Array[i]);
    }
    return btoa(binary);
  }

  /**
   * Get party index from ID (placeholder - should use actual mapping)
   */
  private getPartyIndexFromId(partyId: string): number {
    // This should be populated from the session info
    // For now, extract from party-N format if possible
    const match = partyId.match(/party-(\d+)/);
    return match ? parseInt(match[1], 10) : 0;
  }

  /**
   * Disconnect
   */
  disconnect(): void {
    if (this.heartbeatInterval) {
      clearInterval(this.heartbeatInterval);
      this.heartbeatInterval = null;
    }
    this.stopMessagePolling();
    this.connected = false;
    this.partyId = null;
  }
}
