/* eslint-disable @typescript-eslint/member-ordering */
import { CommonModule } from '@angular/common';
import {
  AfterViewChecked,
  Component,
  ElementRef,
  OnDestroy,
  ViewChild,
  computed,
  effect,
  inject,
  input,
  output,
  signal
} from '@angular/core';
import { Store } from '@ngrx/store';
import { Attachment } from '../../../../../attachment';
import { getMessageTimestamp } from '../../../../domain/rules/message.rules';
import { Message, User } from '../../../../../../shared-kernel';
import {
  ChatMessageDeleteEvent,
  ChatMessageEditEvent,
  ChatMessageEmbedRemoveEvent,
  ChatMessageImageContextMenuEvent,
  ChatMessageReactionEvent,
  ChatMessageReplyEvent
} from '../../models/chat-messages.model';
import { selectAllUsers } from '../../../../../../store/users/users.selectors';
import { ThemeNodeDirective } from '../../../../../theme';
import { ChatMessageItemComponent } from '../message-item/chat-message-item.component';

interface PrismGlobal {
  highlightElement(element: Element): void;
}

declare global {
  interface Window {
    Prism?: PrismGlobal;
  }
}

@Component({
  selector: 'app-chat-message-list',
  standalone: true,
  imports: [
    CommonModule,
    ChatMessageItemComponent,
    ThemeNodeDirective
  ],
  templateUrl: './chat-message-list.component.html',
  host: {
    style: 'display: contents;'
  }
})
export class ChatMessageListComponent implements AfterViewChecked, OnDestroy {
  @ViewChild('messagesContainer') messagesContainer?: ElementRef<HTMLDivElement>;

  private readonly store = inject(Store);
  private readonly allUsers = this.store.selectSignal(selectAllUsers);
  private readonly dateSeparatorFormatter = new Intl.DateTimeFormat('en-GB', {
    day: 'numeric',
    month: 'long',
    year: 'numeric'
  });

  readonly allMessages = input.required<Message[]>();
  readonly channelMessages = input.required<Message[]>();
  readonly loading = input(false);
  readonly syncing = input(false);
  readonly currentUserId = input<string | null>(null);
  readonly isAdmin = input(false);
  readonly bottomPadding = input(120);
  readonly conversationKey = input.required<string>();
  readonly userLookupOverrides = input<User[]>([]);

  readonly replyRequested = output<ChatMessageReplyEvent>();
  readonly deleteRequested = output<ChatMessageDeleteEvent>();
  readonly editSaved = output<ChatMessageEditEvent>();
  readonly reactionAdded = output<ChatMessageReactionEvent>();
  readonly reactionToggled = output<ChatMessageReactionEvent>();
  readonly downloadRequested = output<Attachment>();
  readonly imageOpened = output<Attachment>();
  readonly imageContextMenuRequested = output<ChatMessageImageContextMenuEvent>();
  readonly embedRemoved = output<ChatMessageEmbedRemoveEvent>();

  private readonly PAGE_SIZE = 50;

  readonly displayLimit = signal(this.PAGE_SIZE);
  readonly loadingMore = signal(false);
  readonly showNewMessagesBar = signal(false);

  readonly messages = computed(() => {
    const all = this.channelMessages();
    const limit = this.displayLimit();

    if (all.length <= limit)
      return all;

    return all.slice(all.length - limit);
  });

  readonly hasMoreMessages = computed(() => this.channelMessages().length > this.displayLimit());

  readonly dateSeparatorLabels = computed(() => {
    const labels = new Map<number, string>();

    let previousDayKey: string | null = null;

    this.messages().forEach((message, index) => {
      const timestamp = this.getMessageDateTimestamp(message);
      const currentDayKey = this.getMessageDayKey(timestamp);

      if (currentDayKey !== previousDayKey) {
        labels.set(index, this.dateSeparatorFormatter.format(new Date(timestamp)));
        previousDayKey = currentDayKey;
      }
    });

    return labels;
  });

  readonly userLookup = computed<ReadonlyMap<string, User>>(() => {
    const lookup = new Map<string, User>();

    for (const user of this.allUsers()) {
      lookup.set(user.id, user);

      if (user.oderId && user.oderId !== user.id) {
        lookup.set(user.oderId, user);
      }
    }

    for (const user of this.userLookupOverrides()) {
      lookup.set(user.id, user);

      if (user.oderId && user.oderId !== user.id) {
        lookup.set(user.oderId, user);
      }
    }

    return lookup;
  });

  private initialScrollObserver: MutationObserver | null = null;
  private initialScrollTimer: ReturnType<typeof setTimeout> | null = null;
  private boundOnImageLoad: (() => void) | null = null;
  private isAutoScrolling = false;
  private lastMessageCount = 0;
  private initialScrollPending = true;
  private prismHighlightScheduled = false;

  private readonly onConversationChanged = effect(() => {
    void this.conversationKey();
    this.resetScrollingState();
  });

  private readonly onMessagesChanged = effect(() => {
    const currentCount = this.channelMessages().length;
    const element = this.messagesContainer?.nativeElement;

    if (!element) {
      this.lastMessageCount = currentCount;
      return;
    }

    if (this.initialScrollPending) {
      this.lastMessageCount = currentCount;
      return;
    }

    const distanceFromBottom = element.scrollHeight - element.scrollTop - element.clientHeight;
    const newMessages = currentCount > this.lastMessageCount;

    if (newMessages) {
      if (distanceFromBottom <= 300) {
        this.scheduleScrollToBottomSmooth();
        this.showNewMessagesBar.set(false);
      } else {
        queueMicrotask(() => this.showNewMessagesBar.set(true));
      }
    }

    this.lastMessageCount = currentCount;
  });

  ngAfterViewChecked(): void {
    const element = this.messagesContainer?.nativeElement;

    if (!element)
      return;

    if (this.initialScrollPending) {
      if (this.messages().length > 0) {
        this.initialScrollPending = false;
        this.isAutoScrolling = true;
        element.scrollTop = element.scrollHeight;
        requestAnimationFrame(() => {
          this.isAutoScrolling = false;
        });

        this.startInitialScrollWatch();
        this.showNewMessagesBar.set(false);
        this.lastMessageCount = this.messages().length;
        this.scheduleCodeHighlight();
      } else if (!this.loading()) {
        this.initialScrollPending = false;
        this.lastMessageCount = 0;
      }

      return;
    }

    this.scheduleCodeHighlight();
  }

  ngOnDestroy(): void {
    this.stopInitialScrollWatch();
  }

  findRepliedMessage(messageId?: string | null): Message | undefined {
    if (!messageId)
      return undefined;

    return this.allMessages().find((message) => message.id === messageId);
  }

  onScroll(): void {
    const element = this.messagesContainer?.nativeElement;

    if (!element || this.isAutoScrolling)
      return;

    const distanceFromBottom = element.scrollHeight - element.scrollTop - element.clientHeight;
    const shouldStickToBottom = distanceFromBottom <= 300;

    if (shouldStickToBottom) {
      this.showNewMessagesBar.set(false);
    }

    if (this.initialScrollObserver) {
      this.stopInitialScrollWatch();
    }

    if (element.scrollTop < 150 && this.hasMoreMessages() && !this.loadingMore()) {
      this.loadMore();
    }
  }

  loadMore(): void {
    if (this.loadingMore() || !this.hasMoreMessages())
      return;

    this.loadingMore.set(true);

    const element = this.messagesContainer?.nativeElement;
    const previousScrollHeight = element?.scrollHeight ?? 0;

    this.displayLimit.update((limit) => limit + this.PAGE_SIZE);

    requestAnimationFrame(() => {
      requestAnimationFrame(() => {
        if (element) {
          const newScrollHeight = element.scrollHeight;

          element.scrollTop += newScrollHeight - previousScrollHeight;
        }

        this.loadingMore.set(false);
      });
    });
  }

  readLatest(): void {
    this.scrollToBottomSmooth();
    this.showNewMessagesBar.set(false);
  }

  scrollToMessage(messageId: string): void {
    const container = this.messagesContainer?.nativeElement;

    if (!container)
      return;

    const element = container.querySelector(`[data-message-id="${messageId}"]`) as HTMLElement | null;

    if (element) {
      element.scrollIntoView({
        behavior: 'smooth',
        block: 'center'
      });

      element.classList.add('bg-primary/10');
      setTimeout(() => element.classList.remove('bg-primary/10'), 2000);
    }
  }

  handleReplyRequested(message: ChatMessageReplyEvent): void {
    this.replyRequested.emit(message);
  }

  handleDeleteRequested(message: ChatMessageDeleteEvent): void {
    this.deleteRequested.emit(message);
  }

  handleEditSaved(event: ChatMessageEditEvent): void {
    this.editSaved.emit(event);
  }

  handleReactionAdded(event: ChatMessageReactionEvent): void {
    this.reactionAdded.emit(event);
  }

  handleReactionToggled(event: ChatMessageReactionEvent): void {
    this.reactionToggled.emit(event);
  }

  handleReferenceRequested(messageId: string): void {
    this.scrollToMessage(messageId);
  }

  handleDownloadRequested(attachment: Attachment): void {
    this.downloadRequested.emit(attachment);
  }

  handleImageOpened(attachment: Attachment): void {
    this.imageOpened.emit(attachment);
  }

  handleImageContextMenuRequested(event: ChatMessageImageContextMenuEvent): void {
    this.imageContextMenuRequested.emit(event);
  }

  handleEmbedRemoved(event: ChatMessageEmbedRemoveEvent): void {
    this.embedRemoved.emit(event);
  }

  private resetScrollingState(): void {
    this.initialScrollPending = true;
    this.stopInitialScrollWatch();
    this.showNewMessagesBar.set(false);
    this.lastMessageCount = 0;
    this.displayLimit.set(this.PAGE_SIZE);
  }

  private startInitialScrollWatch(): void {
    this.stopInitialScrollWatch();

    const element = this.messagesContainer?.nativeElement;

    if (!element)
      return;

    const snapToBottom = () => {
      const container = this.messagesContainer?.nativeElement;

      if (!container)
        return;

      this.isAutoScrolling = true;
      container.scrollTop = container.scrollHeight;
      requestAnimationFrame(() => {
        this.isAutoScrolling = false;
      });
    };

    this.initialScrollObserver = new MutationObserver(() => {
      requestAnimationFrame(snapToBottom);
    });

    this.initialScrollObserver.observe(element, {
      childList: true,
      subtree: true,
      attributes: true,
      attributeFilter: ['src']
    });

    this.boundOnImageLoad = () => requestAnimationFrame(snapToBottom);
    element.addEventListener('load', this.boundOnImageLoad, true);

    this.initialScrollTimer = setTimeout(() => this.stopInitialScrollWatch(), 5000);
  }

  private stopInitialScrollWatch(): void {
    if (this.initialScrollObserver) {
      this.initialScrollObserver.disconnect();
      this.initialScrollObserver = null;
    }

    if (this.boundOnImageLoad && this.messagesContainer) {
      this.messagesContainer.nativeElement.removeEventListener('load', this.boundOnImageLoad, true);

      this.boundOnImageLoad = null;
    }

    if (this.initialScrollTimer) {
      clearTimeout(this.initialScrollTimer);
      this.initialScrollTimer = null;
    }
  }

  private getMessageDateTimestamp(message: Message): number {
    return message.timestamp || getMessageTimestamp(message);
  }

  private getMessageDayKey(timestamp: number): string {
    const date = new Date(timestamp);

    return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
  }

  private scrollToBottomSmooth(): void {
    const element = this.messagesContainer?.nativeElement;

    if (!element)
      return;

    try {
      element.scrollTo({
        top: element.scrollHeight,
        behavior: 'smooth'
      });
    } catch {
      element.scrollTop = element.scrollHeight;
    }
  }

  private scheduleScrollToBottomSmooth(): void {
    requestAnimationFrame(() => {
      requestAnimationFrame(() => this.scrollToBottomSmooth());
    });
  }

  private scheduleCodeHighlight(): void {
    if (this.prismHighlightScheduled)
      return;

    this.prismHighlightScheduled = true;
    requestAnimationFrame(() => {
      this.prismHighlightScheduled = false;
      this.highlightRenderedCodeBlocks();
    });
  }

  private highlightRenderedCodeBlocks(): void {
    const container = this.messagesContainer?.nativeElement;
    const prism = window.Prism;

    if (!container || !prism?.highlightElement)
      return;

    const blocks = container.querySelectorAll<HTMLElement>('pre > code[class*="language-"]');

    for (const block of blocks) {
      const signature = this.getCodeBlockSignature(block);

      if (block.dataset['prismSignature'] === signature)
        continue;

      try {
        prism.highlightElement(block);
      } finally {
        block.dataset['prismSignature'] = signature;
      }
    }
  }

  private getCodeBlockSignature(block: HTMLElement): string {
    const value = `${block.className}:${block.textContent ?? ''}`;

    let hash = 0;

    for (let index = 0; index < value.length; index++) {
      hash = ((hash << 5) - hash + value.charCodeAt(index)) | 0;
    }

    return String(hash);
  }
}
