import { Subscription } from 'rxjs';
import { JoinedServerInfo } from '../realtime.types';
import { SignalingManager } from './signaling.manager';

export interface ConnectedSignalingManager {
  signalUrl: string;
  manager: SignalingManager;
}

export interface ServerSignalingCoordinatorCallbacks<TMessage> {
  createManager(
    signalUrl: string,
    getLastJoinedServer: () => JoinedServerInfo | null,
    getMemberServerIds: () => ReadonlySet<string>
  ): SignalingManager;
  handleConnectionStatus(signalUrl: string, connected: boolean, errorMessage?: string): void;
  handleHeartbeatTick(): void;
  handleMessage(message: TMessage, signalUrl: string): void;
}

export class ServerSignalingCoordinator<TMessage> {
  private readonly lastJoinedServerBySignalUrl = new Map<string, JoinedServerInfo>();
  private readonly memberServerIdsBySignalUrl = new Map<string, Set<string>>();
  private readonly serverSignalingUrlMap = new Map<string, string>();
  private readonly peerSignalingUrlMap = new Map<string, string>();
  private readonly peerKnownSignalUrls = new Map<string, Set<string>>();
  private readonly signalingManagers = new Map<string, SignalingManager>();
  private readonly signalingSubscriptions = new Map<string, Subscription[]>();
  private readonly peerServerMap = new Map<string, Map<string, Set<string>>>();

  constructor(
    private readonly callbacks: ServerSignalingCoordinatorCallbacks<TMessage>
  ) {}

  ensureSignalingManager(signalUrl: string): SignalingManager {
    const existingManager = this.signalingManagers.get(signalUrl);

    if (existingManager) {
      return existingManager;
    }

    const manager = this.callbacks.createManager(
      signalUrl,
      () => this.lastJoinedServerBySignalUrl.get(signalUrl) ?? null,
      () => this.getMemberServerIdsForSignalUrl(signalUrl)
    );
    const subscriptions: Subscription[] = [
      manager.connectionStatus$.subscribe(({ connected, errorMessage }) =>
        this.callbacks.handleConnectionStatus(signalUrl, connected, errorMessage)
      ),
      manager.messageReceived$.subscribe((message) =>
        this.callbacks.handleMessage(message as TMessage, signalUrl)
      ),
      manager.heartbeatTick$.subscribe(() => this.callbacks.handleHeartbeatTick())
    ];

    this.signalingManagers.set(signalUrl, manager);
    this.signalingSubscriptions.set(signalUrl, subscriptions);
    return manager;
  }

  getSignalingManager(signalUrl: string): SignalingManager | undefined {
    return this.signalingManagers.get(signalUrl);
  }

  isSignalingConnectedTo(signalUrl: string): boolean {
    return this.signalingManagers.get(signalUrl)?.isSocketOpen() ?? false;
  }

  isAnySignalingConnected(): boolean {
    for (const manager of this.signalingManagers.values()) {
      if (manager.isSocketOpen()) {
        return true;
      }
    }

    return false;
  }

  getConnectedSignalingManagers(): ConnectedSignalingManager[] {
    const connectedManagers: ConnectedSignalingManager[] = [];

    for (const [signalUrl, manager] of this.signalingManagers.entries()) {
      if (!manager.isSocketOpen()) {
        continue;
      }

      connectedManagers.push({ signalUrl,
        manager });
    }

    return connectedManagers;
  }

  async ensureAnySignalingConnected(timeoutMs?: number): Promise<boolean> {
    if (this.isAnySignalingConnected()) {
      return true;
    }

    for (const manager of this.signalingManagers.values()) {
      if (await manager.ensureConnected(timeoutMs)) {
        return true;
      }
    }

    return false;
  }

  setLastJoinedServer(signalUrl: string, joinedServer: JoinedServerInfo): void {
    this.lastJoinedServerBySignalUrl.set(signalUrl, joinedServer);
  }

  getLastJoinedServer(signalUrl: string): JoinedServerInfo | null {
    return this.lastJoinedServerBySignalUrl.get(signalUrl) ?? null;
  }

  clearLastJoinedServers(): void {
    this.lastJoinedServerBySignalUrl.clear();
  }

  setServerSignalUrl(serverId: string, signalUrl: string): void {
    this.serverSignalingUrlMap.set(serverId, signalUrl);
  }

  getServerSignalUrl(serverId: string): string | undefined {
    return this.serverSignalingUrlMap.get(serverId);
  }

  deleteServerSignalUrl(serverId: string): void {
    this.serverSignalingUrlMap.delete(serverId);
  }

  setPeerSignalUrl(peerId: string, signalUrl: string): void {
    const knownSignalUrls = this.peerKnownSignalUrls.get(peerId) ?? new Set<string>();

    knownSignalUrls.add(signalUrl);
    this.peerKnownSignalUrls.set(peerId, knownSignalUrls);
    this.peerSignalingUrlMap.set(peerId, signalUrl);
  }

  getPeerSignalUrl(peerId: string): string | undefined {
    const preferredSignalUrl = this.peerSignalingUrlMap.get(peerId);

    if (preferredSignalUrl) {
      return preferredSignalUrl;
    }

    const knownSignalUrls = this.peerKnownSignalUrls.get(peerId);

    return knownSignalUrls?.values().next().value;
  }

  deletePeerSignalUrl(peerId: string): void {
    this.peerSignalingUrlMap.delete(peerId);
    this.peerKnownSignalUrls.delete(peerId);
  }

  addJoinedServer(signalUrl: string, serverId: string): void {
    this.getOrCreateMemberServerSet(signalUrl).add(serverId);
  }

  removeJoinedServer(signalUrl: string, serverId: string): void {
    const memberServerIds = this.memberServerIdsBySignalUrl.get(signalUrl);

    if (!memberServerIds) {
      this.repairLastJoinedServer(signalUrl, serverId);
      return;
    }

    memberServerIds.delete(serverId);

    if (memberServerIds.size === 0) {
      this.memberServerIdsBySignalUrl.delete(signalUrl);
    }

    this.repairLastJoinedServer(signalUrl, serverId);
  }

  removeJoinedServerEverywhere(serverId: string): void {
    for (const signalUrl of Array.from(this.memberServerIdsBySignalUrl.keys())) {
      this.removeJoinedServer(signalUrl, serverId);
      this.pruneUnusedSignalUrl(signalUrl);
    }
  }

  pruneUnusedSignalUrl(signalUrl: string): void {
    if (this.getMemberServerIdsForSignalUrl(signalUrl).size > 0) {
      return;
    }

    this.memberServerIdsBySignalUrl.delete(signalUrl);
    this.lastJoinedServerBySignalUrl.delete(signalUrl);

    const subscriptions = this.signalingSubscriptions.get(signalUrl);

    if (subscriptions) {
      for (const subscription of subscriptions) {
        subscription.unsubscribe();
      }

      this.signalingSubscriptions.delete(signalUrl);
    }

    const manager = this.signalingManagers.get(signalUrl);

    if (manager) {
      manager.destroy();
      this.signalingManagers.delete(signalUrl);
    }

    this.removeSignalUrlFromPeerTracking(signalUrl);
  }

  getMemberServerIdsForSignalUrl(signalUrl: string): ReadonlySet<string> {
    return this.memberServerIdsBySignalUrl.get(signalUrl) ?? new Set<string>();
  }

  getJoinedServerEntries(): { signalUrl: string; serverIds: ReadonlySet<string> }[] {
    return Array.from(this.memberServerIdsBySignalUrl.entries()).map(([signalUrl, serverIds]) => ({
      signalUrl,
      serverIds
    }));
  }

  clearJoinedServers(): void {
    this.memberServerIdsBySignalUrl.clear();
    this.serverSignalingUrlMap.clear();
  }

  hasJoinedServer(serverId: string): boolean {
    for (const memberServerIds of this.memberServerIdsBySignalUrl.values()) {
      if (memberServerIds.has(serverId)) {
        return true;
      }
    }

    return false;
  }

  getJoinedServerCount(): number {
    let joinedServerCount = 0;

    for (const memberServerIds of this.memberServerIdsBySignalUrl.values()) {
      joinedServerCount += memberServerIds.size;
    }

    return joinedServerCount;
  }

  getJoinedServerIds(): ReadonlySet<string> {
    const joinedServerIds = new Set<string>();

    for (const memberServerIds of this.memberServerIdsBySignalUrl.values()) {
      memberServerIds.forEach((serverId) => joinedServerIds.add(serverId));
    }

    return joinedServerIds;
  }

  trackPeerInServer(peerId: string, serverId: string, signalUrl: string): void {
    if (!peerId || !serverId || !signalUrl)
      return;

    const trackedSignalUrls = this.peerServerMap.get(peerId) ?? new Map<string, Set<string>>();
    const trackedServers = trackedSignalUrls.get(signalUrl) ?? new Set<string>();

    trackedServers.add(serverId);
    trackedSignalUrls.set(signalUrl, trackedServers);
    this.peerServerMap.set(peerId, trackedSignalUrls);
    this.setPeerSignalUrl(peerId, signalUrl);
  }

  hasTrackedPeerServers(peerId: string): boolean {
    return this.getTrackedServerIds(peerId).size > 0;
  }

  replacePeerSharedServers(peerId: string, signalUrl: string, serverIds: string[]): boolean {
    const sharedServerIds = serverIds.filter((serverId) => this.hasJoinedServer(serverId));

    if (sharedServerIds.length === 0) {
      this.removePeerSignalScope(peerId, signalUrl);
      return this.hasTrackedPeerServers(peerId);
    }

    const trackedSignalUrls = this.peerServerMap.get(peerId) ?? new Map<string, Set<string>>();

    trackedSignalUrls.set(signalUrl, new Set(sharedServerIds));
    this.peerServerMap.set(peerId, trackedSignalUrls);
    this.setPeerSignalUrl(peerId, signalUrl);

    return true;
  }

  untrackPeerFromServer(peerId: string, signalUrl: string, serverId: string): boolean {
    const trackedSignalUrls = this.peerServerMap.get(peerId);

    if (!trackedSignalUrls)
      return false;

    const trackedServers = trackedSignalUrls.get(signalUrl);

    if (!trackedServers)
      return this.hasTrackedPeerServers(peerId);

    trackedServers.delete(serverId);

    if (trackedServers.size === 0) {
      trackedSignalUrls.delete(signalUrl);
      this.untrackPeerSignalUrl(peerId, signalUrl);
    } else {
      trackedSignalUrls.set(signalUrl, trackedServers);
    }

    if (trackedSignalUrls.size === 0) {
      this.peerServerMap.delete(peerId);
      return false;
    }

    this.peerServerMap.set(peerId, trackedSignalUrls);
    return true;
  }

  deletePeerTracking(peerId: string): void {
    this.peerServerMap.delete(peerId);
    this.peerSignalingUrlMap.delete(peerId);
    this.peerKnownSignalUrls.delete(peerId);
  }

  clearPeerTracking(): void {
    this.peerServerMap.clear();
    this.peerSignalingUrlMap.clear();
    this.peerKnownSignalUrls.clear();
  }

  getPeersOutsideServer(serverId: string): string[] {
    const peersToClose: string[] = [];

    this.peerServerMap.forEach((_peerServerIdsBySignalUrl, peerId) => {
      if (!this.getTrackedServerIds(peerId).has(serverId)) {
        peersToClose.push(peerId);
      }
    });

    return peersToClose;
  }

  destroy(): void {
    for (const subscriptions of this.signalingSubscriptions.values()) {
      for (const subscription of subscriptions) {
        subscription.unsubscribe();
      }
    }

    for (const manager of this.signalingManagers.values()) {
      manager.destroy();
    }

    this.signalingSubscriptions.clear();
    this.signalingManagers.clear();
    this.clearJoinedServers();
    this.clearLastJoinedServers();
    this.clearPeerTracking();
  }

  private getOrCreateMemberServerSet(signalUrl: string): Set<string> {
    const existingSet = this.memberServerIdsBySignalUrl.get(signalUrl);

    if (existingSet) {
      return existingSet;
    }

    const createdSet = new Set<string>();

    this.memberServerIdsBySignalUrl.set(signalUrl, createdSet);
    return createdSet;
  }

  private getTrackedServerIds(peerId: string): Set<string> {
    const trackedServerIds = new Set<string>();
    const trackedSignalUrls = this.peerServerMap.get(peerId);

    if (!trackedSignalUrls) {
      return trackedServerIds;
    }

    trackedSignalUrls.forEach((serverIds) => {
      serverIds.forEach((serverId) => trackedServerIds.add(serverId));
    });

    return trackedServerIds;
  }

  private repairLastJoinedServer(signalUrl: string, removedServerId: string): void {
    const lastJoined = this.lastJoinedServerBySignalUrl.get(signalUrl);

    if (!lastJoined) {
      return;
    }

    const memberServerIds = this.memberServerIdsBySignalUrl.get(signalUrl);

    if (!memberServerIds || memberServerIds.size === 0) {
      this.lastJoinedServerBySignalUrl.delete(signalUrl);
      return;
    }

    if (lastJoined.serverId !== removedServerId && memberServerIds.has(lastJoined.serverId)) {
      return;
    }

    const nextServerId = memberServerIds.values().next().value as string | undefined;

    if (!nextServerId) {
      this.lastJoinedServerBySignalUrl.delete(signalUrl);
      return;
    }

    this.lastJoinedServerBySignalUrl.set(signalUrl, {
      ...lastJoined,
      serverId: nextServerId
    });
  }

  private removeSignalUrlFromPeerTracking(signalUrl: string): void {
    const peerIds = new Set<string>([...this.peerKnownSignalUrls.keys(), ...this.peerServerMap.keys()]);

    for (const peerId of peerIds) {
      this.removePeerSignalScope(peerId, signalUrl);
    }
  }

  private removePeerSignalScope(peerId: string, signalUrl: string): void {
    const trackedSignalUrls = this.peerServerMap.get(peerId);

    if (!trackedSignalUrls) {
      this.untrackPeerSignalUrl(peerId, signalUrl);
      return;
    }

    trackedSignalUrls.delete(signalUrl);

    if (trackedSignalUrls.size === 0) {
      this.peerServerMap.delete(peerId);
    } else {
      this.peerServerMap.set(peerId, trackedSignalUrls);
    }

    this.untrackPeerSignalUrl(peerId, signalUrl);
  }

  private untrackPeerSignalUrl(peerId: string, signalUrl: string): void {
    const knownSignalUrls = this.peerKnownSignalUrls.get(peerId);

    if (!knownSignalUrls) {
      if (this.peerSignalingUrlMap.get(peerId) === signalUrl) {
        this.peerSignalingUrlMap.delete(peerId);
      }

      return;
    }

    knownSignalUrls.delete(signalUrl);

    if (knownSignalUrls.size === 0) {
      this.peerKnownSignalUrls.delete(peerId);
      this.peerSignalingUrlMap.delete(peerId);
      return;
    }

    this.peerKnownSignalUrls.set(peerId, knownSignalUrls);

    if (this.peerSignalingUrlMap.get(peerId) === signalUrl) {
      this.peerSignalingUrlMap.set(peerId, knownSignalUrls.values().next().value as string);
    }
  }
}
