/* eslint-disable @typescript-eslint/member-ordering */
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import {
  AfterViewInit,
  Component,
  ElementRef,
  OnDestroy,
  ViewChild,
  inject,
  input,
  output,
  signal
} from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import {
  lucideImage,
  lucideReply,
  lucideSend,
  lucideX
} from '@ng-icons/lucide';
import type { ClipboardFilePayload } from '../../../../../../core/platform/electron/electron-api.models';
import { ElectronBridgeService } from '../../../../../../core/platform/electron/electron-bridge.service';
import { KlipyGif, KlipyService } from '../../../../application/services/klipy.service';
import { Message } from '../../../../../../shared-kernel';
import {
  PluginApiActionContribution,
  PluginClientApiService,
  PluginUiRegistryService
} from '../../../../../plugins';
import { ThemeNodeDirective } from '../../../../../theme';
import type { RoomSignalSourceInput } from '../../../../../server-directory';
import { ChatImageProxyFallbackDirective } from '../../../chat-image-proxy-fallback.directive';
import { TypingIndicatorComponent } from '../../../typing-indicator/typing-indicator.component';
import { ChatMarkdownService } from '../../services/chat-markdown.service';
import { ChatMessageComposerSubmitEvent } from '../../models/chat-messages.model';

type LocalFileWithPath = File & {
  path?: string;
};

const DEFAULT_TEXTAREA_HEIGHT = 62;

@Component({
  selector: 'app-chat-message-composer',
  standalone: true,
  imports: [
    CommonModule,
    FormsModule,
    NgIcon,
    ChatImageProxyFallbackDirective,
    TypingIndicatorComponent,
    ThemeNodeDirective
  ],
  viewProviders: [
    provideIcons({
      lucideImage,
      lucideReply,
      lucideSend,
      lucideX
    })
  ],
  templateUrl: './chat-message-composer.component.html',
  styleUrl: './chat-message-composer.component.scss',
  host: {
    '(document:keydown)': 'onDocKeydown($event)',
    '(document:keyup)': 'onDocKeyup($event)'
  }
})
export class ChatMessageComposerComponent implements AfterViewInit, OnDestroy {
  @ViewChild('messageInputRef') messageInputRef?: ElementRef<HTMLTextAreaElement>;
  @ViewChild('composerRoot') composerRoot?: ElementRef<HTMLDivElement>;
  @ViewChild('klipyTrigger') klipyTrigger?: ElementRef<HTMLButtonElement>;

  readonly replyTo = input<Message | null>(null);
  readonly showKlipyGifPicker = input(false);
  readonly klipyEnabled = input(false);
  readonly klipySignalSource = input<RoomSignalSourceInput | null>(null);
  readonly textareaTestId = input<string | null>(null);

  readonly messageSubmitted = output<ChatMessageComposerSubmitEvent>();
  readonly typingStarted = output();
  readonly replyCleared = output();
  readonly heightChanged = output<number>();
  readonly klipyGifPickerToggleRequested = output();

  private readonly klipy = inject(KlipyService);
  private readonly markdown = inject(ChatMarkdownService);
  private readonly electronBridge = inject(ElectronBridgeService);
  private readonly pluginApi = inject(PluginClientApiService);
  private readonly pluginUi = inject(PluginUiRegistryService);

  readonly pendingKlipyGif = signal<KlipyGif | null>(null);
  readonly pluginComposerActions = this.pluginUi.composerActionRecords;
  readonly toolbarVisible = signal(false);
  readonly dragActive = signal(false);
  readonly inputHovered = signal(false);
  readonly ctrlHeld = signal(false);
  readonly textareaExpanded = signal(false);

  messageContent = '';
  pendingFiles: File[] = [];
  inlineCodeToken = '`';

  private toolbarHovering = false;
  private dragDepth = 0;
  private lastTypingSentAt = 0;
  private resizeObserver: ResizeObserver | null = null;

  ngAfterViewInit(): void {
    this.autoResizeTextarea();
    this.observeHeight();
  }

  ngOnDestroy(): void {
    this.resizeObserver?.disconnect();
    this.resizeObserver = null;
  }

  sendMessage(): void {
    const raw = this.messageContent.trim();

    if (!raw && this.pendingFiles.length === 0 && !this.pendingKlipyGif())
      return;

    const content = this.buildOutgoingMessageContent(raw);

    this.messageSubmitted.emit({
      content,
      pendingFiles: [...this.pendingFiles]
    });

    this.messageContent = '';
    this.pendingFiles = [];
    this.pendingKlipyGif.set(null);
    this.replyCleared.emit();

    requestAnimationFrame(() => {
      this.autoResizeTextarea();
      this.messageInputRef?.nativeElement.focus();
    });
  }

  onInputChange(): void {
    const now = Date.now();

    if (now - this.lastTypingSentAt > 1000) {
      this.typingStarted.emit();
      this.lastTypingSentAt = now;
    }
  }

  clearReply(): void {
    this.replyCleared.emit();
  }

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

    if (keyEvent.shiftKey)
      return;

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

  applyInline(token: string): void {
    const result = this.markdown.applyInline(this.messageContent, this.getSelection(), token);

    this.messageContent = result.text;
    this.setSelection(result.selectionStart, result.selectionEnd);
  }

  applyPrefix(prefix: string): void {
    const result = this.markdown.applyPrefix(this.messageContent, this.getSelection(), prefix);

    this.messageContent = result.text;
    this.setSelection(result.selectionStart, result.selectionEnd);
  }

  applyHeading(level: number): void {
    const result = this.markdown.applyHeading(this.messageContent, this.getSelection(), level);

    this.messageContent = result.text;
    this.setSelection(result.selectionStart, result.selectionEnd);
  }

  applyOrderedList(): void {
    const result = this.markdown.applyOrderedList(this.messageContent, this.getSelection());

    this.messageContent = result.text;
    this.setSelection(result.selectionStart, result.selectionEnd);
  }

  applyCodeBlock(): void {
    const result = this.markdown.applyCodeBlock(this.messageContent, this.getSelection());

    this.messageContent = result.text;
    this.setSelection(result.selectionStart, result.selectionEnd);
  }

  applyLink(): void {
    const result = this.markdown.applyLink(this.messageContent, this.getSelection());

    this.messageContent = result.text;
    this.setSelection(result.selectionStart, result.selectionEnd);
  }

  applyImage(): void {
    const result = this.markdown.applyImage(this.messageContent, this.getSelection());

    this.messageContent = result.text;
    this.setSelection(result.selectionStart, result.selectionEnd);
  }

  applyHorizontalRule(): void {
    const result = this.markdown.applyHorizontalRule(this.messageContent, this.getSelection());

    this.messageContent = result.text;
    this.setSelection(result.selectionStart, result.selectionEnd);
  }

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

    this.klipyGifPickerToggleRequested.emit();
  }

  runPluginComposerAction(action: PluginApiActionContribution): void {
    void Promise.resolve()
      .then(() => action.run(this.pluginApi.createActionContext('composerAction')));
  }

  getKlipyTriggerRect(): DOMRect | null {
    return this.klipyTrigger?.nativeElement.getBoundingClientRect() ?? null;
  }

  handleKlipyGifSelected(gif: KlipyGif): void {
    this.pendingKlipyGif.set(gif);

    if (!this.messageContent.trim() && this.pendingFiles.length === 0) {
      this.sendMessage();
      return;
    }

    requestAnimationFrame(() => this.messageInputRef?.nativeElement.focus());
  }

  removePendingKlipyGif(): void {
    this.pendingKlipyGif.set(null);
    requestAnimationFrame(() => this.messageInputRef?.nativeElement.focus());
  }

  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]}`;
  }

  removePendingFile(file: File): void {
    const index = this.pendingFiles.findIndex((pendingFile) => pendingFile === file);

    if (index >= 0) {
      this.pendingFiles.splice(index, 1);
      this.emitHeight();
    }
  }

  onDragEnter(event: DragEvent): void {
    if (!this.hasPotentialFilePayload(event.dataTransfer))
      return;

    event.preventDefault();
    event.stopPropagation();
    this.dragDepth++;
    this.dragActive.set(true);
  }

  onDragOver(event: DragEvent): void {
    if (!this.hasPotentialFilePayload(event.dataTransfer))
      return;

    event.preventDefault();
    event.stopPropagation();

    if (event.dataTransfer) {
      event.dataTransfer.dropEffect = 'copy';
    }

    this.dragActive.set(true);
  }

  onDragLeave(event: DragEvent): void {
    if (!this.dragActive())
      return;

    event.preventDefault();
    event.stopPropagation();
    this.dragDepth = Math.max(0, this.dragDepth - 1);

    if (this.dragDepth === 0) {
      this.dragActive.set(false);
    }
  }

  onDrop(event: DragEvent): void {
    event.preventDefault();
    event.stopPropagation();
    this.dragDepth = 0;
    const droppedFiles = this.extractFilesFromTransfer(event.dataTransfer);

    if (droppedFiles.length === 0) {
      this.dragActive.set(false);
      return;
    }

    this.addPendingFiles(droppedFiles);
    this.dragActive.set(false);
  }

  async onPaste(event: ClipboardEvent): Promise<void> {
    if (!this.hasPotentialFilePayload(event.clipboardData, false))
      return;

    event.preventDefault();
    event.stopPropagation();

    const pastedFiles = await this.extractPastedFiles(event);

    if (pastedFiles.length === 0)
      return;

    this.addPendingFiles(pastedFiles);
  }

  autoResizeTextarea(): void {
    const element = this.messageInputRef?.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';
    this.syncTextareaExpandedState();
  }

  onInputFocus(): void {
    this.toolbarVisible.set(true);
  }

  onInputBlur(): void {
    setTimeout(() => {
      if (!this.toolbarHovering) {
        this.toolbarVisible.set(false);
      }
    }, 150);
  }

  onToolbarMouseEnter(): void {
    this.toolbarHovering = true;
  }

  onToolbarMouseLeave(): void {
    this.toolbarHovering = false;

    if (document.activeElement !== this.messageInputRef?.nativeElement) {
      this.toolbarVisible.set(false);
    }
  }

  onDocKeydown(event: KeyboardEvent): void {
    if (event.key === 'Control') {
      this.ctrlHeld.set(true);
    }
  }

  onDocKeyup(event: KeyboardEvent): void {
    if (event.key === 'Control') {
      this.ctrlHeld.set(false);
    }
  }

  private getSelection(): { start: number; end: number } {
    const element = this.messageInputRef?.nativeElement;

    return {
      start: element?.selectionStart ?? this.messageContent.length,
      end: element?.selectionEnd ?? this.messageContent.length
    };
  }

  private setSelection(start: number, end: number): void {
    const element = this.messageInputRef?.nativeElement;

    if (element) {
      element.selectionStart = start;
      element.selectionEnd = end;
      element.focus();
    }
  }

  private addPendingFiles(files: File[]): void {
    if (files.length === 0)
      return;

    const mergedFiles = this.mergeUniqueFiles(this.pendingFiles, files);

    if (mergedFiles.length === this.pendingFiles.length)
      return;

    this.pendingFiles = mergedFiles;
    this.toolbarVisible.set(true);
    this.emitHeight();

    requestAnimationFrame(() => this.messageInputRef?.nativeElement.focus());
  }

  private hasPotentialFilePayload(dataTransfer: DataTransfer | null, treatMissingTypesAsPotentialFile = true): boolean {
    if (!dataTransfer)
      return false;

    if (dataTransfer.files?.length)
      return true;

    const items = dataTransfer.items;

    if (items?.length) {
      for (const item of items) {
        if (item.kind === 'file') {
          return true;
        }
      }
    }

    const types = dataTransfer.types;

    if (!types || types.length === 0)
      return treatMissingTypesAsPotentialFile;

    for (const type of types) {
      if (
        type === 'Files' ||
        type === 'application/x-moz-file' ||
        type === 'public.file-url' ||
        type === 'text/uri-list' ||
        type === 'x-special/gnome-copied-files'
      ) {
        return true;
      }
    }

    return false;
  }

  private extractFilesFromTransfer(dataTransfer: DataTransfer | null): File[] {
    const extractedFiles: File[] = [];
    const items = dataTransfer?.items ?? null;

    if (items && items.length) {
      for (const item of items) {
        if (item.kind === 'file') {
          const file = item.getAsFile();

          if (file) {
            this.pushUniqueFile(extractedFiles, file);
          }
        }
      }
    }

    const files = dataTransfer?.files;

    if (!files?.length)
      return extractedFiles;

    for (const file of files) {
      this.pushUniqueFile(extractedFiles, file);
    }

    return extractedFiles;
  }

  private mergeUniqueFiles(existingFiles: File[], incomingFiles: File[]): File[] {
    const mergedFiles = [...existingFiles];

    for (const file of incomingFiles) {
      this.pushUniqueFile(mergedFiles, file);
    }

    return mergedFiles;
  }

  private pushUniqueFile(target: File[], candidate: File): void {
    const exists = target.some((file) => this.areFilesEquivalent(file, candidate));

    if (!exists) {
      target.push(candidate);
    }
  }

  private areFilesEquivalent(left: File, right: File): boolean {
    const leftPath = this.getLocalFilePath(left);
    const rightPath = this.getLocalFilePath(right);

    if (leftPath && rightPath) {
      return leftPath === rightPath;
    }

    if (left.name !== right.name || left.size !== right.size) {
      return false;
    }

    const leftType = left.type.trim();
    const rightType = right.type.trim();

    if (leftType && rightType && leftType !== rightType) {
      return false;
    }

    const leftLastModified = Number.isFinite(left.lastModified) ? left.lastModified : 0;
    const rightLastModified = Number.isFinite(right.lastModified) ? right.lastModified : 0;

    if (!leftLastModified || !rightLastModified) {
      return true;
    }

    return Math.abs(leftLastModified - rightLastModified) <= 1000;
  }

  private getLocalFilePath(file: File): string {
    return ((file as LocalFileWithPath).path || '').trim();
  }

  private async extractPastedFiles(event: ClipboardEvent): Promise<File[]> {
    const directFiles = this.extractFilesFromTransfer(event.clipboardData);

    if (directFiles.length > 0)
      return directFiles;

    return await this.readFilesFromElectronClipboard();
  }

  private async readFilesFromElectronClipboard(): Promise<File[]> {
    const electronApi = this.electronBridge.getApi();

    if (!electronApi)
      return [];

    try {
      const clipboardFiles = await electronApi.readClipboardFiles();

      return clipboardFiles.map((clipboardFile) => this.createFileFromClipboardPayload(clipboardFile));
    } catch {
      return [];
    }
  }

  private createFileFromClipboardPayload(payload: ClipboardFilePayload): File {
    const file = new File([this.base64ToArrayBuffer(payload.data)], payload.name, {
      lastModified: payload.lastModified,
      type: payload.mime
    });

    if (payload.path) {
      try {
        Object.defineProperty(file, 'path', {
          configurable: true,
          value: payload.path
        });
      } catch {
        (file as LocalFileWithPath).path = payload.path;
      }
    }

    return file;
  }

  private base64ToArrayBuffer(base64: string): ArrayBuffer {
    const binaryString = atob(base64);
    const bytes = new Uint8Array(binaryString.length);

    for (let index = 0; index < binaryString.length; index++) {
      bytes[index] = binaryString.charCodeAt(index);
    }

    return bytes.buffer;
  }

  private buildOutgoingMessageContent(raw: string): string {
    const withEmbeddedImages = this.markdown.appendImageMarkdown(raw);
    const gif = this.pendingKlipyGif();

    if (!gif)
      return withEmbeddedImages;

    const gifMarkdown = this.buildKlipyGifMarkdown(gif);

    return withEmbeddedImages ? `${withEmbeddedImages}\n${gifMarkdown}` : gifMarkdown;
  }

  private buildKlipyGifMarkdown(gif: KlipyGif): string {
    return `![KLIPY GIF](${this.klipy.normalizeMediaUrl(gif.url)})`;
  }

  private observeHeight(): void {
    const root = this.composerRoot?.nativeElement;

    if (!root)
      return;

    this.syncTextareaExpandedState();
    this.emitHeight();

    if (typeof ResizeObserver === 'undefined')
      return;

    this.resizeObserver = new ResizeObserver(() => {
      this.syncTextareaExpandedState();
      this.emitHeight();
    });

    this.resizeObserver.observe(root);
  }

  private syncTextareaExpandedState(): void {
    const textarea = this.messageInputRef?.nativeElement;

    this.textareaExpanded.set(Boolean(textarea && textarea.offsetHeight > DEFAULT_TEXTAREA_HEIGHT));
  }

  private emitHeight(): void {
    const root = this.composerRoot?.nativeElement;

    if (root) {
      this.heightChanged.emit(root.offsetHeight);
    }
  }
}
