/* eslint-disable @typescript-eslint/member-ordering */
import {
  Component,
  computed,
  effect,
  HostListener,
  inject,
  input,
  signal,
  ViewChild
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute } from '@angular/router';
import { Store } from '@ngrx/store';
import { toSignal } from '@angular/core/rxjs-interop';
import { map } from 'rxjs';
import { AppI18nService, APP_TRANSLATE_IMPORTS } from '../../../../core/i18n';
import { ElectronBridgeService } from '../../../../core/platform/electron/electron-bridge.service';
import { ViewportService } from '../../../../core/platform';
import {
  BottomSheetComponent,
  ProfileCardService,
  UserAvatarComponent
} from '../../../../shared';
import { DirectCallService } from '../../../direct-call';
import { Attachment, AttachmentFacade } from '../../../attachment';
import { ThemeNodeDirective } from '../../../theme';
import { DirectMessageService } from '../../application/services/direct-message.service';
import { selectAllUsers, selectCurrentUser } from '../../../../store/users/users.selectors';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { lucidePhone, lucidePhoneCall } from '@ng-icons/lucide';
import {
  ChatMessageComposerSubmitEvent,
  ChatMessageComposerComponent,
  ChatMessageDeleteEvent,
  ChatMessageEditEvent,
  ChatMessageImageContextMenuEvent,
  ChatMessageListComponent,
  ChatMessageOverlaysComponent,
  ChatMessageReactionEvent,
  ChatMessageReplyEvent,
  hasDedicatedChatEmbed,
  KlipyGif,
  KlipyGifPickerComponent,
  KlipyService,
  LinkMetadataService,
  type ChatMessageEmbedRemoveEvent
} from '../../../chat';
import { stepLightboxIndex } from '../../../chat/domain/rules/chat-message-lightbox.rules';
import { ChatLightboxState, ChatMessageImageLightboxEvent } from '../../../chat/feature/chat-messages/models/chat-messages.model';
import type {
  DirectMessageStatus,
  LinkMetadata,
  Message,
  User
} from '../../../../shared-kernel';

interface DmStatusLabel {
  id: string;
  status: DirectMessageStatus;
}

@Component({
  selector: 'app-dm-chat',
  standalone: true,
  imports: [
    CommonModule,
    ChatMessageComposerComponent,
    ChatMessageListComponent,
    ChatMessageOverlaysComponent,
    KlipyGifPickerComponent,
    BottomSheetComponent,
    NgIcon,
    ThemeNodeDirective,
    UserAvatarComponent,
    ...APP_TRANSLATE_IMPORTS
  ],
  viewProviders: [provideIcons({ lucidePhone, lucidePhoneCall })],
  templateUrl: './dm-chat.component.html',
  host: {
    class: 'block h-full'
  }
})
export class DmChatComponent {
  @ViewChild(ChatMessageComposerComponent) composer?: ChatMessageComposerComponent;

  private readonly route = inject(ActivatedRoute);
  private readonly store = inject(Store);
  private readonly electronBridge = inject(ElectronBridgeService);
  private readonly attachments = inject(AttachmentFacade);
  private readonly klipy = inject(KlipyService);
  private readonly linkMetadata = inject(LinkMetadataService);
  private readonly profileCard = inject(ProfileCardService);
  private readonly viewport = inject(ViewportService);
  private readonly metadataRequestKeys = new Set<string>();
  private openedConversationId: string | null = null;
  private readonly i18n = inject(AppI18nService);
  readonly isMobile = this.viewport.isMobile;
  readonly directCalls = inject(DirectCallService);
  readonly directMessages = inject(DirectMessageService);
  readonly currentUser = this.store.selectSignal(selectCurrentUser);
  readonly allUsers = this.store.selectSignal(selectAllUsers);
  readonly showGifPicker = signal(false);
  readonly conversationId = input<string | null>(null);
  readonly showCallButton = input(true);
  readonly composerBottomPadding = signal(140);
  readonly gifPickerAnchorRight = signal(16);
  readonly linkMetadataByMessageId = signal<Record<string, LinkMetadata[]>>({});
  readonly replyTo = signal<Message | null>(null);
  readonly lightboxState = signal<ChatLightboxState | null>(null);
  readonly galleryAttachments = signal<Attachment[] | null>(null);
  readonly imageContextMenu = signal<ChatMessageImageContextMenuEvent | null>(null);
  readonly routeConversationId = toSignal(this.route.paramMap.pipe(map((params) => params.get('conversationId'))), {
    initialValue: this.route.snapshot.paramMap.get('conversationId')
  });
  readonly effectiveConversationId = computed(() => this.conversationId() ?? this.routeConversationId());
  readonly currentUserId = computed(() => this.currentUser()?.oderId || this.currentUser()?.id || '');
  readonly conversation = this.directMessages.selectedConversation;
  readonly klipyEnabled = computed(() => this.klipy.isEnabled(null));
  readonly conversationKey = computed(() => this.conversation()?.id ?? 'dm:none');
  readonly typingUsers = computed(() => {
    void this.directMessages.typingEntries();

    return this.directMessages.typingUsers(this.conversation()?.id);
  });
  readonly peerUser = computed(() => {
    const conversation = this.conversation();

    return conversation ? this.peerUserFor(conversation) : null;
  });
  readonly isGroupConversation = computed(() => {
    const conversation = this.conversation();

    return !!conversation && (conversation.kind === 'group' || conversation.participants.length > 2);
  });
  readonly participantUsers = computed<User[]>(() => {
    const conversation = this.conversation();
    const knownUsers = this.allUsers();

    if (!conversation) {
      return [];
    }

    return conversation.participants.map((participantId) => {
      const knownUser = knownUsers.find((user) => user.id === participantId || user.oderId === participantId);
      const participant = conversation.participantProfiles[participantId];

      return (
        knownUser ?? {
          id: participantId,
          oderId: participantId,
          username: participant?.username || participant?.displayName || participantId,
          displayName: participant?.displayName || participant?.username || participantId,
          description: participant?.description,
          profileUpdatedAt: participant?.profileUpdatedAt,
          avatarUrl: participant?.avatarUrl,
          avatarHash: participant?.avatarHash,
          avatarMime: participant?.avatarMime,
          avatarUpdatedAt: participant?.avatarUpdatedAt,
          status: 'disconnected',
          role: 'member',
          joinedAt: 0
        }
      );
    });
  });
  readonly messageStatuses = computed<DmStatusLabel[]>(() => {
    const conversation = this.conversation();
    const currentUserId = this.currentUserId();

    if (!conversation || !currentUserId) {
      return [];
    }

    return conversation.messages
      .filter((message) => message.senderId === currentUserId)
      .map((message) => ({
        id: message.id,
        status: message.status
      }));
  });
  readonly chatMessages = computed<Message[]>(() => {
    const conversation = this.conversation();
    const metadataByMessageId = this.linkMetadataByMessageId();

    if (!conversation) {
      return [];
    }

    return conversation.messages.map((message) => {
      const participant = conversation.participantProfiles[message.senderId];
      const knownUser = this.participantUsers().find((user) => user.id === message.senderId || user.oderId === message.senderId);

      return {
        id: message.id,
        roomId: conversation.id,
        channelId: 'direct-message',
        senderId: message.senderId,
        senderName: knownUser?.displayName || participant?.displayName || (message.senderId === this.currentUserId() ? this.i18n.instant('common.labels.you') : message.senderId),
        content: message.content,
        timestamp: message.timestamp,
        kind: message.kind,
        systemEvent: message.systemEvent,
        editedAt: message.editedAt,
        reactions: message.reactions ?? [],
        isDeleted: !!message.isDeleted,
        replyToId: message.replyToId,
        linkMetadata: metadataByMessageId[message.id]
      };
    });
  });
  readonly peerName = computed(() => {
    const conversation = this.conversation();
    const currentUserId = this.currentUserId();

    if (conversation && this.isGroupConversation()) {
      return conversation.title || this.groupConversationTitle(conversation);
    }

    const peerId = conversation?.participants.find((participantId) => participantId !== currentUserId);

    return peerId ? conversation?.participantProfiles[peerId]?.displayName || peerId : this.i18n.instant('dm.chat.defaultTitle');
  });
  readonly peerCallIcon = computed(() => {
    const conversation = this.conversation();

    if (conversation && this.isGroupConversation()) {
      return this.directCalls.isCallingConversation(conversation.id) ? 'lucidePhoneCall' : 'lucidePhone';
    }

    const peer = this.peerUser();

    return peer && this.directCalls.isCallingUser(peer) ? 'lucidePhoneCall' : 'lucidePhone';
  });
  readonly canCallConversation = computed(() => {
    const conversation = this.conversation();

    if (!conversation) {
      return false;
    }

    if (this.isGroupConversation()) {
      return conversation.participants.some((participantId) => participantId !== this.currentUserId());
    }

    return !!this.peerUser();
  });

  constructor() {
    effect(() => {
      const conversationId = this.effectiveConversationId();

      if (!conversationId) {
        this.openedConversationId = null;
        return;
      }

      if (conversationId !== this.openedConversationId) {
        this.openedConversationId = conversationId;
        void this.directMessages.openConversation(conversationId);
      }
    });

    effect(() => {
      void this.effectiveConversationId();
      void this.klipy.refreshAvailability(null);
    });

    effect(() => {
      void this.refreshLinkMetadata(this.chatMessages());
    });

    effect(() => {
      const conversation = this.conversation();
      const peerUser = this.peerUser();

      if (conversation && !peerUser?.avatarUrl) {
        this.directMessages.requestPeerAvatarSync(conversation.id);
      }
    });
  }

  @HostListener('window:resize')
  onWindowResize(): void {
    if (this.showGifPicker()) {
      this.syncGifPickerAnchor();
    }
  }

  handleMessageSubmitted(event: ChatMessageComposerSubmitEvent): void {
    const conversation = this.conversation();

    if (!conversation || (!event.content.trim() && event.pendingFiles.length === 0)) {
      return;
    }

    const content = event.content.trim() || event.pendingFiles.map((file) => file.name).join('\n');

    void this.directMessages.sendMessage(conversation.id, content, this.replyTo()?.id).then((message) => {
      this.replyTo.set(null);

      if (event.pendingFiles.length > 0) {
        this.attachments.rememberMessageRoom(message.id, `direct-message:${conversation.id}`);
        this.attachments.publishAttachments(message.id, event.pendingFiles, this.currentUserId() || undefined);
      }
    });
  }

  handleTypingStarted(): void {
    const conversationId = this.conversation()?.id;

    if (conversationId) {
      this.directMessages.sendTyping(conversationId, true);
    }
  }

  async callConversation(): Promise<void> {
    const conversation = this.conversation();

    if (conversation && this.canCallConversation()) {
      await this.directCalls.startConversationCall(conversation);
    }
  }

  openHeaderProfileCard(event: MouseEvent): void {
    const user = this.peerUser();

    if (!user) {
      return;
    }

    event.stopPropagation();
    this.profileCard.open(event.currentTarget as HTMLElement, user, { editable: false });
  }

  setReplyTo(message: ChatMessageReplyEvent): void {
    this.replyTo.set(message);
  }

  clearReply(): void {
    this.replyTo.set(null);
  }

  handleEditSaved(event: ChatMessageEditEvent): void {
    const conversation = this.conversation();

    if (conversation) {
      void this.directMessages.editMessage(conversation.id, event.messageId, event.content);
    }
  }

  handleDeleteRequested(message: ChatMessageDeleteEvent): void {
    const conversation = this.conversation();

    if (conversation && message.senderId === this.currentUserId()) {
      void this.directMessages.deleteMessage(conversation.id, message.id);
    }
  }

  handleReactionAdded(event: ChatMessageReactionEvent): void {
    const conversation = this.conversation();

    if (conversation) {
      void this.directMessages.addReaction(conversation.id, event.messageId, event.emoji);
    }
  }

  handleReactionToggled(event: ChatMessageReactionEvent): void {
    const conversation = this.conversation();

    if (conversation) {
      void this.directMessages.toggleReaction(conversation.id, event.messageId, event.emoji);
    }
  }

  toggleGifPicker(): void {
    if (!this.klipyEnabled()) {
      return;
    }

    this.showGifPicker.update((visible) => !visible);

    if (this.showGifPicker()) {
      requestAnimationFrame(() => this.syncGifPickerAnchor());
    }
  }

  openProfileLabel(name: string): string {
    return this.i18n.instant('dm.chat.openProfile', { name });
  }

  callPeerLabel(name: string): string {
    return this.i18n.instant('dm.chat.callPeer', { name });
  }

  typingIndicatorLabel(names: string[]): string {
    const verb = names.length === 1
      ? this.i18n.instant('dm.chat.typingOne')
      : this.i18n.instant('dm.chat.typingMany');

    return `${names.join(', ')} ${verb}`;
  }

  closeGifPicker(): void {
    this.showGifPicker.set(false);
  }

  handleGifSelected(gif: KlipyGif): void {
    this.closeGifPicker();
    this.composer?.handleKlipyGifSelected(gif);
  }

  handleEmbedRemoved(event: ChatMessageEmbedRemoveEvent): void {
    this.linkMetadataByMessageId.update((metadataByMessageId) => ({
      ...metadataByMessageId,
      [event.messageId]: (metadataByMessageId[event.messageId] ?? []).filter((metadata) => metadata.url !== event.url)
    }));
  }

  openLightbox(event: ChatMessageImageLightboxEvent): void {
    const attachments = event.attachments.filter((attachment) => attachment.available && attachment.objectUrl);
    const index = attachments.findIndex((attachment) => attachment.id === event.attachment.id);

    if (index < 0) {
      return;
    }

    this.lightboxState.set({
      attachments,
      index
    });
  }

  closeLightbox(): void {
    this.lightboxState.set(null);
  }

  stepLightbox(delta: number): void {
    const state = this.lightboxState();

    if (!state) {
      return;
    }

    const nextIndex = stepLightboxIndex(state.index, delta, state.attachments.length);

    if (nextIndex === null) {
      return;
    }

    this.lightboxState.set({
      attachments: state.attachments,
      index: nextIndex
    });
  }

  openImageGallery(attachments: Attachment[]): void {
    const availableImages = attachments.filter((attachment) => attachment.available && attachment.objectUrl);

    if (availableImages.length < 2) {
      return;
    }

    this.galleryAttachments.set(availableImages);
  }

  closeImageGallery(): void {
    this.galleryAttachments.set(null);
  }

  openImageContextMenu(event: ChatMessageImageContextMenuEvent): void {
    this.imageContextMenu.set(event);
  }

  closeImageContextMenu(): void {
    this.imageContextMenu.set(null);
  }

  async downloadAttachment(attachment: Attachment): Promise<void> {
    if (!attachment.available || !attachment.objectUrl) {
      return;
    }

    const electronApi = this.electronBridge.getApi();

    if (electronApi) {
      const diskPath = this.getAttachmentDiskPath(attachment);

      if (diskPath && electronApi.saveExistingFileAs) {
        try {
          const result = await electronApi.saveExistingFileAs(diskPath, attachment.filename);

          if (result.saved || result.cancelled) {
            return;
          }
        } catch {
          /* fall back to blob/browser download */
        }
      }

      const blob = await this.getAttachmentBlob(attachment);

      if (blob) {
        try {
          const result = await electronApi.saveFileAs(attachment.filename, await this.blobToBase64(blob));

          if (result.saved || result.cancelled) {
            return;
          }
        } catch {
          /* fall back to browser download */
        }
      }
    }

    const link = document.createElement('a');

    link.href = attachment.objectUrl;
    link.download = attachment.filename;
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
  }

  async copyImageToClipboard(attachment: Attachment): Promise<void> {
    this.closeImageContextMenu();

    if (!attachment.objectUrl) {
      return;
    }

    try {
      const response = await fetch(attachment.objectUrl);
      const blob = await response.blob();

      await navigator.clipboard.write([new ClipboardItem({ [blob.type || 'image/png']: blob })]);
    } catch {
      /* ignore */
    }
  }

  private syncGifPickerAnchor(): void {
    const triggerRect = this.composer?.getKlipyTriggerRect();

    if (!triggerRect) {
      this.gifPickerAnchorRight.set(16);
      return;
    }

    const viewportWidth = window.innerWidth;
    const popupWidth = viewportWidth >= 1280 ? 52 * 16 : viewportWidth >= 768 ? 42 * 16 : 34 * 16;
    const preferredRight = viewportWidth - triggerRect.right;
    const minRight = 16;
    const maxRight = Math.max(minRight, viewportWidth - popupWidth - 16);

    this.gifPickerAnchorRight.set(Math.min(Math.max(Math.round(preferredRight), minRight), maxRight));
  }

  private async refreshLinkMetadata(messages: Message[]): Promise<void> {
    const metadataByMessageId = this.linkMetadataByMessageId();

    for (const message of messages) {
      if (metadataByMessageId[message.id]?.length) {
        continue;
      }

      const urls = this.linkMetadata.extractUrls(message.content)
        .filter((url) => !hasDedicatedChatEmbed(url))
        .filter((url) => !this.metadataRequestKeys.has(this.metadataRequestKey(message.id, url)));

      if (urls.length === 0) {
        continue;
      }

      urls.forEach((url) => this.metadataRequestKeys.add(this.metadataRequestKey(message.id, url)));

      const metadata = (await this.linkMetadata.fetchAllMetadata(urls)).filter((entry) => !entry.failed);

      if (metadata.length === 0) {
        continue;
      }

      this.linkMetadataByMessageId.update((currentMetadata) => ({
        ...currentMetadata,
        [message.id]: metadata
      }));
    }
  }

  private metadataRequestKey(messageId: string, url: string): string {
    return `${messageId}:${url}`;
  }

  private async getAttachmentBlob(attachment: Attachment): Promise<Blob | null> {
    if (!attachment.objectUrl) {
      return null;
    }

    if (attachment.objectUrl.startsWith('file:')) {
      return null;
    }

    try {
      const response = await fetch(attachment.objectUrl);

      return await response.blob();
    } catch {
      return null;
    }
  }

  private getAttachmentDiskPath(attachment: Attachment): string | null {
    return attachment.savedPath || attachment.filePath || null;
  }

  private blobToBase64(blob: Blob): Promise<string> {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();

      reader.onload = () => {
        if (typeof reader.result !== 'string') {
          reject(new Error('Failed to encode attachment'));
          return;
        }

        const [, base64 = ''] = reader.result.split(',', 2);

        resolve(base64);
      };

      reader.onerror = () => reject(reader.error ?? new Error('Failed to read attachment'));
      reader.readAsDataURL(blob);
    });
  }

  private peerUserFor(conversation: NonNullable<ReturnType<typeof this.conversation>>): User | null {
    if (conversation.kind === 'group' || conversation.participants.length > 2) {
      return null;
    }

    const currentUserId = this.currentUserId();
    const peerId = conversation.participants.find((participantId) => participantId !== currentUserId);

    if (!peerId) {
      return null;
    }

    return this.participantUsers().find((user) => user.id === peerId || user.oderId === peerId) ?? null;
  }

  private groupConversationTitle(conversation: NonNullable<ReturnType<typeof this.conversation>>): string {
    const names = conversation.participants
      .filter((participantId) => participantId !== this.currentUserId())
      .map((participantId) => conversation.participantProfiles[participantId]?.displayName || participantId);

    if (names.length <= 3) {
      return names.join(', ');
    }

    return `${names.slice(0, 3).join(', ')} +${names.length - 3}`;
  }
}
