/**
 * WebSocket Client for Real-time State Updates
 */

import { FullState, RemoteChange, RemoteLogEntry, RemoteProject } from './types';

export type ConnectionStatus = 'connected' | 'reconnecting' | 'disconnected';

interface WSMessage {
  type: 'full_state' | 'log' | 'change_update' | 'ping' | 'pong';
  /** Server sends projects with nested changes */
  projects?: RemoteProject[];
  /** Per-project worktree information */
  worktrees?: FullState['worktrees'];
  /** Persisted dashboard UI state */
  ui_state?: FullState['ui_state'];
  /** Whether git/sync is available (resolve_command is configured) */
  sync_available?: boolean;
  /** Global orchestration status */
  orchestration_status?: FullState['orchestration_status'];
  data?: FullState;
  /** Log entry payload (when type === 'log') */
  entry?: RemoteLogEntry;
}

export class WebSocketClient {
  private ws: WebSocket | null = null;
  private url: string;
  private listeners: {
    onStateUpdate?: (state: FullState) => void;
    onLogEntry?: (entry: RemoteLogEntry) => void;
    onConnectionChange?: (status: ConnectionStatus) => void;
    onError?: (error: Error) => void;
  } = {};

  private connectAborted = false;
  private reconnectAttempts = 0;
  private maxReconnectAttempts = 10;
  private reconnectDelays = [1000, 2000, 4000, 8000, 16000]; // ms, then max 30s
  private maxReconnectDelay = 30000;
  private reconnectTimeoutId: number | null = null;
  private pingTimeoutId: number | null = null;

  constructor() {
    const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
    this.url = `${protocol}://${window.location.host}/api/v1/ws`;
  }

  /**
   * Connect to the WebSocket server
   */
  connect(): Promise<void> {
    return new Promise((resolve, reject) => {
      try {
        this.connectAborted = false;
        this.ws = new WebSocket(this.url);

        this.ws.onopen = () => {
          if (this.connectAborted) {
            // disconnect() was called while CONNECTING; close cleanly now
            this.ws?.close();
            this.ws = null;
            resolve();
            return;
          }
          this.reconnectAttempts = 0;
          this.notifyConnectionChange('connected');
          this.startPingTimer();
          resolve();
        };

        this.ws.onmessage = (event) => {
          try {
            const message = JSON.parse(event.data);
            console.debug('WS message received:', message.type);

            if (message.type === 'full_state') {
              const projects: RemoteProject[] = message.projects ?? [];
              // Flatten changes from nested project structure
              const changes: RemoteChange[] = projects.flatMap(
                (project) => project.changes ?? [],
              );
              const state: FullState = {
                projects,
                changes,
                worktrees: message.worktrees,
                ui_state: message.ui_state,
                sync_available: message.sync_available,
                orchestration_status: message.orchestration_status,
              };
              this.listeners.onStateUpdate?.(state);
            } else if (message.type === 'log' && message.entry) {
              this.listeners.onLogEntry?.(message.entry);
            }
          } catch (err) {
            console.error('Failed to parse WS message:', err, 'raw:', event.data?.substring?.(0, 100));
          }
        };

        this.ws.onerror = (event) => {
          if (this.ws?.readyState === WebSocket.CLOSED || this.ws?.readyState === WebSocket.CLOSING) return;
          const error = new Error(`WebSocket error: ${event}`);
          this.listeners.onError?.(error);
          console.error('WebSocket error:', event);
        };

        this.ws.onclose = () => {
          this.notifyConnectionChange('disconnected');
          this.attemptReconnect();
        };
      } catch (err) {
        reject(err);
      }
    });
  }

  /**
   * Disconnect from the WebSocket server
   */
  disconnect(): void {
    this.connectAborted = true;

    if (this.reconnectTimeoutId !== null) {
      clearTimeout(this.reconnectTimeoutId);
      this.reconnectTimeoutId = null;
    }
    if (this.pingTimeoutId !== null) {
      clearTimeout(this.pingTimeoutId);
      this.pingTimeoutId = null;
    }
    if (this.ws) {
      const { readyState } = this.ws;
      // Suppress reconnection and error handlers regardless of state
      this.ws.onerror = null;
      this.ws.onclose = null;
      this.ws.onmessage = null;

      if (readyState === WebSocket.CONNECTING) {
        // Do NOT call close() here — it would trigger "closed before established"
        // The onopen handler will see connectAborted and close cleanly
      } else if (readyState === WebSocket.OPEN) {
        this.ws.close();
        this.ws = null;
      } else {
        // CLOSING or CLOSED — no action needed
        this.ws = null;
      }
    }
    this.notifyConnectionChange('disconnected');
  }

  /**
   * Register a listener
   */
  on(
    event: 'stateUpdate' | 'logEntry' | 'connectionChange' | 'error',
    callback: (data: any) => void,
  ): void {
    if (event === 'stateUpdate') {
      this.listeners.onStateUpdate = callback;
    } else if (event === 'logEntry') {
      this.listeners.onLogEntry = callback;
    } else if (event === 'connectionChange') {
      this.listeners.onConnectionChange = callback;
    } else if (event === 'error') {
      this.listeners.onError = callback;
    }
  }

  /**
   * Check if connected
   */
  isConnected(): boolean {
    return this.ws?.readyState === WebSocket.OPEN;
  }

  /**
   * Private: Attempt to reconnect with exponential backoff
   */
  private attemptReconnect(): void {
    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
      console.error('Max reconnection attempts reached');
      return;
    }

    this.reconnectAttempts++;
    this.notifyConnectionChange('reconnecting');

    const delayIndex = Math.min(
      this.reconnectAttempts - 1,
      this.reconnectDelays.length - 1,
    );
    const delay = Math.min(this.reconnectDelays[delayIndex], this.maxReconnectDelay);

    this.reconnectTimeoutId = window.setTimeout(() => {
      this.connect().catch((err) => {
        console.error('Reconnect failed:', err);
        this.attemptReconnect();
      });
    }, delay);
  }

  /**
   * Private: Notify connection status change
   */
  private notifyConnectionChange(status: ConnectionStatus): void {
    this.listeners.onConnectionChange?.(status);
  }

  /**
   * Private: Start periodic ping to keep connection alive
   */
  private startPingTimer(): void {
    this.pingTimeoutId = window.setTimeout(() => {
      if (this.isConnected()) {
        try {
          this.ws!.send(JSON.stringify({ type: 'ping' }));
          this.startPingTimer();
        } catch (err) {
          console.error('Failed to send ping:', err);
        }
      }
    }, 30000); // Every 30 seconds
  }
}

// Singleton instance
export const wsClient = new WebSocketClient();
