/* eslint-disable @typescript-eslint/member-ordering, */
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import {
  Component,
  computed,
  ElementRef,
  effect,
  inject,
  input,
  output,
  signal,
  ViewChild
} from '@angular/core';

import { NgIcon, provideIcons } from '@ng-icons/core';
import {
  lucideCheck,
  lucideDownload,
  lucideEdit,
  lucideExpand,
  lucideImage,
  lucideReply,
  lucideSmile,
  lucideTrash2,
  lucideX
} from '@ng-icons/lucide';
import {
  Attachment,
  AttachmentFacade,
  MAX_AUTO_SAVE_SIZE_BYTES
} from '../../../../../attachment';
import { KlipyService } from '../../../../application/services/klipy.service';
import { hasDedicatedChatEmbed } from '../../../../domain/rules/link-embed.rules';
import {
  DELETED_MESSAGE_CONTENT,
  Message,
  User
} from '../../../../../../shared-kernel';
import { ThemeNodeDirective } from '../../../../../theme';
import { PluginRenderHostComponent } from '../../../../../plugins/feature/plugin-render-host/plugin-render-host.component';
import { PluginUiRegistryService } from '../../../../../plugins';

import {
  ChatAudioPlayerComponent,
  ChatVideoPlayerComponent,
  ProfileCardService,
  UserAvatarComponent
} from '../../../../../../shared';
import { ChatMessageMarkdownComponent } from './chat-message-markdown/chat-message-markdown.component';
import { ChatLinkEmbedComponent } from './chat-link-embed/chat-link-embed.component';
import {
  ChatMessageDeleteEvent,
  ChatMessageEditEvent,
  ChatMessageEmbedRemoveEvent,
  ChatMessageImageContextMenuEvent,
  ChatMessageReactionEvent,
  ChatMessageReplyEvent
} from '../../models/chat-messages.model';

const COMMON_EMOJIS = [
  '👍',
  '❤️',
  '😂',
  '😮',
  '😢',
  '🎉',
  '🔥',
  '👀'
];
const RICH_MARKDOWN_PATTERNS = [
  /(^|\n)(#{1,6}\s|>\s|[-*+]\s|\d+\.\s|```|~~~)/m,
  /!\[[^\]]*\]\([^)]+\)/,
  /\[[^\]]+\]\([^)]+\)/,
  /https?:\/\/\S+/,
  /`[^`\n]+`/,
  /\*\*[^*\n]+\*\*|__[^_\n]+__/,
  /\*[^*\n]+\*|_[^_\n]+_/,
  /(?:^|\n)\|.+\|/m
];

interface ChatMessageAttachmentViewModel extends Attachment {
  isAudio: boolean;
  isUploader: boolean;
  isVideo: boolean;
  mediaActionLabel: string;
  mediaStatusText: string;
  progressPercent: number;
}

@Component({
  selector: 'app-chat-message-item',
  standalone: true,
  imports: [
    CommonModule,
    FormsModule,
    NgIcon,
    ChatAudioPlayerComponent,
    ChatVideoPlayerComponent,
    ChatMessageMarkdownComponent,
    ChatLinkEmbedComponent,
    UserAvatarComponent,
    PluginRenderHostComponent,
    ThemeNodeDirective
  ],
  viewProviders: [
    provideIcons({
      lucideCheck,
      lucideDownload,
      lucideEdit,
      lucideExpand,
      lucideImage,
      lucideReply,
      lucideSmile,
      lucideTrash2,
      lucideX
    })
  ],
  templateUrl: './chat-message-item.component.html',
  styleUrl: './chat-message-item.component.scss',
  host: {
    style: 'display: contents;'
  }
})
export class ChatMessageItemComponent {
  @ViewChild('editTextareaRef') editTextareaRef?: ElementRef<HTMLTextAreaElement>;

  private readonly attachmentsSvc = inject(AttachmentFacade);
  private readonly klipy = inject(KlipyService);
  private readonly pluginUi = inject(PluginUiRegistryService);
  private readonly profileCard = inject(ProfileCardService);
  private readonly attachmentVersion = signal(this.attachmentsSvc.updated());

  readonly message = input.required<Message>();
  readonly repliedMessage = input<Message | undefined>();
  readonly currentUserId = input<string | null>(null);
  readonly isAdmin = input(false);
  readonly userLookup = input<ReadonlyMap<string, User>>(new Map());

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

  readonly commonEmojis = COMMON_EMOJIS;
  readonly deletedMessageContent = DELETED_MESSAGE_CONTENT;
  readonly pluginEmbeds = computed(() => this.findPluginEmbeds(this.message().content));
  readonly isEditing = signal(false);
  readonly showEmojiPicker = signal(false);
  readonly senderUser = computed<User>(() => {
    const msg = this.message();
    const found = this.userLookup().get(msg.senderId);

    return (
      found ?? {
        id: msg.senderId,
        oderId: msg.senderId,
        username: msg.senderName,
        displayName: msg.senderName,
        status: 'disconnected',
        role: 'member',
        joinedAt: 0
      }
    );
  });

  editContent = '';

  openSenderProfileCard(event: MouseEvent): void {
    event.stopPropagation();
    const el = event.currentTarget as HTMLElement;
    const user = this.senderUser();
    const editable = user.id === this.currentUserId();

    this.profileCard.open(el, user, { editable });
  }

  readonly attachmentViewModels = computed<ChatMessageAttachmentViewModel[]>(() => {
    void this.attachmentVersion();

    return this.attachmentsSvc.getForMessage(this.message().id).map((attachment) => this.buildAttachmentViewModel(attachment));
  });
  private readonly syncAttachmentVersion = effect(() => {
    const version = this.attachmentsSvc.updated();

    queueMicrotask(() => {
      if (this.attachmentVersion() !== version) {
        this.attachmentVersion.set(version);
      }
    });
  });

  private findPluginEmbeds(content: string) {
    const match = /^toju:embed:([a-zA-Z0-9._:-]+):([\s\S]*)$/.exec(content.trim());

    if (!match) {
      return [];
    }

    const [
      ,
      embedType,
      payloadText
    ] = match;
    const payload = parseEmbedPayload(payloadText);

    return this.pluginUi.embedRecords()
      .filter((record) => record.contribution.embedType === embedType)
      .map((record) => ({
        ...record,
        render: () => record.contribution.render(payload)
      }));
  }

  startEdit(): void {
    this.editContent = this.message().content;
    this.isEditing.set(true);

    requestAnimationFrame(() => {
      this.autoResizeEditTextarea();

      const element = this.editTextareaRef?.nativeElement;

      if (!element)
        return;

      element.focus();
      element.setSelectionRange(element.value.length, element.value.length);
    });
  }

  onEditEnter(event: Event): void {
    const keyEvent = event as KeyboardEvent;

    if (keyEvent.shiftKey)
      return;

    keyEvent.preventDefault();
    this.saveEdit();
  }

  saveEdit(): void {
    if (!this.editContent.trim())
      return;

    this.editSaved.emit({
      messageId: this.message().id,
      content: this.editContent.trim()
    });

    this.cancelEdit();
  }

  cancelEdit(): void {
    this.isEditing.set(false);
    this.editContent = '';
  }

  autoResizeEditTextarea(): void {
    const element = this.editTextareaRef?.nativeElement;

    if (!element)
      return;

    element.style.height = 'auto';
    element.style.height = Math.min(element.scrollHeight, 520) + 'px';
    element.style.overflowY = element.scrollHeight > 520 ? 'auto' : 'hidden';
  }

  toggleEmojiPicker(): void {
    this.showEmojiPicker.update((current) => !current);
  }

  addReaction(emoji: string): void {
    this.reactionAdded.emit({
      messageId: this.message().id,
      emoji
    });

    this.showEmojiPicker.set(false);
  }

  toggleReaction(emoji: string): void {
    this.reactionToggled.emit({
      messageId: this.message().id,
      emoji
    });
  }

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

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

  removeEmbed(url: string): void {
    this.embedRemoved.emit({
      messageId: this.message().id,
      url
    });
  }

  shouldShowLinkEmbed(url?: string): boolean {
    return !hasDedicatedChatEmbed(url);
  }

  requestReferenceScroll(messageId: string): void {
    this.referenceRequested.emit(messageId);
  }

  isOwnMessage(): boolean {
    return this.message().senderId === this.currentUserId();
  }

  getGroupedReactions(): { emoji: string; count: number; hasCurrentUser: boolean }[] {
    const groups = new Map<string, { count: number; hasCurrentUser: boolean }>();
    const currentUserId = this.currentUserId();

    this.message().reactions.forEach((reaction) => {
      const existing = groups.get(reaction.emoji) || {
        count: 0,
        hasCurrentUser: false
      };

      groups.set(reaction.emoji, {
        count: existing.count + 1,
        hasCurrentUser: existing.hasCurrentUser || reaction.userId === currentUserId
      });
    });

    return Array.from(groups.entries()).map(([emoji, data]) => ({
      emoji,
      ...data
    }));
  }

  formatTimestamp(timestamp: number): string {
    const date = new Date(timestamp);
    const now = new Date();
    const time = date.toLocaleTimeString([], {
      hour: '2-digit',
      minute: '2-digit'
    });
    const toDay = (value: Date) => new Date(value.getFullYear(), value.getMonth(), value.getDate()).getTime();
    const dayDiff = Math.round((toDay(now) - toDay(date)) / (1000 * 60 * 60 * 24));

    if (dayDiff === 0)
      return time;

    if (dayDiff === 1)
      return 'Yesterday ' + time;

    if (dayDiff < 7) {
      return date.toLocaleDateString([], { weekday: 'short' }) + ' ' + time;
    }

    return (
      date.toLocaleDateString([], {
        month: 'short',
        day: 'numeric'
      }) +
      ' ' +
      time
    );
  }

  requiresRichMarkdown(content: string): boolean {
    return RICH_MARKDOWN_PATTERNS.some((pattern) => pattern.test(content));
  }

  formatBytes(bytes: number): string {
    const units = [
      'B',
      'KB',
      'MB',
      'GB'
    ];

    let size = bytes;
    let unitIndex = 0;

    while (size >= 1024 && unitIndex < units.length - 1) {
      size /= 1024;
      unitIndex++;
    }

    return `${size.toFixed(1)} ${units[unitIndex]}`;
  }

  formatSpeed(bytesPerSecond?: number): string {
    if (!bytesPerSecond || bytesPerSecond <= 0)
      return '0 B/s';

    const units = [
      'B/s',
      'KB/s',
      'MB/s',
      'GB/s'
    ];

    let speed = bytesPerSecond;
    let unitIndex = 0;

    while (speed >= 1024 && unitIndex < units.length - 1) {
      speed /= 1024;
      unitIndex++;
    }

    return `${speed.toFixed(speed < 100 ? 2 : 1)} ${units[unitIndex]}`;
  }

  isVideoAttachment(attachment: Attachment): boolean {
    return attachment.mime.startsWith('video/');
  }

  isAudioAttachment(attachment: Attachment): boolean {
    return attachment.mime.startsWith('audio/');
  }

  requiresMediaDownloadAcceptance(attachment: Attachment): boolean {
    return (this.isVideoAttachment(attachment) || this.isAudioAttachment(attachment)) && attachment.size > MAX_AUTO_SAVE_SIZE_BYTES;
  }

  getMediaAttachmentStatusText(attachment: Attachment): string {
    if (attachment.requestError)
      return attachment.requestError;

    if (this.requiresMediaDownloadAcceptance(attachment)) {
      return this.isVideoAttachment(attachment)
        ? 'Large video. Accept the download to watch it in chat.'
        : 'Large audio file. Accept the download to play it in chat.';
    }

    return this.isVideoAttachment(attachment) ? 'Waiting for video source...' : 'Waiting for audio source...';
  }

  getMediaAttachmentActionLabel(attachment: Attachment): string {
    if (this.requiresMediaDownloadAcceptance(attachment)) {
      return attachment.requestError ? 'Retry download' : 'Accept download';
    }

    return attachment.requestError ? 'Retry' : 'Request';
  }

  isUploader(attachment: Attachment): boolean {
    const currentUserId = this.currentUserId();

    return !!attachment.uploaderPeerId && !!currentUserId && attachment.uploaderPeerId === currentUserId;
  }

  requestAttachment(attachment: Attachment): void {
    const liveAttachment = this.getLiveAttachment(attachment.id);

    if (liveAttachment) {
      this.attachmentsSvc.requestFile(this.message().id, liveAttachment);
    }
  }

  cancelAttachment(attachment: Attachment): void {
    const liveAttachment = this.getLiveAttachment(attachment.id);

    if (liveAttachment) {
      this.attachmentsSvc.cancelRequest(this.message().id, liveAttachment);
    }
  }

  retryImageRequest(attachment: Attachment): void {
    const liveAttachment = this.getLiveAttachment(attachment.id);

    if (liveAttachment) {
      this.attachmentsSvc.requestImageFromAnyPeer(this.message().id, liveAttachment);
    }
  }

  openLightbox(attachment: Attachment): void {
    if (attachment.available && attachment.objectUrl) {
      this.imageOpened.emit(attachment);
    }
  }

  openImageContextMenu(event: MouseEvent, attachment: Attachment): void {
    event.preventDefault();
    event.stopPropagation();
    this.imageContextMenuRequested.emit({
      positionX: event.clientX,
      positionY: event.clientY,
      attachment
    });
  }

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

  private buildAttachmentViewModel(attachment: Attachment): ChatMessageAttachmentViewModel {
    const isVideo = this.isVideoAttachment(attachment);
    const isAudio = this.isAudioAttachment(attachment);
    const requiresMediaDownloadAcceptance = (isVideo || isAudio) && attachment.size > MAX_AUTO_SAVE_SIZE_BYTES;

    return {
      ...attachment,
      isAudio,
      isUploader: this.isUploader(attachment),
      isVideo,
      mediaActionLabel: requiresMediaDownloadAcceptance
        ? attachment.requestError
          ? 'Retry download'
          : 'Accept download'
        : attachment.requestError
          ? 'Retry'
          : 'Request',
      mediaStatusText: attachment.requestError
        ? attachment.requestError
        : requiresMediaDownloadAcceptance
          ? isVideo
            ? 'Large video. Accept the download to watch it in chat.'
            : 'Large audio file. Accept the download to play it in chat.'
          : isVideo
            ? 'Waiting for video source...'
            : 'Waiting for audio source...',
      progressPercent: attachment.size > 0 ? ((attachment.receivedBytes || 0) * 100) / attachment.size : 0
    };
  }

  private getLiveAttachment(attachmentId: string): Attachment | undefined {
    return this.attachmentsSvc.getForMessage(this.message().id).find((attachment) => attachment.id === attachmentId);
  }
}

function parseEmbedPayload(payloadText: string | undefined): unknown {
  if (!payloadText?.trim()) {
    return null;
  }

  try {
    return JSON.parse(payloadText) as unknown;
  } catch {
    return payloadText;
  }
}
