/**
 * Socket.IO client for imks real-time messaging.
 *
 * Built on top of EngineClient. Handles Socket.IO namespace authentication,
 * typed event emit/receive, reconnect re-authentication and token refresh.
 */

import { EngineClient } from './engine';
import {
  decodeSocketPacket,
  encodeOutgoing,
  encodeOutgoingConnect,
  encodeOutgoingDisconnect,
} from './protocol';
import type { ClientEventMap, ConnectionState, ServerEventMap } from './types';

type EventHandler<Payload extends unknown[]> = (...args: Payload) => void;

interface PendingConnect {
  resolve: () => void;
  reject: (error: Error) => void;
  timer: ReturnType<typeof setTimeout>;
}

/** Socket.IO client for connecting to imks real-time messaging. */
export class SocketClient {
  private engine: EngineClient;
  private token: string;
  private state: ConnectionState = 'disconnected';
  private namespace = '/';
  private connectTimeout: number;
  private pendingConnect: PendingConnect | null = null;
  private eventHandlers = new Map<string, Set<EventHandler<unknown[]>>>();
  private stateHandlers = new Set<(state: ConnectionState) => void>();

  constructor(options: {
    url: string;
    token: string;
    enginePath?: string;
    connectTimeout?: number;
  }) {
    this.token = options.token;
    this.connectTimeout = options.connectTimeout ?? 10000;

    this.engine = new EngineClient({
      url: options.url,
      path: options.enginePath,
    });

    this.setupEngine();
  }

  get currentState(): ConnectionState {
    return this.state;
  }

  /** Connect to imks and wait for the Socket.IO CONNECT response. */
  async connect(): Promise<void> {
    if (this.state !== 'disconnected') return;

    this.setState('connecting');

    try {
      await this.engine.connect();
      await this.openSocketNamespace();
    } catch (err) {
      this.clearPendingConnect();
      this.engine.close();
      this.setState('disconnected');
      throw err;
    }
  }

  /** Refresh the auth token while preserving registered event handlers. */
  async refreshToken(newToken: string): Promise<void> {
    this.token = newToken;
    if (this.state === 'connected') {
      this.engine.send(encodeOutgoingDisconnect(this.namespace));
    }
    this.clearPendingConnect();
    this.engine.close();
    this.setState('disconnected');
    await this.connect();
  }

  /** Disconnect from Socket.IO and close the underlying Engine.IO transport. */
  disconnect(): void {
    if (this.state === 'disconnected') return;

    if (this.state === 'connected') {
      this.engine.send(encodeOutgoingDisconnect(this.namespace));
    }
    this.clearPendingConnect();
    this.engine.close();
    this.setState('disconnected');
  }

  /** Emit a client event to the server. */
  emit<E extends keyof ClientEventMap>(event: E, ...args: ClientEventMap[E]): void {
    if (this.state !== 'connected') {
      console.warn(`[Socket.IO] Cannot emit "${event}" — not connected`);
      return;
    }

    const payload = args.length === 1 ? args[0] : args;
    const encoded = encodeOutgoing(this.namespace, event as string, payload);
    this.engine.send(encoded);
  }

  /** Register a handler for a server event. Returns an unsubscribe function. */
  on<E extends keyof ServerEventMap>(
    event: E,
    handler: EventHandler<ServerEventMap[E]>,
  ): () => void {
    let handlers = this.eventHandlers.get(event as string);
    if (!handlers) {
      handlers = new Set();
      this.eventHandlers.set(event as string, handlers);
    }
    handlers.add(handler as EventHandler<unknown[]>);

    return () => {
      handlers?.delete(handler as EventHandler<unknown[]>);
    };
  }

  /** Register a handler for connection state changes. */
  onStateChange(handler: (state: ConnectionState) => void): () => void {
    this.stateHandlers.add(handler);
    return () => this.stateHandlers.delete(handler);
  }

  private setupEngine(): void {
    this.engine.onMessage((raw) => {
      const packet = decodeSocketPacket(raw);
      if (!packet) return;

      switch (packet.type) {
        case '0': {
          this.clearPendingConnect(true);
          this.setState('connected');
          break;
        }
        case '1': {
          this.clearPendingConnect();
          this.setState('disconnected');
          break;
        }
        case '2': {
          if (packet.event) {
            this.dispatch(packet.event, packet.data);
          }
          break;
        }
        case '4': {
          const error = new Error(formatConnectError(packet.data));
          this.rejectPendingConnect(error);
          console.error('[Socket.IO] Connect error:', packet.data);
          this.setState('disconnected');
          break;
        }
        default:
          break;
      }
    });

    this.engine.onStateChange((engineState) => {
      if (engineState === 'closed' && (this.state === 'connected' || this.state === 'connecting')) {
        this.clearPendingConnect();
        this.setState('reconnecting');
        return;
      }

      if (engineState === 'open' && this.state === 'reconnecting') {
        this.openSocketNamespace().catch((err) => {
          console.error('[Socket.IO] Reconnect authentication failed:', err);
          this.engine.close();
          this.setState('disconnected');
        });
      }
    });
  }

  private async openSocketNamespace(): Promise<void> {
    const waitForConnect = this.waitForConnectResponse();
    this.engine.send(
      encodeOutgoingConnect(this.namespace, {
        token: this.token,
      }),
    );
    await waitForConnect;
  }

  private waitForConnectResponse(): Promise<void> {
    this.clearPendingConnect();

    return new Promise((resolve, reject) => {
      const timer = setTimeout(() => {
        this.pendingConnect = null;
        reject(new Error('Socket.IO connect timed out'));
      }, this.connectTimeout);

      this.pendingConnect = { resolve, reject, timer };
    });
  }

  private clearPendingConnect(resolve = false): void {
    if (!this.pendingConnect) return;

    const pending = this.pendingConnect;
    this.pendingConnect = null;
    clearTimeout(pending.timer);
    if (resolve) {
      pending.resolve();
    }
  }

  private rejectPendingConnect(error: Error): void {
    if (!this.pendingConnect) return;

    const pending = this.pendingConnect;
    this.pendingConnect = null;
    clearTimeout(pending.timer);
    pending.reject(error);
  }

  private dispatch(event: string, data: unknown): void {
    const handlers = this.eventHandlers.get(event);
    if (!handlers || handlers.size === 0) return;

    for (const handler of handlers) {
      try {
        if (data !== undefined) {
          (handler as (arg: unknown) => void)(data);
        } else {
          (handler as () => void)();
        }
      } catch (err) {
        console.error(`[Socket.IO] Handler error for "${event}":`, err);
      }
    }
  }

  private setState(newState: ConnectionState): void {
    if (this.state !== newState) {
      this.state = newState;
      for (const handler of this.stateHandlers) {
        try {
          handler(newState);
        } catch {
          /* ignore */
        }
      }
    }
  }
}

function formatConnectError(data: unknown): string {
  if (typeof data === 'string') return data;
  if (data && typeof data === 'object' && 'message' in data) {
    const message = (data as { message?: unknown }).message;
    if (typeof message === 'string') return message;
  }
  return 'Socket.IO connect error';
}
