import * as plugins from './smartsocket.plugins.js';
import * as pluginsTyped from './smartsocket.pluginstyped.js';
import * as interfaces from './interfaces/index.js';

import { SocketConnection } from './smartsocket.classes.socketconnection.js';
import {
  type ISocketFunctionCallDataRequest,
  SocketFunction,
} from './smartsocket.classes.socketfunction.js';
import { type ISocketRequestDataObject, SocketRequest } from './smartsocket.classes.socketrequest.js';
import { logger } from './smartsocket.logging.js';

/**
 * interface for class SmartsocketClient
 */
export interface ISmartsocketClientOptions {
  port: number;
  url: string;
  alias: string; // an alias makes it easier to identify this client in a multi client environment
  autoReconnect?: boolean;
  maxRetries?: number; // maximum number of reconnection attempts
  initialBackoffDelay?: number; // initial backoff delay in ms
  maxBackoffDelay?: number; // maximum backoff delay in ms
}

export class SmartsocketClient {
  // a unique id
  public shortId = plugins.isounique.uni();

  // the shortId of the remote we connect to
  public remoteShortId: string | null = null;

  public alias: string;
  public socketConnection?: SocketConnection;
  public serverUrl: string;
  public serverPort: number;
  public autoReconnect: boolean;
  public maxRetries: number;
  public initialBackoffDelay: number;
  public maxBackoffDelay: number;
  public currentRetryCount = 0;
  public currentBackoffDelay: number;

  // status handling
  public eventSubject = new plugins.smartrx.rxjs.Subject<interfaces.TConnectionStatus>();
  public eventStatus: interfaces.TConnectionStatus = 'new';

  public socketFunctions = new plugins.lik.ObjectMap<SocketFunction<any>>();
  public socketRequests = new plugins.lik.ObjectMap<SocketRequest<any>>();

  // tagStore
  private tagStore: { [key: string]: interfaces.ITag } = {};
  private tagStoreSubscription?: plugins.smartrx.rxjs.Subscription;

  /**
   * adds a tag to a connection
   */
  public async addTag(tagArg: interfaces.ITag) {
    if (this.socketConnection) {
      await this.socketConnection.addTag(tagArg);
    } else {
      this.tagStore[tagArg.id] = tagArg;
    }
  }

  /**
   * gets a tag by id
   * @param tagIdArg
   */
  public async getTagById(tagIdArg: interfaces.ITag['id']) {
    return this.tagStore[tagIdArg];
  }

  /**
   * removes a tag from a connection
   */
  public async removeTagById(tagIdArg: interfaces.ITag['id']) {
    if (this.socketConnection) {
      this.socketConnection.removeTagById(tagIdArg);
    } else {
      delete this.tagStore[tagIdArg];
    }
  }

  constructor(optionsArg: ISmartsocketClientOptions) {
    this.alias = optionsArg.alias;
    this.serverUrl = optionsArg.url;
    this.serverPort = optionsArg.port;
    this.autoReconnect = optionsArg.autoReconnect ?? false;
    this.maxRetries = optionsArg.maxRetries ?? 100; // Default to 100 retries
    this.initialBackoffDelay = optionsArg.initialBackoffDelay ?? 1000; // Default to 1 second
    this.maxBackoffDelay = optionsArg.maxBackoffDelay ?? 60000; // Default to 1 minute
    this.currentBackoffDelay = this.initialBackoffDelay;
  }

  public addSocketFunction(socketFunction: SocketFunction<any>) {
    this.socketFunctions.add(socketFunction);
  }

  private isReconnecting = false;
  private isConnecting = false;

  /**
   * connect the client to the server
   */
  public async connect() {
    // Prevent duplicate connection attempts
    if (this.isConnecting) {
      return;
    }
    this.isConnecting = true;

    // Only reset retry counters on fresh connection (not during auto-reconnect)
    if (!this.isReconnecting) {
      this.currentRetryCount = 0;
      this.currentBackoffDelay = this.initialBackoffDelay;
    }
    this.isReconnecting = false;

    const done = plugins.smartpromise.defer();
    const smartenvInstance = new plugins.smartenv.Smartenv();

    logger.log('info', 'trying to connect...');

    // Construct WebSocket URL
    const protocol = this.serverUrl.startsWith('https') ? 'wss' : 'ws';
    const host = this.serverUrl.replace(/^https?:\/\//, '');
    const socketUrl = `${protocol}://${host}:${this.serverPort}`;

    // Get WebSocket implementation (native in browser, ws in Node)
    let WebSocketClass: typeof WebSocket;
    if (typeof WebSocket !== 'undefined') {
      // Browser environment
      WebSocketClass = WebSocket;
    } else {
      // Node.js environment
      const wsModule = await smartenvInstance.getSafeNodeModule('ws');
      WebSocketClass = wsModule.default || wsModule;
    }

    const socket = new WebSocketClass(socketUrl);
    this.currentSocket = socket;

    this.socketConnection = new SocketConnection({
      alias: this.alias,
      authenticated: false,
      side: 'client',
      smartsocketHost: this,
      socket: socket as any,
    });
    const socketConnection = this.socketConnection;

    // Increment attempt ID to invalidate any pending timers from previous attempts
    this.connectionAttemptId++;
    const currentAttemptId = this.connectionAttemptId;

    const timer = new plugins.smarttime.Timer(5000);
    timer.start();
    timer.completed.then(() => {
      // Only fire timeout if this is still the current connection attempt
      if (currentAttemptId === this.connectionAttemptId && this.eventStatus !== 'connected') {
        this.updateStatus('timedOut');
        logger.log('warn', 'connection to server timed out.');
        this.disconnect(true);
      }
    });

    // Handle connection open
    socket.addEventListener('open', () => {
      timer.reset();
    });

    // Handle messages
    socket.addEventListener('message', async (event: MessageEvent | { data: string }) => {
      try {
        const data = typeof event.data === 'string' ? event.data : event.data.toString();
        const message: interfaces.ISocketMessage = JSON.parse(data);

        switch (message.type) {
          case 'authRequest':
            timer.reset();
            const authRequestPayload = message.payload as interfaces.IAuthRequestPayload;
            logger.log('info', `server ${authRequestPayload.serverAlias} requested authentication`);
            this.remoteShortId = authRequestPayload.serverAlias;

            // Send authentication data
            socketConnection.sendMessage({
              type: 'auth',
              payload: { alias: this.alias },
            });
            break;

          case 'authResponse':
            const authResponse = message.payload as interfaces.IAuthResponsePayload;
            if (authResponse.success) {
              logger.log('info', 'client is authenticated');
              socketConnection.authenticated = true;
            } else {
              logger.log('warn', `authentication failed: ${authResponse.error}`);
              await this.disconnect();
            }
            break;

          case 'serverReady':
            // Set up function request listening
            await socketConnection.listenToFunctionRequests();

            // Handle retagging
            const oldTagStore = this.tagStore;
            this.tagStoreSubscription?.unsubscribe();
            for (const keyArg of Object.keys(this.tagStore)) {
              socketConnection.addTag(this.tagStore[keyArg]);
            }
            this.tagStoreSubscription = socketConnection.tagStoreObservable.subscribe(
              (tagStoreArg) => {
                this.tagStore = tagStoreArg;
              }
            );

            for (const tag of Object.keys(oldTagStore)) {
              await this.addTag(oldTagStore[tag]);
            }
            this.updateStatus('connected');
            this.isConnecting = false;
            done.resolve();
            break;

          default:
            // Other messages are handled by SocketConnection
            socketConnection.handleMessage(message);
            break;
        }
      } catch (err) {
        // Not a valid JSON message, ignore
      }
    });

    // Handle disconnection and errors
    const closeHandler = async () => {
      // Only handle close if this is still the current socket and we're not already disconnecting
      if (this.currentSocket === socket && !this.disconnectRunning) {
        logger.log(
          'info',
          `SocketConnection with >alias ${this.alias} on >side client disconnected`
        );
        await this.disconnect(true);
      }
    };

    const errorHandler = async () => {
      if (this.currentSocket === socket && !this.disconnectRunning) {
        await this.disconnect(true);
      }
    };

    socket.addEventListener('close', closeHandler);
    socket.addEventListener('error', errorHandler);

    return done.promise;
  }

  private disconnectRunning = false;
  private currentSocket: WebSocket | null = null;
  private connectionAttemptId = 0; // Increment on each connect attempt to invalidate old timers

  /**
   * disconnect from the server
   */
  public async disconnect(useAutoReconnectSetting = false) {
    if (this.disconnectRunning) {
      return;
    }
    this.disconnectRunning = true;
    this.isConnecting = false;
    this.updateStatus('disconnecting');
    this.tagStoreSubscription?.unsubscribe();

    // Store reference to current socket before cleanup
    const socketToClose = this.currentSocket;
    this.currentSocket = null;

    if (this.socketConnection) {
      await this.socketConnection.disconnect();
      this.socketConnection = undefined;
      logger.log('ok', 'disconnected socket!');
    } else if (!socketToClose) {
      this.disconnectRunning = false;
      logger.log('warn', 'tried to disconnect, without a SocketConnection');
      return;
    }

    logger.log('warn', `disconnected from server ${this.remoteShortId}`);
    this.remoteShortId = null;

    if (this.autoReconnect && useAutoReconnectSetting && this.eventStatus !== 'connecting') {
      this.updateStatus('connecting');

      // Check if we've exceeded the maximum number of retries
      if (this.currentRetryCount >= this.maxRetries) {
        logger.log('warn', `Maximum reconnection attempts (${this.maxRetries}) reached. Giving up.`);
        this.disconnectRunning = false;
        return;
      }

      // Increment retry counter
      this.currentRetryCount++;

      // Calculate backoff with jitter (±20% randomness)
      const jitter = this.currentBackoffDelay * 0.2 * (Math.random() * 2 - 1);
      const delay = Math.min(this.currentBackoffDelay + jitter, this.maxBackoffDelay);

      logger.log('info', `Reconnect attempt ${this.currentRetryCount}/${this.maxRetries} in ${Math.round(delay)}ms`);

      // Apply exponential backoff for next time (doubling with each attempt)
      this.currentBackoffDelay = Math.min(this.currentBackoffDelay * 2, this.maxBackoffDelay);

      await plugins.smartdelay.delayFor(delay);
      this.disconnectRunning = false;
      this.isReconnecting = true;
      await this.connect();
    } else {
      this.disconnectRunning = false;
    }
  }

  /**
   * stops the client completely
   */
  public async stop() {
    this.autoReconnect = false;
    this.currentRetryCount = 0;
    this.currentBackoffDelay = this.initialBackoffDelay;
    await this.disconnect();
  }

  /**
   * dispatches a server call
   * @param functionNameArg
   * @param dataArg
   */
  public async serverCall<T extends plugins.typedrequestInterfaces.ITypedRequest>(
    functionNameArg: T['method'],
    dataArg: T['request']
  ): Promise<T['response']> {
    if (!this.socketConnection) {
      throw new Error('Cannot call server without an active socket connection');
    }
    const socketRequest = new SocketRequest<T>(this, {
      side: 'requesting',
      originSocketConnection: this.socketConnection,
      shortId: plugins.isounique.uni(),
      funcCallData: {
        funcName: functionNameArg,
        funcDataArg: dataArg,
      },
    });
    const response = await socketRequest.dispatch();
    const result = response.funcDataArg;
    return result;
  }

  private updateStatus(statusArg: interfaces.TConnectionStatus) {
    if (this.eventStatus !== statusArg) {
      this.eventSubject.next(statusArg);
    }
    this.eventStatus = statusArg;

    // Reset reconnection state when connection is successful
    if (statusArg === 'connected') {
      this.currentRetryCount = 0;
      this.currentBackoffDelay = this.initialBackoffDelay;
    }
  }

  /**
   * Resets the reconnection state
   */
  public resetReconnectionState() {
    this.currentRetryCount = 0;
    this.currentBackoffDelay = this.initialBackoffDelay;
  }
}
