/* eslint-disable @typescript-eslint/member-ordering, */
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  ElementRef,
  effect,
  inject,
  input,
  OnDestroy,
  output,
  signal,
  TemplateRef,
  ViewChild,
  ViewContainerRef
} from '@angular/core';
import { Overlay, OverlayRef } from '@angular/cdk/overlay';
import { TemplatePortal } from '@angular/cdk/portal';
import { Router } from '@angular/router';

import { NgIcon, provideIcons } from '@ng-icons/core';
import {
  lucideCheck,
  lucideCopy,
  lucideDownload,
  lucideEdit,
  lucideExpand,
  lucideExternalLink,
  lucideImage,
  lucidePlay,
  lucideReply,
  lucideSmile,
  lucideTrash2,
  lucideX
} from '@ng-icons/lucide';
import {
  Attachment,
  AttachmentFacade,
  MAX_BROWSER_INLINE_MEDIA_SIZE_BYTES,
  MAX_AUTO_SAVE_SIZE_BYTES
} from '../../../../../attachment';
import { PlatformService, ViewportService } from '../../../../../../core/platform';
import { ElectronBridgeService } from '../../../../../../core/platform/electron/electron-bridge.service';
import {
  ExperimentalMediaSettingsService
} from '../../../../../experimental-media';
import { ExperimentalVlcPlayerComponent } from '../../../../../experimental-media/feature/experimental-vlc-player/experimental-vlc-player.component';
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 { PluginRequirementStateService, PluginUiRegistryService } from '../../../../../plugins';

import {
  BottomSheetComponent,
  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 {
  canOpenExternally: boolean;
  canUseExperimentalPlayer: boolean;
  experimentalPlayerActive: boolean;
  isAudio: boolean;
  isUploader: boolean;
  isVideo: boolean;
  mediaActionLabel: string;
  mediaStatusText: string;
  progressPercent: number;
}

interface PluginEmbedToken {
  embedType: string;
  payloadText: string;
}

interface MissingPluginEmbedFallback {
  pluginName: string;
  searchTerm: string;
}

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

  private readonly attachmentsSvc = inject(AttachmentFacade);
  private readonly klipy = inject(KlipyService);
  private readonly pluginRequirements = inject(PluginRequirementStateService);
  private readonly pluginUi = inject(PluginUiRegistryService);
  private readonly electronBridge = inject(ElectronBridgeService);
  private readonly platform = inject(PlatformService);
  private readonly experimentalMedia = inject(ExperimentalMediaSettingsService);
  private readonly profileCard = inject(ProfileCardService);
  private readonly router = inject(Router);
  private readonly viewport = inject(ViewportService);
  private readonly overlay = inject(Overlay);
  private readonly viewContainerRef = inject(ViewContainerRef);
  private mobileSheetOverlayRef: OverlayRef | null = null;
  private longPressTimer: number | null = null;
  readonly isMobile = this.viewport.isMobile;
  readonly mobileSheetOpen = signal(false);
  private readonly attachmentVersion = signal(this.attachmentsSvc.updated());
  private readonly experimentalPlayerAttachmentId = signal<string | null>(null);
  private readonly mediaSupportCache = new Map<string, boolean>();

  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 pluginEmbedToken = computed(() => parsePluginEmbedToken(this.message().content));
  readonly pluginEmbeds = computed(() => this.findPluginEmbeds(this.pluginEmbedToken()));
  readonly missingPluginEmbed = computed(() => this.resolveMissingPluginEmbed());
  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);
      }
    });
  });

  openMissingPluginStore(fallback: MissingPluginEmbedFallback): void {
    const returnUrl = this.router.url.startsWith('/plugin-store') ? '/search' : this.router.url;

    void this.router.navigate(['/plugin-store'], {
      queryParams: {
        returnUrl,
        search: fallback.searchTerm
      }
    });
  }

  private findPluginEmbeds(token: PluginEmbedToken | null) {
    if (!token) {
      return [];
    }

    const payload = parseEmbedPayload(token.payloadText);

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

  private resolveMissingPluginEmbed(): MissingPluginEmbedFallback | null {
    const token = this.pluginEmbedToken();

    if (!token || this.pluginEmbeds().length > 0) {
      return null;
    }

    const missingRequirement = this.pluginRequirements.missingRequiredRequirements()
      .find((requirement) => requirement.pluginId === token.embedType || requirement.manifest?.id === token.embedType)
      ?? this.pluginRequirements.missingRequiredRequirements()
        .find((requirement) => requirement.manifest?.capabilities?.includes('ui.embeds'))
      ?? this.pluginRequirements.missingRequiredRequirements()[0];
    const pluginName = missingRequirement?.manifest?.title ?? missingRequirement?.pluginId ?? pluginNameFromEmbedType(token.embedType);

    return {
      pluginName,
      searchTerm: pluginName
    };
  }

  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());
  }

  onMessageTouchStart(event: TouchEvent): void {
    if (!this.isMobile() || this.message().isDeleted) {
      return;
    }

    if (event.touches.length !== 1) {
      this.clearLongPressTimer();
      return;
    }

    if (this.isEditableTarget(event.target)) {
      this.clearLongPressTimer();
      return;
    }

    this.clearLongPressTimer();
    this.longPressTimer = window.setTimeout(() => {
      this.longPressTimer = null;
      this.openMobileSheet();
    }, 500);
  }

  onMessageTouchEnd(): void {
    this.clearLongPressTimer();
  }

  private clearLongPressTimer(): void {
    if (this.longPressTimer !== null) {
      window.clearTimeout(this.longPressTimer);
      this.longPressTimer = null;
    }
  }

  private isEditableTarget(target: EventTarget | null): boolean {
    if (!(target instanceof Element)) {
      return false;
    }

    return target.closest('input, textarea, select, [contenteditable=""], [contenteditable="true"]') !== null;
  }

  closeMobileActions(): void {
    this.detachMobileSheet();
  }

  private openMobileSheet(): void {
    if (this.mobileSheetOverlayRef || !this.mobileSheetTpl) {
      this.mobileSheetOpen.set(true);
      return;
    }

    const overlayRef = this.overlay.create({
      positionStrategy: this.overlay.position().global(),
      scrollStrategy: this.overlay.scrollStrategies.block(),
      hasBackdrop: false,
      panelClass: 'metoyou-chat-actions-sheet-pane'
    });
    const portal = new TemplatePortal(this.mobileSheetTpl, this.viewContainerRef);

    overlayRef.attach(portal);
    this.mobileSheetOverlayRef = overlayRef;
    this.mobileSheetOpen.set(true);
  }

  private detachMobileSheet(): void {
    this.mobileSheetOpen.set(false);

    if (this.mobileSheetOverlayRef) {
      this.mobileSheetOverlayRef.dispose();
      this.mobileSheetOverlayRef = null;
    }
  }

  ngOnDestroy(): void {
    this.clearLongPressTimer();
    this.detachMobileSheet();
  }

  onMobileReact(emoji: string): void {
    this.addReaction(emoji);
    this.closeMobileActions();
  }

  onMobileReply(): void {
    this.requestReply();
    this.closeMobileActions();
  }

  onMobileEdit(): void {
    this.startEdit();
    this.closeMobileActions();
  }

  onMobileDelete(): void {
    this.requestDelete();
    this.closeMobileActions();
  }

  async onMobileCopy(): Promise<void> {
    const text = this.message().content;

    try {
      await navigator.clipboard.writeText(text);
    } catch {
      // Clipboard API unavailable; silently ignore.
    }

    this.closeMobileActions();
  }

  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);
  }

  openExperimentalPlayer(attachment: Attachment): void {
    if (!attachment.available || !attachment.objectUrl) {
      return;
    }

    this.experimentalPlayerAttachmentId.set(attachment.id);
  }

  async openAttachmentExternally(attachment: Attachment): Promise<void> {
    const diskPath = this.getAttachmentDiskPath(attachment);
    const electronApi = this.electronBridge.getApi();

    if (!diskPath || !electronApi?.openFilePath) {
      return;
    }

    await electronApi.openFilePath(diskPath);
  }

  closeExperimentalPlayer(): void {
    this.experimentalPlayerAttachmentId.set(null);
  }

  private buildAttachmentViewModel(attachment: Attachment): ChatMessageAttachmentViewModel {
    const isRawVideo = this.isVideoAttachment(attachment);
    const isRawAudio = this.isAudioAttachment(attachment);
    const isRawPlayableMedia = isRawVideo || isRawAudio;
    const isNativePlayableMedia = this.canPlayMediaType(attachment.mime);
    const shouldUseDefaultFileInterface = isRawPlayableMedia &&
      (!isNativePlayableMedia ||
        (this.platform.isBrowser && attachment.size > MAX_BROWSER_INLINE_MEDIA_SIZE_BYTES));
    const isVideo = isRawVideo && !shouldUseDefaultFileInterface;
    const isAudio = isRawAudio && !shouldUseDefaultFileInterface;
    const requiresMediaDownloadAcceptance = (isVideo || isAudio) && attachment.size > MAX_AUTO_SAVE_SIZE_BYTES;
    const canUseExperimentalPlayer = this.experimentalMedia.vlcJsPlaybackEnabled() &&
      shouldUseDefaultFileInterface &&
      isRawPlayableMedia &&
      attachment.available &&
      !!attachment.objectUrl;

    return {
      ...attachment,
      canOpenExternally: this.platform.isElectron && attachment.available && !!this.getAttachmentDiskPath(attachment),
      canUseExperimentalPlayer,
      experimentalPlayerActive: canUseExperimentalPlayer && this.experimentalPlayerAttachmentId() === attachment.id,
      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);
  }

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

  private canPlayMediaType(mime: string): boolean {
    if (!mime.startsWith('video/') && !mime.startsWith('audio/')) {
      return false;
    }

    const cached = this.mediaSupportCache.get(mime);

    if (cached !== undefined) {
      return cached;
    }

    const element = document.createElement(mime.startsWith('video/') ? 'video' : 'audio');

    const canPlay = element.canPlayType(mime) !== '';

    this.mediaSupportCache.set(mime, canPlay);

    return canPlay;
  }
}

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

  if (!match) {
    return null;
  }

  return {
    embedType: match[1],
    payloadText: match[2]
  };
}

function pluginNameFromEmbedType(embedType: string): string {
  const parts = embedType.split(/[.:_-]/).filter(Boolean);
  const pluginParts = parts.length > 2 ? parts.slice(0, -1) : parts;
  const label = pluginParts.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
    .join(' ')
    .trim();

  return label || embedType;
}

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

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