import type { SignalingMessage } from '../../../shared-kernel';
import { PeerData } from '../realtime.types';
import {
  SIGNALING_TYPE_ACCESS_DENIED,
  SIGNALING_TYPE_ANSWER,
  SIGNALING_TYPE_CONNECTED,
  SIGNALING_TYPE_ICE_CANDIDATE,
  SIGNALING_TYPE_OFFER,
  SIGNALING_TYPE_SERVER_USERS,
  SIGNALING_TYPE_USER_JOINED,
  SIGNALING_TYPE_USER_LEFT
} from '../realtime.constants';
import { PeerConnectionManager } from '../peer-connection-manager/peer-connection.manager';
import { ServerSignalingCoordinator } from './server-signaling-coordinator';
import { WebRTCLogger } from '../logging/webrtc-logger';

interface SignalingUserSummary {
  oderId: string;
  displayName: string;
}

interface IncomingSignalingPayload {
  sdp?: RTCSessionDescriptionInit;
  candidate?: RTCIceCandidateInit;
}

export type IncomingSignalingMessage = Omit<Partial<SignalingMessage>, 'type' | 'payload'> & {
  type: string;
  payload?: IncomingSignalingPayload;
  oderId?: string;
  serverTime?: number;
  serverId?: string;
  serverIds?: string[];
  users?: SignalingUserSummary[];
  displayName?: string;
  fromUserId?: string;
  icon?: string;
  iconUpdatedAt?: number;
  targetUserId?: string;
};

interface IncomingSignalingMessageHandlerDependencies {
  peerManager: PeerConnectionManager;
  signalingCoordinator: ServerSignalingCoordinator<IncomingSignalingMessage>;
  logger: WebRTCLogger;
  getLocalOderId(): string | null;
  getEffectiveServerId(): string | null;
  setServerTime(serverTime: number): void;
}

const USER_JOINED_FALLBACK_OFFER_DELAY_MS = 1_000;
const PEER_NEGOTIATION_GRACE_MS = 3_000;
// Once a local offer has been sent, the peer is actively in negotiation - wait much
// longer before treating it as stale, so a slow answer path doesn't cause an
// unnecessary teardown/re-offer cycle.
const PEER_NEGOTIATION_OFFER_SENT_GRACE_MS = 20_000;
// How long the non-initiator waits for the elected initiator's offer before
// giving up and creating the connection itself.
const NON_INITIATOR_GIVE_UP_MS = 5_000;

export class IncomingSignalingMessageHandler {
  private readonly userJoinedFallbackTimers = new Map<string, ReturnType<typeof setTimeout>>();
  /** Tracks when we first started waiting for a remote-initiated offer from each peer. */
  private readonly nonInitiatorWaitStart = new Map<string, number>();

  constructor(private readonly dependencies: IncomingSignalingMessageHandlerDependencies) {}

  handleMessage(message: IncomingSignalingMessage, signalUrl: string): void {
    this.dependencies.logger.info('Signaling message', {
      signalUrl,
      type: message.type
    });

    switch (message.type) {
      case SIGNALING_TYPE_CONNECTED:
        this.handleConnectedSignalingMessage(message, signalUrl);
        return;

      case SIGNALING_TYPE_SERVER_USERS:
      case 'server_icon_sync_peers':
        this.handleServerUsersSignalingMessage(message, signalUrl);
        return;

      case SIGNALING_TYPE_USER_JOINED:
        this.handleUserJoinedSignalingMessage(message, signalUrl);
        return;

      case SIGNALING_TYPE_USER_LEFT:
        this.handleUserLeftSignalingMessage(message, signalUrl);
        return;

      case SIGNALING_TYPE_OFFER:
        this.handleOfferSignalingMessage(message, signalUrl);
        return;

      case SIGNALING_TYPE_ANSWER:
        this.handleAnswerSignalingMessage(message, signalUrl);
        return;

      case SIGNALING_TYPE_ICE_CANDIDATE:
        this.handleIceCandidateSignalingMessage(message, signalUrl);
        return;

      case SIGNALING_TYPE_ACCESS_DENIED:
        this.handleAccessDeniedSignalingMessage(message, signalUrl);
        return;

      default:
        return;
    }
  }

  private handleConnectedSignalingMessage(message: IncomingSignalingMessage, signalUrl: string): void {
    this.dependencies.logger.info('Server connected', {
      oderId: message.oderId,
      signalUrl
    });

    if (message.serverId) {
      this.dependencies.signalingCoordinator.setServerSignalUrl(message.serverId, signalUrl);
    }

    if (typeof message.serverTime === 'number') {
      this.dependencies.setServerTime(message.serverTime);
    }
  }

  private handleServerUsersSignalingMessage(message: IncomingSignalingMessage, signalUrl: string): void {
    const users = Array.isArray(message.users) ? message.users : [];
    const localOderId = this.dependencies.getLocalOderId();

    this.dependencies.logger.info('Server users', {
      count: users.length,
      signalUrl,
      serverId: message.serverId
    });

    if (message.serverId) {
      this.dependencies.signalingCoordinator.setServerSignalUrl(message.serverId, signalUrl);
    }

    for (const user of users) {
      if (!user.oderId)
        continue;

      if (localOderId && user.oderId === localOderId)
        continue;

      this.clearUserJoinedFallbackOffer(user.oderId);

      this.dependencies.signalingCoordinator.setPeerSignalUrl(user.oderId, signalUrl);

      if (message.serverId) {
        this.dependencies.signalingCoordinator.trackPeerInServer(user.oderId, message.serverId, signalUrl);
      }

      const existing = this.dependencies.peerManager.activePeerConnections.get(user.oderId);

      if (this.hasActivePeerConnection(existing)) {
        // Peer is already up - move on (timer already cleared above).
        this.nonInitiatorWaitStart.delete(user.oderId);
        this.dependencies.logger.info('Reusing active peer connection', {
          connectionState: existing?.connection.connectionState ?? 'unknown',
          dataChannelState: existing?.dataChannel?.readyState ?? 'missing',
          oderId: user.oderId,
          serverId: message.serverId,
          signalUrl
        });

        continue;
      }

      this.scheduleUserJoinedFallbackOffer(user.oderId, signalUrl, message.serverId);

      if (this.isPeerConnectionNegotiating(existing)) {
        this.dependencies.logger.info('Awaiting existing peer negotiation from server_users snapshot', {
          ageMs: existing ? Date.now() - existing.createdAt : undefined,
          connectionState: existing?.connection.connectionState ?? 'unknown',
          dataChannelState: existing?.dataChannel?.readyState ?? 'missing',
          oderId: user.oderId,
          serverId: message.serverId,
          signalUrl
        });

        continue;
      }

      if (!localOderId) {
        this.dependencies.logger.info('Deferring server_users peer initiation until logical identity is ready', {
          oderId: user.oderId,
          serverId: message.serverId,
          signalUrl
        });

        continue;
      }

      const shouldInitiate = this.shouldInitiatePeer(user.oderId, localOderId);

      if (!shouldInitiate) {
        if (existing) {
          this.dependencies.logger.info('Removing stale peer while waiting for remote offer', {
            connectionState: existing.connection.connectionState,
            dataChannelState: existing.dataChannel?.readyState ?? 'missing',
            oderId: user.oderId,
            serverId: message.serverId,
            signalUrl
          });

          this.dependencies.peerManager.removePeer(user.oderId);
        }

        this.dependencies.logger.info('Waiting for remote offer based on deterministic initiator selection', {
          localOderId,
          oderId: user.oderId,
          serverId: message.serverId,
          signalUrl
        });

        continue;
      }

      if (existing) {
        this.dependencies.logger.info('Removing failed peer before recreate', {
          connectionState: existing.connection.connectionState,
          dataChannelState: existing.dataChannel?.readyState ?? 'missing',
          oderId: user.oderId,
          serverId: message.serverId,
          signalUrl
        });

        this.dependencies.peerManager.removePeer(user.oderId);
      }

      this.dependencies.logger.info('Create peer connection to existing user', {
        oderId: user.oderId,
        serverId: message.serverId,
        signalUrl
      });

      this.dependencies.peerManager.createPeerConnection(user.oderId, true);
      void this.dependencies.peerManager.createAndSendOffer(user.oderId);
    }
  }

  private handleUserJoinedSignalingMessage(message: IncomingSignalingMessage, signalUrl: string): void {
    if (message.oderId && message.oderId === this.dependencies.getLocalOderId()) {
      return;
    }

    this.dependencies.logger.info('User joined', {
      displayName: message.displayName,
      oderId: message.oderId,
      signalUrl
    });

    if (message.serverId) {
      this.dependencies.signalingCoordinator.setServerSignalUrl(message.serverId, signalUrl);
    }

    if (message.oderId) {
      this.dependencies.signalingCoordinator.setPeerSignalUrl(message.oderId, signalUrl);
    }

    if (message.oderId && message.serverId) {
      this.dependencies.signalingCoordinator.trackPeerInServer(message.oderId, message.serverId, signalUrl);
    }

    if (message.oderId) {
      const existing = this.dependencies.peerManager.activePeerConnections.get(message.oderId);

      if (this.hasActivePeerConnection(existing)) {
        // Already connected - cancel any stale timer and move on.
        this.clearUserJoinedFallbackOffer(message.oderId);
        this.nonInitiatorWaitStart.delete(message.oderId);
      } else {
        this.scheduleUserJoinedFallbackOffer(message.oderId, signalUrl, message.serverId);
      }
    }
  }

  private handleUserLeftSignalingMessage(message: IncomingSignalingMessage, signalUrl: string): void {
    if (message.oderId && message.oderId === this.dependencies.getLocalOderId()) {
      return;
    }

    this.dependencies.logger.info('User left', {
      displayName: message.displayName,
      oderId: message.oderId,
      signalUrl,
      serverId: message.serverId
    });

    if (message.oderId) {
      this.clearUserJoinedFallbackOffer(message.oderId);
      this.nonInitiatorWaitStart.delete(message.oderId);

      const hasRemainingSharedServers = Array.isArray(message.serverIds)
        ? this.dependencies.signalingCoordinator.replacePeerSharedServers(message.oderId, signalUrl, message.serverIds)
        : message.serverId
          ? this.dependencies.signalingCoordinator.untrackPeerFromServer(message.oderId, signalUrl, message.serverId)
          : false;

      if (!hasRemainingSharedServers) {
        this.dependencies.peerManager.removePeer(message.oderId);
        this.dependencies.signalingCoordinator.deletePeerTracking(message.oderId);
      }
    }
  }

  private handleOfferSignalingMessage(message: IncomingSignalingMessage, signalUrl: string): void {
    const fromUserId = message.fromUserId;
    const sdp = message.payload?.sdp;

    if (!fromUserId || !sdp)
      return;

    if (fromUserId === this.dependencies.getLocalOderId())
      return;

    this.clearUserJoinedFallbackOffer(fromUserId);
    this.nonInitiatorWaitStart.delete(fromUserId);

    this.dependencies.signalingCoordinator.setPeerSignalUrl(fromUserId, signalUrl);

    const effectiveServerId = this.dependencies.getEffectiveServerId();

    if (effectiveServerId && !this.dependencies.signalingCoordinator.hasTrackedPeerServers(fromUserId)) {
      this.dependencies.signalingCoordinator.trackPeerInServer(fromUserId, effectiveServerId, signalUrl);
    }

    this.dependencies.peerManager.handleOffer(fromUserId, sdp);
  }

  private handleAnswerSignalingMessage(message: IncomingSignalingMessage, signalUrl: string): void {
    const fromUserId = message.fromUserId;
    const sdp = message.payload?.sdp;

    if (!fromUserId || !sdp)
      return;

    if (fromUserId === this.dependencies.getLocalOderId())
      return;

    this.clearUserJoinedFallbackOffer(fromUserId);

    this.dependencies.signalingCoordinator.setPeerSignalUrl(fromUserId, signalUrl);
    this.dependencies.peerManager.handleAnswer(fromUserId, sdp);
  }

  private handleIceCandidateSignalingMessage(message: IncomingSignalingMessage, signalUrl: string): void {
    const fromUserId = message.fromUserId;
    const candidate = message.payload?.candidate;

    if (!fromUserId || !candidate)
      return;

    if (fromUserId === this.dependencies.getLocalOderId())
      return;

    this.clearUserJoinedFallbackOffer(fromUserId);

    this.dependencies.signalingCoordinator.setPeerSignalUrl(fromUserId, signalUrl);
    this.dependencies.peerManager.handleIceCandidate(fromUserId, candidate);
  }

  private handleAccessDeniedSignalingMessage(message: IncomingSignalingMessage, signalUrl: string): void {
    if (!message.serverId) {
      return;
    }

    // Remove the server from the coordinator for this signal URL so it won't
    // be re-joined on the next reconnect cycle.
    this.dependencies.signalingCoordinator.removeJoinedServer(signalUrl, message.serverId);
  }

  private scheduleUserJoinedFallbackOffer(peerId: string, signalUrl: string, serverId?: string): void {
    this.clearUserJoinedFallbackOffer(peerId);

    const timer = setTimeout(() => {
      this.userJoinedFallbackTimers.delete(peerId);
      const localOderId = this.dependencies.getLocalOderId();
      const existing = this.dependencies.peerManager.activePeerConnections.get(peerId);

      if (this.hasActivePeerConnection(existing)) {
        this.nonInitiatorWaitStart.delete(peerId);
        this.dependencies.logger.info('Skip user_joined fallback offer - peer already active', {
          connectionState: existing?.connection.connectionState ?? 'unknown',
          dataChannelState: existing?.dataChannel?.readyState ?? 'missing',
          oderId: peerId,
          serverId,
          signalUrl
        });

        return;
      }

      if (!localOderId) {
        this.dependencies.logger.info('Retrying peer initiation once logical identity is ready', {
          oderId: peerId,
          serverId,
          signalUrl
        });

        this.scheduleUserJoinedFallbackOffer(peerId, signalUrl, serverId);
        return;
      }

      if (this.isPeerConnectionNegotiating(existing)) {
        this.dependencies.logger.info('Delaying fallback offer while peer negotiation is still in progress', {
          ageMs: existing ? Date.now() - existing.createdAt : undefined,
          connectionState: existing?.connection.connectionState ?? 'unknown',
          dataChannelState: existing?.dataChannel?.readyState ?? 'missing',
          localOderId,
          oderId: peerId,
          serverId,
          signalUrl
        });

        this.scheduleUserJoinedFallbackOffer(peerId, signalUrl, serverId);
        return;
      }

      const shouldInitiate = this.shouldInitiatePeer(peerId, localOderId);

      if (!shouldInitiate) {
        // Track how long we've been waiting for the remote initiator's offer.
        if (!this.nonInitiatorWaitStart.has(peerId)) {
          this.nonInitiatorWaitStart.set(peerId, Date.now());
        }

        const waitStart = this.nonInitiatorWaitStart.get(peerId) ?? Date.now();
        const waitMs = Date.now() - waitStart;

        if (waitMs < NON_INITIATOR_GIVE_UP_MS) {
          this.dependencies.logger.info('Waiting for remote initiator offer', {
            localOderId,
            oderId: peerId,
            serverId,
            signalUrl,
            waitMs
          });

          this.scheduleUserJoinedFallbackOffer(peerId, signalUrl, serverId);
          return;
        }

        // The elected initiator never sent an offer - take over.
        this.nonInitiatorWaitStart.delete(peerId);

        if (existing) {
          this.dependencies.logger.info('Removing stale peer before non-initiator takeover offer', {
            connectionState: existing.connection.connectionState,
            dataChannelState: existing.dataChannel?.readyState ?? 'missing',
            localOderId,
            oderId: peerId,
            serverId,
            signalUrl,
            waitMs
          });

          this.dependencies.peerManager.removePeer(peerId);
        }

        this.dependencies.logger.info('Non-initiator takeover - creating peer connection after remote initiator timeout', {
          localOderId,
          oderId: peerId,
          serverId,
          signalUrl,
          waitMs
        });

        this.dependencies.peerManager.createPeerConnection(peerId, true);
        void this.dependencies.peerManager.createAndSendOffer(peerId);
        this.scheduleUserJoinedFallbackOffer(peerId, signalUrl, serverId);
        return;
      }

      if (existing) {
        this.dependencies.logger.info('Removing stale peer before user_joined fallback offer', {
          connectionState: existing.connection.connectionState,
          dataChannelState: existing.dataChannel?.readyState ?? 'missing',
          oderId: peerId,
          serverId,
          signalUrl
        });

        this.dependencies.peerManager.removePeer(peerId);
      }

      this.dependencies.logger.info('Create peer connection from user_joined fallback offer', {
        oderId: peerId,
        serverId,
        signalUrl
      });

      this.dependencies.peerManager.createPeerConnection(peerId, true);
      void this.dependencies.peerManager.createAndSendOffer(peerId);

      this.scheduleUserJoinedFallbackOffer(peerId, signalUrl, serverId);
    }, USER_JOINED_FALLBACK_OFFER_DELAY_MS);

    this.userJoinedFallbackTimers.set(peerId, timer);
  }

  private clearUserJoinedFallbackOffer(peerId: string): void {
    const timer = this.userJoinedFallbackTimers.get(peerId);

    if (!timer) {
      return;
    }

    clearTimeout(timer);
    this.userJoinedFallbackTimers.delete(peerId);
  }

  private shouldInitiatePeer(peerId: string, localOderId: string | null = this.dependencies.getLocalOderId()): boolean {
    if (!localOderId)
      return false;

    if (peerId === localOderId)
      return false;

    return localOderId < peerId;
  }

  private hasActivePeerConnection(peer: PeerData | undefined): boolean {
    if (!peer)
      return false;

    const connectionState = peer.connection?.connectionState;

    return connectionState === 'connected' || peer.dataChannel?.readyState === 'open';
  }

  private isPeerConnectionNegotiating(peer: PeerData | undefined): boolean {
    if (!peer || this.hasActivePeerConnection(peer))
      return false;

    const connectionState = peer.connection?.connectionState;

    if (connectionState === 'closed' || connectionState === 'failed')
      return false;

    const signalingState = peer.connection?.signalingState;
    const ageMs = Date.now() - peer.createdAt;

    // If a local offer (or pranswer) has already been sent, the peer is actively
    // negotiating with the remote side.  Use a much longer grace period so that
    // a slow signaling round-trip does not trigger a premature teardown.
    if (signalingState === 'have-local-offer' || signalingState === 'have-local-pranswer')
      return ageMs < PEER_NEGOTIATION_OFFER_SENT_GRACE_MS;

    // ICE negotiation in progress (offer/answer exchange already complete, candidates being checked).
    // TURN relay can take 5-15 s on high-latency networks, so use the same extended grace.
    if (connectionState === 'connecting')
      return ageMs < PEER_NEGOTIATION_OFFER_SENT_GRACE_MS;

    return ageMs < PEER_NEGOTIATION_GRACE_MS;
  }
}
