import { Observable, of } from 'rxjs';
import type { SignalingMessage } from '../../../shared-kernel';
import {
  DEFAULT_DISPLAY_NAME,
  SIGNALING_TYPE_IDENTIFY,
  isTransientSignalingOutboundType
} from '../realtime.constants';
import { IdentifyCredentials } from '../realtime.types';
import { ConnectedSignalingManager, ServerSignalingCoordinator } from './server-signaling-coordinator';
import { WebRTCLogger } from '../logging/webrtc-logger';

export interface ResolvedSignalCredential {
  userId: string;
  token: string;
  displayName: string;
  homeSignalServerUrl?: string;
}

interface SignalingTransportHandlerDependencies<TMessage> {
  signalingCoordinator: ServerSignalingCoordinator<TMessage>;
  logger: WebRTCLogger;
  getLocalPeerId(): string;
  resolveCredential(signalUrl?: string): ResolvedSignalCredential | null;
  getHomeCredential(): ResolvedSignalCredential | null;
  getClientInstanceId(): string;
}

export class SignalingTransportHandler<TMessage> {
  private readonly lastIdentifyCredentialsBySignalUrl = new Map<string, IdentifyCredentials>();

  constructor(
    private readonly dependencies: SignalingTransportHandlerDependencies<TMessage>
  ) {}

  getIdentifyCredentials(): IdentifyCredentials | null {
    const homeCredential = this.dependencies.getHomeCredential();

    if (!homeCredential) {
      const firstCredential = this.lastIdentifyCredentialsBySignalUrl.values().next().value;

      return firstCredential ?? null;
    }

    return this.toIdentifyCredentials(homeCredential);
  }

  getIdentifyCredentialsForSignalUrl(signalUrl: string): IdentifyCredentials | null {
    const stored = this.lastIdentifyCredentialsBySignalUrl.get(signalUrl);

    if (stored) {
      return stored;
    }

    // Fall back to resolving the credential directly from the credential store so a
    // freshly (re)connected socket can re-identify *before* it sends any join_server /
    // view_server. Without this, a new socket can come up and emit a join while the
    // per-URL identify cache is still empty, and the server drops that join as
    // unauthenticated, leaving the user permanently absent from the presence roster.
    const resolved = this.dependencies.resolveCredential(signalUrl);

    if (!resolved) {
      return null;
    }

    return {
      oderId: resolved.userId,
      token: resolved.token,
      displayName: resolved.displayName,
      homeSignalServerUrl: resolved.homeSignalServerUrl,
      clientInstanceId: this.dependencies.getClientInstanceId()
    };
  }

  getIdentifyOderId(): string {
    return this.getIdentifyCredentials()?.oderId || this.dependencies.getLocalPeerId();
  }

  getIdentifyDisplayName(): string {
    return this.getIdentifyCredentials()?.displayName || DEFAULT_DISPLAY_NAME;
  }

  getIdentifyDescription(): string | undefined {
    return this.getIdentifyCredentials()?.description;
  }

  getConnectedSignalingManagers(): ConnectedSignalingManager[] {
    return this.dependencies.signalingCoordinator.getConnectedSignalingManagers();
  }

  getCurrentSignalingUrl(activeServerId: string | null): string | null {
    if (activeServerId) {
      const activeServerSignalUrl = this.dependencies.signalingCoordinator.getServerSignalUrl(activeServerId);

      if (activeServerSignalUrl) {
        return activeServerSignalUrl;
      }
    }

    return this.getConnectedSignalingManagers()[0]?.signalUrl ?? null;
  }

  connectToSignalingServer(serverUrl: string): Observable<boolean> {
    const manager = this.dependencies.signalingCoordinator.ensureSignalingManager(serverUrl);

    if (manager.isSocketOpen()) {
      return of(true);
    }

    return manager.connect(serverUrl);
  }

  isSignalingConnectedTo(serverUrl: string): boolean {
    return this.dependencies.signalingCoordinator.isSignalingConnectedTo(serverUrl);
  }

  async ensureSignalingConnected(timeoutMs?: number): Promise<boolean> {
    return await this.dependencies.signalingCoordinator.ensureAnySignalingConnected(timeoutMs);
  }

  sendSignalingMessage(message: Omit<SignalingMessage, 'from' | 'timestamp'>): void {
    const targetPeerId = message.to;

    if (targetPeerId) {
      const targetSignalUrl = this.dependencies.signalingCoordinator.getPeerSignalUrl(targetPeerId);

      if (targetSignalUrl) {
        const targetManager = this.dependencies.signalingCoordinator.ensureSignalingManager(targetSignalUrl);

        targetManager.sendSignalingMessage(message, this.dependencies.getLocalPeerId());
        return;
      }
    }

    const connectedManagers = this.getConnectedSignalingManagers();

    if (connectedManagers.length === 0) {
      this.dependencies.logger.error('[signaling] No active signaling connection for outbound message', new Error('No signaling manager available'), {
        type: message.type
      });

      return;
    }

    for (const { manager } of connectedManagers) {
      manager.sendSignalingMessage(message, this.dependencies.getLocalPeerId());
    }
  }

  sendRawMessage(message: Record<string, unknown>): void {
    const targetPeerId = typeof message['targetUserId'] === 'string' ? message['targetUserId'] : null;
    const messageType = typeof message['type'] === 'string' ? message['type'] : 'unknown';

    if (targetPeerId) {
      const targetSignalUrl = this.dependencies.signalingCoordinator.getPeerSignalUrl(targetPeerId);

      if (targetSignalUrl && this.sendRawMessageToSignalUrl(targetSignalUrl, message)) {
        return;
      }

      if (isTransientSignalingOutboundType(messageType)) {
        return;
      }

      this.dependencies.logger.warn('[signaling] Missing peer signal route for outbound raw message', {
        targetPeerId,
        type: messageType
      });
    }

    const serverId = typeof message['serverId'] === 'string' ? message['serverId'] : null;

    if (serverId) {
      const serverSignalUrl = this.dependencies.signalingCoordinator.getServerSignalUrl(serverId);

      if (serverSignalUrl && this.sendRawMessageToSignalUrl(serverSignalUrl, message)) {
        return;
      }

      this.dependencies.logger.warn('[signaling] Missing server signal route for outbound raw message', {
        serverId,
        type: messageType
      });

      return;
    }

    const connectedManagers = this.getConnectedSignalingManagers();

    if (connectedManagers.length === 0) {
      if (isTransientSignalingOutboundType(messageType)) {
        return;
      }

      this.dependencies.logger.warn('[signaling] No active signaling connection for outbound message', {
        type: messageType
      });

      return;
    }

    this.dependencies.logger.warn('[signaling] Broadcasting raw message to all signaling managers due to unresolved route', {
      connectedSignalUrls: connectedManagers.map(({ signalUrl }) => signalUrl),
      serverId,
      targetPeerId,
      type: messageType
    });

    for (const { manager } of connectedManagers) {
      manager.sendRawMessage(message);
    }
  }

  sendRawMessageToSignalUrl(signalUrl: string, message: Record<string, unknown>): boolean {
    const manager = this.dependencies.signalingCoordinator.getSignalingManager(signalUrl);

    if (!manager || !manager.isSocketOpen()) {
      return false;
    }

    return manager.sendRawMessage(message);
  }

  identify(
    oderId: string,
    displayName: string,
    signalUrl?: string,
    profile?: Pick<IdentifyCredentials, 'description' | 'profileUpdatedAt' | 'homeSignalServerUrl'>
  ): void {
    const normalizedDisplayName = displayName.trim() || DEFAULT_DISPLAY_NAME;
    const normalizedDescription = typeof profile?.description === 'string'
      ? (profile.description.trim() || undefined)
      : undefined;
    const normalizedProfileUpdatedAt = typeof profile?.profileUpdatedAt === 'number'
      && Number.isFinite(profile.profileUpdatedAt)
      && profile.profileUpdatedAt > 0
      ? profile.profileUpdatedAt
      : undefined;
    const normalizedHomeSignalServerUrl = typeof profile?.homeSignalServerUrl === 'string'
      ? (profile.homeSignalServerUrl.trim().replace(/\/+$/, '') || undefined)
      : undefined;
    const clientInstanceId = this.dependencies.getClientInstanceId();

    if (signalUrl) {
      this.identifyOnSignalUrl(signalUrl, {
        fallbackOderId: oderId,
        displayName: normalizedDisplayName,
        description: normalizedDescription,
        profileUpdatedAt: normalizedProfileUpdatedAt,
        homeSignalServerUrl: normalizedHomeSignalServerUrl,
        clientInstanceId
      });

      return;
    }

    const connectedManagers = this.getConnectedSignalingManagers();

    if (connectedManagers.length === 0) {
      return;
    }

    for (const { signalUrl: managerSignalUrl, manager } of connectedManagers) {
      const credentials = this.identifyOnSignalUrl(managerSignalUrl, {
        fallbackOderId: oderId,
        displayName: normalizedDisplayName,
        description: normalizedDescription,
        profileUpdatedAt: normalizedProfileUpdatedAt,
        homeSignalServerUrl: normalizedHomeSignalServerUrl,
        clientInstanceId
      });

      if (!credentials) {
        continue;
      }

      manager.sendRawMessage({
        type: SIGNALING_TYPE_IDENTIFY,
        token: credentials.token,
        oderId: credentials.oderId,
        displayName: credentials.displayName,
        description: credentials.description,
        profileUpdatedAt: credentials.profileUpdatedAt,
        homeSignalServerUrl: credentials.homeSignalServerUrl,
        connectionScope: managerSignalUrl,
        clientInstanceId: credentials.clientInstanceId
      });
    }
  }

  private identifyOnSignalUrl(
    signalUrl: string,
    params: {
      fallbackOderId: string;
      displayName: string;
      description?: string;
      profileUpdatedAt?: number;
      homeSignalServerUrl?: string;
      clientInstanceId: string;
    }
  ): IdentifyCredentials | null {
    const resolved = this.dependencies.resolveCredential(signalUrl);

    if (!resolved) {
      this.dependencies.logger.warn('Skipping identify because no session token is available', {
        signalUrl,
        oderId: params.fallbackOderId
      });

      return null;
    }

    const credentials: IdentifyCredentials = {
      oderId: resolved.userId,
      token: resolved.token,
      displayName: resolved.displayName || params.displayName,
      description: params.description,
      profileUpdatedAt: params.profileUpdatedAt,
      homeSignalServerUrl: params.homeSignalServerUrl ?? resolved.homeSignalServerUrl,
      clientInstanceId: params.clientInstanceId
    };

    this.lastIdentifyCredentialsBySignalUrl.set(signalUrl, credentials);
    this.sendRawMessageToSignalUrl(signalUrl, {
      type: SIGNALING_TYPE_IDENTIFY,
      token: credentials.token,
      oderId: credentials.oderId,
      displayName: credentials.displayName,
      description: credentials.description,
      profileUpdatedAt: credentials.profileUpdatedAt,
      homeSignalServerUrl: credentials.homeSignalServerUrl,
      connectionScope: signalUrl,
      clientInstanceId: credentials.clientInstanceId
    });

    return credentials;
  }

  private toIdentifyCredentials(credential: ResolvedSignalCredential): IdentifyCredentials {
    return {
      oderId: credential.userId,
      token: credential.token,
      displayName: credential.displayName,
      homeSignalServerUrl: credential.homeSignalServerUrl
    };
  }
}
