/* eslint-disable @typescript-eslint/member-ordering */
import {
  Component,
  HostListener,
  ViewChild,
  computed,
  effect,
  inject,
  signal
} from '@angular/core';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { switchMap } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { ElectronBridgeService } from '../../../../core/platform/electron/electron-bridge.service';
import { ViewportService } from '../../../../core/platform';
import { BottomSheetComponent } from '../../../../shared';
import { RealtimeSessionFacade } from '../../../../core/realtime';
import { Attachment, AttachmentFacade } from '../../../attachment';
import { KlipyGif, KlipyService } from '../../application/services/klipy.service';
import { MessagesActions } from '../../../../store/messages/messages.actions';
import {
  selectActiveChannelMessages,
  selectCurrentRoomMessages,
  selectConversationExhausted,
  selectMessagesLoading,
  selectMessagesLoadingOlder,
  selectMessagesSyncing
} from '../../../../store/messages/messages.selectors';
import { selectCurrentUser, selectIsCurrentUserAdmin } from '../../../../store/users/users.selectors';
import { selectActiveChannelId, selectCurrentRoom } from '../../../../store/rooms/rooms.selectors';
import { Message } from '../../../../shared-kernel';
import { APP_TRANSLATE_IMPORTS } from '../../../../core/i18n';
import { ThemeNodeDirective } from '../../../theme';
import { ChatMessageComposerComponent } from './components/message-composer/chat-message-composer.component';
import { KlipyGifPickerComponent } from '../klipy-gif-picker/klipy-gif-picker.component';
import { ChatMessageListComponent } from './components/message-list/chat-message-list.component';
import { ChatMessageOverlaysComponent } from './components/message-overlays/chat-message-overlays.component';
import { stepLightboxIndex } from '../../domain/rules/chat-message-lightbox.rules';
import {
  ChatLightboxState,
  ChatMessageComposerSubmitEvent,
  ChatMessageDeleteEvent,
  ChatMessageEditEvent,
  ChatMessageEmbedRemoveEvent,
  ChatMessageImageContextMenuEvent,
  ChatMessageImageLightboxEvent,
  ChatMessageReactionEvent,
  ChatMessageReplyEvent
} from './models/chat-messages.model';

@Component({
  selector: 'app-chat-messages',
  standalone: true,
  imports: [
    ChatMessageComposerComponent,
    KlipyGifPickerComponent,
    ChatMessageListComponent,
    ChatMessageOverlaysComponent,
    BottomSheetComponent,
    ThemeNodeDirective,
    ...APP_TRANSLATE_IMPORTS
  ],
  templateUrl: './chat-messages.component.html',
  styleUrl: './chat-messages.component.scss'
})
export class ChatMessagesComponent {
  @ViewChild(ChatMessageComposerComponent) composer?: ChatMessageComposerComponent;
  @ViewChild(ChatMessageListComponent) messageList?: ChatMessageListComponent;

  private readonly electronBridge = inject(ElectronBridgeService);
  private readonly store = inject(Store);
  private readonly webrtc = inject(RealtimeSessionFacade);
  private readonly attachmentsSvc = inject(AttachmentFacade);
  private readonly klipy = inject(KlipyService);
  private readonly viewport = inject(ViewportService);

  readonly isMobile = this.viewport.isMobile;

  readonly roomMessages = this.store.selectSignal(selectCurrentRoomMessages);
  readonly channelMessages = this.store.selectSignal(selectActiveChannelMessages);
  private readonly activeChannelId = this.store.selectSignal(selectActiveChannelId);
  readonly currentRoom = this.store.selectSignal(selectCurrentRoom);

  readonly loading = this.store.selectSignal(selectMessagesLoading);
  readonly syncing = this.store.selectSignal(selectMessagesSyncing);
  readonly loadingOlder = this.store.selectSignal(selectMessagesLoadingOlder);
  readonly currentUser = this.store.selectSignal(selectCurrentUser);
  readonly isAdmin = this.store.selectSignal(selectIsCurrentUserAdmin);

  readonly conversationKey = computed(() => `${this.currentRoom()?.id ?? 'no-room'}:${this.activeChannelId() ?? 'general'}`);
  readonly conversationExhausted = toSignal(
    toObservable(this.conversationKey).pipe(switchMap((key) => this.store.select(selectConversationExhausted(key)))),
    { initialValue: false }
  );
  readonly klipyEnabled = computed(() => this.klipy.isEnabled(this.currentRoom()));
  readonly composerBottomPadding = signal(140);
  readonly klipyGifPickerAnchorRight = signal(16);
  readonly replyTo = signal<Message | null>(null);
  readonly showKlipyGifPicker = signal(false);
  readonly lightboxState = signal<ChatLightboxState | null>(null);
  readonly galleryAttachments = signal<Attachment[] | null>(null);
  readonly imageContextMenu = signal<ChatMessageImageContextMenuEvent | null>(null);

  constructor() {
    effect(() => {
      void this.klipy.refreshAvailability(this.currentRoom());
    });
  }

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

  handleMessageSubmitted(event: ChatMessageComposerSubmitEvent): void {
    this.messageList?.scrollToBottomAfterLocalSend();

    this.store.dispatch(
      MessagesActions.sendMessage({
        content: event.content,
        replyToId: this.replyTo()?.id,
        channelId: this.activeChannelId()
      })
    );

    this.clearReply();

    if (event.pendingFiles.length > 0) {
      setTimeout(() => this.attachFilesToLastOwnMessage(event.content, event.pendingFiles), 100);
    }
  }

  handleTypingStarted(): void {
    const roomId = this.currentRoom()?.id;

    if (!roomId) {
      return;
    }

    try {
      this.webrtc.sendRawMessage({
        type: 'typing',
        serverId: roomId,
        channelId: this.activeChannelId() ?? 'general'
      });
    } catch {
      /* ignore */
    }
  }

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

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

  handleEditSaved(event: ChatMessageEditEvent): void {
    this.store.dispatch(
      MessagesActions.editMessage({
        messageId: event.messageId,
        content: event.content
      })
    );
  }

  handleDeleteRequested(message: ChatMessageDeleteEvent): void {
    if (this.isOwnMessage(message)) {
      this.store.dispatch(MessagesActions.deleteMessage({ messageId: message.id }));
    } else if (this.isAdmin()) {
      this.store.dispatch(MessagesActions.adminDeleteMessage({ messageId: message.id }));
    }
  }

  handleReactionAdded(event: ChatMessageReactionEvent): void {
    this.store.dispatch(
      MessagesActions.addReaction({
        messageId: event.messageId,
        emoji: event.emoji
      })
    );
  }

  handleReactionToggled(event: ChatMessageReactionEvent): void {
    const message = this.channelMessages().find((entry) => entry.id === event.messageId);
    const currentUserId = this.currentUser()?.id;

    if (!message || !currentUserId)
      return;

    const hasReacted = message.reactions.some((reaction) => reaction.emoji === event.emoji && reaction.userId === currentUserId);

    if (hasReacted) {
      this.store.dispatch(
        MessagesActions.removeReaction({
          messageId: event.messageId,
          emoji: event.emoji
        })
      );
    } else {
      this.store.dispatch(
        MessagesActions.addReaction({
          messageId: event.messageId,
          emoji: event.emoji
        })
      );
    }
  }

  handleComposerHeightChanged(height: number): void {
    this.composerBottomPadding.set(height + 20);
  }

  handleEmbedRemoved(event: ChatMessageEmbedRemoveEvent): void {
    this.store.dispatch(
      MessagesActions.removeLinkEmbed({
        messageId: event.messageId,
        url: event.url
      })
    );
  }

  handleLoadOlderRequested(event: { beforeTimestamp: number; limit: number }): void {
    const roomId = this.currentRoom()?.id;

    if (!roomId)
      return;

    this.store.dispatch(
      MessagesActions.loadOlderMessages({
        roomId,
        channelId: this.activeChannelId() ?? 'general',
        beforeTimestamp: event.beforeTimestamp,
        limit: event.limit
      })
    );
  }

  toggleKlipyGifPicker(): void {
    const nextState = !this.showKlipyGifPicker();

    this.showKlipyGifPicker.set(nextState);

    if (nextState) {
      requestAnimationFrame(() => this.syncKlipyGifPickerAnchor());
    }
  }

  closeKlipyGifPicker(): void {
    this.showKlipyGifPicker.set(false);
  }

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

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

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

    const viewportWidth = window.innerWidth;
    const popupWidth = this.getKlipyGifPickerWidth(viewportWidth);
    const preferredRight = viewportWidth - triggerRect.right;
    const minRight = 16;
    const maxRight = Math.max(minRight, viewportWidth - popupWidth - 16);

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

  private getKlipyGifPickerWidth(viewportWidth: number): number {
    if (viewportWidth >= 1280)
      return 52 * 16;

    if (viewportWidth >= 768)
      return 42 * 16;

    if (viewportWidth >= 640)
      return 34 * 16;

    return Math.max(0, viewportWidth - 32);
  }

  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();
      const pngBlob = await this.convertToPng(blob);

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

  private isOwnMessage(message: Message): boolean {
    return message.senderId === this.currentUser()?.id;
  }

  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 convertToPng(blob: Blob): Promise<Blob> {
    return new Promise((resolve, reject) => {
      if (blob.type === 'image/png') {
        resolve(blob);
        return;
      }

      const image = new Image();
      const url = URL.createObjectURL(blob);

      image.onload = () => {
        const canvas = document.createElement('canvas');

        canvas.width = image.naturalWidth;
        canvas.height = image.naturalHeight;
        const context = canvas.getContext('2d');

        if (!context) {
          reject(new Error('Canvas not supported'));
          return;
        }

        context.drawImage(image, 0, 0);
        canvas.toBlob((pngBlob) => {
          URL.revokeObjectURL(url);

          if (pngBlob)
            resolve(pngBlob);
          else
            reject(new Error('PNG conversion failed'));
        }, 'image/png');
      };

      image.onerror = () => {
        URL.revokeObjectURL(url);
        reject(new Error('Image load failed'));
      };

      image.src = url;
    });
  }

  private attachFilesToLastOwnMessage(content: string, pendingFiles: File[]): void {
    const currentUserId = this.currentUser()?.id;

    if (!currentUserId)
      return;

    const message = [...this.channelMessages()]
      .reverse()
      .find((entry) => entry.senderId === currentUserId && entry.content === content && !entry.isDeleted);

    if (!message) {
      setTimeout(() => this.attachFilesToLastOwnMessage(content, pendingFiles), 150);
      return;
    }

    this.attachmentsSvc.publishAttachments(message.id, pendingFiles, currentUserId || undefined);
  }
}
