/**
 * TextMe Real-time WebSocket Module
 * 
 * Handles real-time communication with TextMe's pubsub WebSocket server
 * using a SIP-style messaging protocol (MESSAGE, INVITE, REGISTER, SUBSCRIBE)
 */

import WebSocket from 'ws';

// ============================================================================
// Types & Interfaces
// ============================================================================

export type RealtimeEventType = 'message' | 'call' | 'typing' | 'error' | 'connected' | 'disconnected';

export interface IncomingMessage {
  id: string;
  conversationId: string;
  senderId: string;
  senderName?: string;
  text: string;
  timestamp: Date;
  mediaUrl?: string;
  mediaType?: 'image' | 'video' | 'audio';
}

export interface CallEvent {
  type: 'incoming' | 'outgoing' | 'ended' | 'missed';
  conversationId: string;
  callerId: string;
  callerName?: string;
  timestamp: Date;
  duration?: number;
}

export interface TypingEvent {
  conversationId: string;
  userId: string;
  isTyping: boolean;
}

export interface RealtimeError {
  code: string;
  message: string;
  details?: unknown;
}

type EventCallback<T> = (data: T) => void;

interface EventCallbacks {
  message: Set<EventCallback<IncomingMessage>>;
  call: Set<EventCallback<CallEvent>>;
  typing: Set<EventCallback<TypingEvent>>;
  error: Set<EventCallback<RealtimeError>>;
  connected: Set<EventCallback<void>>;
  disconnected: Set<EventCallback<{ reason: string }>>;
}

interface SIPMessage {
  command: 'MESSAGE' | 'INVITE' | 'REGISTER' | 'SUBSCRIBE' | 'ACK' | 'BYE' | 'NOTIFY';
  headers: Record<string, string>;
  body?: string;
}

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

export interface RealtimeOptions {
  /** WebSocket URL (default: wss://pubsub.textme-app.com/ws/textme) */
  wsUrl?: string;
  /** Enable auto-reconnection (default: true) */
  autoReconnect?: boolean;
  /** Initial reconnect delay in ms (default: 1000) */
  reconnectDelay?: number;
  /** Max reconnect delay in ms (default: 30000) */
  maxReconnectDelay?: number;
  /** Reconnect backoff multiplier (default: 2) */
  reconnectMultiplier?: number;
  /** Max reconnection attempts (default: 10, -1 for infinite) */
  maxReconnectAttempts?: number;
  /** Heartbeat interval in ms (default: 30000) */
  heartbeatInterval?: number;
  /** Request timeout in ms (default: 10000) */
  requestTimeout?: number;
}

// ============================================================================
// TextMeRealtime Class
// ============================================================================

export class TextMeRealtime {
  private ws: WebSocket | null = null;
  private token: string | null = null;
  private userId: string | null = null;
  private isConnected = false;
  private isConnecting = false;
  private shouldReconnect = true;
  private reconnectAttempts = 0;
  private currentReconnectDelay: number;
  private reconnectTimer: NodeJS.Timeout | null = null;
  private heartbeatTimer: NodeJS.Timeout | null = null;
  private pendingRequests = new Map<string, PendingRequest>();
  private subscribedConversations = new Set<string>();
  private callIdCounter = 0;

  private readonly options: Required<RealtimeOptions>;
  private readonly callbacks: EventCallbacks = {
    message: new Set(),
    call: new Set(),
    typing: new Set(),
    error: new Set(),
    connected: new Set(),
    disconnected: new Set(),
  };

  constructor(options: RealtimeOptions = {}) {
    this.options = {
      wsUrl: options.wsUrl ?? 'wss://pubsub.textme-app.com/ws/textme',
      autoReconnect: options.autoReconnect ?? true,
      reconnectDelay: options.reconnectDelay ?? 1000,
      maxReconnectDelay: options.maxReconnectDelay ?? 30000,
      reconnectMultiplier: options.reconnectMultiplier ?? 2,
      maxReconnectAttempts: options.maxReconnectAttempts ?? 10,
      heartbeatInterval: options.heartbeatInterval ?? 30000,
      requestTimeout: options.requestTimeout ?? 10000,
    };
    this.currentReconnectDelay = this.options.reconnectDelay;
  }

  // ==========================================================================
  // Public API
  // ==========================================================================

  /**
   * Connect to the TextMe WebSocket server
   */
  async connect(token: string): Promise<void> {
    if (this.isConnected) {
      return;
    }
    if (this.isConnecting) {
      throw new Error('Connection already in progress');
    }

    this.token = token;
    this.shouldReconnect = true;
    this.isConnecting = true;

    return new Promise((resolve, reject) => {
      try {
        this.ws = new WebSocket(this.options.wsUrl, {
          headers: {
            'Authorization': `Bearer ${token}`,
            'X-TextMe-Client': 'clawdbot-realtime/1.0',
          },
        });

        const connectionTimeout = setTimeout(() => {
          if (this.isConnecting) {
            this.ws?.close();
            reject(new Error('Connection timeout'));
          }
        }, this.options.requestTimeout);

        this.ws.on('open', async () => {
          clearTimeout(connectionTimeout);
          try {
            await this.handleOpen();
            this.isConnecting = false;
            resolve();
          } catch (err) {
            this.isConnecting = false;
            reject(err);
          }
        });

        this.ws.on('message', (data) => this.handleMessage(data));
        this.ws.on('close', (code, reason) => this.handleClose(code, reason.toString()));
        this.ws.on('error', (err) => this.handleError(err));

      } catch (err) {
        this.isConnecting = false;
        reject(err);
      }
    });
  }

  /**
   * Disconnect from the WebSocket server
   */
  disconnect(): void {
    this.shouldReconnect = false;
    this.cleanup();
    
    if (this.ws) {
      this.ws.close(1000, 'Client disconnect');
      this.ws = null;
    }
    
    this.isConnected = false;
    this.emit('disconnected', { reason: 'Client initiated disconnect' });
  }

  /**
   * Subscribe to conversation events
   */
  subscribe(conversationId: string): void {
    if (!conversationId) {
      throw new Error('conversationId is required');
    }

    this.subscribedConversations.add(conversationId);

    if (this.isConnected) {
      this.sendSubscribe(conversationId);
    }
  }

  /**
   * Unsubscribe from conversation events
   */
  unsubscribe(conversationId: string): void {
    this.subscribedConversations.delete(conversationId);
    
    if (this.isConnected) {
      this.sendSIPMessage({
        command: 'BYE',
        headers: {
          'Call-ID': this.generateCallId(),
          'To': `sip:conversation-${conversationId}@textme-app.com`,
          'From': `sip:${this.userId}@textme-app.com`,
        },
      });
    }
  }

  /**
   * Send a message via WebSocket
   */
  async sendMessage(conversationId: string, text: string): Promise<void> {
    if (!this.isConnected) {
      throw new Error('Not connected to WebSocket');
    }
    if (!conversationId || !text) {
      throw new Error('conversationId and text are required');
    }

    const callId = this.generateCallId();
    
    return this.sendWithAck(callId, {
      command: 'MESSAGE',
      headers: {
        'Call-ID': callId,
        'To': `sip:conversation-${conversationId}@textme-app.com`,
        'From': `sip:${this.userId}@textme-app.com`,
        'Content-Type': 'text/plain',
        'Content-Length': String(Buffer.byteLength(text, 'utf8')),
      },
      body: text,
    });
  }

  /**
   * Send typing indicator
   */
  sendTypingIndicator(conversationId: string, isTyping: boolean): void {
    if (!this.isConnected) return;

    this.sendSIPMessage({
      command: 'NOTIFY',
      headers: {
        'Call-ID': this.generateCallId(),
        'To': `sip:conversation-${conversationId}@textme-app.com`,
        'From': `sip:${this.userId}@textme-app.com`,
        'Event': 'typing',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ typing: isTyping }),
    });
  }

  /**
   * Register event listener
   */
  on(event: 'message', callback: EventCallback<IncomingMessage>): void;
  on(event: 'call', callback: EventCallback<CallEvent>): void;
  on(event: 'typing', callback: EventCallback<TypingEvent>): void;
  on(event: 'error', callback: EventCallback<RealtimeError>): void;
  on(event: 'connected', callback: EventCallback<void>): void;
  on(event: 'disconnected', callback: EventCallback<{ reason: string }>): void;
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  on(event: RealtimeEventType, callback: EventCallback<any>): void {
    const set = this.callbacks[event] as Set<EventCallback<any>>;
    set.add(callback);
  }

  /**
   * Remove event listener
   */
  off(event: 'message', callback: EventCallback<IncomingMessage>): void;
  off(event: 'call', callback: EventCallback<CallEvent>): void;
  off(event: 'typing', callback: EventCallback<TypingEvent>): void;
  off(event: 'error', callback: EventCallback<RealtimeError>): void;
  off(event: 'connected', callback: EventCallback<void>): void;
  off(event: 'disconnected', callback: EventCallback<{ reason: string }>): void;
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  off(event: RealtimeEventType, callback: EventCallback<any>): void {
    const set = this.callbacks[event] as Set<EventCallback<any>>;
    set.delete(callback);
  }

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

  /**
   * Get subscribed conversation IDs
   */
  get subscriptions(): string[] {
    return Array.from(this.subscribedConversations);
  }

  // ==========================================================================
  // WebSocket Event Handlers
  // ==========================================================================

  private async handleOpen(): Promise<void> {
    // Send REGISTER to authenticate
    const callId = this.generateCallId();
    
    await this.sendWithAck(callId, {
      command: 'REGISTER',
      headers: {
        'Call-ID': callId,
        'To': 'sip:pubsub.textme-app.com',
        'From': `sip:user@textme-app.com`,
        'Authorization': `Bearer ${this.token}`,
        'Expires': '3600',
      },
    });

    this.isConnected = true;
    this.reconnectAttempts = 0;
    this.currentReconnectDelay = this.options.reconnectDelay;
    
    // Start heartbeat
    this.startHeartbeat();

    // Re-subscribe to all conversations
    for (const conversationId of this.subscribedConversations) {
      this.sendSubscribe(conversationId);
    }

    this.emit('connected', undefined);
  }

  private handleMessage(data: WebSocket.Data): void {
    try {
      const raw = data.toString();
      const sipMessage = this.parseSIPMessage(raw);
      
      if (!sipMessage) {
        // Might be a heartbeat response or malformed message
        return;
      }

      switch (sipMessage.command) {
        case 'MESSAGE':
          this.handleIncomingMessage(sipMessage);
          break;
        case 'INVITE':
          this.handleIncomingCall(sipMessage);
          break;
        case 'NOTIFY':
          this.handleNotify(sipMessage);
          break;
        case 'ACK':
          this.handleAck(sipMessage);
          break;
        case 'BYE':
          this.handleBye(sipMessage);
          break;
        default:
          // Unknown command, log for debugging
          console.debug('[TextMeRealtime] Unknown SIP command:', sipMessage.command);
      }
    } catch (err) {
      this.emit('error', {
        code: 'PARSE_ERROR',
        message: 'Failed to parse incoming message',
        details: err,
      });
    }
  }

  private handleClose(code: number, reason: string): void {
    this.isConnected = false;
    this.cleanup();

    this.emit('disconnected', { reason: reason || `WebSocket closed with code ${code}` });

    if (this.shouldReconnect && this.options.autoReconnect) {
      this.scheduleReconnect();
    }
  }

  private handleError(err: Error): void {
    this.emit('error', {
      code: 'WEBSOCKET_ERROR',
      message: err.message,
      details: err,
    });
  }

  // ==========================================================================
  // SIP Message Handlers
  // ==========================================================================

  private handleIncomingMessage(sip: SIPMessage): void {
    const conversationId = this.extractConversationId(sip.headers['To'] || sip.headers['From']);
    const senderId = this.extractUserId(sip.headers['From']);

    // Send ACK
    this.sendAck(sip.headers['Call-ID']);

    const message: IncomingMessage = {
      id: sip.headers['Call-ID'] || this.generateCallId(),
      conversationId,
      senderId,
      senderName: sip.headers['X-Sender-Name'],
      text: sip.body || '',
      timestamp: new Date(sip.headers['Date'] || Date.now()),
      mediaUrl: sip.headers['X-Media-URL'],
      mediaType: sip.headers['X-Media-Type'] as IncomingMessage['mediaType'],
    };

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

  private handleIncomingCall(sip: SIPMessage): void {
    const conversationId = this.extractConversationId(sip.headers['To'] || sip.headers['From']);
    const callerId = this.extractUserId(sip.headers['From']);

    const callEvent: CallEvent = {
      type: 'incoming',
      conversationId,
      callerId,
      callerName: sip.headers['X-Caller-Name'],
      timestamp: new Date(),
    };

    this.emit('call', callEvent);
  }

  private handleNotify(sip: SIPMessage): void {
    const event = sip.headers['Event'];
    const conversationId = this.extractConversationId(sip.headers['To'] || sip.headers['From']);
    const userId = this.extractUserId(sip.headers['From']);

    if (event === 'typing' && sip.body) {
      try {
        const data = JSON.parse(sip.body);
        this.emit('typing', {
          conversationId,
          userId,
          isTyping: Boolean(data.typing),
        });
      } catch {
        // Invalid JSON, ignore
      }
    } else if (event === 'call-ended' && sip.body) {
      try {
        const data = JSON.parse(sip.body);
        this.emit('call', {
          type: 'ended',
          conversationId,
          callerId: userId,
          timestamp: new Date(),
          duration: data.duration,
        });
      } catch {
        // Invalid JSON, ignore
      }
    }
  }

  private handleAck(sip: SIPMessage): void {
    const callId = sip.headers['Call-ID'];
    const pending = this.pendingRequests.get(callId);
    
    if (pending) {
      clearTimeout(pending.timeout);
      pending.resolve();
      this.pendingRequests.delete(callId);
    }
  }

  private handleBye(sip: SIPMessage): void {
    const conversationId = this.extractConversationId(sip.headers['To'] || sip.headers['From']);
    this.subscribedConversations.delete(conversationId);
  }

  // ==========================================================================
  // SIP Protocol Helpers
  // ==========================================================================

  private parseSIPMessage(raw: string): SIPMessage | null {
    const lines = raw.split('\r\n');
    if (lines.length === 0) return null;

    // First line: COMMAND sip:target SIP/2.0 or SIP/2.0 200 OK (response)
    const firstLine = lines[0];
    const commandMatch = firstLine.match(/^(MESSAGE|INVITE|REGISTER|SUBSCRIBE|ACK|BYE|NOTIFY)\s/);
    
    if (!commandMatch) {
      // Might be a response (SIP/2.0 200 OK) - treat as ACK for pending requests
      if (firstLine.startsWith('SIP/2.0')) {
        // Find Call-ID in headers and resolve pending request
        const headers: Record<string, string> = {};
        for (let i = 1; i < lines.length && lines[i]; i++) {
          const colonIdx = lines[i].indexOf(':');
          if (colonIdx > 0) {
            const key = lines[i].substring(0, colonIdx).trim();
            const value = lines[i].substring(colonIdx + 1).trim();
            headers[key] = value;
          }
        }
        if (headers['Call-ID']) {
          return { command: 'ACK', headers };
        }
      }
      return null;
    }

    const command = commandMatch[1] as SIPMessage['command'];
    const headers: Record<string, string> = {};
    let bodyStartIdx = -1;

    // Parse headers
    for (let i = 1; i < lines.length; i++) {
      if (lines[i] === '') {
        bodyStartIdx = i + 1;
        break;
      }
      const colonIdx = lines[i].indexOf(':');
      if (colonIdx > 0) {
        const key = lines[i].substring(0, colonIdx).trim();
        const value = lines[i].substring(colonIdx + 1).trim();
        headers[key] = value;
      }
    }

    // Parse body
    let body: string | undefined;
    if (bodyStartIdx > 0 && bodyStartIdx < lines.length) {
      body = lines.slice(bodyStartIdx).join('\r\n');
    }

    return { command, headers, body };
  }

  private formatSIPMessage(sip: SIPMessage): string {
    const lines: string[] = [];
    
    // Start line
    lines.push(`${sip.command} sip:pubsub.textme-app.com SIP/2.0`);
    
    // Headers
    for (const [key, value] of Object.entries(sip.headers)) {
      lines.push(`${key}: ${value}`);
    }
    
    // Blank line before body
    lines.push('');
    
    // Body
    if (sip.body) {
      lines.push(sip.body);
    }
    
    return lines.join('\r\n');
  }

  private sendSIPMessage(sip: SIPMessage): void {
    if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
      throw new Error('WebSocket not connected');
    }
    
    const formatted = this.formatSIPMessage(sip);
    this.ws.send(formatted);
  }

  private async sendWithAck(callId: string, sip: SIPMessage): Promise<void> {
    return new Promise((resolve, reject) => {
      const timeout = setTimeout(() => {
        this.pendingRequests.delete(callId);
        reject(new Error(`Request ${callId} timed out`));
      }, this.options.requestTimeout);

      this.pendingRequests.set(callId, { resolve, reject, timeout });
      
      try {
        this.sendSIPMessage(sip);
      } catch (err) {
        clearTimeout(timeout);
        this.pendingRequests.delete(callId);
        reject(err);
      }
    });
  }

  private sendAck(callId: string): void {
    this.sendSIPMessage({
      command: 'ACK',
      headers: {
        'Call-ID': callId,
        'From': `sip:${this.userId}@textme-app.com`,
      },
    });
  }

  private sendSubscribe(conversationId: string): void {
    this.sendSIPMessage({
      command: 'SUBSCRIBE',
      headers: {
        'Call-ID': this.generateCallId(),
        'To': `sip:conversation-${conversationId}@textme-app.com`,
        'From': `sip:${this.userId}@textme-app.com`,
        'Event': 'message',
        'Expires': '3600',
      },
    });
  }

  private extractConversationId(sipUri: string): string {
    const match = sipUri?.match(/conversation-([^@]+)/);
    return match?.[1] ?? '';
  }

  private extractUserId(sipUri: string): string {
    const match = sipUri?.match(/sip:([^@]+)/);
    return match?.[1] ?? '';
  }

  private generateCallId(): string {
    this.callIdCounter++;
    return `${Date.now()}-${this.callIdCounter}-${Math.random().toString(36).substring(2, 8)}`;
  }

  // ==========================================================================
  // Reconnection & Heartbeat
  // ==========================================================================

  private scheduleReconnect(): void {
    if (this.reconnectTimer) return;
    
    const maxAttempts = this.options.maxReconnectAttempts;
    if (maxAttempts >= 0 && this.reconnectAttempts >= maxAttempts) {
      this.emit('error', {
        code: 'MAX_RECONNECT_ATTEMPTS',
        message: `Max reconnection attempts (${maxAttempts}) reached`,
      });
      return;
    }

    this.reconnectAttempts++;
    
    this.reconnectTimer = setTimeout(async () => {
      this.reconnectTimer = null;
      
      if (!this.shouldReconnect || !this.token) return;
      
      try {
        await this.connect(this.token);
      } catch {
        // Exponential backoff
        this.currentReconnectDelay = Math.min(
          this.currentReconnectDelay * this.options.reconnectMultiplier,
          this.options.maxReconnectDelay
        );
        this.scheduleReconnect();
      }
    }, this.currentReconnectDelay);
  }

  private startHeartbeat(): void {
    this.heartbeatTimer = setInterval(() => {
      if (this.ws?.readyState === WebSocket.OPEN) {
        // Send a minimal keep-alive message
        this.ws.ping();
      }
    }, this.options.heartbeatInterval);
  }

  private cleanup(): void {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer);
      this.heartbeatTimer = null;
    }
    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer);
      this.reconnectTimer = null;
    }
    
    // Reject all pending requests
    for (const [callId, pending] of this.pendingRequests) {
      clearTimeout(pending.timeout);
      pending.reject(new Error('Connection closed'));
      this.pendingRequests.delete(callId);
    }
  }

  // ==========================================================================
  // Event Emission
  // ==========================================================================

  private emit<T extends RealtimeEventType>(
    event: T,
    data: T extends 'message' ? IncomingMessage
        : T extends 'call' ? CallEvent
        : T extends 'typing' ? TypingEvent
        : T extends 'error' ? RealtimeError
        : T extends 'connected' ? undefined
        : T extends 'disconnected' ? { reason: string }
        : never
  ): void {
    const set = this.callbacks[event] as Set<EventCallback<typeof data>>;
    for (const callback of set) {
      try {
        callback(data);
      } catch (err) {
        console.error(`[TextMeRealtime] Error in ${event} callback:`, err);
      }
    }
  }
}

// ============================================================================
// Factory Function
// ============================================================================

export function createTextMeRealtime(options?: RealtimeOptions): TextMeRealtime {
  return new TextMeRealtime(options);
}

export default TextMeRealtime;
