import { API_URLS } from '@/config/constants';

// --- Types ---

export type SyncClientConfig = {
  token: string;
  sessionId: string;
  trackId: string;
  // Callback to get current player state
  getPlayerState: () => {
    playbackPositionMs: number;
    clientTimestampMs: number;
  };
  // Callback to apply drift correction
  applyAdjustment: (driftMs: number) => void;
  // Callback for debug logging
  onDebug?: (message: string, data?: any) => void;
  // Callback when sync is stable
  onStable?: () => void;
  // Callback on error
  onError?: (error: Error) => void;
};

// WebSocket Message Types (Server -> Client)
export type SyncServerMessage =
  | {
      type: 'SyncInit';
      session_id: string;
      track_id: string;
      server_timestamp_ms: number;
      position_ms: number;
    }
  | { type: 'SyncPing'; ping_id: string; server_timestamp_ms: number }
  | { type: 'SyncAdjustment'; session_id: string; drift_ms: number }
  | { type: 'SyncStable'; session_id: string }
  | { type: 'error'; message: string };

// WebSocket Message Types (Client -> Server)
type SyncClientMessage =
  | { type: 'SyncPong'; ping_id: string; client_timestamp_ms: number }
  | {
      type: 'SyncClientState';
      position_ms: number;
      client_timestamp_ms: number;
    };

export class SyncClient {
  private ws: WebSocket | null = null;
  private config: SyncClientConfig;
  private isConnected = false;
  private pingInterval: number | null = null;
  private stateInterval: number | null = null;
  private reconnectAttempts = 0;
  private maxReconnectAttempts = 5;
  private reconnectTimer: number | null = null;

  constructor(config: SyncClientConfig) {
    this.config = config;
  }

  public connect() {
    if (this.ws) {
      this.disconnect();
    }

    // Build WebSocket URL with query params
    const wsUrl = new URL(`${API_URLS.WS}/ws`);
    wsUrl.searchParams.append('token', this.config.token);
    wsUrl.searchParams.append('session_id', this.config.sessionId);
    // track_id might be needed if the backend uses it for initial routing/validation
    // but usually session_id is enough for the connection itself.
    // The previous implementation used stream_id/session_id.
    // Let's assume the standard WS endpoint.

    this.log('Connecting to WebSocket...', { url: wsUrl.toString() });

    try {
      this.ws = new WebSocket(wsUrl.toString());
      this.setupEventListeners();
    } catch (error) {
      this.handleError(
        error instanceof Error ? error : new Error('WebSocket creation failed'),
      );
    }
  }

  public disconnect() {
    this.log('Disconnecting...');
    this.stopHealthCheck();
    this.stopStateReporting();

    if (this.reconnectTimer) {
      window.clearTimeout(this.reconnectTimer);
      this.reconnectTimer = null;
    }

    if (this.ws) {
      // Remove listeners to avoid side effects during close
      this.ws.onopen = null;
      this.ws.onclose = null;
      this.ws.onerror = null;
      this.ws.onmessage = null;

      this.ws.close();
      this.ws = null;
    }
    this.isConnected = false;
  }

  private setupEventListeners() {
    if (!this.ws) return;

    this.ws.onopen = () => {
      this.log('WebSocket connected');
      this.isConnected = true;
      this.reconnectAttempts = 0;
      this.startStateReporting();
    };

    this.ws.onclose = (event) => {
      this.log('WebSocket closed', { code: event.code, reason: event.reason });
      this.isConnected = false;
      this.stopStateReporting();
      this.attemptReconnect();
    };

    this.ws.onerror = (event) => {
      this.log('WebSocket error', event);
      // Attempt reconnect will be triggered by onclose
    };

    this.ws.onmessage = (event) => {
      try {
        const message = JSON.parse(event.data) as SyncServerMessage;
        this.handleMessage(message);
      } catch (error) {
        this.log('Failed to parse message', { data: event.data, error });
      }
    };
  }

  private handleMessage(message: SyncServerMessage) {
    this.log('Received message', message);

    switch (message.type) {
      case 'SyncInit':
        this.handleSyncInit(message);
        break;
      case 'SyncPing':
        this.handleSyncPing(message);
        break;
      case 'SyncAdjustment':
        this.handleSyncAdjustment(message);
        break;
      case 'SyncStable':
        this.handleSyncStable(message);
        break;
      case 'error':
        this.log('Server error', message);
        if (this.config.onError) {
          this.config.onError(new Error(message.message));
        }
        break;
      default:
        this.log('Unknown message type', message);
    }
  }

  private handleSyncInit(message: {
    position_ms: number;
    server_timestamp_ms: number;
  }) {
    // On Init, we might want to snap to the server position immediately
    // or just acknowledge. For now, let's log and verify logic.
    // If the server sends position, we respect it.
    this.log('Initializing Sync', message);

    // Potentially snap to position if provided and useful
    // For now we trust the SyncAdjustment loop to fix drifts,
    // unless the difference is huge.
    const playerState = this.config.getPlayerState();
    const drift = playerState.playbackPositionMs - message.position_ms;

    // If very far off (> 2 sec), snap immediately?
    if (Math.abs(drift) > 2000) {
      this.log('Large initial drift, applying immediate correction', { drift });
      this.config.applyAdjustment(-drift); // Negative to correct
    }
  }

  private handleSyncPing(message: { ping_id: string }) {
    const pong: SyncClientMessage = {
      type: 'SyncPong',
      ping_id: message.ping_id,
      client_timestamp_ms: Date.now(),
    };
    this.send(pong);
  }

  private handleSyncAdjustment(message: { drift_ms: number }) {
    this.log('Applying adjustment', { drift: message.drift_ms });
    this.config.applyAdjustment(message.drift_ms);
  }

  private handleSyncStable(_message: { session_id: string }) {
    this.log('Sync Stable');
    if (this.config.onStable) {
      this.config.onStable();
    }
  }

  private startStateReporting() {
    this.stopStateReporting();
    // Report state every 1 second (can be tuned)
    this.stateInterval = window.setInterval(() => {
      if (!this.isConnected) return;

      const state = this.config.getPlayerState();
      const msg: SyncClientMessage = {
        type: 'SyncClientState',
        position_ms: state.playbackPositionMs,
        client_timestamp_ms: state.clientTimestampMs,
      };
      this.send(msg);
    }, 1000);
  }

  private stopStateReporting() {
    if (this.stateInterval) {
      window.clearInterval(this.stateInterval);
      this.stateInterval = null;
    }
  }

  private send(message: SyncClientMessage) {
    if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
      // Fail silently or buffer? For real-time sync, buffering old messages is bad.
      return;
    }
    this.ws.send(JSON.stringify(message));
  }

  private attemptReconnect() {
    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
      const error = new Error('Max reconnect attempts reached');
      this.handleError(error);
      return;
    }

    this.reconnectAttempts++;
    const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 10000); // Exp backoff
    this.log(
      `Reconnecting in ${delay}ms... (Attempt ${this.reconnectAttempts})`,
    );

    this.reconnectTimer = window.setTimeout(() => {
      this.connect();
    }, delay);
  }

  private handleError(error: Error) {
    this.log('SyncClient Error', error);
    if (this.config.onError) {
      this.config.onError(error);
    }
  }

  private log(msg: string, data?: any) {
    if (this.config.onDebug) {
      this.config.onDebug(msg, data);
    }
    // Also log to console in dev mode if acceptable
    if (import.meta.env.DEV) {
      // console.debug(`[SyncClient] ${msg}`, data || '');
    }
  }

  // Placeholder for health check if needed
  private stopHealthCheck() {
    if (this.pingInterval) {
      window.clearInterval(this.pingInterval);
      this.pingInterval = null;
    }
  }
}
