/* eslint-disable @typescript-eslint/member-ordering */
import {
  CUSTOM_ELEMENTS_SCHEMA,
  Component,
  DestroyRef,
  HostListener,
  computed,
  effect,
  inject,
  input,
  signal,
  untracked
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute, Router } from '@angular/router';
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
import { FormsModule } from '@angular/forms';
import { Store } from '@ngrx/store';
import { NgIcon, provideIcons } from '@ng-icons/core';
import {
  lucidePhone,
  lucideX,
  lucideUsers,
  lucideUserPlus
} from '@ng-icons/lucide';
import { map } from 'rxjs';
import {
  DirectCallService,
  participantToUser,
  type DirectCallSession
} from '../../domains/direct-call';
import { DmChatComponent } from '../../domains/direct-message/feature/dm-chat/dm-chat.component';
import {
  VoiceActivityService,
  VoiceConnectionFacade,
  VoicePlaybackService
} from '../../domains/voice-connection';
import {
  ScreenShareFacade,
  ScreenShareQuality,
  ScreenShareStartOptions
} from '../../domains/screen-share';
import { loadVoiceSettingsFromStorage, saveVoiceSettingsToStorage } from '../../domains/voice-session';
import { ScreenShareQualityDialogComponent } from '../../shared';
import { ViewportService } from '../../core/platform';
import { selectAllUsers, selectCurrentUser } from '../../store/users/users.selectors';
import { UsersActions } from '../../store/users/users.actions';
import { User } from '../../shared-kernel';
import { VoiceWorkspaceStreamItem } from '../room/voice-workspace/voice-workspace.models';
import { VoiceWorkspaceStreamTileComponent } from '../room/voice-workspace/voice-workspace-stream-tile/voice-workspace-stream-tile.component';
import { PrivateCallControlsComponent } from './private-call-controls.component';
import { PrivateCallParticipantCardComponent } from './private-call-participant-card.component';

@Component({
  selector: 'app-private-call',
  standalone: true,
  imports: [
    CommonModule,
    DmChatComponent,
    FormsModule,
    NgIcon,
    PrivateCallControlsComponent,
    PrivateCallParticipantCardComponent,
    ScreenShareQualityDialogComponent,
    VoiceWorkspaceStreamTileComponent
  ],
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  host: { class: 'block h-full w-full' },
  viewProviders: [
    provideIcons({
      lucidePhone,
      lucideX,
      lucideUsers,
      lucideUserPlus
    })
  ],
  templateUrl: './private-call.component.html'
})
export class PrivateCallComponent {
  private readonly route = inject(ActivatedRoute);
  private readonly router = inject(Router);
  private readonly destroyRef = inject(DestroyRef);
  private readonly store = inject(Store);
  private readonly calls = inject(DirectCallService);
  private readonly voice = inject(VoiceConnectionFacade);
  private readonly voiceActivity = inject(VoiceActivityService);
  private readonly playback = inject(VoicePlaybackService);
  private readonly screenShare = inject(ScreenShareFacade);
  private readonly viewport = inject(ViewportService);
  private chatResizing = false;

  readonly allUsers = this.store.selectSignal(selectAllUsers);
  readonly currentUser = this.store.selectSignal(selectCurrentUser);
  readonly isMobile = this.viewport.isMobile;
  readonly callIdInput = input<string | null>(null);
  readonly overlayMode = input(false);
  readonly routeCallId = toSignal(this.route.paramMap.pipe(map((params) => params.get('callId'))), {
    initialValue: this.route.snapshot.paramMap.get('callId')
  });
  readonly callId = computed(() => this.callIdInput() ?? this.routeCallId());
  readonly session = computed(() => this.calls.sessionById(this.callId()));
  readonly participantUsers = computed(() => {
    const session = this.session();

    if (!session) {
      return [] as User[];
    }

    return session.participantIds
      .map((participantId) => this.userForSessionParticipant(session, participantId))
      .filter((user): user is User => !!user);
  });
  readonly isConnected = computed(() => {
    const session = this.session();
    const currentUserId = this.currentUserKey();

    return !!session && !!currentUserId && !!session.participants[currentUserId]?.joined;
  });
  readonly isMuted = this.voice.isMuted;
  readonly isDeafened = this.voice.isDeafened;
  readonly isCameraEnabled = this.voice.isCameraEnabled;
  readonly isScreenSharing = this.screenShare.isScreenSharing;
  readonly remoteStreamRevision = signal(0);
  readonly includeSystemAudio = signal(false);
  readonly screenShareQuality = signal<ScreenShareQuality>('balanced');
  readonly askScreenShareQuality = signal(true);
  readonly showScreenShareQualityDialog = signal(false);
  readonly inviteUserId = signal('');
  readonly focusedStreamId = signal<string | null>(null);
  readonly showAllStreamsMode = signal(false);
  readonly chatWidthPx = signal(384);
  readonly inviteCandidates = computed(() => {
    const participantIds = new Set(this.session()?.participantIds ?? []);
    const currentUserId = this.currentUserKey();

    return this.allUsers().filter((user) => {
      const userId = this.userKey(user);

      return userId !== currentUserId && !participantIds.has(userId);
    });
  });
  readonly activeShares = computed<VoiceWorkspaceStreamItem[]>(() => {
    this.remoteStreamRevision();

    const shares: VoiceWorkspaceStreamItem[] = [];
    const localUser = this.currentUser();
    const localPeerKey = localUser ? this.userKey(localUser) : null;
    const isJoinedToCurrentCall = this.isConnected();
    const localScreenStream = isJoinedToCurrentCall ? this.screenShare.screenStream() : null;
    const localCameraStream = isJoinedToCurrentCall && this.voice.isCameraEnabled() ? this.voice.getLocalCameraStream() : null;

    if (localUser && localPeerKey && localScreenStream) {
      shares.push(this.buildShare(localPeerKey, localUser, localScreenStream, true, 'screen'));
    }

    if (localUser && localPeerKey && localCameraStream) {
      shares.push(this.buildShare(localPeerKey, localUser, localCameraStream, true, 'camera'));
    }

    for (const user of this.participantUsers()) {
      const peerKey =
        this.getPeerKeyCandidates(user).find(
          (candidate) =>
            candidate !== localPeerKey && (!!this.screenShare.getRemoteScreenShareStream(candidate) || !!this.voice.getRemoteCameraStream(candidate))
        ) ?? this.userKey(user);

      if (peerKey === localPeerKey) {
        continue;
      }

      const screenStream = this.screenShare.getRemoteScreenShareStream(peerKey);
      const cameraStream = this.voice.getRemoteCameraStream(peerKey);

      if (screenStream && this.hasActiveVideo(screenStream)) {
        shares.push(this.buildShare(peerKey, user, screenStream, false, 'screen'));
      }

      if (cameraStream && this.hasActiveVideo(cameraStream)) {
        shares.push(this.buildShare(peerKey, user, cameraStream, false, 'camera'));
      }
    }

    return shares;
  });
  readonly featuredShare = computed(() => this.activeShares()[0] ?? null);
  readonly hasMultipleShares = computed(() => this.activeShares().length > 1);
  readonly focusedShareId = computed(() => {
    const requested = this.focusedStreamId();
    const activeShares = this.activeShares();

    if (this.showAllStreamsMode() && activeShares.length > 1) {
      return null;
    }

    if (requested && activeShares.some((share) => share.id === requested)) {
      return requested;
    }

    if (activeShares.length === 1) {
      return activeShares[0].id;
    }

    return null;
  });
  readonly focusedShare = computed(() => this.activeShares().find((share) => share.id === this.focusedShareId()) ?? null);
  readonly thumbnailShares = computed(() => {
    const focusedShareId = this.focusedShareId();

    if (!focusedShareId) {
      return [] as VoiceWorkspaceStreamItem[];
    }

    return this.activeShares().filter((share) => share.id !== focusedShareId);
  });
  constructor() {
    effect(() => {
      const callId = this.callId();

      if (callId) {
        untracked(() => void this.calls.openCall(callId));
      }
    });

    effect(() => {
      const session = this.session();

      if (session && !this.calls.hasOngoingActivity(session)) {
        if (this.overlayMode()) {
          untracked(() => this.calls.closeMobileCallOverlay());
          return;
        }

        untracked(() => void this.router.navigate(['/dm', session.conversationId], { replaceUrl: true }));
      }
    });

    effect(() => {
      const callId = this.callId();
      const session = this.session();

      if (callId && session?.conversationId && this.isMobile() && !this.overlayMode()) {
        untracked(() => {
          void this.calls.openMobileCallOverlay(callId);
          void this.router.navigate(['/pm', session.conversationId], { replaceUrl: true });
        });
      }
    });

    effect(() => {
      const session = this.session();
      const currentUserId = this.currentUserKey();
      const peerIds = session ? this.remoteParticipantPeerIds(session, currentUserId) : [];

      this.screenShare.syncRemoteScreenShareRequests(peerIds, this.isConnected() && !!session && session.status === 'connected');
    });

    effect(() => {
      this.session();

      if (this.isConnected()) {
        this.trackLocalMic();
        return;
      }

      this.untrackLocalMic();
    });

    this.screenShare.onRemoteStream.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => this.bumpRemoteStreamRevision());

    this.screenShare.onPeerDisconnected.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => this.bumpRemoteStreamRevision());

    this.destroyRef.onDestroy(() => {
      this.screenShare.syncRemoteScreenShareRequests([], false);
    });
  }

  @HostListener('window:mousemove', ['$event'])
  onWindowMouseMove(event: MouseEvent): void {
    if (!this.chatResizing) {
      return;
    }

    event.preventDefault();
    this.chatWidthPx.set(this.clampChatWidth(window.innerWidth - event.clientX));
  }

  @HostListener('window:mouseup')
  onWindowMouseUp(): void {
    this.chatResizing = false;
  }

  async join(): Promise<void> {
    const session = this.session();

    if (session) {
      await this.calls.joinCall(session.callId);
    }
  }

  leave(): void {
    const session = this.session();

    if (!session) {
      return;
    }

    this.calls.leaveCall(session.callId);
    this.calls.closeMobileCallOverlay();
    this.untrackLocalMic();

    if (!this.overlayMode()) {
      void this.router.navigate(['/dm', session.conversationId]);
    }
  }

  minimizeCall(): void {
    const session = this.session();

    if (!session) {
      return;
    }

    if (this.overlayMode()) {
      this.calls.closeMobileCallOverlay();
      return;
    }

    void this.router.navigate(['/dm', session.conversationId]);
  }

  onMobileCallSlideChange(event: Event): void {
    const detail = (event as CustomEvent).detail;
    const swiper = Array.isArray(detail) ? detail[0] : detail;

    if (this.isMobile() && swiper?.activeIndex === 0) {
      this.minimizeCall();
    }
  }

  toggleMute(): void {
    this.voice.toggleMute(!this.isMuted());
    this.broadcastLocalVoiceState();
  }

  toggleDeafen(): void {
    const nextDeafened = !this.isDeafened();

    this.voice.toggleDeafen(nextDeafened);
    this.playback.updateDeafened(nextDeafened);

    if (nextDeafened && !this.isMuted()) {
      this.voice.toggleMute(true);
    }

    this.broadcastLocalVoiceState();
  }

  async toggleCamera(): Promise<void> {
    const user = this.currentUser();

    if (!this.isConnected() || !user?.id) {
      return;
    }

    if (this.isCameraEnabled()) {
      this.voice.disableCamera();
      this.store.dispatch(UsersActions.updateCameraState({ userId: user.id, cameraState: { isEnabled: false } }));
      this.bumpRemoteStreamRevision();
      return;
    }

    await this.voice.enableCamera();
    this.store.dispatch(UsersActions.updateCameraState({ userId: user.id, cameraState: { isEnabled: true } }));
    this.bumpRemoteStreamRevision();
  }

  async toggleScreenShare(): Promise<void> {
    if (this.isScreenSharing()) {
      this.screenShare.stopScreenShare();
      this.bumpRemoteStreamRevision();
      return;
    }

    this.syncScreenShareSettings();

    if (this.askScreenShareQuality()) {
      this.showScreenShareQualityDialog.set(true);
      return;
    }

    await this.startScreenShareWithOptions(this.screenShareQuality());
  }

  onScreenShareQualityCancelled(): void {
    this.showScreenShareQualityDialog.set(false);
  }

  async onScreenShareQualityConfirmed(quality: ScreenShareQuality): Promise<void> {
    this.showScreenShareQualityDialog.set(false);
    this.screenShareQuality.set(quality);
    saveVoiceSettingsToStorage({ screenShareQuality: quality });
    await this.startScreenShareWithOptions(quality);
  }

  inviteSelectedUser(): void {
    const callId = this.callId();
    const userId = this.inviteUserId();
    const user = this.allUsers().find((candidate) => this.userKey(candidate) === userId);

    if (!callId || !user) {
      return;
    }

    void this.calls.inviteUser(callId, user);
    this.inviteUserId.set('');
  }

  isSpeaking(user: User): boolean {
    return this.voiceActivity.isSpeaking(this.userKey(user))();
  }

  isParticipantConnected(user: User): boolean {
    const session = this.session();
    const userId = this.userKey(user);

    if (!session) {
      return false;
    }

    return (
      !!session.participants[userId]?.joined ||
      !!(user.voiceState?.isConnected && user.voiceState.roomId === session.callId && user.voiceState.serverId === session.callId)
    );
  }

  participantIssueLabel(user: User): string | null {
    return this.isParticipantConnected(user) ? null : 'Waiting';
  }

  streamLabel(share: VoiceWorkspaceStreamItem): string {
    if (!share.isLocal) {
      return share.user.displayName;
    }

    return share.kind === 'camera' ? 'Your camera' : 'Your screen';
  }

  focusShare(shareId: string): void {
    this.showAllStreamsMode.set(false);
    this.focusedStreamId.set(shareId);
  }

  showAllStreams(): void {
    this.showAllStreamsMode.set(true);
    this.focusedStreamId.set(null);
  }

  startChatResize(event: MouseEvent): void {
    if (event.button !== 0) {
      return;
    }

    event.preventDefault();
    this.chatResizing = true;
  }

  userKey(user: User): string {
    return user.oderId || user.id;
  }

  readonly trackUserKey = (index: number, user: User): string => this.userKey(user);

  private currentUserKey(): string {
    const user = this.currentUser();

    return user ? this.userKey(user) : '';
  }

  private broadcastLocalVoiceState(): void {
    const session = this.session();
    const user = this.currentUser();

    if (!session || !user?.id) {
      return;
    }

    this.store.dispatch(
      UsersActions.updateVoiceState({
        userId: user.id,
        voiceState: {
          isConnected: this.isConnected(),
          isMuted: this.isMuted(),
          isDeafened: this.isDeafened(),
          roomId: session.callId,
          serverId: session.callId
        }
      })
    );
  }

  private remoteParticipantPeerIds(session: DirectCallSession, currentUserId: string): string[] {
    const peerIds = new Set<string>();

    for (const participantId of session.participantIds) {
      if (participantId === currentUserId) {
        continue;
      }

      const user = this.userForSessionParticipant(session, participantId);

      for (const peerId of [participantId, ...this.getPeerKeyCandidates(user)]) {
        if (peerId && peerId !== currentUserId) {
          peerIds.add(peerId);
        }
      }
    }

    return Array.from(peerIds);
  }

  private clampChatWidth(width: number): number {
    const maxWidth = Math.min(640, Math.max(360, window.innerWidth - 560));

    return Math.round(Math.max(320, Math.min(maxWidth, width)));
  }

  private getPeerKeyCandidates(user: User | null | undefined): string[] {
    if (!user) {
      return [];
    }

    return [
      user.oderId,
      user.peerId,
      user.id
    ].filter((peerId, index, peerIds): peerId is string => !!peerId && peerIds.indexOf(peerId) === index);
  }

  private userForSessionParticipant(session: DirectCallSession, participantId: string): User | null {
    const knownUser = this.calls.userForParticipant(participantId);

    if (knownUser) {
      return knownUser;
    }

    const participant = session.participants[participantId]?.profile;

    return participant ? participantToUser(participant) : null;
  }

  private trackLocalMic(): void {
    const userId = this.currentUserKey();
    const stream = this.voice.getRawMicStream() ?? this.voice.getLocalStream();

    if (userId && stream) {
      this.voiceActivity.trackLocalMic(userId, stream);
    }
  }

  private untrackLocalMic(): void {
    const userId = this.currentUserKey();

    if (userId) {
      this.voiceActivity.untrackLocalMic(userId);
    }
  }

  private syncScreenShareSettings(): void {
    const settings = loadVoiceSettingsFromStorage();

    this.includeSystemAudio.set(settings.includeSystemAudio);
    this.screenShareQuality.set(settings.screenShareQuality);
    this.askScreenShareQuality.set(settings.askScreenShareQuality);
  }

  private async startScreenShareWithOptions(quality: ScreenShareQuality): Promise<void> {
    const options: ScreenShareStartOptions = {
      includeSystemAudio: this.includeSystemAudio(),
      quality
    };

    try {
      await this.screenShare.startScreenShare(options);
      this.bumpRemoteStreamRevision();
    } catch {}
  }

  private buildShare(
    peerKey: string,
    user: User,
    stream: MediaStream,
    isLocal: boolean,
    kind: VoiceWorkspaceStreamItem['kind']
  ): VoiceWorkspaceStreamItem {
    return {
      id: `${kind}:${peerKey}`,
      peerKey,
      user,
      stream,
      isLocal,
      kind,
      hasAudio: stream.getAudioTracks().some((track) => track.readyState === 'live')
    };
  }

  private hasActiveVideo(stream: MediaStream): boolean {
    return stream.getVideoTracks().some((track) => track.readyState === 'live');
  }

  private bumpRemoteStreamRevision(): void {
    this.remoteStreamRevision.update((value) => value + 1);
  }
}
