import { reportJsErrorTelemetry } from '../utils/jsErrorTelemetry.js';

class SocketManager {
  constructor() {
    this._socket = null;
    this._socketReady = null;
    // Listeners registered before socket.io-client has loaded
    this._preSocketListeners = [];

    this.emit = this.emit.bind(this);
    this.on = this.on.bind(this);
    this.off = this.off.bind(this);
    this.connectPromise = null;
    this.reauthPromise = null;
    this.pendingListeners = new Map();
  }

  _getStoredUser() {
    const storedUser = sessionStorage.getItem('user');
    if (!storedUser) {
      return null;
    }

    try {
      return JSON.parse(storedUser);
    } catch (error) {
      console.error('Failed to parse stored user for socket reauth:', error);
      sessionStorage.removeItem('user');
      return null;
    }
  }

  _buildSocketAuth() {
    const token = sessionStorage.getItem('authToken');
    const user = this._getStoredUser();
    if (!token && !user) {
      return {};
    }

    // Provide both nested and flat shapes for backend compatibility.
    return {
      token,
      user,
      userId: user ? user.id : undefined,
      email: user ? user.email : undefined
    };
  }

  _reauthenticate() {
    if (!this._socket || !this._socket.connected) {
      return Promise.resolve();
    }

    const token = sessionStorage.getItem('authToken');
    if (!token) {
      return Promise.resolve();
    }

    if (this.reauthPromise) {
      return this.reauthPromise;
    }

    this.reauthPromise = new Promise((resolve) => {
      let settled = false;
      const done = () => {
        if (settled) return;
        settled = true;
        this.reauthPromise = null;
        resolve();
      };

      // Don't block emits indefinitely if backend ignores this event.
      const timeoutId = setTimeout(done, 800);

      this._socket.emit('verifyToken', { token }, (response) => {
        clearTimeout(timeoutId);
        if (response && response.success && response.user) {
          sessionStorage.setItem('user', JSON.stringify(response.user));
        }
        if (response && response.success && response.token) {
          sessionStorage.setItem('authToken', response.token);
        }
        done();
      });
    });

    return this.reauthPromise;
  }

  /**
   * Report socket.io client errors to /api/telemetry/js-errors (richer than promise rejections alone).
   */
  _installSocketTelemetry(socket) {
    const report = (eventName, err) => {
      const message =
        err && typeof err.message === 'string' && err.message.trim()
          ? err.message
          : err != null
            ? String(err)
            : eventName;
      reportJsErrorTelemetry({
        message,
        name: err && err.name,
        stack: err && err.stack,
        url: typeof window !== 'undefined' ? window.location.href : undefined,
        context: {
          type: 'socket.io',
          event: eventName,
          connected: Boolean(socket.connected),
          ...(socket.id ? { socketId: socket.id } : {}),
          ...(socket.io?.engine?.transport?.name
            ? { transport: socket.io.engine.transport.name }
            : {}),
        },
      });
    };

    socket.on('connect_error', (err) => {
      report('connect_error', err);
    });

    socket.on('error', (err) => {
      report('error', err);
    });

    if (socket.io) {
      socket.io.on('reconnect_error', (err) => {
        report('reconnect_error', err);
      });
      socket.io.on('reconnect_failed', () => {
        report('reconnect_failed', new Error('reconnect_failed'));
      });
    }

    if (socket.io?.engine) {
      socket.io.engine.on('error', (err) => {
        report('engine_error', err);
      });
    }
  }

  // Lazily import socket.io-client and create the socket on first use.
  // Subsequent calls return the same promise.
  _ensureSocket() {
    if (this._socket) return Promise.resolve(this._socket);
    if (this._socketReady) return this._socketReady;

    this._socketReady = import('socket.io-client').then(({ io }) => {
      this._socket = io('', {
        transports: ['websocket', 'polling'],
        autoConnect: false,
        withCredentials: true,
        auth: this._buildSocketAuth()
      });

      // Always refresh auth data before reconnect attempts.
      if (this._socket.io && this._socket.io.on) {
        this._socket.io.on('reconnect_attempt', () => {
          this._socket.auth = this._buildSocketAuth();
        });
      }

      // Re-authenticate every time a new socket connection is established.
      this._socket.on('connect', () => {
        this.reauthPromise = this._reauthenticate();
      });

      this._installSocketTelemetry(this._socket);

      // Register any listeners that arrived before the socket was ready
      this._preSocketListeners.forEach(({ event, callback }) => {
        this._socket.on(event, callback);
      });
      this._preSocketListeners = [];

      return this._socket;
    });

    return this._socketReady;
  }

  on(event, callback) {
    if (this._socket) {
      // Socket already loaded — mirror the original behaviour
      if (!this.pendingListeners.has(event)) {
        this.pendingListeners.set(event, new Set());
      }
      this.pendingListeners.get(event).add(callback);
      this._socket.on(event, callback);
    } else {
      // Queue for when socket.io-client finishes loading
      this._preSocketListeners.push({ event, callback });
      if (!this.pendingListeners.has(event)) {
        this.pendingListeners.set(event, new Set());
      }
      this.pendingListeners.get(event).add(callback);
    }
  }

  off(event, callback) {
    console.log('off', event, callback);

    // Remove from pre-socket queue (component unmounted before socket loaded)
    this._preSocketListeners = this._preSocketListeners.filter(
      (item) => !(item.event === event && item.callback === callback)
    );

    if (this.pendingListeners.has(event)) {
      const listeners = this.pendingListeners.get(event);
      listeners.delete(callback);
      if (listeners.size === 0) {
        this.pendingListeners.delete(event);
      }
    }

    if (this._socket) {
      this._socket.off(event, callback);
    }
  }

  connect() {
    if (this.connectPromise) return this.connectPromise;

    this.connectPromise = this._ensureSocket().then(
      (socket) =>
        new Promise((resolve, reject) => {
          socket.auth = this._buildSocketAuth();
          socket.connect();
          socket.once('connect', () => {
            this._reauthenticate()
              .then(() => resolve())
              .catch(() => resolve());
          });
          socket.once('connect_error', (error) => {
            this.connectPromise = null;
            reject(error);
          });
        })
    );

    return this.connectPromise;
  }

  emit(event, ...args) {
    return new Promise((resolve, reject) => {
      this._ensureSocket()
        .then((socket) => {
          if (!socket.connected) {
            if (!this.connectPromise) {
              this.connect();
            }
            this.connectPromise
              .then(() => {
                socket.emit(event, ...args);
                resolve();
              })
              .catch((error) => {
                reject(error);
              });
          } else {
            const emitNow = () => {
              socket.emit(event, ...args);
              resolve();
            };

            if (this.reauthPromise) {
              this.reauthPromise.then(emitNow).catch(emitNow);
            } else {
              emitNow();
            }
          }
        })
        .catch(reject);
    });
  }
}

// Create singleton instance and expose globally so all components can reach it
const socketManager = new SocketManager();
window.socketManager = socketManager;

export default socketManager;
